Answer streaming for machine consumers
Consume final answer revisions, citations and evidence from the versioned event stream.
Q2.4 adds an opt-in version 1 event contract to POST /v1/answer. Python and TypeScript expose answer.events and answer.collect; React's useAnswerEngine and QAnswerEngineChat use the same TypeScript reducer. These changes are in the prepared, unpublished 1.1.0 packages. Deploy the matching API before adopting them. There is no silent fallback to legacy streams.
Start and reconnect
Send the normal answer request with "stream": true, "stream_version": "1" and a bearer credential with ai:answer. The server chooses a UUIDv4 run_id, independently of caller request IDs. A successful response has Content-Type: text/event-stream, Cache-Control: no-store, X-Q-Stream-Version: 1, X-Q-Run-ID and X-Q-Replay-Expires-At (Unix seconds). CORS exposes these response headers. Check the handshake before interpreting events.
After a lost connection, use GET /v1/answer/streams/{run_id}, with the same credential identity and Last-Event-ID: {run_id}:{sequence}. With no cursor, observation starts at the first retained event; {run_id}:0 is also accepted. The response is ordered after that cursor. GET observes the existing run; it never calls the engine or starts tools. started.resume_path is relative to the configured API prefix, so custom prefixes are preserved. Do not send a replay cursor on POST. If the initial POST fails before a valid handshake, its outcome is unknown: reconcile it rather than issuing another POST automatically.
Both SDK machine readers default to two additional GET connections, with a configurable maximum of five, and a 600-second total observation deadline across connections. Python uses streaming_timeout; TypeScript uses streamingTimeoutMs or per-call timeoutMs. These machine-reader deadlines differ from the older Python readers' inactivity timeout. Protocol errors, explicit terminal failures and HTTP permission/expiry errors propagate; EOF alone never reports success. Closing/cancelling the original response reader stops synchronous generation. A replay reader only observes retained events. Generation remains capped at the configured server stream budget, at most 600 seconds. Use an explicitly durable agent job when execution must survive an observer disconnect; see cancellation and deadlines.
Wire format and state
Each UTF-8 SSE frame contains id, event and JSON data. The frame's id equals the envelope cursor, and event equals its type:
id: 11111111-1111-4111-8111-111111111111:2
event: delta
data: {"version":"1","type":"delta","run_id":"11111111-1111-4111-8111-111111111111","sequence":2,"cursor":"11111111-1111-4111-8111-111111111111:2","revision":0,"payload":{"delta":"Draft text","provisional":true}}
Sequences begin at 1 and increase by exactly one; cursors are opaque to applications. Revisions begin at 0. A consumer validates identity/version, ignores already-applied sequences, rejects forward gaps and unexpected revision changes, and only advances its cursor after validating a complete frame. SSE comments, CRLF/CR/LF line endings, multi-line data, a leading BOM and UTF-8 split across network chunks are supported. An incomplete last frame is discarded and resumed from the preceding cursor. Unknown event types and extra fields are permitted: advance the cursor without altering answer/evidence state. Only a recognised revision event may change the revision number. A new envelope version requires an explicit client update.
| Type | Payload and behaviour |
|---|---|
started | First event only: expires_at, resume_path, initial replay_safe eligibility. Eligibility can subsequently become false as resources are used. |
delta | delta string and provisional: true. Append for draft display only. |
revision | reason and replace: true; revision increases by one. Clear draft text, sources and citations. Grounding rewrites and Quick-to-Pro escalation emit this boundary. |
progress | stage and flexible details. Informational engine/tool activity; not final evidence or a successful tool receipt. |
approval_required | approval_id, kind (plan, tool, budget), action_version, policy (required, advisory) and details. Expose the proposed action for an explicit decision. |
heartbeat | Empty payload while execution is waiting; does not change the draft. |
completed | result conforming to AnswerResponse. Replace all draft text, sources and citations with this authoritative result, even when its answer is empty. Completion means a result is available; inspect result.quality.degraded, result.quality.completeness and result.quality.issues for its quality. |
failed | Safe code, message, retryable: false and outcome (failed or unknown). Stop and reconcile any uncertain actions. Never infer that side effects were rolled back. |
Exactly one terminal event (completed or failed) ends a healthy producer. SDKs expose failed then raise AnswerStreamError; malformed frames raise AnswerProtocolError. answer.collect returns only after completed. Provider exceptions are not copied into failure payloads. Missing terminal frames, lease loss and transport-store failure are incomplete outcomes, never implicit completion. Known payload schemas and envelope references are published under x-q-answer-stream in public OpenAPI; progress/provider details remain extensible leaf data.
Retention, permissions and recovery limits
Q3.2 uses the shared PostgreSQL journal; Redis is no longer the version-one replay store. Events expire 900 seconds after creation, without extension on read/reconnect. Each run retains at most 1,024 events / 8 MiB and accepts at most 262,144 bytes per encoded event. The answer producer keeps its existing 8 MiB total emitted bytes budget (plus a small terminal failure). Reads are bounded to 16 events per page and drain all remaining pages before terminal closure. Expired owner metadata returns 410 until tenant-scoped cleanup removes it, then 404. Producers send heartbeats while waiting; an active run with no stored progress for 20 seconds has lost its lease and returns stream_interrupted.
Every observation checks tenant and original principal, delegation and selected space. Retained local-document derivatives additionally require current document access and the original content/ACL/tombstone fingerprint across all chunk families. Permission revocation or source changes invalidate replay. No substitute source or cached permission permits delivery. The implementation checks at each selected batch; as with any response, it cannot revoke bytes already delivered.
Replay is conservatively unavailable for workspace, attachment, conversation/session, source-page, explicit-context, federated or MCP-bound requests, and when memory, clarification or unclassified tool activity introduces evidence without revision checks. Web evidence classified as external also disables replay. Initial observation can continue while authorised; reconnect returns stream_evidence_requires_refetch. The initial replay_safe flag is a hint, not a guarantee. A source count above 256 fails closed. This deliberately preserves Q1.3's permission boundary.
Typical errors: 400 stream_cursor_invalid; 404 stream_not_found (including another principal); 403 revoked source/delegation access; 409 stream_cursor_ahead, stream_interrupted, stream_evidence_changed or stream_evidence_requires_refetch; 410 stream_expired/stream_cursor_expired; 503 stream_store_unavailable/stream_evidence_unavailable. Before headers these use the normal HTTP error envelope. After headers a failure to authorise/read closes the connection; GET then supplies the precise error. Clients must not render EOF as completion.
This is transport retention, not durable engine recovery. Disconnecting the initial consumer cancels generation. GET replay reads retained events and never restarts generation; process loss also does not restart it. Approval records retain their independent Q1.7 durability. Full legacy trace/checkpoint generation remains on the legacy answer path; machine final conversation persistence is best-effort. A new run after an unknown outcome is an explicit application/user decision, with side-effect reconciliation where needed.
SDK usage and approvals
Python:
from q_client import AnswerState
state = AnswerState()
async for event in q.answer.events(query="Explain the evidence", state=state):
# state is already reduced; display state.answer as provisional until completed.
if event["type"] == "approval_required":
pending = event["payload"] # Present details to the authorised decision maker.
# Or: result = await q.answer.collect(query="Explain the evidence")TypeScript:
import { initialAnswerState, reduceAnswerEvent } from '@getq/sdk';
let state = initialAnswerState();
for await (const event of q.answer.events({ query: 'Explain the evidence' }, { signal })) {
state = reduceAnswerEvent(state, event);
}
// Or: const result = await q.answer.collect({ query: 'Explain the evidence' });Applications may persist the full reduced state and pass it to events to reconnect after their own observer restart. A cursor alone cannot reconstruct the preceding draft. Treat persisted state as sensitive and scope it to the original credential. The supplied Python state mutates; TypeScript's reducer is pure and returns new state.
List pending records with q.answer.approvals(run_id). After an explicit decision, call Python await q.answer.decide(run_id, approval, approved=True) or TypeScript await q.answer.decide(runId, approval, true). approval must be the versioned proposal presented to the decision maker. Both carry its action_version; stale, expired, foreign and contradictory decisions are rejected by the durable approval API. No helper approves by default. React exposes approvals/decideApproval and renders review details plus separate Approve/Reject buttons. Its final message and source panel use canonical citation indexes.
Legacy answer.stream, Sonar streams, unmanaged agent/job streams and /answer/resume retain their existing contracts; unversioned replay remains unavailable. Go and CLI have no version-1 reducer/reconnect convenience API in this phase. See SDK support and ADR 0013.
Evidence availability
The final result includes the evidence policy and availability report. Strict or mandatory-evidence requests withhold provisional derived output until completion. Read evidence.answer_status before treating the final text as an answer; abstained is a valid completion. Unknown reports from older producers do not establish successful retrieval.
Final answers also expose claim verification and structured output. Schema-constrained requests withhold draft text; inspect structured_output.status before using the JSON value. A completed event can carry an explicit failed structured result.