Payloads
Real-time streaming is not enabled on accounts yet. See the overview for what to do instead.
What lands on each node: incremental chunks, the run status a composer keys off, and the finalized message.
Streaming Chunks
Listen on streaming/{conversationId}/{messageSequence}. Each write replaces the whole node, so you receive the accumulated chunks array plus a meta object every time.
{
"meta": {
"run_id": "64403669-5989-4ec3-ad9c-d84223f9679f",
"status": "streaming", // "streaming" | "complete" | "error"
"started_at": "2026-04-23T22:43:44.700Z",
"finished_at": null,
"error_message": null
},
"chunks": [
{ "content": "I can ", "type": "STREAM_CHUNK_TYPE_TEXT", "timestamp": "…", "is_final": false },
{ "content": "help ", "type": "STREAM_CHUNK_TYPE_TEXT", "timestamp": "…", "is_final": false },
{ "content": "with…", "type": "STREAM_CHUNK_TYPE_TEXT", "timestamp": "…", "is_final": true, "finish_reason": "stop" }
]
}
Chunk fields
| Field | Meaning |
|---|---|
content | The token text (or reasoning/tool-call fragment) for this chunk |
type | STREAM_CHUNK_TYPE_TEXT, STREAM_CHUNK_TYPE_REASONING, or STREAM_CHUNK_TYPE_TOOL_CALL |
is_final | true on the last chunk of the message |
finish_reason | stop, tool_calls, length, or error — present on the final chunk |
The chunks array is indexed by position and may contain gaps (nulls) mid-stream as writes arrive out of order — always render by accumulating non-null entries in order, don't assume the array is dense.
Filter chunk types for display: render STREAM_CHUNK_TYPE_TEXT into the message bubble; treat STREAM_CHUNK_TYPE_REASONING and STREAM_CHUNK_TYPE_TOOL_CALL separately (or hide them). When reasoning is enabled, reasoning chunks arrive first, then text chunks.
Detecting completion
A stream is done when any of these is true — check meta.status first, it is authoritative:
meta.status == "complete"(or"error"), or- the last chunk has
is_final: truewith afinish_reason, or conversations/{conversationId}/statusreturns to a non-generatingstate.
Conversation Status
Listen on conversations/{conversationId}/status for a single last-write-wins node that tracks what the conversation is doing. Use it to drive a busy indicator and to de-duplicate out-of-order writes via seq.
{
"state": "CONVERSATION_ACTIVITY_STATE_GENERATING",
"run_id": "64403669-5989-4ec3-ad9c-d84223f9679f",
"seq": "7", // int64 as string
"since": "2026-04-23T22:43:44.712Z",
"error": null // { "code": "...", "message": "..." } on failure
}
state value | Meaning |
|---|---|
CONVERSATION_ACTIVITY_STATE_IDLE | Nothing running — safe to send the next message |
CONVERSATION_ACTIVITY_STATE_QUEUED | Generation accepted, not started yet |
CONVERSATION_ACTIVITY_STATE_GENERATING | Model is producing tokens |
CONVERSATION_ACTIVITY_STATE_EXECUTING_TOOLS | Running tool calls |
CONVERSATION_ACTIVITY_STATE_COMPACTING | Compacting context |
CONVERSATION_ACTIVITY_STATE_ERROR | Last run failed — see error |
Finalized Messages
Once a turn settles, the full message is written under events/{eventName}/{conversationId}/messages (event name llm_v1_common_v1_conversationmessagepublishedevent). Fields here use snake_case:
{
"message": {
"role": 2,
"content": [ { "type": "CONTENT_PART_TYPE_TEXT", "content": "I can assist you with…" } ],
"generated_by": "64403669-5989-4ec3-ad9c-d84223f9679f",
"sequence": 8,
"tool_calls": [
{ "id": "call_abc123", "name": "get_weather", "status": "…",
"is_client_tool": false, "arguments_json": "{\"city\":\"NYC\"}" }
]
},
"eventContext": { "correlation_id": "…", "event_id": "…", "event_name": "…", "emitted_at": "…", "version": "1" }
}
Run lifecycle markers ({ "runId": "...", "status": "..." }) appear under events/{eventName}/{conversationId}/runs for the generation-started and generation-completed events.