Skip to main content

Overview

The thread message stream pushes JSON frames to the client as the thread changes. It is the client’s only change signal: on every (re)connect the server first sends the current run status, then each change as it happens, so clients never poll.
  • thread_status — authoritative run status: a snapshot on connect, then every change
  • message_data — a message was created or updated
  • message_chunk — streamed visible content of an assistant message
  • reasoning_chunk — streamed model reasoning (opt-in)
  • event — custom events from tools, hooks, and endpoints (ThreadState.emit())
  • telemetry frames — execution progress (step_started, tool_started, stopped, …)
The official SDKs (@standardagents/client, @standardagents/react, @standardagents/vue) decode all of these; the sections below are the wire contract for other clients.

Connecting

Browsers cannot set an Authorization header on a WebSocket, so authenticated clients first mint a short-lived single-use ticket for the exact path and pass it as ?ticket=:
The pathname must be the normalized path of the WebSocket URL, and the upgrade path must equal it as an exact string. The endpoint returns 400 and no ticket when pathname is missing, is not beneath /api/, contains a query or fragment, has a . or .. segment, an empty segment (//), or a trailing slash, or holds a percent-encoded ., /, or \ (%2E, %2F, %5C, in either letter case).

Refused upgrades

A refused upgrade returns an HTTP response and does not open the socket. The body is { "error": { "code", "message" } }. The local development server and a deployed project answer the same way. Browsers hide the status of a failed WebSocket handshake, so a browser client sees only the error and close events. Request a new ticket and reconnect.

Query parameters

string
WebSocket ticket from POST /api/auth/websocket-ticket (required when the runtime requires authentication).
number
Deepest message depth to receive. 0 (default) is the top-level thread; higher values include subagent / sub-prompt messages. Applies to message_data, message_chunk, and reasoning_chunk frames alike.
boolean
true to also receive silent messages. Default false.
boolean
Truthy (1, true) to receive reasoning_chunk frames. Default off.

Server → Client Frames

thread_status

The thread’s authoritative run status. Sent immediately after the socket is accepted (a snapshot of the current status) and then on every change (state or reason). A reconnecting client therefore always resynchronizes from the snapshot; no polling is ever needed.
string
  • idle — nothing is running and the last run did not fail (it replied, called a stop tool, handed the turn to a human side, was stopped by the user, was refused by a hook, or hit a step/turn cap), or the thread has never run (since is 0).
  • running — an execution is in flight.
  • failed — the last run ended in an error.
string | null
The stop reason code of the last transition out of running, for clients that want detail: stop_on_response, stop_tool, awaiting_human, hook_blocked, max_total_steps, max_side_steps, max_session_turns, session_stop, aborted, aborted_by_user, step_error, repeated_tool_error, tool_execution_error, session_fail, fatal_error, execution_lost. Treat unknown codes as opaque. null while running.
A run the runtime lost (deploy, eviction, crash) never stays running: it settles to failed with reason execution_lost.

message_data

A message was created or updated (including status changes such as pending → completed). Filtered by the socket’s depth and includeSilent. data is the complete message record: every member that GET /threads/{id}/messages returns for that message, with the same value and in the same form. A member with no value is present as null, attachments is an array or null, and silent is a boolean. Replace the record you hold with the one in the frame. While a message is still being generated its status is pending and its content may be incomplete. A message has one created_at: its first frame, a read while it waits for a turn, and every later frame and read carry the same value.

message_chunk

A streamed fragment of an assistant message’s visible content. Accumulate by message_id; the authoritative final content arrives as message_data. depth is the depth of that message, and the frame is filtered by the socket’s depth like message_data: a sub-prompt’s chunks arrive only on a socket whose depth admits them.
Append chunks in socket order and keep provisional buffers until their terminal message_data records arrive. Do not erase already-painted text merely because a provider retry or reconnect exposes another message_id; retain it as a reconciliation alias (or as a separate keyed draft).

reasoning_chunk

A streamed fragment of model reasoning. Only sent to sockets connected with ?reasoning=1; not persisted, so it cannot be replayed from history. depth is the depth of the message the reasoning accompanies, and the frame is filtered by the socket’s depth: a sub-prompt’s reasoning arrives only on an opted-in socket whose depth admits that message.

event

A custom event emitted by application code with ThreadState.emit() (tools, hooks, effects, and thread endpoints), and runtime notifications such as file_created, file_updated, file_deleted, and effect_changed.
data is always present. An event emitted with no data, or with undefined, carries "data": null.

Telemetry frames

Execution progress, sent as they happen. Each frame’s type is the telemetry kind itself; timestamp is milliseconds since epoch. Telemetry is advisory — use thread_status for whether the thread is running.

Keepalive

Send the string stream_ping periodically; the runtime answers stream_pong at the edge without waking a hibernating thread. (Older servers answer a plain ping with pong.) These heartbeats are protocol keepalives, not state checks.

Handling Frames

Reconnection

Reconnect with backoff when the socket closes. On reconnect, mint a new ticket, then reconcile once: the first frame is a fresh thread_status snapshot, and a single GET /api/threads/:id/messages read catches up any messages missed while disconnected. Do not add timers that re-read thread state. A close with code 1000 and reason Thread deleted means the thread was deleted. The connection is closed before the delete request returns, and a new upgrade for that thread returns 404, so do not reconnect.

React and Vue

useThread() in @standardagents/react and @standardagents/vue manages the socket, reconnects, and exposes runStatus (the latest thread_status, null until the first frame) alongside connectionStatus/status (the socket’s own connection state).
See React Integration for full documentation.