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

Wire-format source of truth for the `lisp_eval` tool surface.

Owns the canonical tool description (per capability profile) and the
shared response-payload renderers (`render_success/2`,
`render_error/3`) used across:

  * **In-process v1 PTC `:tool_call`** — `output: :ptc_lisp,
    ptc_transport: :tool_call` agents that expose `lisp_eval`
    as the only provider-native tool. Profile:
    `:in_process_with_app_tools`.
  * **In-process text-mode (combined mode)** — `output: :text,
    ptc_transport: :tool_call` agents that expose `lisp_eval`
    alongside `:both`-tagged app tools. Profile:
    `:in_process_text_mode`.
  * **MCP server** — standalone JSON-RPC server that advertises
    `lisp_eval` with no app tools available inside programs.
    Profile: `:mcp_no_tools`.

The three feature plans share four conventions; this module is where
all four are pinned (see "Coupling Points" in
`Plans/text-mode-ptc-compute-tool.md`):

  1. **Profile-string convention.** Each profile is one canonical
     string constant. `tool_description/1` returns it directly — no
     runtime concatenation of base + capability note. If the
     representation ever changes (e.g., to a structured map), all
     three profiles change together.
  2. **`error_reason()` is a closed union.** Adding a new reason
     requires updating the `@type` and `render_error/3`'s reason
     handling in lockstep. `render_error/3` MUST handle every member
     without crashing.
  3. **Renderer signatures are keyword-driven.** `render_success/2`
     and `render_error/3` take a keyword list for any non-essential
     parameter so future additions are non-breaking. Unknown opts are
     silently ignored, not rejected.
  4. **`tool_description/1` carries capability statements only.**
     Cache-reuse guidance, prompt cards, and other workflow guidance
     live in plan-specific surfaces (system prompt, `cache_hint`, MCP
     server documentation), not here.

See `Plans/text-mode-ptc-compute-tool.md` § "Prerequisite: Shared
Protocol Module" and `Plans/ptc-runner-mcp-server.md` § "Tool
Description Capability Profiles" for the spec.

# `error_reason`

```elixir
@type error_reason() ::
  :parse_error
  | :runtime_error
  | :timeout
  | :memory_limit
  | :args_error
  | :fail
  | :validation_error
```

Closed union of error reasons surfaced through `render_error/3`.

Members:

  * `:parse_error`        — PTC-Lisp source failed to parse.
  * `:runtime_error`      — runtime evaluation error inside a program
    (also covers return-validation errors against the agent's
    signature).
  * `:timeout`            — sandbox timeout exceeded.
  * `:memory_limit`       — sandbox memory cap exceeded.
  * `:args_error`         — tool arguments malformed (missing
    `program`, non-string, wrong shape, oversized). Emitted only by
    MCP v1; in-process surfaces never construct it (Addendum #25).
  * `:fail`               — program called `(fail v)` to terminate
    with an error value. Only reason carrying a `result` payload
    (Addendum #4).
  * `:validation_error`   — return-value signature mismatch.
    Reserved for MCP v1; no in-process surface emits it today
    (Tier 0 scope expansion).

# `atomize_value`

```elixir
@spec atomize_value(term(), term()) :: term()
```

Delegates to `PtcRunner.SubAgent.Loop.JsonHandler.atomize_value/2`.

Used by surfaces that need to coerce a raw JSON value into the
shape implied by a parsed signature before validation.

# `lisp_run`

```elixir
@spec lisp_run(
  String.t(),
  keyword()
) :: {:ok, PtcRunner.Step.t()} | {:error, PtcRunner.Step.t()}
```

Delegates to `PtcRunner.Lisp.run/2`.

Re-exported here so non-v1 callers (text-mode combined loop, MCP
server) can drive PTC-Lisp programs through the protocol module
without depending on the v1 loop's transitive aliases.

# `parse_signature`

```elixir
@spec parse_signature(String.t()) ::
  {:ok, PtcRunner.SubAgent.Signature.signature()} | {:error, String.t()}
```

Parse a PTC signature string for use by out-of-tree callers.

Thin wrapper over `PtcRunner.SubAgent.Signature.parse/1`. Per § 13.1
of `Plans/ptc-runner-mcp-server.md`, `:ptc_runner_mcp` consumes
signatures exclusively through this function so the parser can
later move out of the `SubAgent` namespace without breaking the MCP
package.

## Examples

    iex> PtcRunner.PtcToolProtocol.parse_signature("() -> {count :int}")
    {:ok, {:signature, [], {:map, [{"count", :int}]}}}

    iex> {:error, _reason} = PtcRunner.PtcToolProtocol.parse_signature("not a signature")

# `render_error`

```elixir
@spec render_error(error_reason(), String.t(), keyword()) :: String.t()
```

Render an error `lisp_eval` response as JSON.

Every member of `error_reason()` is handled. Output payload keys:

  * `"status"` — always `"error"`.
  * `"reason"` — `Atom.to_string/1` of the reason atom.
  * `"message"` — the supplied human-readable message.
  * `"feedback"` — defaults to `message`; overridden via
    `feedback:` opt.
  * `"result"` — present **only when** `reason == :fail`. The value
    is taken from the `result:` opt (Addendum #4: `:fail` is the
    only reason that carries a value).

## Recognized opts

  * `:result`   — only meaningful for `reason: :fail`. Encoded as
    the top-level `"result"` field (typically a string preview of
    the failed value). Ignored for any other reason.
  * `:feedback` — string. Defaults to `message` when not provided.

Unknown opts are ignored (Addendum #12).

# `render_success`

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

Render a successful `lisp_eval` invocation as JSON.

Success payload shape: `status`, optional `result`, `prints`,
`feedback`, top-level `truncated`, and (when `include_memory: true`,
the default) `memory.{changed,stored_keys,truncated}`. One-shot
callers omit memory by passing `include_memory: false` or by going
through `render_success_from_step/2` which sets it for them.

## Required input shape

`lisp_step` is the `Step.t()` result of `Lisp.run/2`. Only the
`:return` field is consulted directly (to decide whether to drop the
`"result"` field when nil).

## Recognized opts

  * `:execution` — required for v1 callers. A
    `TurnFeedback.execution_feedback/3` result map carrying
    `:result`, `:prints`, `:feedback`, `:memory.{changed,stored_keys,truncated}`,
    and `:truncated`. The renderer reads these directly. Future
    callers (MCP server, text-mode) MAY pass their own equivalent
    map.
  * `:validated` — JSON-encodable value. When present, included as
    a top-level `"validated"` field. Used by MCP v1 to surface
    schema-validated return values.
  * `:include_memory` — boolean (default `true`). When `false`, the
    `"memory"` key is omitted entirely from the payload. One-shot
    callers (MCP server) set this to `false` because state never
    persists across calls (issue #879). Multi-turn `SubAgent` loops
    keep the default since `defn`'d names DO persist across turns.

Unknown opts are ignored (Addendum #12).

## Result-field elision

Matches the v1 invariant: when both `execution.result` and
`lisp_step.return` are `nil`, the `"result"` field is dropped from
the JSON. Any other combination keeps the field (even when the
rendered value is `null`).

# `render_success_from_step`

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

Render a successful `lisp_eval` invocation directly from a `Lisp.run/2` result.

High-level convenience wrapper over `render_success/2`. Builds the
`:execution` map internally via
`PtcRunner.SubAgent.Loop.TurnFeedback.execution_feedback/3` so callers
outside `:ptc_runner` (e.g. `:ptc_runner_mcp`) never have to reach
into `TurnFeedback` themselves. Per § 13.1 of
`Plans/ptc-runner-mcp-server.md`, this is the only canonical way for
out-of-tree callers to render an R22 success payload.

## Required input shape

`lisp_step` is a `PtcRunner.Step.t()` (the success-branch result of
`PtcRunner.Lisp.run/2`). Only the structured fields the renderer
consults — `:return`, `:prints`, `:memory` — need to be populated.

## Recognized opts

  * `:validated` — JSON-encodable value forwarded into
    `render_success/2` and surfaced as the top-level `"validated"`
    field. Only meaningful when the caller validated the program's
    return value against a signature.
  * `:include_memory` — boolean (default `false`). When `true`, the
    rendered payload includes `memory.{changed,stored_keys,truncated}`.
    Use this for stateful embedding loops where Lisp memory persists
    across invocations.
  * `:prior_memory` — memory map from before this turn (default `%{}`).
    Used only to compute `memory.changed` when `include_memory: true`.
  * `:format_options` — optional format options for result, print, and
    memory previews.

Unknown opts are silently ignored, matching `render_success/2`.

## One-shot and stateful semantics

By default, this wrapper keeps one-shot behavior: state never persists
across invocations, so the response omits the `memory` field entirely
(issue #879). Stateful embedders can pass `include_memory: true` and
`prior_memory: previous_memory` to get the same LLM-facing execution
feedback without reaching into `PtcRunner.SubAgent.Loop.TurnFeedback`.

## Example

    iex> {:ok, step} = PtcRunner.Lisp.run("(+ 1 2)")
    iex> json = PtcRunner.PtcToolProtocol.render_success_from_step(step)
    iex> %{"status" => "ok", "result" => "user=> 3"} = Jason.decode!(json)
    iex> json |> Jason.decode!() |> Map.fetch!("status")
    "ok"

# `to_json_value`

```elixir
@spec to_json_value(term()) :: {:ok, term()} | {:error, String.t()}
```

Convert a typed Elixir term into a JSON-encodable value.

Used by surfaces that surface signature-validated return values as
structured JSON (currently only the MCP server's `validated` field;
see § 13 of `Plans/ptc-runner-mcp-server.md`). This is the inverse
direction of `atomize_value/2`, which goes JSON → typed Elixir.

## Conversion rules

| Elixir term | JSON form |
|---|---|
| Integer | number |
| Float | number |
| Binary (string) | string |
| Boolean | boolean |
| `nil` | null |
| Map with binary or atom keys | object with string keys |
| List | array |
| Atom (non-key) | string (`:foo` → `"foo"`, no leading colon) |
| Tuple | array |
| `%DateTime{}` | ISO-8601 string |
| `%Date{}`, `%Time{}` | ISO-8601 string |
| Anything else | `{:error, "non-JSON-encodable value at <path>"}` |

Errors propagate the path to the offending sub-value. Map-key path
segments are dot-joined; list/tuple indices use `[<index>]`.

## Examples

    iex> PtcRunner.PtcToolProtocol.to_json_value(42)
    {:ok, 42}

    iex> PtcRunner.PtcToolProtocol.to_json_value(1.5)
    {:ok, 1.5}

    iex> PtcRunner.PtcToolProtocol.to_json_value(:foo)
    {:ok, "foo"}

    iex> PtcRunner.PtcToolProtocol.to_json_value({1, :ok, "a"})
    {:ok, [1, "ok", "a"]}

    iex> {:ok, dt, _} = DateTime.from_iso8601("2026-05-07T12:00:00Z")
    iex> PtcRunner.PtcToolProtocol.to_json_value(dt)
    {:ok, "2026-05-07T12:00:00Z"}

    iex> PtcRunner.PtcToolProtocol.to_json_value(%{count: 2, items: [:a, :b]})
    {:ok, %{"count" => 2, "items" => ["a", "b"]}}

    iex> PtcRunner.PtcToolProtocol.to_json_value(%{rows: [%{ts: make_ref()}]})
    {:error, "non-JSON-encodable value at rows[0].ts"}

# `tool_description`

```elixir
@spec tool_description(
  :in_process_with_app_tools
  | :in_process_text_mode
  | :mcp_no_tools
) :: String.t()
```

Capability profile for the `lisp_eval` tool description.

Returns the canonical description string for the requested profile.
Per Addendum #11, each profile is one constant returned directly —
no runtime concatenation. The `:in_process_with_app_tools` string is
byte-for-byte locked to the existing v1 wording (Addendum #10).

# `validate_program`

```elixir
@spec validate_program(term()) ::
  {:ok, String.t()} | {:error, :args_error, String.t()}
```

Validate the `program` argument of a `lisp_eval` invocation.

Shared across the in-process `:tool_call` and text-mode loop branches
so the wire-format error wording for an absent, mistyped, or empty
`program` stays in lockstep with the rest of the protocol surface.

Returns `{:ok, program}` for a non-empty binary, or
`{:error, :args_error, message}` describing the violation.

## Examples

    iex> PtcRunner.PtcToolProtocol.validate_program("(+ 1 2)")
    {:ok, "(+ 1 2)"}

    iex> PtcRunner.PtcToolProtocol.validate_program(nil)
    {:error, :args_error, "lisp_eval requires a non-empty `program` string argument."}

    iex> PtcRunner.PtcToolProtocol.validate_program(42)
    {:error, :args_error, "lisp_eval `program` must be a string, got 42."}

    iex> PtcRunner.PtcToolProtocol.validate_program("   ")
    {:error, :args_error, "lisp_eval `program` must be a non-empty string."}

# `validate_return`

```elixir
@spec validate_return(map(), term()) :: :ok | {:error, list()}
```

Delegates to `PtcRunner.SubAgent.Loop.JsonHandler.validate_return/2`.

Used by surfaces that need to validate a return value against an
agent's parsed signature.

---

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