Streaming

The server-sent stream events every agent run produces, and how to consume them.

Next release

The streaming operations answer 501 not_implemented until the agent runtime ships. The event shapes below are final for contract 0.5.0, and the TypeScript types are available today.

Two operations stream: POST /v1/sessions/{session_id}/messages and GET /v1/runs/{run_id}/stream. Both answer 200 with text/event-stream. Each event is an id: <seq> line and a data: <JSON> line.

id: 1
data: {"type":"run_started","seq":1,"run_id":"…","at":"…","agent":{"name":"support","version":"1.4.0"},"release_id":"…","session_id":"…"}

id: 2
data: {"type":"token","seq":2,"run_id":"…","at":"…","text":"Your refund"}

Every event has type, seq (also the SSE id), run_id and at.

Events are durable except token. Durable events are stored: their seq rises by exactly 1 within a run, and a replay returns every one of them. token events are best effort: a token carries the seq of the message it belongs to and a delta that counts fragments within that message, and it may be lost. message_snapshot events make the text whole: each one carries the full text of the message so far, and you replace the message's text with it.

Event types

typeCarriesRender it as
run_startedagent, release_id, session_idA start marker
stepindex, kindProgress
tokentext, deltaAppend to the open message (best effort)
message_snapshotmessage_id, text, delta, attemptReplace the message's text with text
planitemsA checklist
tasktask_id, title, status, progressA task row
tool_startedtool_call_id, tool"Looking up …"
tool_completedtool_call_id, tool, outcome, duration_msDone or failed
tool_outcome_unknowntool_call_id, tool, reason"Checking whether this happened": the tool's effect is unknown, it is not retried automatically, and it is reconciled
questionquestionA question card; answer with POST /v1/runs/{id}/resume
approval_requiredapprovalAn approval card; decide with POST /v1/approvals/{id}
artifactartifactA link or preview
citationsourceA footnote
completedusage, status, outputFinal state and cost
errorstatus, problem, usageFinal state with a problem object
stream_gapafter_seq, reasonNothing; reconnect from your last durable seq
reconnectafter_seq, retry_msNothing; reconnect after retry_ms with after=after_seq

Skip event types you do not recognise. New types may be added in a minor version and are not a breaking change.

Reconnecting

Keep the seq of the last durable event you processed. To resume, call GET /v1/runs/{run_id}/stream?after=<seq>: you receive every durable event with a larger seq, and a message_snapshot for a message that is still being written.

  • A stream lasts at most 15 minutes. Before closing it, the server sends reconnect; reconnect after retry_ms.
  • stream_gap means a stored event could not be read: reconnect from your last durable seq.
  • If after is older than what is kept, the stream answers 410 with replay_expired: read the run's current state instead of replaying it.

Typed events in TypeScript

import type { Schema } from "@sparq/client";

type StreamEvent = Schema<"StreamEvent">;

function handle(event: StreamEvent) {
  switch (event.type) {
    case "token":
      appendText(event.text);
      break;
    case "message_snapshot":
      replaceText(event.message_id, event.text);
      break;
    case "approval_required":
      showApproval(event.approval);
      break;
    case "completed":
      finish(event.usage);
      break;
    default:
      // Unknown or unhandled types are skipped.
      break;
  }
}

On this page