# Core Concepts

This guide covers the foundational concepts for library users: context management, memory, and how agents complete their work.

## How SubAgents Work

When you call `SubAgent.run/2`, the library:

1. Sends your prompt and context to the LLM
2. The LLM generates a PTC-Lisp program (a Clojure subset)
3. The program executes in a sandboxed environment
4. Results are validated against your signature
5. On success, `{:ok, step}` returns with `step.return` containing the result

You don't write PTC-Lisp - the LLM does. You configure the agent with Elixir.

**Alternative: Text Mode.** For classification, extraction, or tool-based tasks without PTC-Lisp, use `output: :text`. The behavior auto-detects based on whether tools are provided and the return type. See [Text Mode Guide](subagent-text-mode.md).

## SubAgent Context Boundaries

SubAgents solve a fundamental problem: LLMs need information to make decisions, but context windows are expensive and limited. SubAgents let agents work with large datasets through tools and return compact, validated summaries to the parent.

```
┌─────────────┐                      ┌─────────────┐
│ Main Agent  │ ── "Find urgent  ──► │  SubAgent   │
│ (strategic) │     emails"          │ (isolated)  │
│             │                      │             │
│  Context:   │      CONTRACT:       │  Has tools: │
│  ~100 tokens│   {summary, ids}     │  - list     │
│             │                      │  - search   │
│             │ ◄── validated ─────  │             │
│             │     data only        │  Processes  │
│             │                      │  50KB data  │
└─────────────┘                      └─────────────┘
```

The parent only sees what the signature exposes. Heavy data stays inside the SubAgent.

## Chaining Return Data

```elixir
# Step 1: Find emails
{:ok, step1} = PtcRunner.SubAgent.run(
  "Find all urgent emails",
  signature: "{summary :string, count :int, email_ids [:int]}",
  tools: email_tools,
  llm: llm
)

step1.return.summary     #=> "Found 3 urgent emails"
step1.return.count       #=> 3
step1.return.email_ids   #=> [101, 102, 103]

# Step 2: Chain to next agent
{:ok, step2} = PtcRunner.SubAgent.run(
  "Draft replies for these {{count}} urgent emails",
  context: step1,  # Auto-chains return data
  tools: drafting_tools,
  llm: llm
)
```

In Step 2, chained return data is ordinary context. Do not put secrets or sensitive identifiers into SubAgent return data unless it is acceptable for generated programs and prompt/context renderers to see them.

## Context

Values passed to `context:` become available to the LLM's generated programs:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "Get details for order {{order_id}}",
  context: %{order_id: "ORD-123", customer_tier: "gold"},
  tools: order_tools,
  llm: llm
)
```

### Template Expansion

The `{{placeholder}}` syntax in prompts expands from context:

```elixir
prompt: "Find emails for {{user.name}} about {{topic}}"
context: %{user: %{name: "Alice"}, topic: "billing"}
# Expands to: "Find emails for Alice about billing"
```

### Temporal values

Pass `DateTime`, `NaiveDateTime`, `Date`, and `Time` values directly. PtcRunner
normalizes them to ISO 8601 strings at every LLM-facing boundary — Mustache
template substitution, data inventory rendering, tool result encoding, `:string`
coercion, and PTC-Lisp `(str ...)`. The LLM never sees Elixir's `~U[...]`
sigil form.

```elixir
prompt: "Event happened at {{when}}"
context: %{when: ~U[2026-05-03 09:14:00Z]}
# Expands to: "Event happened at 2026-05-03T09:14:00Z"
```

In `:ptc_lisp` mode, generated programs can pass tool-returned temporal values
straight to date primitives:

```clojure
;; tool/get_ticket returns a map with :opened_at (a %DateTime{} on the Elixir side)
(.getTime (java.util.Date. (:opened_at (tool/get_ticket {:id 123}))))
```

### Chaining Context

When passing a previous `Step` to `context:`, the return data is automatically extracted:

```elixir
# These are equivalent:
run(prompt, context: step1.return)
run(prompt, context: step1)  # Auto-extraction
```

## How Agents Complete

Agents complete their work in one of two ways:

### Single-turn (Expression Result)

For simple tasks with `max_turns: 1`, the LLM's expression result is returned directly:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "Classify this text: {{text}}",
  signature: "{category :string, confidence :float}",
  context: %{text: "..."},
  max_turns: 1,
  llm: llm
)

step.return  #=> %{category: "positive", confidence: 0.95}
```

### Multi-turn (Explicit Return)

For agentic tasks with tools, the LLM must explicitly signal completion. It does this by calling `return` or `fail` in its generated program:

```elixir
{:ok, step} = PtcRunner.SubAgent.run(
  "Find the report with highest anomaly score",
  signature: "{report_id :int, reasoning :string}",
  tools: report_tools,
  max_turns: 5,
  llm: llm
)
```

The agent loops until the LLM's program calls `return` with valid data, or `fail` to abort.

## Error Handling

SubAgents handle errors at three levels:

### 1. Turn Errors (Recoverable)

Syntax errors, tool failures, and validation errors are fed back to the LLM. It sees the error and can adapt in the next turn.

### 2. Mission Failures (Explicit)

When the LLM determines it cannot complete the task, it calls `fail`. Your code receives:

```elixir
{:error, step} = SubAgent.run(...)
step.fail  #=> %{reason: :not_found, message: "User does not exist"}
```

### 3. System Crashes

Programming bugs in your tool functions follow "let it crash" - they're returned as internal errors for investigation.

## Multi-turn State

In multi-turn agents, the LLM can store values that persist across turns. This happens automatically - values defined in one turn are available in subsequent turns.

From your perspective as a library user:
- **You see** the final result in `step.return`
- **You see** execution history in `step.turns`
- **You don't need** to manage intermediate state

The LLM handles state internally to cache tool results, track progress, and avoid redundant work.

For progress visibility, use the `plan:` option to define step labels. A progress checklist is rendered between turns. Optionally, the LLM can mark steps complete with `(step-done "id" "summary")`. The rendering can be customized via `progress_fn:`. See [Navigator Pattern](subagent-navigator.md#semantic-progress-with-plans).

## Defaults

| Option | Default | Description |
|--------|---------|-------------|
| `max_turns` | `5` | Maximum LLM turns before timeout |
| `timeout` | `5000` | Per-turn sandbox timeout (ms) |
| `max_heap` | `nil` | Per-turn sandbox heap limit (words, nil = app config or ~10MB) |
| `mission_timeout` | `nil` | Total mission timeout (ms, nil = no limit) |
| `memory_limit` | `1_048_576` | Max bytes for memory map (1MB) |
| `memory_strategy` | `:strict` | `:strict` (fatal) or `:rollback` (recover) on memory limit exceeded |
| `float_precision` | `2` | Decimal places for floats in results |
| `compaction` | `false` | Enable pressure-triggered context compaction (multi-turn only) |
| `pmap_timeout` | `5000` | Timeout (ms) for parallel `pmap` operations |
| `max_depth` | `3` | Maximum recursion depth for nested agents |
| `turn_budget` | `20` | Total turn budget across retries |
| `retry_turns` | `0` | Retry budget after return validation failures |
| `output` | `:ptc_lisp` | Output mode (`:ptc_lisp` or `:text`) |

## See Also

- [Getting Started](subagent-getting-started.md) - Build your first SubAgent
- [Text Mode Guide](subagent-text-mode.md) - Text mode for structured output and native tool calling
- [Observability](subagent-observability.md) - Debug mode, compaction, and tracing
- [Patterns](subagent-patterns.md) - Chaining, orchestration, and composition
- [Signature Syntax](../signature-syntax.md) - Full signature syntax reference
- [Advanced Topics](subagent-advanced.md) - Prompt structure and internals
- `PtcRunner.SubAgent` - API reference
