Pixir.ACP.Translate (pixir v0.1.0)

Copy Markdown View Source

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_deltas become agent_message_chunks; 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.

Summary

Functions

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

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).

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).

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.

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 the canonical messages ARE the transcript: user_messageuser_message_chunk, assistant_messageagent_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). Returns nil for events with no transcript form.

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).

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.

Types

acp_sid()

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

await_outcome()

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

Functions

kind(arg1)

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

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

message_chunk(text, acp_sid)

@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).

permission_outcome(arg1)

@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(request, acp_sid)

@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(event, acp_sid, opts \\ [])

@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 the canonical messages ARE the transcript: user_messageuser_message_chunk, assistant_messageagent_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). Returns nil for events with no transcript form.

stop_reason(arg1, arg2)

@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(event, acp_sid, opts \\ [])

@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.