The UI-agnostic multi-turn driver (ADR 0008): the conversational loop over
Session/Turn/Events, with no presentation. Every front-end — the terminal CLI,
a future HTTP/WebSocket tier, an editor extension, or an embedding Elixir app — drives
Pixir through this module.
It is a stateless functional module, not a process: the Session GenServer already
owns turn state, History, seq, and interrupt (ADR 0001). Per-client state (a socket,
a pending permission reply) belongs to the transport tier, never here.
Shape
{:ok, sid} = Conversation.start(workspace: ".") # new Session
{:ok, sid} = Conversation.start(id: sid) # resume / reattach
{:ok, ref} = Conversation.send(sid, "do the thing") # run one Turn (non-blocking)Observation is the Events bus (ADR 0004), full stop — this module invents no new
streaming abstraction. An out-of-process UI subscribes via Pixir.Events.subscribe/1
and forwards each {:pixir_event, event} over its transport as JSON. For in-process
callers (tests, an optional terminal presenter) await/2 consumes the bus until the
Turn reaches a terminal status, with an optional on_event callback.
Permissions stay injectable (ADR 0006): pass an :asker function through send/3;
this module implements no prompting. Async, remote permission decisions are a
transport-tier concern (an asker that blocks the Turn while round-tripping over a
socket).
Summary
Functions
Block until the current Turn reaches a terminal status, consuming bus events. The
caller must already be subscribed (Pixir.Events.subscribe/1) — typically call
subscribe, then send, then await.
The Session's History, folded from the Log (pass-through to Session).
Interrupt the running Turn, if any (pass-through to Session).
Run one Turn for prompt in the Session. Non-blocking — returns {:ok, ref} once the
Turn task is started ({:error, :busy} if a Turn is already running). Observe progress
via the bus, or block with await/2.
Start a conversation, returning its Session id.
Subscribe the calling process to the Session's event bus (pass-through).
Types
@type session_id() :: String.t()
Functions
@spec await( session_id(), keyword() ) :: :done | :error | :interrupted | :timeout
Block until the current Turn reaches a terminal status, consuming bus events. The
caller must already be subscribed (Pixir.Events.subscribe/1) — typically call
subscribe, then send, then await.
Returns :done | :error | :interrupted | :timeout. interrupted is terminal (ADR
0008 — the Renderer's old loop hung on it). Options: :on_event (a 1-arity callback
invoked per event, for in-process rendering) and :idle_timeout (ms, default 120_000).
@spec history(session_id()) :: {:ok, Pixir.Log.history()} | {:error, map()}
The Session's History, folded from the Log (pass-through to Session).
@spec interrupt(session_id()) :: :ok | {:error, :no_turn}
Interrupt the running Turn, if any (pass-through to Session).
@spec send(session_id(), String.t(), keyword()) :: {:ok, reference()} | {:error, :busy}
Run one Turn for prompt in the Session. Non-blocking — returns {:ok, ref} once the
Turn task is started ({:error, :busy} if a Turn is already running). Observe progress
via the bus, or block with await/2.
Options are passed to Turn.run/3: :permission_mode (default :auto), :asker
(default deny), :provider, :provider_opts, :dry_run, :max_iterations.
@spec start(keyword()) :: {:ok, session_id()} | {:error, map()}
Start a conversation, returning its Session id.
- no
:id— mint a new Session. :id— resume a persisted Session (or idempotently reattach if it is already running). A missing Log is a structured:not_founderror; a corrupt Log surfaces as a structured error rather than crashing the caller.
Options: :id, :workspace (default cwd), :role (default :build).
@spec subscribe(session_id()) :: :ok
Subscribe the calling process to the Session's event bus (pass-through).