# 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