# `PtcRunner.Lisp.Eval.Context`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/lisp/eval/context.ex#L1)

Evaluation context for the Lisp interpreter.

Bundles the parameters that flow through recursive evaluation:
- `ctx`: External data (read-only)
- `user_ns`: User namespace (mutable bindings from `def`)
- `env`: Lexical environment (variable bindings)
- `tool_exec`: Tool executor function
- `turn_history`: Previous turn results for multi-turn loops

## Limits

| Field | Default | Hard Cap | Purpose |
|-------|---------|----------|---------|
| `loop_limit` | 1,000 | 10,000 | Max loop/recur jumps |
| `max_print_length` | 2,000 | — | Max chars per `println` call |
| `max_tool_call_result_bytes` | 16,384 | — | Per-entry cap on the `:result` retained in the in-eval tool ledger |
| `pmap_max_concurrency` | `schedulers * 2` | — | Max concurrent pmap/pcalls tasks |

## Tool-ledger retention

`tool_calls` records every call's `:result` and `:args` for post-eval
telemetry/envelope rendering. To stop a long-running or looping tool use
(e.g. a paginated read fold) from accumulating full payloads in live eval
state, `append_tool_call/2` bounds each entry's `:result` to a preview once
it exceeds `max_tool_call_result_bytes`, marking the entry with
`:result_truncated`. Only the LEDGER copy is bounded — the value returned to
the program and any `tool_cache` entry keep the full result (they are built
separately in `record_tool_call`). `:args` is left intact (it is tiny in the
fold case and `TurnEvent.tool_call_summary/1` needs the raw map for upstream
identity + the canonical args hash), as are `:child_trace_id`/`:child_step`.

# `catalog_op`

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

Discovery operation record for tracing.

Fields:
- `operation`: Which discovery operation was called
- `args`: Arguments passed to the operation
- `outcome`: `:ok`, `:nil_world_fault`, or `:error`
- `reason`: Reason for nil/error outcome (e.g., `:catalog_cap_exhausted`)
- `duration_ms`: How long the operation took

# `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

# `recur_effects`

```elixir
@type recur_effects() :: %{
  prints: [String.t()],
  tool_calls: [tool_call()],
  pmap_calls: [pmap_call()],
  catalog_ops: [catalog_op()],
  tool_cache: map()
}
```

# `t`

```elixir
@type t() :: %PtcRunner.Lisp.Eval.Context{
  budget: map() | nil,
  catalog_ops: [catalog_op()],
  ctx: map(),
  discovery_exec: (atom(), list() -&gt; term()) | nil,
  env: map(),
  iteration_count: integer(),
  journal: map() | nil,
  locals: term(),
  loop_limit: integer(),
  max_heap: pos_integer() | nil,
  max_print_length: pos_integer(),
  max_tool_call_result_bytes: pos_integer(),
  max_tool_calls: pos_integer() | nil,
  origin_stack: [map()],
  parallel_budget: PtcRunner.Lisp.Eval.ParallelBudget.t() | nil,
  pmap_calls: [pmap_call()],
  pmap_deadline: integer() | nil,
  pmap_max_concurrency: pos_integer(),
  pmap_timeout: pos_integer(),
  prelude: PtcRunner.Lisp.Prelude.t() | nil,
  prelude_caller_user_ns_stack: [map()],
  prelude_exports: %{required(String.t()) =&gt; {term(), map()}},
  prints: [String.t()],
  strict_data: boolean(),
  summaries: %{required(String.t()) =&gt; String.t()},
  tool_cache: map(),
  tool_calls: [tool_call()],
  tool_exec: (String.t(), map(), map() | nil -&gt; term()),
  tools_meta: %{required(String.t()) =&gt; %{cache: boolean()}},
  trace_context: trace_context(),
  turn_history: list(),
  user_ns: map(),
  worker_max_heap: pos_integer() | nil
}
```

# `tool_call`

```elixir
@type tool_call() :: %{
  :name =&gt; String.t(),
  :args =&gt; map(),
  :result =&gt; term(),
  :error =&gt; String.t() | nil,
  :timestamp =&gt; DateTime.t(),
  :duration_ms =&gt; non_neg_integer(),
  optional(:child_trace_id) =&gt; String.t(),
  optional(:child_step) =&gt; term(),
  optional(:cached) =&gt; boolean()
}
```

Tool call record for tracing.

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
- `child_trace_id`: Trace ID of nested SubAgentTool execution (if any)

# `trace_context`

```elixir
@type trace_context() ::
  %{
    trace_id: String.t(),
    parent_span_id: String.t() | nil,
    depth: non_neg_integer()
  }
  | nil
```

Trace context for nested agent execution tracing.

Fields:
- `trace_id`: Unique identifier for this trace session
- `parent_span_id`: Span ID of the parent operation (nil for root)
- `depth`: Nesting depth for visualization

# `append_catalog_op`

```elixir
@spec append_catalog_op(t(), catalog_op()) :: t()
```

Appends a catalog operation record to the context.

# `append_pmap_call`

```elixir
@spec append_pmap_call(t(), pmap_call()) :: t()
```

Appends a pmap/pcalls execution record to the context.

# `append_print`

```elixir
@spec append_print(t(), String.t()) :: t()
```

Appends a print message to the context.

Long messages are truncated to `max_print_length` characters (default: 2000).

# `append_tool_call`

```elixir
@spec append_tool_call(t(), tool_call()) :: t()
```

Appends a tool call record to the context.

The entry's `:result` and `:args` are bounded to a preview when they exceed
`max_tool_call_result_bytes`, so a looping/large tool use cannot accumulate
full payloads in live eval state. See the "Tool-ledger retention" moduledoc
section. Only the ledger copy is bounded; callers keep the full result for
the program return and cache separately.

# `check_tool_call_limit`

```elixir
@spec check_tool_call_limit(t()) :: :ok | {:error, :tool_call_limit_exceeded}
```

Checks whether the tool call limit has been reached.

Returns `:ok` when unlimited (`nil`) or under the limit,
`{:error, :tool_call_limit_exceeded}` when at or over.

# `current_origin`

```elixir
@spec current_origin(t()) :: map() | nil
```

Returns the current evaluator origin, if any.

# `current_prelude_caller_user_ns`

```elixir
@spec current_prelude_caller_user_ns(t()) :: map() | nil
```

Returns the user namespace active before the current prelude export, if any.

# `increment_iteration`

```elixir
@spec increment_iteration(t()) :: {:ok, t()} | {:error, :loop_limit_exceeded}
```

Increments the iteration count and checks against the limit.

# `inherit_prelude`

```elixir
@spec inherit_prelude(t(), t()) :: t()
```

Copies the attached prelude tables (`prelude_exports`/`prelude`) from
`source` onto `context`.

Sub-contexts built with `new/6` for closure/thunk evaluation start with empty
prelude tables; this re-installs them so a qualified prelude call made from
inside a user closure still resolves (Capability Prelude V1, plan §5).

# `merge_env`

```elixir
@spec merge_env(t(), map()) :: t()
```

Merges new bindings into the environment.

# `new`

```elixir
@spec new(
  map(),
  map(),
  map(),
  (String.t(), map(), map() | nil -&gt; term()),
  list(),
  keyword()
) :: t()
```

Creates a new evaluation context.

## Options

- `:max_print_length` - Max characters per `println` call (default: 2000)
- `:budget` - Budget info map for `(budget/remaining)` introspection (default: nil)
- `:pmap_timeout` - Timeout in ms for each pmap task (default: 5000). Increase for LLM-backed tools.
- `:pmap_max_concurrency` - Max concurrent tasks in pmap/pcalls (default: `System.schedulers_online() * 2`)
- `:max_heap` - Sandbox per-process heap cap in words (default: nil).
- `:worker_max_heap` - FIXED `max_heap_size` (in words) for every
  pmap/pcalls worker, top-level and nested (default: the `:max_heap`
  value). Not divided by concurrency. See `PtcRunner.Lisp.Eval.ParallelRunner`.
- `:parallel_budget` - shared `PtcRunner.Lisp.Eval.ParallelBudget`
  semaphore bounding the number of parallel workers alive at once
  across the whole run (default: nil = uncounted).
- `:trace_context` - Trace context for nested agent tracing (default: nil)

## Examples

    iex> ctx = PtcRunner.Lisp.Eval.Context.new(%{}, %{}, %{}, fn _, _, _ -> nil end, [])
    iex> ctx.user_ns
    %{}

    iex> ctx = PtcRunner.Lisp.Eval.Context.new(%{}, %{}, %{}, fn _, _, _ -> nil end, [], max_print_length: 500)
    iex> ctx.max_print_length
    500

    iex> ctx = PtcRunner.Lisp.Eval.Context.new(%{}, %{}, %{}, fn _, _, _ -> nil end, [], budget: %{turns: 10})
    iex> ctx.budget
    %{turns: 10}

    iex> ctx = PtcRunner.Lisp.Eval.Context.new(%{}, %{}, %{}, fn _, _, _ -> nil end, [], pmap_timeout: 60_000)
    iex> ctx.pmap_timeout
    60000

# `push_prelude_caller_user_ns`

```elixir
@spec push_prelude_caller_user_ns(t(), map()) :: t()
```

Saves the user namespace active before entering a prelude export.

# `push_prelude_origin`

```elixir
@spec push_prelude_origin(t(), map()) :: t()
```

Pushes a prelude-export origin for private tool authorization.

# `push_user_origin`

```elixir
@spec push_user_origin(t()) :: t()
```

Pushes a user-code origin that masks inherited prelude-export authority.

# `recur_effects`

```elixir
@spec recur_effects(t()) :: recur_effects()
```

Extracts accumulated side effects that must survive a `recur` jump.

# `restore_recur_effects`

```elixir
@spec restore_recur_effects(t(), recur_effects()) :: t()
```

Restores side effects carried by a `recur` signal onto the next iteration context.

# `update_user_ns`

```elixir
@spec update_user_ns(t(), map()) :: t()
```

Updates the user namespace in the context.

---

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