# `Pixir.ACP.Translate`
[🔗](https://github.com/Ranvier-Technologies/pixir/blob/main/lib/pixir/acp/translate.ex#L1)

Pure mapping from Pixir bus `Events` to ACP `session/update` notification params, and
from a terminal `Conversation.await/2` outcome to an ACP `PromptResponse` stopReason
(ADR 0009, §§4-5). No IO, no process state.

This is **presentation only** — the canonical Log is never altered. Streamed
`text_delta`s become `agent_message_chunk`s; the canonical `assistant_message` (the
same text) is intentionally dropped here to avoid duplication, and is re-emitted only
by the no-deltas fallback path in `Pixir.ACP.Server`. File-oriented tool calls can
include ACP locations when the Server supplies the session workspace; paths are resolved
from structured tool args, never inferred from prose.

## TODO(presenter-current-scope)

Subagent and Workflow projections should carry enough structured scope for a Presenter
to distinguish "children from this current tool/workflow/delegate" from "all children
on the parent Session". This is a presentation concern: add parent session id, current
turn/tool/workflow identifiers, and checkpoint/partial metadata to ACP-friendly
`_meta.pixir` shapes before changing any canonical Event vocabulary.

# `acp_sid`

```elixir
@type acp_sid() :: String.t()
```

# `await_outcome`

```elixir
@type await_outcome() :: :done | :error | :interrupted | :timeout
```

# `kind`

```elixir
@spec kind(String.t()) :: String.t()
```

ACP tool `kind` for a Pixir tool registry name (cosmetic — drives only an icon).

# `message_chunk`

```elixir
@spec message_chunk(String.t(), acp_sid()) :: map()
```

Build a one-off `agent_message_chunk` `session/update` for a piece of assistant text.
Used by the Server's fallback when a Turn streamed no `text_delta` (ADR 0009 §4).

# `output_warning_summary`

```elixir
@spec output_warning_summary(non_neg_integer(), acp_sid()) :: map()
```

Build the terminal ACP summary after the 256-notice cap was exceeded.

# `output_warning_update`

```elixir
@spec output_warning_update(map(), acp_sid()) :: map()
```

Build the pinned schema-valid, non-transcript ACP warning update.

# `permission_outcome`

```elixir
@spec permission_outcome({:ok, map()} | {:error, term()}) ::
  :allow | {:deny, String.t()}
```

Map a `RequestPermissionResponse` (the `{:ok, result}` / `{:error, _}` from
`Server.request_permission/2`) to the Executor's decision: `:allow | {:deny,
reason}`. A `selected` outcome whose optionId is an allow option → `:allow`;
a reject option, a `cancelled` outcome, or any error/malformed response →
`{:deny, reason}` (default-deny: anything we can't read as an explicit allow
is a denial).

# `permission_request`

```elixir
@spec permission_request(map(), acp_sid()) :: map()
```

Build the `session/request_permission` PARAMS (A.2) from an asker request
`%{tool, args, reason, call_id}`. Offers exactly two options per decision #7:
`allow_once` / `reject_once` (ADR 0006 defers persistent allow-lists). Reuses
`title/2`/`kind/1` so the toolCall reads like the live `tool_call` update.

# `replay`

```elixir
@spec replay(Pixir.Event.t(), acp_sid(), keyword()) :: map() | nil
```

Map a canonical History Event to a `session/update` for `session/load` REPLAY
(epic A.6) — a fuller mapping than `update/2`, which intentionally drops
user/assistant (live streaming re-emits deltas, not the canonical message).
On load there are no deltas, so clean canonical messages ARE the transcript:
`user_message`→`user_message_chunk`, clean `assistant_message`→`agent_message_chunk`,
`tool_call`/`tool_result` as in live. **Reasoning is omitted** (opaque encrypted
`rs_` items carry no displayable summary — ADR 0007; the summary text is
ephemeral and never logged). Partial assistant evidence and `turn_failed` are audit
evidence, not clean transcript content. Returns `nil` for events with no transcript
form.

# `stop_reason`

```elixir
@spec stop_reason(await_outcome(), boolean()) :: String.t()
```

Map a terminal `await` outcome to an ACP stopReason. `cancel_requested?` covers the
cancel-vs-terminal race: if `session/cancel` arrived, resolve `"cancelled"` even if a
`done`/`error` slipped in first. A turn-level error is reported as content, not a
protocol error, so it resolves `"end_turn"` (ADR 0009 §5).

# `update`

```elixir
@spec update(Pixir.Event.t(), acp_sid(), keyword()) :: map() | nil
```

Translate a Pixir `Event` into the full `session/update` params
(`%{"sessionId" => acp_sid, "update" => update}`), or `nil` when the event maps to
nothing on the wire.

---

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