Skip to main content

Streaming & event contracts

Kaizen streams a lot: chat turns, prompt-chain runs, task run traces, orchestration progress. They share one model.

The core idea: positions, not pushes​

A stream is a cursor over a durable record, not a fire-and-forget push.

The conversation journal in PostgreSQL is the truth. Clients receive journal positions in strict journal order, remember the last position applied, and reconnect from there.

Valkey carries short-lived leases, "the room head changed" hints, and bounded artifact recovery material. Reconnect always re-authorizes membership and fills any durable gap from PostgreSQL. None of those transient records replaces the journal.

Client contract​

  1. Remember the last applied position.
  2. Ignore exact duplicates. Redelivery is expected and safe.
  3. Reconnect from your position, not from the beginning.
  4. Treat a gap as a fill, not a loss — request the missing range.
  5. If the cursor is unusable, reload an authorized snapshot and resume live updates from there.
  6. Never treat a transient disconnect as a failed run. Committed messages, queue transitions, answers, and terminal outcomes catch up in order.
  7. Wake active work after hydration. A private or shared detail carrying an active input wakes that exact input through its resume endpoint. The initiating page follows this rule too: local sending state is not runtime authority. Retry the idempotent wake on a bounded cadence while the input remains active; do not infer or mint a replacement run.
  8. Honor stream_reset. A slow consumer must reload its authorized room projection and reconnect; it must not keep applying an arbitrary delta suffix after its bounded subscriber queue overflowed.

A Reconnecting or Live updates are delayed state means the transport is degraded. It does not mean a message or a run was lost.

"Transient" includes an abort the client never asked for. A browser cancels in-flight requests on its own — a navigation, a restored page, a renderer under pressure — and that surfaces as an AbortError indistinguishable from a deliberate close unless the client checks its own abort signal. Only the client's own signal ends the loop; every other interruption reconnects from the last applied position and is announced to the member. Ending the loop on a foreign abort is how a live-looking conversation goes permanently silent, and it is the one failure the member reads as the whole application freezing.

The reconnect is observable rather than implicit: the next attempt re-opens GET /api/conversations/{id}/events with after=<last applied position>, the same position in Last-Event-ID, and a fresh connection_id, after a bounded backoff. The member-visible notice applies to private conversations as well as shared rooms — both run on the same journal stream, so both say Reconnecting while the transport is down.

What is projected​

Only closed, member-safe facts enter the conversation journal and therefore the stream:

  • messages and their attribution,
  • queue transitions,
  • answers and citations,
  • bounded member-visible Thinking attached to a completed answer,
  • artifacts and linked resources,
  • membership changes,
  • terminal outcomes.

Never projected: hidden model reasoning, raw tool arguments or results, credentials, hidden prompts, presence, actor-only approvals. A failed tool step's error_class is a word from a closed vocabulary, not content derived from the result.

Internal runtime events are a separate, more detailed journal that does not reach the browser.

Visible Thinking is the reasoning text that the selected provider elects to return through Pi's thinking_delta stream. It is not the provider's hidden chain of thought. With Plain-speak disabled, Kaizen forwards those visible fragments in arrival order without summarizing or rewriting them. Providers may still return a detailed narrative, a short summary, coarse batches, or no reasoning text at all. The selected effort is a reasoning budget, not a promise of a particular transcript length or cadence.

The conversation event stream carries durable member-visible Thinking progress and transient answer delta and artifact-content artifact_delta frames for the active turn. Publishing a durable head flushes the transient publisher first, so a terminal journal frame never overtakes fragments already admitted from the same runtime stream. Fragments the runtime emits after the terminal was committed fall outside that barrier, and a durable row can also reach a client by journal catch-up while a transient frame cannot — so a late fragment can still arrive after the terminal in the browser. A turn that has recorded its terminal is therefore settled on the client, and later transient frames for it are ignored rather than applied: applying them would reopen a finished turn, restore its in-progress activity, re-append text the committed answer already contains, and reopen its artifact session. A rerun reopens the same turn deliberately, and its frames apply normally.

In a shared room, the same member-safe progress is fanned out to current participants. The projector persists the first visible status, the first meaningful fragment, and later 512-character checkpoints, then folds the exact bounded visible Thinking stream into the terminal assistant message as thinking_trace; hidden reasoning is still prohibited. If a process or provider attempt stops after a durable checkpoint but before its terminal answer, the next exact run projection rehydrates that bounded trace and appends new visible Thinking before committing the terminal message. Thinking alone does not prevent a bounded provider retry. With the turn engine enabled, a failed attempt's Thinking never enters the retry request or later model conversation history; the legacy compatibility rollback does not provide that isolation. New Thinking from a retry is genuine new progress and remains visible even if its wording repeats. The retained trace is presentation recovery only and is never returned to the model as conversation context. The projector publishes a live Thinking fragment before persisting its bounded checkpoint. That checkpoint carries the fragment's cumulative character offset, allowing a current viewer to recognize bytes it already rendered while a replaying viewer appends the same bytes once. The checkpoint drains through a bounded ordered writer while later provider deltas continue. A non-Thinking journal event, cancellation, failure, or terminal is a hard barrier that waits for every earlier checkpoint, so removing the database round trip from token cadence does not weaken journal order or recovery. The client counts each received fragment once and accumulates every admitted provider request, so the live turn estimate follows the same multi-call basis as terminal usage across reasoning, prose, and artifact generation. The latest request-local input remains available for context diagnostics. Pi attaches provider-normalized usage to each completed assistant turn. The runtime emits that cumulative prompt, output, cache, and call-count checkpoint immediately, and the client replaces open-call estimates through that output-character watermark before estimating the next call. Provider-reported checkpoints are exact. If a provider omits usable usage, the gateway supplies a tokenizer-based checkpoint that remains explicitly estimated; its provenance is not inferred inside Pi. The live label remains approximate because the currently open provider call has no terminal usage yet; the value can adjust at a checkpoint. Each call transition also projects one bounded, non-model-visible agent_progress.usage_progress journal checkpoint. This is deliberately not a per-token write: it gives a participant who joins or refreshes mid-turn the same cumulative call boundary as existing viewers without coupling provider token cadence to PostgreSQL. Replaying the checkpoint through the normal context projection is idempotent when the same viewer already received its transient frame. The terminal answer carries the same exact-or-estimated provenance. A provider that does not emit incremental bytes still follows the same lifecycle, but the UI can expose only the coarser frame cadence that provider supplies. For an artifact update, it keeps the open view's identity stable and applies the partial next content to that same canvas or editor. The terminal tool result freezes the same checkpoint as the completed immutable version. A rejected update leaves the previous version untouched. An interrupted update retains the last streamed content in its already-opened version, and a later semantic follow-up resumes that same version rather than appending another one.

The live bus keeps the latest full artifact argument snapshot per active tool call for five minutes. Every artifact fragment advances that cumulative snapshot, including staged append_artifact calls whose runtime event carries no preassembled arguments. The published live frame carries the same cumulative value. Replay reads once during preparation and again after subscription, closing the window in which a fragment could otherwise land between those operations. Clients accept only an equal or longer snapshot, so the second read and already-queued frames are idempotent and cannot roll the Workspace backward or duplicate content. A tool terminal clears its call snapshot and a run terminal clears remaining snapshots. These are transient recovery hints, not journal events or completed artifact versions.

artifact_interaction frames carry a client-generated event id, exact artifact id, deterministic element path, closed event kind, and bounded input fields. The browser may submit up to 64 ordered interactions in one 16 ms artifact_interactions batch. The server authorizes the member once, verifies that every referenced exact artifact is active room content before publishing anything, and preserves child order. Valkey expands the batch into the latest 256 individual replay entries for one hour so a reconnect can rebuild the lightweight browser state. Durable application state remains outside this protocol.

Live publication uses a bounded shared Valkey command pool and one pattern subscription per API process, which multiplexes room signals into bounded local viewer queues instead of allocating one Valkey connection per browser. Source fragments drain through independent conversation lanes: order is strict within a room, a slow room does not stall another room, and only a full lane applies bounded backpressure to its producer. Before a durable journal head is announced, its room lane is flushed so completion cannot overtake already admitted Thinking, answer, or artifact fragments.

The Pi-sidecar reconnect relay retains that same sequence in a bounded in-memory log. Ordinary token frames append in constant time; copying and compaction occur only when the configured byte bound is crossed. A detached backend can replay its missing suffix without making each live token progressively more expensive.

Durable head hints coalesce to the newest position. If transient traffic fills a viewer's queue, the server replaces the incomplete suffix with a stream_reset instruction; that client reloads its authorized PostgreSQL projection and bounded artifact snapshots while other room viewers continue without backpressure.

Keepalive and presence-heartbeat control frames mark initial subscription or an idle interval. A transient event or durable-head hint still triggers the authoritative journal catch-up, but active traffic does not manufacture an idle control frame between consecutive live events.

Clients initialize the active assistant as streaming at input acceptance, not at the first prose delta. That keeps the flashing cursor and provider-turn token indicator together through reasoning and tool-only intervals. Native sandbox outputs use the terminal answer's run_artifacts projection; the journal stores that bounded list on the terminal answer itself, so the live frame and replay cannot disagree about which files belong to the turn. The projector binds each saved file to the producing tool's room-safe call reference, the same reference on durable tool progress. After a lost completion frame, the client can finish a building pane only from a saved file attributed to that exact call; a second call in the same run may produce the same filename. Older resource-only files remain available through their authorized links, but without a retained call reference they cannot settle a building pane. The room's durable resource_linked event supplies the exact conversation and reference identifiers used by preview and download. The collaboration reducer binds that durable link to the exact active run immediately and retains it when the terminal answer omits a redundant artifact list; replay projects the same link onto the originating turn after reload. Private turns use the same durable resource_linked projection, so reload reconstructs the originating answer and Workspace output instead of depending on transient state.

A delegated child has two answer projections with different consumers. Its complete member-safe final answer is carried by the child card's terminal frame. The parent model receives a separate readable, sentence-bounded digest with a backend-only status indicating whether the projection was shortened. The browser keeps the established Working, Completed, and Failed presentation; it does not expose internal partial or truncated projection labels.

The adapter computes the trusted pre-execution approval decision before an artifact fragment can cross the backend stream. Ordinary artifact_delta frames for an existing artifact update pass through immediately after the owner-resolved building version is committed. For a gated create or update, the backend holds every fragment under the scoped tool-call identity and sends none of its content to the browser. A successful durable decision marks that exact call; denial drops the buffered fragments. The execution callback independently rejects a matching pending approval, so transport ordering cannot turn presentation progress into tool authority.

New create_artifact calls have a stricter checkpoint boundary. Every delta is buffered until the complete arguments form one unique-key top-level JSON object. The adapter rejects duplicates and the reserved renderable-artifact pair before creating an owner-only row, then forwards the buffered ordinary call with that durable reference. A stop, failure, or interruption before the full object terminally fences the call, so late provider fragments or a late callback cannot materialize it and there is no partial create to resume.

Existing-artifact updates stream after the backend creates their owner-only building version. Persistence advances that call-correlated row in bounded 512-character checkpoints; the exact execution, failure, or interruption boundary forces the latest in-memory content durable. The checkpoint reference rides each transient frame so the pane uses the durable version identity from the outset. Stop or transport failure marks the row interrupted or failed; tool success marks that same row completed. Published and room-shared reads continue to resolve the last completed version until then.

How an open row settles depends on whether the call ever reached execution and on whether this call opened the row, and the cases are deliberately different. A run that ends abnormally leaves its open rows interrupted: the call was in flight, its partial content is real work, and a follow-up resumes that same version. A run that ends normally can still hold an open row, because the projection opens durably while the model is writing the call and a model may abandon a call it has begun. That row records work no tool ever performed, so normal completion discards it rather than settling it. Settling it instead would leave a version the member cannot resume — no later call carries its call id — and one no conversation-scoped read can serve, since the room reference is only written at completion. A discard is refused for any row that already reached a member-visible resting place: a completed generation, a published version, one another version descends from or was remixed out of, or one a conversation already references.

Two further conditions narrow it, because neither is visible from the row itself. A discard applies only to a row this call opened: both checkpoint paths can instead rebind an interrupted, failed, or superseded version onto a later call, and a rebound row carries work the member may still resume. And a terminal frame is not proof of a clean finish — a runtime-budget abort and a length-truncated answer both arrive through the same done event as an ordinary completion — so a turn that stopped short interrupts its open rows rather than discarding them.

note

agent_run_completed and generation_status are independent facts. A run completing says nothing about whether a given artifact completed, and a non-completed artifact is not evidence that its run failed.

The conversation-scoped read GET /api/conversations/{id}/artifacts/{id} resolves through conversation_resource_reference, which is written when the generation completes. A non-completed artifact therefore has no reference and the route answers 404 — by design, and not a status filter. The status is not served with the row, because a version that is still being written has no stable content for a conversation reader to cite.

New artifact imperatives use a transaction-shaped writer. begin_artifact creates one owner-, turn-, call-, and execution-fenced draft; append_artifact accepts an exact sequence, prior digest, and idempotent receipt. Its per-run character bound is derived from the same trusted effective model-output ceiling bound into the run manifest, after reserving answer space, and never exceeds the 400,000-character artifact total. This lets a capable model finish most artifacts in one complete append without weakening the checkpoint boundary. finalize_artifact accepts only the current sequence and content digest. discard_artifact terminalizes a draft that cannot be validated. The browser accumulates append deltas against one workspace identity, but append success remains building; only finalization sets the stream and durable row to completed. A recovered attempt rotates the private execution fence and may continue the interrupted cursor, so callbacks from the expired attempt fail closed instead of racing the replacement.

The four staged operations are advertised to each provider step as one atomic capability: Kaizen either exposes the complete authorized protocol or defers all of it. If an older or stale step nevertheless requests a staged operation that was not in its captured tool binding, Pi records one typed failure and stops that attempt while retaining the private draft checkpoint. It does not spend the remaining model-call budget retrying an impossible protocol.

The client retains its exact-call buffer as rolling-version defense, but it is not the security boundary. A released replay stays transient until the durable tool terminal settles it; the answer's committed artifact reference is a recovery signal if transport ordering hides that terminal. Update replay subdivides provider-coalesced argument fragments into bounded presentation frames. Create replay remains buffered until the complete unique-key object is admitted, so it cannot expose a durable ordinary identity before the final kind and language are authoritative. Code replay inherits an already-open update's language so approval does not degrade syntax highlighting.

An explicit Stop or Cancel is itself an actor-bound conversation input. It terminalizes the active accepted input before the producer loses authority. The append-only journal retains the member request and interruption; a later request reads that boundary and unfinished artifact metadata for semantic continuity without relying on a client-side continue keyword.

Fencing also closes the detached producer's upstream async iterator. This must happen even if cancellation interrupts orchestration-frame persistence: the iterator's finally path marks open artifact checkpoints interrupted and uses the server-authored cancellation signal to abort the sidecar run. Closing the iterator because its host task or process is disappearing carries no such signal: it detaches delivery, persists no cancellation terminal, and leaves the accepted input to the fenced lease-recovery path. A subscriber disconnect is likewise observer-only.

POST /api/conversations/{id}/inputs commits the member event and queue event before returning 202 Accepted. The event stream is only an observer, so a navigation or disconnect after acceptance cannot cancel or erase the turn.

Exactly one terminal event​

A run produces exactly one terminal conversation event, even when callbacks, timeouts, cancellation, salvage, and process recovery race each other.

Clients can rely on this: seeing a terminal event means the run is over, and a second one will not arrive to contradict it.

Authority on reconnect​

Reconnect is not a resumption of trust. Every reconnect re-authorizes active membership before replaying anything.

If a member left or was removed, the connection stops and retained room state is cleared from the client. An old cursor cannot regain access.

Any resumed stream, tool callback, timeout salvage, or terminal publication that presents a different member, conversation, audience, agent, or resource scope than the run was authorized with is rejected rather than reconciled.

For any chat, wake an accepted input with POST /api/conversations/{id}/inputs/{input_id}/resume. That endpoint returns acceptance, not transcript bytes; continue consuming the room journal. The server checks current write access and exact actor identity on every wake.

Known limit: a reconnect does not replay the answer's prose​

Only closed, member-safe facts enter the journal, and assistant text fragments are not among them. Thinking checkpoints, tool steps and the terminal answer are durable, so a client that reconnects mid-turn recovers them from the journal; in-progress artifact content comes back from the short-lived artifact snapshot described above, not from the journal. The transient delta frames that stream the answer a fragment at a time are not durable, and there is no cursor that can bring them back.

The consequence is narrow but real. A member whose connection drops partway through an answer reconnects, is served the turn from the journal, and receives the terminal answer in one piece. Nothing is lost from the conversation — the reply is complete and correct — but it materialises in a single jump, and the progressive rendering that was running before the drop does not resume.

This is a deliberate trade, not an oversight. Journaling every text fragment would put a PostgreSQL write on the provider's token cadence, which is exactly the coupling the agent_progress.usage_progress checkpoint above is designed to avoid. A bounded prose checkpoint on that same model would be the honest way to close the gap if the behavior ever needs to change; per-fragment durability would not.

Two things bound how often it can happen. Every long-lived stream carries a keepalive, so a silent one does not read as idle to a proxy that would otherwise close it — the conversation event stream on SHARED_CHAT_PRESENCE_HEARTBEAT_SECONDS, and the agent-builder and sandbox streams on MEMBER_CHAT_STREAM_HEARTBEAT_SECONDS; see Idle timeouts in front of the API. And a turn that has already recorded its terminal is settled, so a reconnect after the answer lands replays it whole with nothing missing.

The agent-builder and sandbox keepalive spans the agent run itself, not the preparation before it. Assembling the spec, tightening capabilities and loading context all happen after the opening frame and before the first run event, and they emit nothing. That window is short in practice, but an upstream idle timeout set below it will still close the response there — so size a proxy timeout against the whole turn rather than against the keepalive interval.

note

If a member reports that streaming "sometimes stops working" while the answers themselves are fine, this is the first thing to check. Thinking and tool steps surviving while only the prose stops streaming is the signature of a dropped connection, not of a model or pipeline fault.

Streaming endpoints​

StreamEndpoint
Conversation eventsGET /api/conversations/{id}/events
Condense progressGET /api/conversations/{id}/condense/{task_id}/events
Task run traceGET /api/task-runs/{run_id}/events/stream
Prompt chain runPOST /api/prompts/{id}/run/stream
Prompt chain debugPOST /api/prompts/{id}/debug/stream

The condense stream is an observer over a durable task. Its browser client reopens the same task URL after a transient disconnect; the endpoint re-authorizes the owner and replays current progress or the terminal result. Reconnect never creates another task or model call.

Prompt-chain POST streams create a fenced producer before returning the first frame. The response is an observer over an owner-scoped orchestration journal; disconnecting the response does not cancel the producer. Frames carry a gap-free numeric SSE id. A client reconnects with GET /api/prompts/{id}/runs/{run_id}/stream?after_sequence={id} and may also send Last-Event-ID. The server re-authorizes prompt ownership on every reconnect and sends a comment heartbeat while a step is otherwise silent.

Cancellation is explicit: POST /api/prompts/{id}/runs/{run_id}/cancel terminalizes the journal before fencing local execution. GET /api/prompts/{id}/runs projects interactive and Task-launched executions into one audit history; Task records remain authoritative in the Task ledger.

The upstream model stream​

The streams above are Kaizen's own contract with a browser. Behind them sits a different one: the OpenAI-compatible SSE stream the agent runtime consumes from the model, proxied through /internal/pi/model/v1/chat/completions.

The proxy boundary always receives the selected reasoning level as reasoning_effort, even when Pi used provider-specific catalog metadata to identify the deployment. Reasoning capability still gates the field, so a non-reasoning deployment receives no unsupported option. Provider-visible reasoning deltas are forwarded in arrival order; private reasoning that the provider does not emit is never synthesized or exposed.

That stream is normalized in one place before the runtime sees it, because OpenAI-compatible deployments disagree on two points in practice. Servers that resend the full accumulated content or reasoning_content in each chunk are reduced to the unseen tail, so a delta-only consumer does not replay the answer. Tool calls a chat template rendered into content rather than tool_calls are withheld from the answer text and re-emitted as tool_calls deltas. For the XML form of an artifact create or update, the normalizer emits the content argument as soon as that parameter begins and continues emitting only its newly arrived tail on each provider frame. Its incremental parser holds split closing markers across chunk boundaries without rescanning the accumulated artifact source. The arguments become valid JSON only when the provider closes the call, so an interrupted or malformed call can be displayed as partial progress but cannot be dispatched as a tool invocation.

Pi structural events such as thinking_start and toolcall_start do not count as visible progress. The observable model-activity heartbeat continues until a non-empty Thinking, answer, or artifact fragment is actually projected, so a quiet provider cannot make the member-facing response state disappear early. When a room snapshot reconciles during an active turn, a durable Thinking prefix cannot replace a longer live prefix; the terminal snapshot then confirms the same monotonic trace for reload and audit.

Tool-call identity is nevertheless retained as structural stream state. Some Pi-supported providers repeat the call id and name on every delta, some supply them only at start, and others resolve them only at toolcall_end. The shared consumer binds by content index, buffers argument fragments until identity is known, and flushes them in order. If a provider supplies only the completed tool call, its finalized arguments produce one artifact frame. This normalization is descriptor-driven; it contains no model-name or provider-name branch.

Content blocks are reduced by their provider-supplied content index, so interleaved Thinking, answer, and tool-call deltas do not assume that one block will finish before another begins. A response that reaches its output limit cannot dispatch any tool calls from that response: their arguments may be truncated, so the whole batch becomes ordered error tool results and the model must issue complete calls on the next step. A structurally empty length stop receives one typed continuation at the same reasoning effort. That continuation is semantic recovery, not a transport retry, and a second empty terminal result fails honestly.

If you are adding a consumer of provider frames, consume them after this seam rather than re-deriving the corrections. Usage accounting deliberately runs on the raw frames the provider sent, so estimates describe upstream output rather than the corrected projection. See Model gateway.

Task run traces​

A task's trace is an append-only, gap-free member timeline. A current worker lease allocates its sequence; authenticated member controls append through a separate audited path.

Durable event identity makes redelivery idempotent, and SSE replay uses the durable sequence — replaying creates no new event and no audit row.

Trace envelopes carry state, reason category, timing, usage, version, and opaque source references. Opening source detail re-authorizes it and returns one of:

StatusMeaning
authorizedA bounded, redacted projection with provenance
redactedReveals no source existence or content
payload_unavailableRetained trace metadata outlived its source

A running step emits step.progress (payload status: running and step_index) at most every ten seconds while its runtime is active. Clients should treat it as liveness for that step and attempt, not as a settled state. The task's run list omits draft test runs; read a draft run's id from the task detail's latest_test.

Input acceptance and the queue​

POST /api/conversations/{id}/inputs accepts a message into the room's durable FIFO lane and returns its position.

Acceptance is idempotent by key. A duplicate submission converges on the same logical input rather than running twice, and a worker or backend restart resumes the accepted turn without creating a second model request.

In a collaborative room, a normal message queues; steering targets the active run and is recorded separately.

Presence​

Presence is a separate, disposable channel — short-lived viewing and typing leases in Valkey.

It is never journal content and never audit data. If the presence service is unavailable, the roster shows Presence unavailable while durable messages continue to replay normally. Build clients so presence failure degrades a badge, never the transcript.

A presence frame is delivered on the stream's idle keepalive, and a typing change also wakes subscribers immediately, so a typing lease reaches the other tabs promptly rather than waiting for the next keepalive. Joining and leaving are carried by the keepalive frame, so a roster change can take up to one heartbeat to appear. Each frame is projected for the member receiving it: a member never appears in their own typing_members, and only members currently present are listed. Treat every frame as the complete current state rather than a delta.