Skip to content

Approvals & clarify

Two ambient, session-scoped SSE streams tell a client when the agent is waiting on the user — either for permission to run a tool (approval) or for an answer to a question (clarify). A native client keeps both open for the active session so these prompts surface instantly, then responds over a plain POST. Both use the same SSE transport as chat.

Both streams do an atomic subscribe-plus-snapshot: on connect, the server (under a single module lock) registers your subscriber and sends the current pending state as the first frame — closing the race where a prompt could be raised in the gap between subscribe and snapshot. The first frame is always event: initial; subsequent live prompts arrive as event: approval / event: clarify. Idle keepalives arrive as : keepalive SSE comments (~5s) — ignore them.


Approvals

The agent pauses before running a tool that needs consent (e.g. a shell command). The client shows the prompt and the user picks how to respond.

GET /api/approval/pending — snapshot

?session_id=…{ "pending": <PendingApproval|null>, "pending_count": <int> }.

PendingApproval{ "approval_id", "command", "description", "pattern_key", "pattern_keys": [] }.

GET /api/approval/stream — SSE

?session_id=… (required, else 400).

  • First frame — event: initialdata: { "pending": <PendingApproval|null>, "pending_count": <int> }.
  • Each subsequent prompt — event: approvaldata: <PendingApproval>.

A client parses the initial frame's pending, then updates on each approval frame. (The payload tolerates both the wrapped { pending, … } shape and a bare PendingApproval object.)

POST /api/approval/respond

Body { "session_id", "choice": "once|session|always|deny", "approval_id"? }{ "ok": bool, "choice", "stale_cleared"?, "relayed"? }.

  • choiceonce (allow this call), session (allow for the rest of the session), always (persist the allow pattern), deny.
  • 400 on missing session_id / invalid choice; 409 { "code": "gateway_run_unavailable" } if the run can't be reached; 502 on gateway relay failure.
  • A benign stale click (the prompt already resolved) returns { "ok": true, "stale_cleared": true } — not an error.

Clarify

The agent asks a free-form or multiple-choice question mid-task and blocks on the answer.

GET /api/clarify/pending — snapshot

?session_id=…{ "pending": <PendingClarification|null> }. (Note: the GET path returns no pending_count; the SSE initial frame does include it.)

PendingClarification{ "clarify_id", "question", "choices_offered": [], "session_id", "kind", "requested_at", "timeout_seconds", "expires_at" }.

GET /api/clarify/stream — SSE

?session_id=… (required).

  • First frame — event: initialdata: { "pending": <PendingClarification|null>, "pending_count": <int> }.
  • Each subsequent question — event: clarifydata: <PendingClarification>.

A client distinguishes clarify from approval by the presence of question / choices_offered markers in the payload.

POST /api/clarify/respond

Body { "session_id", "response", "clarify_id"? } (the server also accepts answer / choice as aliases for response) → { "ok": true, "response" }.

  • 400 on missing session_id / response.
  • 409 { "ok": false, "stale": true, "error": "Clarification prompt expired or not found…" } for a stale, expired, or wrong-session answer. Passing the stable clarify_id lets the server reject a stale answer precisely.

Where prompts come from

During a chat turn the agent raises approvals/clarifications as it works. They are delivered on these dedicated streams (not the chat stream), so keep /api/approval/stream and /api/clarify/stream open for the active session alongside /api/chat/stream. Responding unblocks the running turn.