The ACP agent (server side) over stdio (ADR 0009): a single GenServer that owns the
stdout writer and the acp_session_id ↔ pixir_session_id map, decodes ndjson JSON-RPC
from stdin, dispatches by method onto Pixir.Conversation, and runs each
session/prompt in a supervised Task.
Channel discipline (ADR 0005)
stdout carries only JSON-RPC. Every write goes through this one process, so the
ndjson stream never interleaves. Prompt Tasks never touch stdout directly — they call
emit/2. Diagnostics go to stderr. The caller (run/0) redirects Logger to stderr
before starting so no log line corrupts the stream.
stdin
A dedicated reader process blocks on IO.read(io, :line) and forwards {:line, l} /
:eof / {:io_error, r} to this server, so the server mailbox is never blocked on raw
stdin. run/0 explicitly configures stdio as Unicode because GUI launchers can start
Pixir without a UTF-8 locale; ACP wire text must remain UTF-8 regardless of the parent
process environment. On EOF the server stops normally and run/0 unblocks (exit 0).
Scope
Implements initialize, session/new, session/prompt, session/cancel,
authenticate + logout (ACP handshake no-ops; Pixir owns Auth out of band),
session/set_mode + session/set_config_option (modes and models, D.2),
session/set_model (legacy Pixir/T3 compatibility), and session/load + session/resume
(lifecycle, A.6); emits session/update (incl. current_mode_update and
plan) and ORIGINATES session/request_permission (interactive permissions,
A.2 — correlating the client's response against pending_requests). Per-turn
knobs (model, reasoning effort, permission_mode) ride on session/prompt
_meta; sticky model selection is exposed through configOptions; the legacy
model catalog + auth status ride on initialize._meta.pixir.
Other methods get -32601. JSON-RPC errors are reserved for protocol faults; a
failed Turn is reported as content with stopReason:"end_turn" (ADR 0009 §5).
Permission posture follows the session mode (plan → read-only) and
_meta.permission_mode "ask" (→ interactive approval via the ACP asker).
Summary
Functions
Returns a specification to start this module under a supervisor.
Emit a session/update notification (called by prompt Tasks; serializes writes).
Translate and emit a Pixir Event with server-owned presentation state.
Feed one already-decoded JSON-RPC line into the Server. Test seam that drives the same
handle_info({:line, _}) path the reader uses, without real stdio.
Originate a session/request_permission request to the client and BLOCK until
the client responds (A.2). Returns the raw RequestPermissionResponse result
(a map) for Translate.permission_outcome/1 to interpret, or {:error, reason}.
Blocking entrypoint for pixir acp. Redirects Logger to stderr, starts a linked
Server reading :stdio, and blocks until the Server stops on EOF. Returns :ok so the
CLI router exits 0.
Start the Server. Opts
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec emit(GenServer.server(), map()) :: :ok
Emit a session/update notification (called by prompt Tasks; serializes writes).
@spec emit_event(GenServer.server(), binary(), Pixir.Event.t()) :: :ok
Translate and emit a Pixir Event with server-owned presentation state.
@spec feed(GenServer.server(), binary()) :: :ok
Feed one already-decoded JSON-RPC line into the Server. Test seam that drives the same
handle_info({:line, _}) path the reader uses, without real stdio.
@spec request_permission(GenServer.server(), map()) :: {:ok, map()} | {:error, term()}
Originate a session/request_permission request to the client and BLOCK until
the client responds (A.2). Returns the raw RequestPermissionResponse result
(a map) for Translate.permission_outcome/1 to interpret, or {:error, reason}.
Called from inside the Executor's Task (the Turn's tool loop), so blocking here blocks only that one Task — never the Server GenServer (which keeps writing and reading lines, including the eventual response). The Server owns the timeout and removes the pending request if a silent client never replies.
@spec run() :: :ok
Blocking entrypoint for pixir acp. Redirects Logger to stderr, starts a linked
Server reading :stdio, and blocks until the Server stops on EOF. Returns :ok so the
CLI router exits 0.
@spec start_link(keyword()) :: GenServer.on_start()
Start the Server. Opts:
:io— the stdio device (default:stdio; inject aStringIO/pipe in tests).:provider,:provider_opts— passed through to each Turn (test seam).:name— optional registered name.