Stream Chunk Protocol
Updated 3 days ago • October 4, 2026
The stream protocol is defined in @kortyx/stream as StreamChunk.
Core chunk types
sessioncheckpointstatustext-starttext-deltatext-endtool-call-starttool-call-resulttool-call-errortool-resultmessagestructured-datastructured-data-invalidatedinterrupttransitiondoneerror
Typical client loop
Structured Data
Simple mental model:
text-*is for text you show as textstructured-datais for objects you render as UI statetool-call-*is for MCP/tool lifecycle updates whentoolExecution.emitis enabled
For current useReason MCP tool execution, prefer the tool-call-* chunks. tool-call-result is the emitted lifecycle event for one concrete model-requested tool call and includes the provider/MCP toolCallId. tool-result is a legacy generic stream chunk shape retained in the public union for compatibility; it is not emitted by the MCP tool loop.
Examples:
- email drafts
- project tables
- growing result lists
- progress panels
- validation results
- MCP search or action progress
Tool Call Chunks
When useReason(...) runs MCP tools with toolExecution.emit: true, Kortyx emits a small lifecycle channel beside assistant text.
Example start chunk:
Example result chunk:
Example error chunk:
Tool chunks are emitted only when the call opts in with toolExecution.emit. Tool results are still fed back to the model when emission is disabled.
Structured chunk shape
Output contracts selected by useReason({ outputs }) use these same chunks. A contract with stream.fields emits provisional set, append, or text-delta chunks while its dedicated JSON model pass is streaming, followed by a validated final chunk. All chunks for one emission share a streamId; a later emission uses a different one. Text before and after the emission uses distinct segmentId values, so @kortyx/react preserves the order of text and structured pieces. The dedicated JSON pass consumes a model step. Contracts without stream.fields emit only the final chunk after their tool call finishes.
Example incremental chunk:
Example final chunk:
Field meanings:
streamId: stable identity for one logical structured streamdataType: app-defined routing labelkind: how the client should apply this updateschemaIdandschemaVersion: optional schema metadataid: optional app identifiernode: workflow node that emitted the payload
kind values
set: set one field atpathappend: appenditemsto an array atpathtext-delta: appenddeltato a string atpathfinal: replace the whole object withdataand mark the stream done
That is the full public mental model. Most clients should key by streamId and apply chunks in arrival order.
Path contract
pathmeans a dot-separated location inside the object being built, such asdraft.body- dotted paths are valid in raw
structured-datachunks, manualuseStructuredData(...)calls, anduseReason({ structured: { fields } }) - numeric path segments target array indexes, such as
sections.0.body useReason({ structured: { fields } })also accepts*as a single-segment pattern and emits concrete paths, such asassessment_points.commercial_resilience.criteria_label- one
streamIdrepresents one logical object; if multiple fields in that object stream incrementally, they all use the samestreamId - use separate
streamIdvalues only when you are building separate objects
Reducer guarantees
applyStructuredChunk(...) is the canonical reducer for this protocol.
setwrites the value atpathappendappends to an existing array atpath, or creates that array when the path is unsettext-deltaappends to an existing string atpath, or creates that string when the path is unsetfinalreplaces any previously accumulated partial object completely and becomes the source of truth- no chunk is allowed after
finalfor the samestreamId
Runtime enforcement:
- invalid empty or malformed paths are rejected
- impossible container-shape conflicts are rejected
appendon a non-array target is rejectedtext-deltaon a non-string target is rejected- a chunk for a different
streamIdcannot be reduced into existing state for another stream
Checkpoint Chunks
When session checkpoints are enabled through a FrameworkAdapter, Kortyx emits a checkpoint boundary before the public done chunk:
Clients should store the checkpoint id on the assistant message finalized by that stream. @kortyx/react does this automatically in useChat(...).
Field meanings:
id: checkpoint id used by rollback and fork APIssessionId: active session idturnIndex: zero-based session checkpoint positionlabel: optional app-facing label
Good to know: The checkpoint chunk is emitted before
doneso transports that stop reading ondonestill receive the rollback boundary first.
Structured Data Invalidation
Rollback can discard structured streams that were produced after the restored checkpoint. Kortyx identifies those streams with structured-data-invalidated chunks or rollback API results:
Clients that render structured data outside chat bubbles should remove or mark stale any local state keyed by streamId. useStructuredStreams().applyStreamChunk and useChat(...) handle invalidation automatically.
A streamed output contract also sends structured-data-invalidated without checkpointId if its provisional fields were shown but generation or final validation fails. Clients should remove that streamId immediately; a final validated chunk will not follow. The checkpointId field is present only for rollback invalidations.
Producer expectations:
- emit chunks for a given
streamIdin order - use non-empty dot-separated paths or single-segment
*patterns in output-contractstream.fields(the deprecatedstructured.fieldspath also supports them) - do not rely on partial chunks being validated against the final output schema
Good to know:
useReason(...)validates the final object against its output contract (or deprecatedoutputSchema), but incremental structured chunks are enforced only at the path and operation level. ManualuseStructuredData(...)calls can add optional schema checks fordata,value, or appended items.
Recommended React helper
For React clients, prefer useStructuredStreams() from @kortyx/react.
That gives you:
items: stable ordered structured piecesbyStreamId: map-style access for render logicget(streamId): direct lookupclear(): reset live state between runs
Good to know:
useChat()in@kortyx/reactbuilds on top of this and also handles assistant text, interrupts, and message history. UseuseStructuredStreams()directly only when you want custom UI without the full chat abstraction.
Advanced: low-level reducer
If you are outside React or want the raw protocol reducer, use applyStructuredChunk(...) from kortyx/browser.
applyStructuredChunk(...) returns:
data: current accumulated objectstatus: "streaming" | "done"streamIddataType
If you already have an array of structured chunks, you can also use reduceStructuredChunks(...).
How useReason({ structured }) uses this protocol
When you call useReason(...) with structured, there are two main modes.
Final-only mode
If you do not configure structured.fields, Kortyx emits one structured-data chunk with kind: "final" after the model output validates.
Incremental mode
If you set structured.fields, Kortyx can turn selected parts of the streamed JSON into deterministic structured updates before the final object arrives.
Example:
In that mode:
- the model still returns JSON for the full schema
- Kortyx watches the stream and emits early structured updates for declared fields
- all updates from that reasoning call share one
streamId - the final validated object still arrives as
kind: "final"
Current limits:
setfield paths- string field paths as
text-delta - array field paths as
append *wildcard patterns that match one object key or array index segment- non-interrupt flows only
The raw structured-data protocol and useReason(... structured.fields ...) both support dotted path values. useReason(...) extracts those paths conservatively from streamed model JSON, resolves wildcard matches to concrete paths in emitted chunks, and still treats the final validated object as the source of truth.
If a field should appear once and stay stable, it is often simpler to emit it from node logic with useStructuredData({ kind: "set", ... }).
Consuming text and structured data together
Many clients render both channels at once: text for conversation and structured data for UI state.
Notes
doneis terminal for a stream runerrormay be followed bydonesessionhelps clients persist conversation identity
Complete a response before execution finishes
See Background Continuation for completeResponse(), independent execution lifetime, and discovering human requests through agent.listInterrupts() without Studio. A done chunk ends client delivery; it does not prove successful execution.