# `PtcRunner.SubAgent`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/sub_agent.ex#L1)

Agentic loop for LLM-driven PTC-Lisp execution.

A SubAgent prompts an LLM to write programs, executes them in a sandbox,
and loops until completion. Define agents with `new/1`, execute with `run/2`.

## Execution Modes

| Mode | Condition | Behavior |
|------|-----------|----------|
| Single-shot | `max_turns == 1` and `tools == %{}` | One LLM call, expression returned |
| Loop | Otherwise | Multi-turn with tools until `return` or `fail` |

## Examples

    # Simple single-shot
    {:ok, step} = SubAgent.run("What's 2 + 2?", llm: my_llm, max_turns: 1)
    step.return  #=> 4

    # With tools and signature
    agent = SubAgent.new(
      prompt: "Find expensive products",
      signature: "{name :string, price :float}",
      tools: %{"list_products" => &MyApp.list/0}
    )
    {:ok, step} = SubAgent.run(agent, llm: my_llm)

## See Also

- [Getting Started](guides/subagent-getting-started.md) - Full walkthrough
- [Core Concepts](guides/subagent-concepts.md) - Context and memory
- [Patterns](guides/subagent-patterns.md) - Composition and orchestration
- [Text Mode + PTC-Lisp Compute](guides/text-mode-ptc-compute.md) - Combined mode (`output: :text, ptc_transport: :tool_call`)
- `new/1` - All struct fields and options
- `run/2` - Runtime options and LLM registry
- `chat/3` - Multi-turn chat with history threading
- [Phoenix Streaming](guides/phoenix-streaming.md) - Real-time streaming in LiveView

# `compaction_opts`

```elixir
@type compaction_opts() :: PtcRunner.SubAgent.Definition.compaction_opts()
```

# `format_options`

```elixir
@type format_options() :: PtcRunner.SubAgent.Definition.format_options()
```

# `language_spec`

```elixir
@type language_spec() :: PtcRunner.SubAgent.Definition.language_spec()
```

# `llm_callback`

```elixir
@type llm_callback() :: PtcRunner.SubAgent.Definition.llm_callback()
```

# `llm_registry`

```elixir
@type llm_registry() :: PtcRunner.SubAgent.Definition.llm_registry()
```

# `llm_response`

```elixir
@type llm_response() :: PtcRunner.SubAgent.Definition.llm_response()
```

# `output_mode`

```elixir
@type output_mode() :: PtcRunner.SubAgent.Definition.output_mode()
```

# `plan_step`

```elixir
@type plan_step() :: PtcRunner.SubAgent.Definition.plan_step()
```

# `system_prompt_opts`

```elixir
@type system_prompt_opts() :: PtcRunner.SubAgent.Definition.system_prompt_opts()
```

# `t`

```elixir
@type t() :: PtcRunner.SubAgent.Definition.t()
```

# `as_tool`

```elixir
@spec as_tool(
  t(),
  keyword()
) :: PtcRunner.SubAgent.SubAgentTool.t()
```

Wraps a SubAgent as a tool callable by other agents.

Returns a `SubAgentTool` struct that parent agents can include
in their tools map. When called, the wrapped agent inherits
LLM and registry from the parent unless overridden.

## Options

- `:llm` - Bind specific LLM (atom or function). Overrides parent inheritance.
- `:description` - Override agent's description (falls back to `agent.description`)
- `:name` - Suggested tool name (informational, not enforced by the struct)
- `:cache` - Cache results by input args (default: `false`). Only use for
  deterministic agents where same inputs always produce same outputs.

## Description Requirement

A description is required for tools. It can be provided either:
- On the SubAgent via `new(description: "...")`, or
- Via the `:description` option when calling `as_tool/2`

Raises `ArgumentError` if neither is provided.

## LLM Resolution

When the tool is called, the LLM is resolved in priority order:
1. `agent.llm` - The agent's own LLM override (highest priority)
2. `bound_llm` - LLM bound via the `:llm` option
3. Parent's llm - Inherited from the calling agent (lowest priority)

## Examples

    iex> child = PtcRunner.SubAgent.new(
    ...>   prompt: "Double {{n}}",
    ...>   signature: "(n :int) -> {result :int}",
    ...>   description: "Doubles a number"
    ...> )
    iex> tool = PtcRunner.SubAgent.as_tool(child)
    iex> tool.signature
    "(n :int) -> {result :int}"
    iex> tool.description
    "Doubles a number"

    iex> child = PtcRunner.SubAgent.new(prompt: "Process data", description: "Default desc")
    iex> tool = PtcRunner.SubAgent.as_tool(child, llm: :haiku, description: "Processes data")
    iex> tool.bound_llm
    :haiku
    iex> tool.description
    "Processes data"

    iex> child = PtcRunner.SubAgent.new(prompt: "Analyze {{text}}", signature: "(text :string) -> :string", description: "Analyzes text")
    iex> tool = PtcRunner.SubAgent.as_tool(child, name: "analyzer")
    iex> tool.signature
    "(text :string) -> :string"

    iex> child = PtcRunner.SubAgent.new(prompt: "No description")
    iex> PtcRunner.SubAgent.as_tool(child)
    ** (ArgumentError) as_tool requires description to be set - pass description: option or set description on the SubAgent

# `chat`

```elixir
@spec chat(t(), String.t(), keyword()) ::
  {:ok, term(), [map()], map()} | {:error, term()}
```

Multi-turn chat with conversation history threading.

Wraps `run/2` for chat use cases where conversation history must persist
across calls. Auto-detects mode based on `agent.output`:

- **`:text`** — Forces text mode, clears signature. Returns plain text.
- **`:ptc_lisp`** — Keeps PTC-Lisp mode and signature. Returns structured data
  and memory (variables defined via `def`).

## Parameters

- `agent` - A `SubAgent.t()` struct
- `user_message` - The user's message for this turn
- `opts` - Runtime options (same as `run/2`, plus `:messages` and `:memory`)

## Options

- `:messages` - Prior conversation history (default: `[]`). Pass the
  `updated_messages` from a previous `chat/3` call to continue the conversation.
- `:memory` - Prior memory map (default: `%{}`). For PTC-Lisp mode, pass the
  memory from a previous `chat/3` call so the LLM can access prior variables.
- All other options are forwarded to `run/2` (e.g., `:llm`, `:context`)

## Returns

- `{:ok, result, updated_messages, memory}` — the result (text or structured),
  the full message history, and the memory map (empty for text mode)
- `{:error, reason}` — on failure

## Examples

    # Text mode
    agent = SubAgent.new(
      prompt: "placeholder",
      output: :text,
      system_prompt: "You are a helpful assistant."
    )

    {:ok, reply, messages, _memory} = SubAgent.chat(agent, "Hello!", llm: my_llm)
    {:ok, reply2, messages2, _memory} = SubAgent.chat(
      agent, "Tell me more",
      llm: my_llm, messages: messages
    )

    # PTC-Lisp mode with memory threading
    agent = SubAgent.new(
      prompt: "placeholder",
      output: :ptc_lisp,
      system_prompt: "You are a helpful assistant.",
      tools: my_tools
    )

    {:ok, result, messages, memory} = SubAgent.chat(agent, "Look up X", llm: my_llm)
    {:ok, result2, messages2, memory2} = SubAgent.chat(
      agent, "Now use that result",
      llm: my_llm, messages: messages, memory: memory
    )

# `compile`

See `PtcRunner.SubAgent.Compiler.compile/2`.

# `default_format_options`

Returns the default format options.

# `effective_tools`

# `expand_builtin_tools`

# `new`

Creates a SubAgent struct from keyword options.

Raises `ArgumentError` if validation fails (missing required fields or invalid types).

## Parameters

- `opts` - Keyword list of options

## Required Options

- `prompt` - String template describing what to accomplish (supports `{{placeholder}}` expansion)

## Optional Options

- `signature` - String contract defining expected inputs and outputs
- `tools` - Map of callable tools (default: %{})
- `max_turns` - Positive integer for maximum LLM calls (default: 5)
- `retry_turns` - Non-negative integer for extra turns in must-return mode (default: 0)
- `prompt_limit` - Map with truncation config for LLM view
- `timeout` - Positive integer for max milliseconds per Lisp execution (default: 5000)
- `max_heap` - Positive integer for max heap size in words per Lisp execution (default: app config or 1,250,000 ~10MB)
- `mission_timeout` - Positive integer for max milliseconds for entire execution
- `llm_retry` - Map with infrastructure retry config
- `llm` - Atom or function for optional LLM override
- `system_prompt` - System prompt customization (map, function, or string)
- `memory_limit` - Positive integer for max bytes for memory map (default: 1MB = 1,048,576 bytes)
- `memory_strategy` - How to handle memory limit exceeded: `:strict` (fatal, default) or `:rollback` (roll back memory, feed error to LLM)
- `name` - Short display name shown in traces and the ptc-viewer (e.g. `"meta_agent"`, `"task_agent"`)
- `description` - String describing the agent's purpose (for external docs)
- `field_descriptions` - Map of field names to descriptions for signature fields
- `context_descriptions` - Map of context variable names to descriptions (shown in Data Inventory)
- `format_options` - Keyword list controlling output truncation (merged with defaults)
- `float_precision` - Non-negative integer for decimal places in floats (default: 2)
- `compaction` - Pressure-triggered context compaction (see `t:compaction_opts/0`)
- `pmap_timeout` - Positive integer for max milliseconds per `pmap` parallel operation (default: 5000)
- `pmap_max_concurrency` - Positive integer for max concurrent tasks in pmap/pcalls (default: `System.schedulers_online() * 2`). Reduce to avoid overflowing connection pools or API rate limits.
- `max_depth` - Positive integer for maximum recursion depth in nested agents (default: 3)
- `turn_budget` - Positive integer for total turn budget across retries (default: 20)
- `output` - Output mode: `:ptc_lisp` (default) or `:text`
- `ptc_reference` - Combined-mode reference card: `:compact` (default; only valid value in v1)
- `thinking` - Boolean enabling thinking section in output format (default: false)
- `llm_query` - Boolean enabling LLM query mode (default: false)
- `builtin_tools` - List of builtin tool families to enable (default: []). Available: `:grep` (adds grep and grep-n tools)
- `plan` - List of plan steps (strings, `{id, description}` tuples, or keyword list)
- `runtime_prelude` - Compiled `%PtcRunner.Lisp.Prelude{}` artifact attached to every run of this agent
- `prelude_store` / `preludes` - Resolve versioned store refs during construction and freeze the compiled bundle into `runtime_prelude`

## Returns

A `SubAgent.t()` struct.

## Raises

- `ArgumentError` - if prompt is missing or not a string, max_turns is not positive, tools is not a map, any optional field has an invalid type, or prompt placeholders don't match signature parameters (when signature is provided)

## Examples

    iex> agent = PtcRunner.SubAgent.new(prompt: "Analyze the data")
    iex> agent.prompt
    "Analyze the data"

    iex> email_tools = %{"list_emails" => fn _args -> [] end}
    iex> agent = PtcRunner.SubAgent.new(
    ...>   prompt: "Find urgent emails for {{user}}",
    ...>   signature: "(user :string) -> {count :int, ids [:int]}",
    ...>   tools: email_tools,
    ...>   max_turns: 10
    ...> )
    iex> agent.max_turns
    10

# `preview_prompt`

```elixir
@spec preview_prompt(
  t(),
  keyword()
) :: %{
  system: String.t(),
  user: String.t(),
  tool_schemas: [map()],
  schema: map() | nil
}
```

Preview the system and user prompts that would be sent to the LLM.

This function generates and returns the prompts without executing the agent,
useful for debugging prompt generation, verifying template expansion, and
reviewing what the LLM will see.

## Parameters

- `agent` - A `SubAgent.t()` struct
- `opts` - Keyword list with:
  - `context` - Context map for template expansion (default: %{})

## Returns

A map with:
- `:system` - The static system prompt (cacheable - does NOT include mission)
- `:user` - The full first user message (context sections + mission)
- `:tool_schemas` - List of tool schema maps with name, signature, and description fields
- `:schema` - JSON schema for the return type (text mode only, nil for PTC-Lisp)

## Examples

    iex> agent = PtcRunner.SubAgent.new(
    ...>   prompt: "Find emails for {{user}}",
    ...>   signature: "(user :string) -> {count :int}",
    ...>   tools: %{"list_emails" => fn _ -> [] end}
    ...> )
    iex> preview = PtcRunner.SubAgent.preview_prompt(agent, context: %{user: "alice"})
    iex> preview.user =~ "Find emails for alice"
    true
    iex> preview.user =~ "<mission>"
    true
    iex> preview.system =~ "<return_rules>"
    true
    iex> preview.system =~ "<mission>"
    false

# `run`

```elixir
@spec run(
  t() | String.t(),
  keyword()
) :: {:ok, PtcRunner.Step.t()} | {:error, PtcRunner.Step.t()}
```

Executes a SubAgent with the given options.

Returns a `Step` struct containing the result, metrics, and execution trace.

## Parameters

- `agent` - A `SubAgent.t()` struct or a string prompt (for convenience)
- `opts` - Keyword list of runtime options

## Runtime Options

- `llm` - Required. LLM callback function `(map() -> {:ok, String.t()} | {:error, term()})` or atom
- `llm_registry` - Map of atom to LLM callback for atom-based LLM references (default: %{})
- `context` - Map of input data (default: %{})
- `debug` - Deprecated, no longer needed. Turn structs always capture `raw_response`.
  Use `SubAgent.Debug.print_trace(step, raw: true)` to view full LLM output.
- `trace` - Trace collection mode (default: true):
  - `true` - Always collect trace in Step
  - `false` - Never collect trace
  - `:on_error` - Only include trace when execution fails
- `llm_retry` - Optional map to configure retry behavior for transient LLM failures:
  - `max_attempts` - Maximum retry attempts (default: 1, meaning no retries unless explicitly configured)
  - `backoff` - Backoff strategy: `:exponential`, `:linear`, or `:constant` (default: `:exponential`)
  - `base_delay` - Base delay in milliseconds (default: 1000)
  - `retryable_errors` - List of error types to retry (default: `[:rate_limit, :timeout, :server_error]`)
- `collect_messages` - Capture full conversation history in Step.messages (default: false).
  When enabled, messages are in OpenAI format: `[%{role: :system | :user | :assistant, content: String.t()}]`
- `prelude_store` / `preludes` - For string prompts or `%SubAgent{}` structs,
  resolve versioned store refs for this invocation. String prompts freeze the
  bundle into the constructed agent; struct runs use a per-run copy and leave
  the original struct unchanged. Child `SubAgentTool`s do not implicitly inherit
  a parent's selected bundle; select preludes on the child definition when needed.
- Other options from agent definition can be overridden

## LLM Registry

When using atom LLMs (like `:haiku` or `:sonnet`), provide an `llm_registry` map:

    registry = %{
      haiku: fn input -> MyApp.LLM.haiku(input) end,
      sonnet: fn input -> MyApp.LLM.sonnet(input) end
    }

    SubAgent.run(agent, llm: :sonnet, llm_registry: registry)

The registry is automatically inherited by all child SubAgents, so you only need
to provide it once at the top level.

## Returns

- `{:ok, Step.t()}` on success
- `{:error, Step.t()}` on failure

## Examples

    # Using a SubAgent struct
    iex> agent = PtcRunner.SubAgent.new(prompt: "Calculate {{x}} + {{y}}", max_turns: 1)
    iex> llm = fn %{messages: [%{content: _prompt}]} -> {:ok, "```clojure\n(+ data/x data/y)\n```"} end
    iex> {:ok, step} = PtcRunner.SubAgent.run(agent, llm: llm, context: %{x: 5, y: 3})
    iex> step.return
    8

    # Using string convenience form
    iex> llm = fn %{messages: [%{content: _prompt}]} -> {:ok, "```clojure\n42\n```"} end
    iex> {:ok, step} = PtcRunner.SubAgent.run("Return 42", max_turns: 1, llm: llm)
    iex> step.return
    42

    # Using atom LLM with registry
    iex> registry = %{test: fn %{messages: [%{content: _}]} -> {:ok, "```clojure\n100\n```"} end}
    iex> {:ok, step} = PtcRunner.SubAgent.run("Test", max_turns: 1, llm: :test, llm_registry: registry)
    iex> step.return
    100

# `run!`

```elixir
@spec run!(
  t() | String.t(),
  keyword()
) :: PtcRunner.Step.t()
```

Bang variant of `run/2` that raises on failure.

Returns the `Step` struct directly instead of `{:ok, step}`. Raises
`SubAgentError` if execution fails.

## Examples

    iex> agent = PtcRunner.SubAgent.new(prompt: "Say hello", max_turns: 1)
    iex> mock_llm = fn _ -> {:ok, "```clojure\n\"Hello!\"\n```"} end
    iex> step = PtcRunner.SubAgent.run!(agent, llm: mock_llm)
    iex> step.return
    "Hello!"

    # Failure case (using loop mode)
    iex> agent = PtcRunner.SubAgent.new(prompt: "Fail", max_turns: 2)
    iex> mock_llm = fn _ -> {:ok, ~S|(fail {:reason :test :message "Error"})|} end
    iex> PtcRunner.SubAgent.run!(agent, llm: mock_llm)
    ** (PtcRunner.SubAgentError) SubAgent failed: failed - %{"message" => "Error", "reason" => "test"}

# `text_return?`

Returns true if the agent's return type is plain text (`:string` or no signature).

Used by TextMode to decide between raw text and JSON response handling.

## Examples

    iex> agent = PtcRunner.SubAgent.new(prompt: "Hello", output: :text)
    iex> PtcRunner.SubAgent.text_return?(agent)
    true

    iex> agent = PtcRunner.SubAgent.new(prompt: "Get data", signature: "() -> {name :string}", output: :text)
    iex> PtcRunner.SubAgent.text_return?(agent)
    false

# `then`

Chains SubAgent/CompiledAgent executions with error propagation.

See `PtcRunner.SubAgent.Chaining.then/3` for full documentation.

# `then!`

Chains agents in a pipeline, passing the previous step as context.

See `PtcRunner.SubAgent.Chaining.then!/3` for full documentation.

# `unwrap_sentinels`

Unwraps internal sentinel values from a search result.

Handles:
- `{:__ptc_return__, value}` -> `{:ok, step_with_raw_value}`
- `{:__ptc_fail__, value}` -> `{:error, error_step}`

Used by single-shot mode and compiled agents to provide clean results.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
