Packages

Secure BEAM sandbox runtime for LLM code mode and MCP aggregation. Run concurrent LLM/tool clients safely while agents orchestrate approved tools, call upstream MCP/OpenAPI servers, and transform data.

Current section

Files

Jump to
ptc_runner lib ptc_runner step.ex
Raw

lib/ptc_runner/step.ex

defmodule PtcRunner.Step do
@moduledoc """
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.
### `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]
"""
defstruct [
:return,
:fail,
:memory,
:journal,
:signature,
:usage,
:turns,
:trace_id,
:parent_trace_id,
:name,
:field_descriptions,
:prints,
:tool_calls,
:pmap_calls,
:catalog_ops,
:child_traces,
:child_steps,
:messages,
:prompt,
:original_prompt,
:tools,
summaries: %{},
tool_cache: %{}
]
@typedoc """
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.
- `message`: Human-readable description
- `op`: Optional operation/tool that failed
- `details`: Optional additional context
"""
@type fail :: %{
required(:reason) => atom() | String.t(),
required(:message) => String.t(),
optional(:op) => String.t(),
optional(:details) => map()
}
@typedoc """
Execution metrics.
Fields:
- `duration_ms`: Total execution time
- `memory_bytes`: Peak memory usage
- `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)
"""
@type usage :: %{
required(:duration_ms) => non_neg_integer(),
required(:memory_bytes) => non_neg_integer(),
optional(:turns) => pos_integer(),
optional(:input_tokens) => non_neg_integer(),
optional(:output_tokens) => non_neg_integer(),
optional(:total_tokens) => non_neg_integer(),
optional(:llm_requests) => non_neg_integer(),
optional(:schema_used) => boolean(),
optional(:schema_bytes) => non_neg_integer()
}
@typedoc """
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
"""
@type tool_call :: %{
name: String.t(),
args: map(),
result: term(),
error: String.t() | nil,
timestamp: DateTime.t(),
duration_ms: non_neg_integer()
}
@typedoc """
A single message in OpenAI format.
Fields:
- `role`: The message role (:system, :user, or :assistant)
- `content`: The message content
"""
@type message :: %{
role: :system | :user | :assistant,
content: String.t()
}
@typedoc """
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
"""
@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()
}
@typedoc """
PTC-Lisp discovery invocation record (aggregator mode).
Captured for REPL discovery forms such as `mcp/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
"""
@type catalog_op :: %{
operation: atom(),
args: map(),
outcome: :ok | :nil_world_fault | :error,
reason: atom() | nil,
duration_ms: non_neg_integer()
}
@type t :: %__MODULE__{
return: term() | nil,
fail: fail() | nil,
memory: map(),
journal: map() | nil,
signature: String.t() | nil,
usage: usage() | nil,
turns: [PtcRunner.Turn.t()] | nil,
trace_id: String.t() | nil,
parent_trace_id: String.t() | nil,
field_descriptions: map() | nil,
prints: [String.t()],
tool_calls: [tool_call()],
pmap_calls: [pmap_call()],
catalog_ops: [catalog_op()],
child_traces: [String.t()],
child_steps: [t()],
messages: [message()] | nil,
prompt: String.t() | nil,
tools: map() | nil,
summaries: %{String.t() => String.t()},
tool_cache: map()
}
@doc """
Creates a new successful Step.
## Examples
iex> step = PtcRunner.Step.ok(%{count: 5}, %{})
iex> step.return
%{count: 5}
iex> step.fail
nil
"""
@spec ok(term(), map()) :: t()
def ok(return, memory) do
%__MODULE__{
return: return,
fail: nil,
memory: memory,
signature: nil,
usage: nil,
turns: nil,
trace_id: nil,
parent_trace_id: nil,
field_descriptions: nil,
prints: [],
tool_calls: [],
pmap_calls: [],
catalog_ops: [],
child_traces: [],
child_steps: []
}
end
@doc """
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
"""
@spec error(atom(), String.t(), map()) :: t()
def error(reason, message, memory) do
error(reason, message, memory, %{})
end
@doc """
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
}
"""
@spec error(atom(), String.t(), map(), map()) :: t()
def error(reason, message, memory, details) do
error(reason, message, memory, details, [])
end
@doc """
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}
"""
@spec error(atom(), String.t(), map(), map(), keyword()) :: t()
def error(reason, message, memory, details, opts) do
%__MODULE__{
return: nil,
fail: %{reason: reason, message: message, details: details},
memory: memory,
journal: Keyword.get(opts, :journal),
tool_cache: Keyword.get(opts, :tool_cache, %{}),
signature: nil,
usage: nil,
turns: nil,
trace_id: nil,
parent_trace_id: nil,
field_descriptions: nil,
prints: [],
tool_calls: [],
pmap_calls: [],
catalog_ops: [],
child_traces: [],
child_steps: []
}
end
end