# `PtcRunner.TraceLog.TurnEvent`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/trace_log/turn_event.ex#L1)

Shared builder for the canonical *turn* event — the substrate-level record
of one driver turn, emitted identically by both turn drivers (plan D1):

- `PtcRunner.Session` turns (external LLM drives via MCP / `mix ptc.repl`), and
- the `PtcRunner.SubAgent` loop (the internal loop drives the LLM).

Both drivers build their turn record through `build/1` so the top-level shape
is identical and queryable through the same `PtcRunner.TraceLog.Analyzer`
calls, regardless of which driver produced it. Driver-specific richness lives
in the `data` bag; the top-level fields never diverge.

The map is a v2-flat-compatible envelope (`event: "turn"`, `schema_version: 2`)
so it rides the existing `PtcRunner.TraceLog` JSONL/in-memory sinks. The sink
stamps `trace_id`, `timestamp`, and `seq`; this builder never sets them.

## Top-level fields

    schema_version, event ("turn"),
    driver ("session" | "sub_agent"),
    session_id, agent_id, agent_name,
    turn, attempt, committed, status,
    duration_ms, input_tokens, output_tokens, total_tokens,
    data

`turn` is the committed-state counter (advances only when an attempt commits);
`attempt` is the monotonic per-attempt counter (advances on every attempt,
including failed ones); `committed` flags whether this attempt advanced
committed state. Failed/parse-error/budget-stop attempts are recorded with
`committed: false` so wasted work is visible without mutating driver state
(plan P2 notes).

## `data` bag

    program, raw_response, result_preview, prints, memory_diff,
    tool_calls, limits_hit, preludes, fail, turn_type

Per-driver fields that don't apply are nil/empty. `raw_response` carries what
the driver's LLM generated when there is no parsed `program` (SubAgent
parse/text-mode failures); it is nil for session turns and normal turns. The whole bag is run through
`PtcRunner.TraceLog.Event.sanitize/1`, which bounds large strings/lists/maps —
that is where memory-diff values and prints get their byte bounds.

# `attrs`

```elixir
@type attrs() :: map() | keyword()
```

Normalized attributes accepted by `build/1` (atom-keyed).

# `build`

```elixir
@spec build(attrs()) :: map()
```

Builds the canonical turn-event map from normalized attributes.

Required: `:driver` (`:session` | `:sub_agent`). All other keys are optional
and default to nil / empty. `trace_id`/`timestamp`/`seq` are intentionally
omitted — the sink stamps them.

# `memory_diff`

```elixir
@spec memory_diff(map(), map()) :: %{changed_keys: [String.t()], values: map()} | nil
```

Computes a memory diff (`changed_keys` + bounded `values`) between the
pre-turn and post-turn memory maps. Keys whose value is unchanged are
excluded. PTC-Lisp `def` cannot remove bindings, so this only surfaces
additions and rebindings.

# `prelude_provenance`

```elixir
@spec prelude_provenance(map() | nil) :: [map()]
```

Slims a prelude trace summary (`PtcRunner.Lisp.Prelude.trace_summary/1`) to the
turn-event `preludes` provenance shape — `[%{"source_hash" => ...,
"namespaces" => ..., "components" => [...]}]`, or `[]` when no prelude was
attached. The `components` key is omitted for single-source prelude artifacts.

Shared by both drivers so the provenance field (the single field that makes
A/B benchmarking and derivation provenance trivial, per the plan) reads
identically whether a session or a SubAgent turn produced it.

# `preview`

```elixir
@spec preview(term()) :: String.t()
```

Renders a bounded, JSON-safe preview string for a turn result value.

Shared by both drivers so `result_preview` reads the same regardless of who
produced the turn.

# `tool_call_summary`

```elixir
@spec tool_call_summary(map()) :: map()
```

Builds the credential-free turn-log projection for a single tool call.

The projection intentionally excludes raw arguments and results. It keeps a
stable `args_hash` derived from the same canonical argument identity used by
tool caching, so PTC-Lisp log analysis can detect duplicate fetches without
ingesting potentially large or sensitive payloads.

---

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