# Getting Started with SubAgents

This guide walks you through your first SubAgent - from a minimal example to understanding the core execution model.

## Prerequisites

- Elixir 1.15+
- An LLM provider (OpenRouter, Anthropic, OpenAI, etc.)

## The Simplest SubAgent

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "How many r's are in raspberry?",
  llm: "haiku"
)

step.return  #=> 3
```

That's it. No tools, no signature, no validation - just a prompt and a model name.

### Why This Matters

The SubAgent doesn't answer directly - it writes a *program* that computes the answer:

```clojure
(count (filter #(= % "r") (seq "raspberry")))
```

This is the core insight of PTC (Programmatic Tool Calling): instead of asking the LLM to *be* the computer, ask it to *program* the computer. The LLM reasons and generates code; the actual computation runs in a sandboxed interpreter where results are deterministic.

### With Context

Pass data to the prompt using `{{placeholders}}`:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "Summarize in one sentence: {{text}}",
  context: %{text: "Long article about climate change..."},
  llm: my_llm
)

step.return  #=> "Climate change poses significant global challenges..."
```

### With Type Validation

Add a signature to validate the output structure:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "Rate this review sentiment",
  context: %{review: "Great product, love it!"},
  signature: "{sentiment :string, score :float}",
  llm: my_llm
)

step.return["sentiment"]  #=> "positive"
step.return["score"]      #=> 0.95
```

### Text Mode (Simpler Alternative)

For tasks that don't need PTC-Lisp, use `output: :text`. The behavior auto-detects based on whether tools are provided and the return type:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "Extract the person's name and age from: {{text}}",
  context: %{text: "John is 25 years old"},
  output: :text,
  signature: "(text :string) -> {name :string, age :int}",
  llm: my_llm
)

step.return["name"]  #=> "John"
step.return["age"]   #=> 25
```

With a complex return type and no tools, the LLM returns structured JSON directly. With no signature or a `:string` return type, it returns raw text. Use it when you need structured output but not computation.

Text mode supports full Mustache templating including sections for lists:

```elixir
# Iterate over list data with {{#section}}...{{/section}}
SubAgent.new(
  prompt: "Summarize these items: {{#items}}{{name}}, {{/items}}",
  output: :text,
  signature: "(items [{name :string}]) -> {summary :string}"
)
```

**Constraints:** Signature is optional. Tools are optional. Compaction is not supported.

See [Text Mode Guide](subagent-text-mode.md) for Mustache syntax, validation rules, tool calling, and examples.

### Text Mode with Tools (For Smaller LLMs)

For smaller or faster LLMs that can use native tool calling but can't generate PTC-Lisp, use `output: :text` with tools:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "What is 17 + 25? Use the add tool.",
  output: :text,
  signature: "() -> {result :int}",
  tools: %{
    "add" => {fn args -> args["a"] + args["b"] end,
              signature: "(a :int, b :int) -> :int",
              description: "Add two numbers"}
  },
  llm: my_llm
)

step.return["result"]  #=> 42
```

Text mode auto-detects tool calling when tools are provided. It converts tool signatures to JSON Schema and uses the LLM provider's native tool calling API. The LLM calls tools, ptc_runner executes them, and the loop continues until the LLM returns a final answer. If a complex return type is specified, the answer is validated as JSON against the signature. If no signature or `:string` return type, the raw text answer is returned.

**Constraints:** No memory persistence between turns.

See [Text Mode Guide](subagent-text-mode.md) for multi-tool scenarios, limits, and error handling.

### Text Mode with Deterministic Compute (Combined Mode)

LLMs are unreliable at tasks like counting characters, slicing strings, exact arithmetic, and other deterministic operations — tokenization hides individual characters, and the model's "calculator in its head" silently miscounts. Combined mode (`output: :text, ptc_transport: :tool_call`) gives the LLM an escape hatch: a `lisp_eval` tool the model can call to run a small program in a sandbox.

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "How many letter r in raspberry?",
  prompt: "You are a helpful assistant. For deterministic computation
           (counting, arithmetic, string manipulation), call
           lisp_eval instead of computing in your head.",
  output: :text,
  ptc_transport: :tool_call,
  llm: my_llm,
  max_turns: 4
)

step.return  #=> "There are 3 letter r's in 'raspberry'."
```

What happens under the hood:

1. The LLM sees `lisp_eval` in the tool list plus a compact PTC-Lisp reference card in the system prompt (~270 tokens).
2. It calls `lisp_eval` with a program like `(count (filter #(= \r %) "raspberry"))`.
3. The runtime returns `3` as a tool result.
4. The LLM composes the final text answer from that tool result.

**No app tools are required.** Combined mode is useful even with zero tools registered — `lisp_eval` alone covers counting, regex matching, joining, sorting, slicing, and arbitrary arithmetic. When you do register app tools with `expose: :both, cache: true`, the LLM can also escalate large native results into PTC-Lisp programs without re-fetching (see [Text Mode + PTC-Lisp Compute](text-mode-ptc-compute.md) for that pattern).

**When to use combined mode:**
- Chat agents that occasionally need character counting, exact arithmetic, or list/string transformations.
- Agents whose tools return data the LLM should aggregate, filter, or join deterministically.
- Cases where you'd otherwise write a one-off `count_chars`/`sort`/`filter` tool for every task.

**Trade-offs:**
- The reference card adds ~270 tokens to every system prompt — a permanent tax. Worth it only if you actually expect deterministic-compute questions.
- The model has to decide to escalate. The system-prompt nudge above ("call lisp_eval instead of computing in your head") matters; without it, smaller models will still try to count by themselves.

## Adding Tools

Tools let the agent call functions to gather information:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "What is the most expensive product?",
  signature: "{name :string, price :float}",
  tools: %{"list_products" => &MyApp.Products.list/0},
  llm: my_llm
)

step.return["name"]   #=> "Widget Pro"
step.return["price"]  #=> 299.99
```

With tools, the SubAgent enters an **agentic loop** - it calls tools and reasons until it has enough information to return.

## Execution Behavior

| Mode | Condition | Behavior |
|------|-----------|----------|
| Single-shot | `max_turns: 1` and no tools | One LLM call, expression returned directly |
| Loop (PTC-Lisp) | Tools or `max_turns > 1` | Multiple turns until `(return ...)` or `(fail ...)` |
| Loop (Text) | `output: :text` with tools | LLM calls tools via native API, returns final text or JSON |

In **single-shot mode**, the LLM's expression is evaluated and returned directly. In **PTC-Lisp loop mode**, the agent must explicitly call `return` or `fail` to complete. In **text mode with tools**, the loop ends when the LLM returns content without tool calls.

> **Common Pitfall:** If your agent produces correct results but keeps looping until
> `max_turns_exceeded`, it's likely in loop mode without calling `return`. Either set
> `max_turns: 1` for single-shot execution, or ensure your prompt guides the LLM to
> use `(return {:value ...})` when done.

## Validation Retries with retry_turns

By default, if return value validation fails, the agent stops with an error. To enable automatic recovery, use the `retry_turns` option to give agents a limited budget for retrying after validation failures:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "Extract and return user data",
  signature: "{name :string, age :int}",
  retry_turns: 3,  # Budget for 3 retry attempts if validation fails
  llm: my_llm
)
```

When validation fails and retries are available:
1. The agent enters **retry mode** with the original error message and guidance
2. The LLM sees feedback like "Retry 1 of 3" to understand how many attempts remain
3. The agent must call `(return new_value)` to complete
4. If validation passes, the loop continues normally
5. If retries are exhausted, the agent returns an error

The `retry_turns` option uses a **unified budget model** alongside `max_turns`:
- **Work turns** (`max_turns`): Used for normal execution with tools available
- **Retry turns** (`retry_turns`): Used only after validation failures, with no tools

This separation lets agents safely explore solutions during work turns, then recover from validation errors during retry turns without consuming the main work budget.

> **Note:** Single-shot agents with `retry_turns > 0` accumulate the failed attempts in raw form — the retry budget is small by design, so context inflation isn't a concern. For multi-turn agents that need long-running history management, see [Context Compaction](subagent-compaction.md).

## Debugging Execution

To see what the agent is doing, use `PtcRunner.SubAgent.Debug.print_trace/2`:

```elixir
{:ok, step} = SubAgent.run(prompt, llm: my_llm)
PtcRunner.SubAgent.Debug.print_trace(step)
```

For more detail, include raw LLM output (reasoning) or the actual messages sent:

```elixir
# Include LLM reasoning/commentary
PtcRunner.SubAgent.Debug.print_trace(step, raw: true)

# Show full messages sent to LLM
PtcRunner.SubAgent.Debug.print_trace(step, messages: true)
```

This is essential for identifying why a model might be failing or ignoring tool instructions.

> **More options:** See [Observability](subagent-observability.md) for compaction, telemetry, and production tips.

## Signatures (Optional)

Signatures define a contract for inputs and outputs:

```elixir
# Output only
signature: "{name :string, price :float}"

# With inputs (for reusable agents)
signature: "(query :string) -> [{id :int, title :string}]"
```

When provided, signatures:
- Validate return data (agent retries on mismatch)
- Document expected shape to the LLM
- Give your Elixir code predictable types

See [Signature Syntax](../signature-syntax.md) for full syntax.

## Providing an LLM

Add `{:req_llm, "~> 1.8"}` to your deps for the built-in adapter:

```elixir
# Pass model alias directly - simplest approach
{:ok, step} = PtcRunner.SubAgent.run("What is 2 + 2?", llm: "haiku")

# Or with explicit provider
{:ok, step} = PtcRunner.SubAgent.run("What is 2 + 2?", llm: "bedrock:haiku")

# Or full model ID
{:ok, step} = PtcRunner.SubAgent.run("What is 2 + 2?", llm: "openrouter:anthropic/claude-haiku-4.5")
```

You can also create a callback explicitly (using a full `provider:model` string)
or supply a custom function:

```elixir
# Create callback with options (requires provider:model, not a bare alias)
llm = PtcRunner.LLM.callback("openrouter:anthropic/claude-haiku-4.5", cache: true)
{:ok, step} = PtcRunner.SubAgent.run(agent, llm: llm)

# Or supply any callback function directly
llm = fn %{system: system, messages: messages} ->
  # Call your LLM provider here
  {:ok, response_text}
end
```

See [LLM Setup](subagent-llm-setup.md) for provider configuration, model aliases,
custom registries, streaming, and framework integration.

## Defining Tools

Tools are functions the SubAgent can call. Provide them as a map:

```elixir
tools = %{
  "list_products" => &MyApp.Products.list/0,
  "get_product" => &MyApp.Products.get/1,
  "search" => fn %{query: q, limit: l} -> MyApp.search(q, l) end
}
```

### Auto-Extraction from @spec and @doc

Tool signatures and descriptions are auto-extracted when available:

```elixir
# In your module
@doc "Search for items matching the query string"
@spec search(String.t(), integer()) :: [map()]
def search(query, limit), do: ...

# Auto-extracted:
#   signature: "(query :string, limit :int) -> [:map]"
#   description: "Search for items matching the query string"
tools = %{"search" => &MyApp.search/2}
```

### Explicit Signatures

For functions without specs, provide a signature explicitly:

```elixir
tools = %{
  "search" => {&MyApp.search/2, "(query :string, limit :int) -> [{id :int}]"}
}
```

For production tools, add descriptions and explicit signatures using keyword list format:

```elixir
tools = %{
  "search" => {&MyApp.search/2,
    signature: "(query :string, limit :int?) -> [{id :int, title :string}]",
    description: "Search for items matching query. Returns up to limit results (default 10)."
  }
}
```

### Result Caching

For tools with stable, pure outputs (same inputs always produce the same result),
enable `cache: true` to avoid redundant calls across turns:

```elixir
tools = %{
  "get-config" => {&MyApp.get_config/1,
    signature: "(key :string) -> :any",
    cache: true
  }
}
```

Cached results persist across turns within a single `SubAgent.run/2` call. Only
successful results are cached — errors are never stored. Do not use on tools that
read mutable state modifiable by other tools in the session.

See `PtcRunner.Tool` for all supported tool formats.

## Builtin LLM Queries

Enable `llm_query: true` to let the agent make ad-hoc LLM calls from PTC-Lisp without defining separate tools:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "Classify each item by urgency",
  signature: "(items [:map]) -> {urgent [:map]}",
  llm_query: true,
  llm: my_llm,
  context: %{items: items}
)
```

The agent can call `tool/llm-query` with a prompt and optional signature for classification, judgment, or extraction tasks. See [Composition Patterns](subagent-patterns.md#builtin-ad-hoc-llm-queries-llm_query) for details.

## Builtin Tools

Use `builtin_tools` to enable utility tool families without defining them yourself:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "Find lines mentioning 'error' in the log",
  builtin_tools: [:grep],
  llm: my_llm,
  context: %{log: log_text}
)
```

The `:grep` family adds `tool/grep` and `tool/grep-n` (line-numbered variant). Multiple families can be combined: `builtin_tools: [:grep]`. User-defined tools with the same name take precedence.

**Text mode note:** In text mode (`output: :text`), tool names with hyphens are automatically sanitized to underscores for the LLM provider API (e.g., `grep-n` becomes `grep_n`). The mapping is handled transparently.

## Agent as Data

For reusable agents, create the struct separately:

```elixir
# Define once
product_finder = PtcRunner.SubAgent.new(
  prompt: "Find the most expensive product",
  signature: "{name :string, price :float}",
  tools: product_tools,
  max_turns: 5
)

# Execute with runtime params
{:ok, step} = PtcRunner.SubAgent.run(product_finder, llm: my_llm)
```

This separation enables testing, composition, and reuse.

SubAgents also support fields for documentation (`description`, `field_descriptions`, `context_descriptions`), output formatting (`format_options`, `float_precision`), and memory limits (`memory_limit`, `memory_strategy`). See `PtcRunner.SubAgent.new/1` for all options.

## State Persistence

Use `def` to store values that persist across turns within a single `run`:

```clojure
(def cache result)   ; store
cache                ; access as plain symbol
```

Use `defn` to define reusable functions:

```clojure
(defn expensive? [item] (> (:price item) 1000))
(filter expensive? data/items)
```

State is scoped per-agent and hidden from prompts. See [Core Concepts](subagent-concepts.md) for details.

## Multi-Turn Chat

For chat applications where conversation history persists across calls, use `chat/3`:

```elixir
agent = PtcRunner.SubAgent.new(
  prompt: "placeholder",
  output: :text,
  system_prompt: "You are a helpful assistant."
)

# First turn
{:ok, reply, messages, _memory} = PtcRunner.SubAgent.chat(agent, "Hello!", llm: my_llm)

# Second turn — pass messages back to continue the conversation
{:ok, reply2, messages2, _memory} = PtcRunner.SubAgent.chat(
  agent, "Tell me more",
  llm: my_llm, messages: messages
)
```

`chat/3` auto-detects mode based on `agent.output`:

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

The system prompt is managed by the agent struct — you don't need to include it in the messages list.

### PTC-Lisp Mode Chat

For chatbots that need tools, memory, or computed results:

```elixir
agent = PtcRunner.SubAgent.new(
  prompt: "placeholder",
  output: :ptc_lisp,
  system_prompt: "You are a helpful assistant.",
  tools: my_tools
)

# First turn — LLM can use tools and define variables
{:ok, result, messages, memory} = PtcRunner.SubAgent.chat(agent, "Look up X", llm: my_llm)

# Second turn — thread both messages and memory
{:ok, result2, messages2, memory2} = PtcRunner.SubAgent.chat(
  agent, "Now use that result",
  llm: my_llm, messages: messages, memory: memory
)
```

The `:memory` option seeds the PTC-Lisp environment with variables from a prior call, so the LLM can reference them without re-computing.

### Streaming

Streaming works via `on_chunk`:

```elixir
{:ok, reply, messages, _memory} = PtcRunner.SubAgent.chat(agent, "Hello!",
  llm: my_llm,
  on_chunk: fn %{delta: text} -> IO.write(text) end
)
```

See [Phoenix Streaming](phoenix-streaming.md) for a full LiveView integration recipe.

## See Also

- [LLM Setup](subagent-llm-setup.md) - Providers, streaming, custom adapters, framework integration
- [Text Mode Guide](subagent-text-mode.md) - Text mode, Mustache templates, tool calling, and structured output
- [PTC-Lisp Transport](subagent-ptc-transport.md) - `ptc_transport: :content` (default) vs `:tool_call` (opt-in)
- [Text Mode + PTC-Lisp Compute](text-mode-ptc-compute.md) - Combined mode (`output: :text, ptc_transport: :tool_call`) for chat agents that escalate to deterministic compute
- [Output Modes in an App Loop](../../livebooks/output_modes_in_app_loops.livemd) - Runnable livebook showing how to pick `:text` plain, `:text` structured, or `:ptc_lisp` per user message
- [Phoenix Streaming](phoenix-streaming.md) - Real-time streaming in LiveView
- [Core Concepts](subagent-concepts.md) - Context and memory
- [Observability](subagent-observability.md) - Telemetry, debug mode, and tracing
- [Patterns](subagent-patterns.md) - Chaining, orchestration, and composition
- [Signature Syntax](../signature-syntax.md) - Full signature syntax reference
- [Advanced Topics](subagent-advanced.md) - ReAct patterns and the compile pattern
- `PtcRunner.SubAgent` - API reference
