Raxol.AgentClientProtocol.Session.Emitter behaviour (Raxol Agent Client Protocol v0.1.0)

Copy Markdown View Source

The Session emit seam — one injection point through which every session/update and every turn boundary flows.

The base package binds Emitter.Direct (emit/2 calls Connection.notify; the boundary hooks are no-ops). Emitter.Journal reroutes updates + turn boundaries through the single-publisher Writer (Ext.Journal.Writer) so every delivered update is ALSO durable and a reattacher replays an exact offset-addressed journal.

Callbacks

  • emit(session_state, notification) — a session/update (in-turn post_update and out-of-turn set_mode's current_mode_update).
  • turn_started(session_state, prompt_req, turn_id) — a turn begins; the per-session ordinal turn_id is threaded from the Session. Returns {:error, :turn_in_flight} when a cross-connection turn latch is already held for this session_id — the Session then replies busy and runs nothing; :ok otherwise (Direct always :ok; no latch).
  • turn_completed(session_state, rendered, turn_id) — a turn ends; rendered is the SINGLE-render-site value that ALSO becomes the prompt response (both are built from one render(outcome) call).

session_state is the %Raxol.AgentClientProtocol.Session{} struct; an emitter reads conn/conn_mod/session_id/journal from it.

Summary

Callbacks

OPTIONAL idle-reap hook (durable-sessions robustness). Called once when the Session idle-reaps: an emitter that owns a live durable process (the Journal's single-publisher Writer) STOPS it here so a long-lived node does not accrue a never-stopped Writer per session. The journal store outlives the Writer, so a fresh reattach respawns + replays. Direct has no such process — this is a no-op — and any emitter that omits the callback is treated as a no-op.

OPTIONAL steer-accept hook. Called once per ACCEPTED steer (never on a reject or a duplicate). ref is the accept reference (%{turn_id, offset, client_msg_id}) and request is the resolved steer map (%{text, expected_turn_id, client_msg_id}). A durable emitter (Journal) appends one "steer" record so a reattacher replays the redirected turn; the default Direct emitter omits this callback (it keeps no journal) and the Session treats an omitted callback as a no-op.

Callbacks

emit(session_state, notification)

@callback emit(session_state :: struct(), notification :: struct()) :: :ok

reap(session_state)

(optional)
@callback reap(session_state :: struct()) :: :ok

OPTIONAL idle-reap hook (durable-sessions robustness). Called once when the Session idle-reaps: an emitter that owns a live durable process (the Journal's single-publisher Writer) STOPS it here so a long-lived node does not accrue a never-stopped Writer per session. The journal store outlives the Writer, so a fresh reattach respawns + replays. Direct has no such process — this is a no-op — and any emitter that omits the callback is treated as a no-op.

steer_accepted(session_state, ref, request)

(optional)
@callback steer_accepted(session_state :: struct(), ref :: map(), request :: map()) :: :ok

OPTIONAL steer-accept hook. Called once per ACCEPTED steer (never on a reject or a duplicate). ref is the accept reference (%{turn_id, offset, client_msg_id}) and request is the resolved steer map (%{text, expected_turn_id, client_msg_id}). A durable emitter (Journal) appends one "steer" record so a reattacher replays the redirected turn; the default Direct emitter omits this callback (it keeps no journal) and the Session treats an omitted callback as a no-op.

turn_completed(session_state, rendered, turn_id)

@callback turn_completed(
  session_state :: struct(),
  rendered :: term(),
  turn_id :: pos_integer()
) :: :ok

turn_started(session_state, prompt_req, turn_id)

@callback turn_started(
  session_state :: struct(),
  prompt_req :: term(),
  turn_id :: pos_integer()
) ::
  :ok | {:error, :turn_in_flight}