Skip to main content

Streaming

GET /v1/ask/:id/stream delivers an agent turn as server-sent events: persisted replay, live frames, keep-alives, terminal close, and a 20-stream workspace cap.

GET /v1/ask/:id/stream streams a turn started with [POST /v1/ask](post-ask) as server-sent events, so an integration can show the answer forming, surface tool activity as it happens, and render text deltas token by token instead of waiting out the poll interval. Streaming is additive: the turn is started and charged once by the submission, the [poll contract](ask-results) is unchanged, and streaming then polling the same turn (or the reverse) never double charges. Use whichever read model fits each consumer; both observe the same turn.

The stream opens by replaying every event the turn has already persisted, then follows with live events as the turn produces them, and closes when the turn's status reaches completed, failed, or awaiting_confirmation. Because the replay is sourced from the durable turn record, a dropped connection loses nothing: reconnect and the full sequence arrives again from the start. If the turn is already settled when you connect, you get the whole recorded sequence and an immediate close, which makes the stream a valid way to fetch a finished turn's event log.

A missing or cross-tenant ask_id is a real 404 issued before any SSE header is sent, never a 200 stream carrying an error frame, so standard HTTP error handling works before you enter stream parsing. The response is Content-Type: text/event-stream; each frame is a data: {json} line followed by a blank line, and a : keep-alive comment line goes out every 25 seconds so proxies and load balancers see traffic on an idle socket. SSE clients ignore comment lines automatically.

GET/v1/ask/:id/stream

Path parameters

id*uuidThe ask_id returned by POST /v1/ask.

Frame kinds

Every frame is a JSON object discriminated by kind. The stream carries the answer and the audit trail of the turn; the model's raw reasoning (thinking.delta) is deliberately withheld from the public stream, because reasoning text is model-version-dependent and not a contract to build on.

Frame kinds

statusframeThe turn's lifecycle status changed. A terminal status (completed, failed, awaiting_confirmation) is followed by the stream closing.
stageframeA loop stage started or completed.
textframeA block of answer text attributed to a stage.
text.deltaframeAn incremental piece of answer text. Concatenate deltas to render the answer as it forms.
tool.callframeThe agent invoked a tool: id, name, impact, title, and the arguments it chose.
tool.progressframeProgress inside a long-running tool: label, optional done/total counters, optional detail.
tool.resultframeA tool finished: ok flag, a one-line summary, and the duration in milliseconds.
confirmation.requiredframeThe turn paused for a confirmation it cannot grant itself. The stream closes with status awaiting_confirmation.
cardframeA generative UI card the turn produced.
artifactframeAn artifact the turn produced: type, id, label, link.
doneframeThe turn finished composing: a headline summary and the token total.
A workspace can hold at most 20 concurrent streams; the 21st connect is refused with 429. The ceiling exists because a held socket is a resource, not a request: close streams you are done with, and fall back to polling for consumers that do not need live frames. Stream connects are metered in their own daily rate-limit namespace, ask_stream, sized above the ask budget so reconnects never compete with submissions.

End to end: scope, stream, then read the citations

The three surfaces compose. Scope an ask to one pipeline so the answer can only draw on that pipeline's documents, hold the stream to watch the turn work, and when the status frame reaches completed, fetch the poll payload once for the structured citations and the verification verdict. The stream is for watching; the poll payload is the settled, citable result.

1. Submit a scoped ask

2. Stream the turn (curl -N disables buffering)

3. Read the citations from the poll payload

Consuming the stream in TypeScript