Build
Streaming & recovery
Follow an admitted Invocation by ID, replay durable frames with a cursor, and treat text deltas as disposable previews.
Stream after admission. The hosted API keeps durable admission separate from the live connection:
POST /v1/invocations -> 202 + invocation_id
GET /v1/invocations/{invocation_id}/streamThis keeps retries easy to reason about. If the admission response is lost,
retry the exact POST body and idempotency key. Once you have the
invocation_id, reconnecting the GET stream can never create another turn.
Let the SDK handle the loop
The TypeScript SDK admits with JSON, synthesizes the accepted event, and then follows the GET stream:
for await (const event of agent.stream("Write a short status update.")) {
if (event.type === "output_text.delta") {
process.stdout.write(event.text);
}
if (event.type === "invocation.result") {
console.log(`\nStatus: ${event.result.invocation.status}`);
}
}Use client.invocation("invk_…").stream() to reconnect to work admitted by an
earlier process.
Know which events are durable
Invocation streams contain:
| Event | Durable cursor? | Use |
|---|---|---|
invocation.accepted | Yes on streamed admission | Admission acknowledgement |
invocation.update | Yes | Canonical lifecycle and new messages |
invocation.result | Yes | Terminal composed result |
output_text.delta | No | Live text preview |
thinking.delta | No | Live thinking preview when available |
stream.resync | No | Drop previews and wait for canonical state |
stream.end | No | Reconnect or stop according to its reason |
Persist only the SSE ID from durable frames. On reconnect, send it as the
cursor query parameter or Last-Event-ID. The explicit query parameter wins
when both are present.
Text and thinking deltas may be lost. Do not store them as transcript messages or use them to decide whether a turn succeeded.
Handle resync and retries
stream.resync means the live preview path could not prove continuity. Clear
the provisional text on screen and wait for the next durable update or result.
Each Invocation also carries an attempt. A higher attempt means execution was
claimed again after recovery. Discard provisional output from the earlier
attempt even if no resync event was seen.
stream.end has two ordinary reasons:
rotate: reconnect with the last durable cursor.terminal: the server reconciled the durable terminal state.
An abnormal network close says nothing about execution. Reconnect or read the Invocation. Closing a stream never cancels the turn.
Follow a whole Session
Use the Session transcript stream to follow every turn in one conversation:
GET /v1/sessions/{session_id}/transcript/streamDurable transcript.update frames contain ordered messages and Invocation
changes. Apply messages before lifecycle changes from the same frame so a UI
does not show a turn as complete before its final message exists.
The JSON transcript endpoint uses the same read model for workers that prefer
polling. Drain one fixed cut to completion, save its resume_cursor, and use
that cursor for the next incremental read.
Browser clients need a fetch-based stream
Runtime authentication uses a bearer header. The browser's built-in
EventSource constructor cannot set that header, so use an SDK or an SSE client
built on fetch.
Do not put the bearer credential in a query string. Query strings are too easy to retain in logs, history, and monitoring tools.
Always reconcile the final state
A stream is an efficient view of the Invocation, not the authority. On any uncertain exit, read:
GET /v1/invocations/{invocation_id}
GET /v1/invocations/{invocation_id}/resultOnly completed, incomplete, failed, or cancelled means the turn is over.
Provider output and preview text are not success proof.