Pixir.ACP.Server (pixir v0.1.0)

Copy Markdown View Source

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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

emit(server, update_params)

@spec emit(GenServer.server(), map()) :: :ok

Emit a session/update notification (called by prompt Tasks; serializes writes).

emit_event(server, acp_sid, event)

@spec emit_event(GenServer.server(), binary(), Pixir.Event.t()) :: :ok

Translate and emit a Pixir Event with server-owned presentation state.

feed(server, line)

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

request_permission(server, params)

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

run()

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

start_link(opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

Start the Server. Opts:

  • :io — the stdio device (default :stdio; inject a StringIO/pipe in tests).
  • :provider, :provider_opts — passed through to each Turn (test seam).
  • :name — optional registered name.