Streaming protocol
The HTTP wire protocol for delivering messages to an agent conversation and reading it back by offset, over JSON or SSE.
Every persistent agent conversation is an append-only, offset-addressed record
stream. Delivery and reading are separate operations: a POST durably admits a
message and returns immediately, and the reply arrives in the conversation,
which you read from an offset.
@fabric-harness/client and @fabric-harness/react wrap this surface, and most
applications should use them. Read this page when you are writing a client in
another language, debugging a transport, or building your own UI layer.
Routes
Relative to a persistent agent instance at /agents/:name/:instanceId:
| Route | Purpose |
|---|---|
POST /agents/:name/:instanceId | Deliver one message. Returns 202 on durable admission. |
GET /agents/:name/:instanceId/conversation | Offset-based catch-up read (JSON). |
GET /agents/:name/:instanceId/stream | Catch-up plus live tail (SSE). |
GET /agents/:name/:instanceId/submissions/:submissionId | One submission's status. |
POST /agents/:name/:instanceId/abort | Abort in-flight and queued work. |
conversation and stream both accept session (default default) and
offset. conversation also accepts limit.
Delivering a message
POST /agents/support/ticket-8472
Content-Type: application/json
{ "message": { "kind": "user", "body": "Summarize the open issues on my case." } }message accepts a full DeliveredMessage as above, or a bare string as
shorthand for { "kind": "user", "body": "..." }. The body also accepts
session (default default), initialData for instance creation, and uid
to pin a generation.
The response is 202 as soon as the message is durably admitted — before the
model runs:
{
"submissionId": "sub_01HZX...",
"streamUrl": "/agents/support/ticket-8472/conversation?session=default&offset=42",
"conversationUrl": "/agents/support/ticket-8472/conversation?session=default&offset=42",
"updatesUrl": "/agents/support/ticket-8472/stream?session=default&offset=42",
"offset": "42"
}offset is the stream position before this delivery, so reading from it
returns this message and everything the agent produces in response — with no
race against the agent starting. uid is present when the instance has a
resolved generation.
There is no "wait for the reply" mode on this route in the durable path; read the outcome from the conversation, or poll the submission.
Reading by offset
GET /agents/support/ticket-8472/conversation?offset=42&limit=100{
"incarnation": "inc_01HZ...",
"batches": [{ "offset": "42", "records": [] }],
"nextOffset": "57",
"upToDate": true
}Pass nextOffset as the next request's offset to continue. upToDate is
true when the read reached the tail. A conversation that does not exist yet
returns incarnation: null with an empty batches array rather than a 404, so
a reader can start before the first message lands.
Offsets are opaque strings. Compare them for equality, never order them numerically or arithmetically.
Incarnation
incarnation identifies the stream's producer generation. If it changes between
reads, the stream was rebuilt and offsets from the previous incarnation are
meaningless — discard projected state and re-read from "0".
Live tail over SSE
GET /agents/support/ticket-8472/stream?offset=42
Accept: text/event-streamThe server replays from offset, then tails. Every frame is a JSON object in
data:; : keepalive comments are sent periodically and carry no data.
Frame type | Payload | Meaning |
|---|---|---|
stream_checkpoint | incarnation, offset | Producer generation. A change means re-read from "0". |
conversation | offset, records | Durable records. offset is the next offset to resume from. |
delta | eventId, kind (text | reasoning), delta, submissionId?, turnId? | Token-level text or reasoning fragment. |
toolcall_delta | eventId, toolCallId, toolName, argumentTextDelta, submissionId?, turnId? | Streaming tool-call arguments. |
Only conversation frames are durable. delta and toolcall_delta are
presentation-only: they let a UI render tokens as they arrive, are not replayed
on reconnect, and are always superseded by the conversation records covering
the same turn. A client that ignores them loses nothing but interactivity; a
client that persists them will double-count.
Reconnect by re-requesting stream with the last offset you received in a
conversation frame. @fabric-harness/client reconnects after 90 seconds of
transport silence by default (idleTimeoutMs), and falls back to JSON polling
when SSE is unavailable (live: 'auto' | 'sse' | 'poll').
Records
Each record in a records array is a ConversationStreamRecord:
type ConversationStreamRecord =
| { kind: 'entry'; entry: SessionEntry }
| { kind: 'truncated'; rewoundTo: string; newLeafId: string; reason: string };A truncated record means the conversation path branched — a replay, a
checkpoint restore, or a fork. Drop every projected record after rewoundTo and
continue from the records that follow. Ignoring truncated leaves a client
showing messages that are no longer on the active path.
Projecting records for display
Raw entries are the canonical log, not a render-ready view. projectConversationRecords
from @fabric-harness/sdk/conversation reduces a record sequence into a stable
UI protocol:
import { projectConversationRecords } from '@fabric-harness/sdk/conversation';
const { messages, settlements } = projectConversationRecords(records);A ConversationMessage carries id, role, purpose, display, optional
submissionId/turnId/metadata, and an ordered parts array:
Part type | Shape |
|---|---|
text | text, state: 'streaming' | 'done' |
reasoning | text, state: 'streaming' | 'done' |
file | mediaType, optional id, size, filename |
dynamic-tool | toolName, toolCallId, and a state-specific payload (input-streaming, input-available, output-available, output-error) |
data-${name} | data — agent-authored structured parts written with useDataWriter() |
Honor display: hidden messages are not for users, and diagnostic ones
belong behind a debug affordance. purpose distinguishes a user turn from a
dispatched signal or an advisory injection.
settlements reports terminal submission outcomes — completed, failed, or
aborted, with error and answeredBySubmissionId where they apply. A
submission that joined a busy conversation is answered by another submission's
assistant turn, which answeredBySubmissionId names.
Implementation checklist
For a client in another language:
POSTthe message; keepsubmissionIdandoffsetfrom the202.- Read from that
offset— SSE if you want live tokens, JSON polling otherwise. - Treat offsets as opaque; resume from
nextOffsetor aconversationframe'soffset. - Re-read from
"0"wheneverincarnationchanges. - Apply
truncatedrecords by discarding everything afterrewoundTo. - Render from
deltaframes if you want streaming, but persist onlyconversationrecords. - Stop when a settlement for your
submissionIdappears, or when the submission status is terminal.
See also
- Events — the session entry types that appear inside records.
- Agent behavior — built-in tool semantics reflected in tool parts.
- React — the hook layer built on this protocol.
Agent Events
Subscribe to a typed event stream from any Fabric Harness agent — text deltas, tool calls, shell commands, compaction, approvals, tasks, errors. Available from both import entrypoints.
Context Compaction
Automatic, event-emitting context compaction for long sessions. Default import enables it; /strict opts in explicitly. Threshold and overflow modes.