# SubAgent Examples

```elixir
repo_root = Path.expand("..", __DIR__)

deps =
  if File.exists?(Path.join(repo_root, "mix.exs")) do
    [{:ptc_runner, path: repo_root}]
  else
    [{:ptc_runner, "~> 0.11.0"}]
  end

Mix.install(deps ++ [{:req_llm, "~> 1.8"}, {:kino, "~> 0.14"}], consolidate_protocols: false)
```

## Setup

```elixir
# For testing locally and reloading the library
# IEx.Helpers.recompile()

# Load LLM setup: local file if available, otherwise fetch from GitHub
local_path = Path.join(__DIR__, "llm_setup.exs")

if File.exists?(local_path) do
  Code.require_file(local_path)
else
  %{body: code} = Req.get!("https://raw.githubusercontent.com/andreasronge/ptc_runner/main/livebooks/llm_setup.exs")
  Code.eval_string(code)
end

setup = LLMSetup.setup()
```

```elixir
setup = LLMSetup.choose_provider(setup)
```

```elixir
my_llm = LLMSetup.choose_model(setup)
```

## Output Modes

SubAgents support two output modes:

| Mode                  | Use When                                              | Output                  |
| --------------------- | ----------------------------------------------------- | ----------------------- |
| `:text`               | Classification, extraction, summarization             | Structured JSON         |
| `:ptc_lisp` (default) | Computation, tool orchestration, multi-step reasoning | PTC-Lisp program result |

### Text Mode - Direct LLM Tasks

Use `output: :text` when the LLM can answer directly without computation:

```elixir
alias PtcRunner.SubAgent
alias PtcRunner.SubAgent.Debug

review = "Great product, fast shipping! Would buy again."

{_, step} = SubAgent.run(
  "Classify as positive/negative/neutral with confidence 0.0-1.0: {{review}}",
  output: :text,
  signature: "(review :string) -> {sentiment :string, confidence :float}",
  context: %{review: review},
  llm: my_llm
)

# Debug.print_trace(step, raw: true)
step.return
```

### PTC-Lisp Mode - Computational Tasks

The default mode. The LLM writes a program to solve tasks that need accurate computation:

```elixir
{_, step} = SubAgent.run(
  "How many r's are in raspberry?",
  llm: my_llm,
  max_turns: 1
)

Debug.print_trace(step)
step.return
```

### Execution Modes

| `max_turns`         | Mode        | Behavior                              |
| ------------------- | ----------- | ------------------------------------- |
| `1`                 | Single-shot | One LLM call, answer immediately      |
| `> 1` (default: 10) | Multi-turn  | Can iterate, fix errors, explore data |

**Single-shot** is faster and cheaper - use when the task is straightforward.

**Multi-turn** allows the LLM to inspect results with `println`, retry on errors, and call `return` when confident.

## Signatures

Signatures define input/output types. They work with both output modes.

**Format:** `(input1 :type, input2 :type) -> output_type`

| Type                                 | Examples                 |
| ------------------------------------ | ------------------------ |
| `:string`, `:int`, `:float`, `:bool` | Primitives               |
| `{field :type, ...}`                 | Object with named fields |
| `[element_type]`                     | List of elements         |
| `{:optional, :type}`                 | Optional field           |

```elixir
# Input: two strings, Output: object with score and explanation
sig1 = "(text1 :string, text2 :string) -> {similarity :float, explanation :string}"

# Input: list of items, Output: object with categorized lists
sig2 = "(items [{name :string, price :float}]) -> {expensive [{name :string}], cheap [{name :string}]}"

# Output only (no inputs from context)
sig3 = "{count :int, items [:string]}"

:ok
```

## Compiled SubAgents

Compile an agent once to derive reusable PTC-Lisp logic. Runs without further LLM calls:

```elixir
agent = SubAgent.new(
  prompt: "Count r's in {{word}}",
  signature: "(word :string) -> :int",
  max_turns: 1
)

{:ok, compiled} = SubAgent.compile(agent, llm: my_llm)

IO.puts("Compiled source:\n#{compiled.source}")
```

```elixir
# Execute on multiple inputs - no LLM calls
words = ["strawberry", "raspberry", "program", "error"]

for word <- words do
  step = compiled.execute.(%{"word" => word}, [])
  "#{word}: #{step.return}"
end
```

## Working with Tools

Tools let agents fetch external data or perform actions:

```elixir
expenses = [
  %{"id" => 1, "category" => "travel", "amount" => 450.00, "vendor" => "Airlines Inc"},
  %{"id" => 2, "category" => "food", "amount" => 32.50, "vendor" => "Cafe Luna"},
  %{"id" => 3, "category" => "travel", "amount" => 189.00, "vendor" => "Hotel Central"},
  %{"id" => 4, "category" => "office", "amount" => 299.99, "vendor" => "Tech Store"},
  %{"id" => 5, "category" => "food", "amount" => 28.00, "vendor" => "Deli Express"}
]

tools = %{
  "list-expenses" => {fn _ -> expenses end,
    signature: "() -> [{id :int, category :string, amount :float, vendor :string}]",
    description: "Returns all expense records"
  }
}

Kino.DataTable.new(expenses)
```

```elixir
{:ok, step} = SubAgent.run(
  "What is the total travel expense?",
  tools: tools,
  signature: "{total :float}",
  llm: my_llm
)

Debug.print_trace(step, raw: true)
step.return
```

## Interactive Query

```elixir
question_input = Kino.Input.textarea("Question", default: "Show spending by category")
```

```elixir
question = Kino.Input.read(question_input)

case SubAgent.run(question, tools: tools, llm: my_llm) do
  {:ok, step} ->
    # Debug.print_trace(step)
    step.return

  {:error, step} ->
    Debug.print_trace(step)
    "Failed: #{step.fail["message"]}"
end
```

## Ad-Hoc LLM Queries (`llm_query`)

Enable `llm_query: true` to let the agent make runtime LLM calls from PTC-Lisp — useful when the task requires **LLM judgment combined with programmatic logic**.

```elixir
tickets = [
  %{"id" => 1, "text" => "Server is completely down, all customers affected"},
  %{"id" => 2, "text" => "Typo in the footer of the about page"},
  %{"id" => 3, "text" => "Payment processing failing for all users"},
  %{"id" => 4, "text" => "Would be nice to have dark mode"}
]

alias PtcRunner.TraceLog

{:ok, result, trace_path} = TraceLog.with_trace(fn ->
  SubAgent.run(
    """
    Classify each ticket's urgency using tool/llm-query, then return only the critical ones.
    """,
    signature: "(tickets [{id :int, text :string}]) -> {critical [{id :int, text :string, reason :string}]}",
    llm_query: true,
    context: %{tickets: tickets},
    llm: my_llm
  )
end)

# Show execution trace
case result do
  {:ok, step} ->
    Debug.print_trace(step, raw: true)
    step.return

  {:error, step} ->
    Debug.print_trace(step, raw: true)
    "Failed: #{step.fail["message"]}"
end
```

**Example generated program** (for those without LLM access):

```
(def tickets data/tickets)

(def classified
  (pmap (fn [ticket]
          (assoc ticket
                 :classification
                 (tool/llm-query {:prompt "Is this support ticket critical/urgent? Respond with {is_critical :bool, reason :string}\n\nTicket: {{text}}"
                                  :signature "{is_critical :bool, reason :string}"
                                  :text (:text ticket)})))
        tickets))

(def critical
  (filter (fn [t] (get-in t [:classification :is_critical])) classified))

(return {:critical (map (fn [t] {:id (:id t) :text (:text t) :reason (get-in t [:classification :reason])}) critical)})
```

This uses `pmap` to classify all tickets in parallel via `tool/llm-query`, then filters for critical ones.

### Analyzing the Trace

`TraceLog` captures all events (LLM calls, turns) to a JSONL file. Use `TraceLog.Analyzer` to inspect timing and token usage:

```elixir
events = TraceLog.Analyzer.load(trace_path)
summary = TraceLog.Analyzer.summary(events)

IO.puts("Duration: #{summary.duration_ms}ms")
IO.puts("LLM calls: #{summary.llm_calls}")
IO.puts("Turns: #{summary.turns}")
IO.puts("Tokens: #{summary.total_tokens}")

# Print timeline of all events
TraceLog.Analyzer.print_timeline(events)
```

> **Performance tip:** Replace `mapv` with `pmap` to classify all tickets in parallel.
> TraceLog supports concurrent traces — each `tool/llm-query` call is captured with
> its own trace ID, so parallel execution is fully observable.

## Debug Options

```elixir
# Preview the prompt before running
agent = SubAgent.new(prompt: "What is 2 + 2?")
SubAgent.preview_prompt(agent).system |> IO.puts()
```

**print_trace options:**

| Option              | Description                               |
| ------------------- | ----------------------------------------- |
| `raw: true`         | Show raw LLM input/output                 |
| `messages: true`    | Show all messages including system prompt |
| `usage: true`       | Show token usage                          |

## Learn More

* [Playground](ptc_runner_playground.livemd) - PTC-Lisp basics
* [SubAgent Guide](https://hexdocs.pm/ptc_runner/subagent-getting-started.html)
* [PTC-Lisp Spec](https://hexdocs.pm/ptc_runner/ptc-lisp-specification.html)
