# `PtcRunner.Step`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/step.ex#L1)

Result of executing a PTC program or SubAgent mission.

Returned by both `PtcRunner.Lisp.run/2` and `PtcRunner.SubAgent.run/2`.

## Fields

### `return`

The computed result value on success.

- **Type:** `term() | nil`
- **Set when:** Mission/program completed successfully
- **Nil when:** Execution failed (check `fail` field)

### `fail`

Error information on failure. See `t:fail/0` for the structure.

- **Type:** `t:fail/0 | nil`
- **Set when:** Execution failed
- **Nil when:** Execution succeeded

### `memory`

Final memory state after execution.

- **Type:** `map()`
- **Always set:** Contains accumulated memory from all operations
- **Access in PTC-Lisp:** values available as plain symbols

### `signature`

The contract used for validation.

- **Type:** `String.t() | nil`
- **Set when:** Signature was provided to `run/2`
- **Used for:** Type propagation when chaining steps

### `usage`

Execution metrics. See `t:usage/0` for available fields.

- **Type:** `t:usage/0 | nil`
- **Set when:** Execution completed (success or failure after running)
- **Nil when:** Early validation failure (before execution)

### `turns`

List of Turn structs capturing each LLM interaction cycle. See `PtcRunner.Turn`.

- **Type:** `[PtcRunner.Turn.t()] | nil`
- **Set when:** SubAgent execution
- **Nil when:** Lisp execution

### `trace_id`

Unique identifier for this execution (for tracing correlation).

- **Type:** `String.t() | nil`
- **Set when:** SubAgent execution (32-character hex string)
- **Nil when:** Lisp execution
- **Used for:** Correlating traces in parallel and nested agent executions

### `parent_trace_id`

ID of parent trace for nested agent calls.

- **Type:** `String.t() | nil`
- **Set when:** This agent was spawned by another agent
- **Nil when:** Root-level execution (no parent)
- **Used for:** Linking child executions to their parent

See `PtcRunner.Tracer` for trace generation and management.

### `prelude_trace`

Trace/debug summary of the attached Capability Prelude (V1, plan §12).

- **Type:** `PtcRunner.Lisp.Prelude.trace_summary() | nil`
- **Set when:** A prelude was attached via `:prelude` (or `runtime_prelude`)
- **Nil when:** No prelude was attached
- **Contains:** prelude source hash, compiled-artifact hash, the selected
  protected namespaces, a public export-record summary, and a host-policy
  hash slot. Credential-free and JSON-serializable — never carries captured
  closures, the private prelude env, or host secrets.

### `field_descriptions`

Descriptions for signature fields, propagated from SubAgent.

- **Type:** `map() | nil`
- **Set when:** SubAgent had `field_descriptions` option
- **Nil when:** No field descriptions provided
- **Used for:** Passing field documentation through chained executions

### `messages`

Full conversation history in OpenAI format.

- **Type:** `[t:message/0] | nil`
- **Set when:** `collect_messages: true` option passed to `SubAgent.run/2`
- **Nil when:** `collect_messages: false` (default)
- **Used for:** Debugging, persistence, and displaying the LLM conversation

### `summaries`

Accumulated `step-done` summaries from semantic progress reporting.

- **Type:** `%{String.t() => String.t()}`
- **Default:** `%{}`
- **Set when:** LLM calls `(step-done "id" "summary")` during execution
- **Used for:** Progress checklist rendering when agent has a `plan`

## Error Reasons

Common system error reasons in `step.fail.reason`:

| Reason | Source | Description |
|--------|--------|-------------|
| `:parse_error` | Lisp | Invalid PTC-Lisp syntax |
| `:analysis_error` | Lisp | Semantic error (undefined variable, etc.) |
| `:eval_error` | Lisp | Runtime error (division by zero, etc.) |
| `:timeout` | Both | Execution exceeded time limit |
| `:memory_exceeded` | Both | Process exceeded heap limit |
| `:validation_error` | Both | Input or output doesn't match signature |
| `:tool_error` | SubAgent | Tool raised an exception |
| `:tool_not_found` | SubAgent | Called non-existent tool |
| `:reserved_tool_name` | SubAgent | Attempted to register `return` or `fail` |
| `:max_turns_exceeded` | SubAgent | Turn limit reached without termination |
| `:max_depth_exceeded` | SubAgent | Nested agent depth limit exceeded |
| `:turn_budget_exhausted` | SubAgent | Total turn budget exhausted |
| `:mission_timeout` | SubAgent | Total mission duration exceeded |
| `:llm_error` | SubAgent | LLM callback failed after retries |
| `:llm_required` | SubAgent | LLM option is required for agent execution |
| `:no_code_found` | SubAgent | No PTC-Lisp code found in LLM response |
| `:llm_not_found` | SubAgent | LLM atom not in registry |
| `:llm_registry_required` | SubAgent | Atom LLM used without registry |
| `:invalid_llm` | SubAgent | Registry value not a function |
| `:chained_failure` | SubAgent | Chained onto a failed step |
| `:template_error` | SubAgent | Template placeholder missing |
| Custom atoms/strings | SubAgent | Caller-defined structured fail reasons; novel keyword reasons externalize as strings |

## Usage Patterns

### Success Check

    case SubAgent.run(prompt, opts) do
      {:ok, step} ->
        IO.puts("Result: #{inspect(step.return)}")
        IO.puts("Took #{step.usage.duration_ms}ms")

      {:error, step} ->
        IO.puts("Failed: #{step.fail.reason} - #{step.fail.message}")
    end

### Chaining Steps

Pass a successful step's return and signature to the next step:

    {:ok, step1} = SubAgent.run("Find emails",
      signature: "() -> {count :int, ids [:int]}",
      llm: llm
    )

    # Option 1: Explicit
    {:ok, step2} = SubAgent.run("Process emails",
      context: step1.return,
      context_signature: step1.signature,
      llm: llm
    )

    # Option 2: Auto-extraction (SubAgent only)
    {:ok, step2} = SubAgent.run("Process emails",
      context: step1,  # Extracts return and signature automatically
      llm: llm
    )

### Accessing Return Data

    {:ok, step} = SubAgent.run("Find emails",
      signature: "() -> {count :int, email_ids [:int]}",
      llm: llm
    )

    step.return.count     #=> 5
    step.return.email_ids #=> [101, 102, 103, 104, 105]

# `catalog_op`

```elixir
@type catalog_op() :: %{
  operation: atom(),
  args: map(),
  outcome: :ok | :nil_world_fault | :error,
  reason: atom() | nil,
  duration_ms: non_neg_integer()
}
```

PTC-Lisp discovery invocation record (aggregator mode).

Captured for REPL discovery forms such as `tool/servers`, `apropos`,
`dir`, `doc`, and `meta` dispatched through the configured discovery
executor.

Fields:
- `operation`: The discovery operation (`:servers`, `:apropos`,
  `:dir`, `:doc`, or `:meta`)
- `args`: Normalized argument map (shape depends on operation)
- `outcome`: `:ok` on success, `:nil_world_fault` when a world fault
  was swallowed to `nil`, `:error` on programmer faults that raised
- `reason`: World-fault reason atom when `outcome == :nil_world_fault`
- `duration_ms`: How long the catalog dispatch took

# `fail`

```elixir
@type fail() :: %{
  :reason =&gt; atom() | String.t(),
  :message =&gt; String.t(),
  optional(:op) =&gt; String.t(),
  optional(:details) =&gt; map()
}
```

Error information on failure.

Fields:
- `reason`: Machine-readable error code. System failures use atoms;
  structured `(fail {:reason ...})` can carry a caller-defined atom or string.
  Core SubAgent reasons include `:partial_side_effects` when a continuation
  guard stops after an observed write/unknown upstream call.
- `message`: Human-readable description
- `op`: Optional operation/tool that failed
- `details`: Optional additional context

# `message`

```elixir
@type message() :: %{role: :system | :user | :assistant, content: String.t()}
```

A single message in OpenAI format.

Fields:
- `role`: The message role (:system, :user, or :assistant)
- `content`: The message content

# `pmap_call`

```elixir
@type pmap_call() :: %{
  type: :pmap | :pcalls,
  count: non_neg_integer(),
  child_trace_ids: [String.t()],
  child_steps: [any()],
  timestamp: DateTime.t(),
  duration_ms: non_neg_integer(),
  success_count: non_neg_integer(),
  error_count: non_neg_integer()
}
```

Parallel map/calls execution record for tracing.

Fields:
- `type`: `:pmap` or `:pcalls`
- `count`: Number of parallel tasks
- `child_trace_ids`: List of trace IDs from SubAgentTool executions
- `timestamp`: When execution started
- `duration_ms`: Total execution time
- `success_count`: Number of successful executions
- `error_count`: Number of failed executions

# `t`

```elixir
@type t() :: %PtcRunner.Step{
  catalog_ops: [catalog_op()],
  child_steps: [t()],
  child_traces: [String.t()],
  fail: fail() | nil,
  field_descriptions: map() | nil,
  journal: map() | nil,
  memory: map(),
  messages: [message()] | nil,
  name: term(),
  original_prompt: term(),
  parent_trace_id: String.t() | nil,
  pmap_calls: [pmap_call()],
  prelude_trace: PtcRunner.Lisp.Prelude.trace_summary() | nil,
  prints: [String.t()],
  prompt: String.t() | nil,
  return: term() | nil,
  signature: String.t() | nil,
  summaries: %{required(String.t()) =&gt; String.t()},
  tool_cache: map(),
  tool_calls: [tool_call()],
  tools: map() | nil,
  trace_id: String.t() | nil,
  turns: [PtcRunner.Turn.t()] | nil,
  usage: usage() | nil
}
```

# `tool_call`

```elixir
@type tool_call() :: %{
  name: String.t(),
  args: map(),
  result: term(),
  error: String.t() | nil,
  timestamp: DateTime.t(),
  duration_ms: non_neg_integer()
}
```

Tool call information in trace.

Fields:
- `name`: Tool name
- `args`: Arguments passed to tool
- `result`: Tool result
- `error`: Error message if tool failed
- `timestamp`: When tool was called
- `duration_ms`: How long tool took

# `usage`

```elixir
@type usage() :: %{
  :duration_ms =&gt; non_neg_integer(),
  :memory_bytes =&gt; non_neg_integer(),
  optional(:eval_reductions) =&gt; non_neg_integer(),
  optional(:turns) =&gt; pos_integer(),
  optional(:input_tokens) =&gt; non_neg_integer(),
  optional(:output_tokens) =&gt; non_neg_integer(),
  optional(:total_tokens) =&gt; non_neg_integer(),
  optional(:llm_requests) =&gt; non_neg_integer(),
  optional(:schema_used) =&gt; boolean(),
  optional(:schema_bytes) =&gt; non_neg_integer()
}
```

Execution metrics.

Fields:
 - `duration_ms`: Total execution time
 - `memory_bytes`: Peak memory usage
 - `eval_reductions`: BEAM reductions used by the sandbox child eval path
 - `turns`: Number of LLM turns used (SubAgent only, optional)
- `input_tokens`: Total input tokens (SubAgent only, optional)
- `output_tokens`: Total output tokens (SubAgent only, optional)
- `total_tokens`: Input + output tokens (SubAgent only, optional)
- `llm_requests`: Number of LLM API calls (SubAgent only, optional)
- `schema_used`: Whether JSON schema was sent to LLM (text mode only, optional)
- `schema_bytes`: Size of JSON schema in bytes (text mode only, optional)

# `error`

```elixir
@spec error(atom(), String.t(), map()) :: t()
```

Creates a new failed Step.

## Examples

    iex> step = PtcRunner.Step.error(:timeout, "Execution exceeded time limit", %{})
    iex> step.fail.reason
    :timeout
    iex> step.return
    nil

# `error`

```elixir
@spec error(atom(), String.t(), map(), map()) :: t()
```

Creates a failed Step with additional details.

## Examples

    iex> PtcRunner.Step.error(:validation_failed, "Invalid input", %{}, %{field: "name"})
    %PtcRunner.Step{
      return: nil,
      fail: %{reason: :validation_failed, message: "Invalid input", details: %{field: "name"}},
      memory: %{},
      signature: nil,
      usage: nil,
      turns: nil,
      trace_id: nil,
      parent_trace_id: nil,
      name: nil,
      field_descriptions: nil
    }

# `error`

```elixir
@spec error(atom(), String.t(), map(), map(), keyword()) :: t()
```

Creates a failed Step with additional details and options.

## Options

- `:journal` - journal to preserve on the error step

## Examples

    iex> step = PtcRunner.Step.error(:timeout, "timed out", %{}, %{}, journal: %{"a" => 1})
    iex> step.journal
    %{"a" => 1}

# `ok`

```elixir
@spec ok(term(), map()) :: t()
```

Creates a new successful Step.

## Examples

    iex> step = PtcRunner.Step.ok(%{count: 5}, %{})
    iex> step.return
    %{count: 5}
    iex> step.fail
    nil

---

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