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 changemessage_data— a message was created or updatedmessage_chunk— streamed visible content of an assistant messagereasoning_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, …)
@standardagents/client, @standardagents/react,
@standardagents/vue) decode all of these; the sections below are the wire
contract for other clients.
Connecting
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=:
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.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 (sinceis0).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, session_fail, fatal_error, execution_lost.
Treat unknown codes as opaque. null while running.message_data
A message was created or updated (including status changes such aspending → completed). Filtered by the socket’s depth and includeSilent.
message_chunk
A streamed fragment of an assistant message’s visible content. Accumulate bymessage_id; the authoritative final content arrives as message_data.
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.
event
A custom event emitted by application code withThreadState.emit() (tools,
hooks, effects, and thread endpoints), and runtime notifications such as
file_created, file_updated, file_deleted, and effect_changed.
Telemetry frames
Execution progress, sent as they happen. Each frame’stype 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 stringstream_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 freshthread_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.
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).