Stream a turn's answer into your UI
You send a prompt and you want the answer to appear the way a person types it, not all at once several seconds later. This page shows you how to draw the agent’s message as it arrives — and how to be sure what you drew is what the host actually said.
The one idea to take away: deltas are for drawing, item/completed is the
truth. Append deltas as they land so your UI feels alive, then let the
completed frame have the last word.
The three frames you care about
Section titled “The three frames you care about”A turn’s agent message arrives in three acts:
item/started— the message exists now, and it is empty. This is where you create the bubble, keyed by theitemIdon the frame.item/delta— a piece of text to append. Each one names theitemIdit extends and thefieldit extends (text), so you always know where it goes.item/completed— the message is final, and the frame carries the whole text. Replace what you accumulated with it.
The deltas themselves are small and boring, which is the point:
{"dir":"server","raw":"{\"jsonrpc\":\"2.0\",\"method\":\"item/delta\",\"params\":{\"sessionId\":\"0198f0aa-1111-7000-8000-0000000000aa\",\"viewCursor\":\"v:0198f0aa-1111-7000-8000-0000000000aa:7\",\"itemId\":\"0198f0ac-4242-7000-8000-000000000042\",\"field\":\"text\",\"delta\":\"All 214 tests pass\"},\"emittedAtMs\":1754590941100}"}{"dir":"server","raw":"{\"jsonrpc\":\"2.0\",\"method\":\"item/delta\",\"params\":{\"sessionId\":\"0198f0aa-1111-7000-8000-0000000000aa\",\"viewCursor\":\"v:0198f0aa-1111-7000-8000-0000000000aa:8\",\"itemId\":\"0198f0ac-4242-7000-8000-000000000042\",\"field\":\"text\",\"delta\":\" except two in tbh-agent...\"},\"emittedAtMs\":1754590941200}"}Two frames, and together they spell All 214 tests pass except two in tbh-agent... — exactly the text the completed frame carries.
The code
Section titled “The code”Subscribe once, before you start the turn, and route by method.
A notification’s params is Record<string, unknown> — the wire is not typed
for you — so read each member by name and narrow it:
const drawn = new Map<string, string>();
msp.connection.onNotification((notification) => { const params = notification.params ?? {}; switch (notification.method) { case "item/started": { // The message exists and is empty. Make room for it. const item = params["item"] as { itemId: string }; drawn.set(item.itemId, ""); break; } case "item/delta": { // Append — but only text deltas, and only to the item they name. if (params["field"] !== "text") break; const itemId = params["itemId"] as string; const delta = params["delta"] as string; drawn.set(itemId, (drawn.get(itemId) ?? "") + delta); break; } case "item/completed": { // The authoritative text. Take it over your own accumulation. const item = params["item"] as { itemId: string; text: string }; drawn.set(item.itemId, item.text); break; } case "turn/completed": { // The turn is over. `params["terminal"]` says how it ended, and // `params["usage"]` is what it cost. break; } }});A real client would validate rather than cast, but the member names are the part that has to be right — those are the wire contract, and the tested recipe reads exactly these.
Subscribing before turn/start matters. The host may push the first frames
as soon as it accepts the command, and a listener attached afterwards can miss
the opening of the very message it exists to draw.
Why take item/completed over your own text
Section titled “Why take item/completed over your own text”Your accumulation is a guess that no delta was dropped, reordered, or applied to the wrong item. The completed frame is the host telling you what the message is. Taking it is one line, and it makes those failure modes invisible to your user instead of permanent.
The tested recipe asserts the two agree — it accumulates the deltas exactly as
the code above does and compares the result to item.text on the completed
frame. If they ever diverge, the cookbook-journeys job goes red.
Do not settle on the last delta
Section titled “Do not settle on the last delta”The deltas stopping does not mean the turn stopped. Wait for turn/completed:
// WRONG: the agent may still be working — running a tool, starting another// message — long after the last delta of the first one.if (noDeltasFor(500)) markTurnFinished();turn/completed is the one frame that says the turn is over, and it carries
the terminal (how it ended) and the usage (what it cost) that a UI shows
when the answer settles.
Run it yourself
Section titled “Run it yourself”You need Node 20 or newer and the SDK your own application depends on —
npm install @muse-code/sdk. This recipe never spawns a real host, so an
installed muse is not part of it. The recipe programs live in the SDK
repository:
git clone https://github.com/meta-models/muse-code-sdkcd muse-code-sdknpm ci && npm run build --workspace @muse-code/sdknpm run recipes --workspace @muse-code/sdk-cookbook -- --only stream-a-turnThis recipe replays a committed transcript through a canned host, so it needs
no credentials and no model — but the canned host is a conformance fixture that
is not published yet, so the command above reports the recipe as needing a host
it cannot find. Until that fixture ships, the verified program on this page is
the complete source of truth. The recipes that need only a real muse host —
the fingerprint check,
surviving a host crash,
switching models mid-session — run end
to end today.
Where the tested code lives
Section titled “Where the tested code lives”The runnable source for this page is published in full, comments and all, as the stream-a-turn example. It is the source that runs, not a retelling of it: it is executed end to end on every change to this area, and every docs build checks the wire frames above against the committed transcript, so this page cannot drift from what actually runs.
The harness spawns its hosts with MUSE_EXPERIMENTAL_SDK_ENABLED set
explicitly. muse serve now runs by default; the variable survives as an
off-switch, and the harness pins the run open so an operator’s off in the
environment cannot silently change what this page proves. That is not advice
for your application — build against muse serve as a supported command.