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: initial→data: { "pending": <PendingApproval|null>, "pending_count": <int> }. - Each subsequent prompt —
event: approval→data: <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"? }.
choice—once(allow this call),session(allow for the rest of the session),always(persist the allow pattern),deny.400on missingsession_id/ invalidchoice;409 { "code": "gateway_run_unavailable" }if the run can't be reached;502on 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: initial→data: { "pending": <PendingClarification|null>, "pending_count": <int> }. - Each subsequent question —
event: clarify→data: <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" }.
400on missingsession_id/response.409 { "ok": false, "stale": true, "error": "Clarification prompt expired or not found…" }for a stale, expired, or wrong-session answer. Passing the stableclarify_idlets 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.