nvoken

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}/stream

This 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:

EventDurable cursor?Use
invocation.acceptedYes on streamed admissionAdmission acknowledgement
invocation.updateYesCanonical lifecycle and new messages
invocation.resultYesTerminal composed result
output_text.deltaNoLive text preview
thinking.deltaNoLive thinking preview when available
stream.resyncNoDrop previews and wait for canonical state
stream.endNoReconnect 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/stream

Durable 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}/result

Only completed, incomplete, failed, or cancelled means the turn is over. Provider output and preview text are not success proof.