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

Core agentic loop that manages LLM↔tool cycles.

The loop repeatedly calls the LLM, parses PTC-Lisp from the response,
executes it, and continues until `return`/`fail` is called or `max_turns` is exceeded.

## Flow

1. Build LLM input with system prompt, messages, and tool names
2. Call LLM to get response (resolving atoms via `llm_registry` if needed)
3. Parse PTC-Lisp code from response (code blocks or raw s-expressions)
4. Execute code via `Lisp.run/2`
5. Check for return/fail or continue to next turn
6. Build trace entry and update message history
7. Merge execution results into context for next turn

## Termination Conditions

The loop terminates when any of these occur:

| Condition | Result | Reason |
|-----------|--------|--------|
| `(return value)` called | `{:ok, step}` | Normal completion |
| `(fail error)` called | `{:error, step}` | Explicit failure |
| `max_turns` exceeded | `{:error, step}` | `:max_turns_exceeded` |
| `max_depth` exceeded | `{:error, step}` | `:max_depth_exceeded` |
| `turn_budget` exhausted | `{:error, step}` | `:turn_budget_exhausted` |
| `mission_timeout` exceeded | `{:error, step}` | `:mission_timeout` |
| LLM error after retries | `{:error, step}` | `:llm_error` |

## Memory Handling

Memory persists across turns within a single `run/2` call. After each successful
Lisp execution:

1. `Lisp.run/2` applies the memory contract (see `PtcRunner.Lisp` for details)
2. `step.memory` contains the updated memory state
3. Loop updates `state.memory` for the next turn
4. Memory is merged into context via `state.context`

The memory contract determines how return values affect memory:
- Non-map returns: no memory update
- Map without `:return`: merged into memory
- Map with `:return`: rest merged, `:return` value returned

See `PtcRunner.Lisp.run/2` for the authoritative memory contract documentation.

## LLM Inheritance

Child SubAgents inherit the `llm_registry` from their parent, enabling atom-based
LLM references (like `:haiku` or `:sonnet`) to work throughout the agent hierarchy.
The registry only needs to be provided once at the top-level `SubAgent.run/2` call.

Resolution order for LLM selection:
1. `agent.llm` - Set in SubAgent struct
2. `as_tool(..., llm:)` - Bound at tool creation
3. Parent's LLM - Inherited from calling agent
4. Required at top level

This is an internal module called by `SubAgent.run/2`.

# `run`

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

Execute a SubAgent in loop mode (multi-turn with tools).

## Parameters

- `agent` - A `%Definition{}` struct
- `opts` - Keyword list with:
  - `llm` - Required. LLM callback function
  - `context` - Initial context map (default: %{})
  - `cache` - Enable prompt caching (default: false). When true, the LLM callback receives
    `cache: true` in its input map. The callback should pass this to the provider to enable
    caching of system prompts for cost savings on multi-turn agents.
  - `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 filtering: true (always), false (never), :on_error (only on failure) (default: true)
  - `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()}]`
  - `llm_retry` - Optional retry configuration map with:
    - `max_attempts` - Maximum number of 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])
  - `token_limit` - Max total tokens before budget check triggers (default: nil)
  - `on_budget_exceeded` - Action when token_limit or budget callback returns :stop (default: :fail)
    - `:fail` - Return error with :budget_callback_exceeded
    - `:return_partial` - Try to return last successful expression result
  - `budget` - Custom budget callback function `(usage_map -> :continue | :stop)` (default: nil)
    Usage map contains: `%{total_tokens: int, input_tokens: int, output_tokens: int, llm_requests: int}`

## Returns

- `{:ok, Step.t()}` on success (when `return` is called)
- `{:error, Step.t()}` on failure (when `fail` is called or max_turns exceeded)

## Examples

    iex> agent = PtcRunner.SubAgent.new(prompt: "Add {{x}} and {{y}}", tools: %{}, max_turns: 2)
    iex> llm = fn %{messages: _} -> {:ok, "```clojure\n(return {:result (+ data/x data/y)})\n```"} end
    iex> {:ok, step} = PtcRunner.SubAgent.Loop.run(agent, llm: llm, context: %{x: 5, y: 3})
    iex> step.return
    %{"result" => 8}

---

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