# `PtcRunner.SubAgent.Loop.Shared`
[🔗](https://github.com/andreasronge/ptc_runner/blob/main/lib/ptc_runner/sub_agent/loop/shared.ex#L1)

Helpers shared across the loop drivers (`Loop`, `TextMode`, `PtcToolCall`,
`JsonHandler`, `StepAssembler`).

These functions used to be copy-pasted into each driver and had begun to
drift. Keeping a single definition here guarantees the drivers agree on
memory accounting, error classification, final-text parsing, collected-message
assembly, and schema usage metrics.

# `add_schema_metrics`

```elixir
@spec add_schema_metrics(map(), term()) :: map()
```

Annotate a usage map with schema-usage metrics (`:schema_used` and, when a
schema map is present, `:schema_bytes`).

# `build_collected_messages`

```elixir
@spec build_collected_messages(map(), list() | nil) :: list() | nil
```

Build the optional collected-message list returned to callers that requested
`collect_messages: true`. Prepends the current system prompt; returns `nil`
when collection is disabled.

# `check_memory_limit`

```elixir
@spec check_memory_limit(map(), non_neg_integer() | nil) ::
  {:ok, non_neg_integer()} | {:error, :memory_limit_exceeded, non_neg_integer()}
```

Check whether `memory` exceeds `limit` bytes.

Returns `{:ok, size}` when within the limit (or when `limit` is `nil`), and
`{:error, :memory_limit_exceeded, size}` when exceeded.

# `classify_lisp_error`

Map a `Step.fail` map onto a coarse error reason atom
(`:parse_error`, `:timeout`, `:memory_limit`, or `:runtime_error`).

`reason` may be an atom or a string (`t:PtcRunner.Step.fail/0` allows both).
No `@spec` is declared on purpose: the callers pass `lisp_step.fail`, whose
type is `fail | nil`, and dialyzer's success-typing narrowing constrains it
to the non-nil map here — which keeps the subsequent `fail.message` access
safe. A narrower explicit contract would break that narrowing.

# `memory_size`

```elixir
@spec memory_size(map()) :: non_neg_integer()
```

Approximate in-memory byte size of the agent memory map.

# `parse_for_type`

```elixir
@spec parse_for_type(binary(), term()) :: {:ok, term()} | {:error, binary()}
```

Parse a piece of final text into a value of the expected return type.

`:datetime` accepts both JSON-quoted ISO-8601 (`"\"2026-05-06T...\""`) and a
bare ISO-8601 string. All other types parse via `Jason.decode/1`.

# `terminal_lisp_failure?`

```elixir
@spec terminal_lisp_failure?(map() | nil) :: boolean()
```

Whether a `Lisp.run` failure must terminate the SubAgent run rather than become
a recoverable retry turn — consulted by every Lisp-running transport (`:content`
and `:tool_call`).

A `:prelude_attach_failed` means a public capability-prelude export's required
upstream backing is missing. That is not a program error the LLM can repair by
rewriting, and feeding it back as a retry turn would let earlier side-effecting
turns stand. Failing closed here preserves the prelude guarantee on every
multi-turn path (plan §3.5 #2).

---

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