A graph that does not exist yet
The agent backend is the part the demo never shows — and a scripted mock that emits the same event stream shape as the real thing lets the surface be built first and swapped later without touching a component.
Someone from a Vista portfolio company describes a problem at eleven in the morning. By four you are supposed to have something they can click. The surface is the thing they will judge: how a tool call appears, what the approval step feels like, whether the wait reads as thinking or as broken. None of that needs a real agent behind it. All of it needs something that streams.
So do not build the agent. Build a route that lies convincingly about being one.
The contract is the wire, not the code
A React chat surface built on the AI SDK does not talk to a model. It talks to an HTTP response, and it only understands one vocabulary. The SDK’s own documentation puts it plainly: “A data stream follows a special protocol that the AI SDK provides to send information to the frontend”. In version 7 that stream is carried over Server-Sent Events, which the same page justifies as buying “improved standardization, keep-alive through ping, reconnect capabilities, and better cache handling”.
That protocol is a fixed, enumerated set of message parts. The ones you will actually script:
| Part | What it marks |
|---|---|
start, finish | The outer boundary of one response |
start-step, finish-step | One step inside a multi-step run — the unit a trace view draws a row for |
text-start, text-delta, text-end | A block of assistant text, opened, filled and closed |
reasoning-* | The same three-part shape for reasoning content |
tool-input-start, tool-input-delta, tool-input-available | Arguments to a tool call, arriving progressively |
tool-output-available, tool-output-denied | What came back, or that it was refused |
tool-approval-request, tool-approval-response | The human gate, as wire events rather than component state |
error, abort | A run that failed, and a run that was cancelled |
The stream ends with a [DONE] terminator. Nothing in that list mentions a model, a provider or a graph. The frontend cannot tell the difference, because there is no channel on which the difference could reach it.
What a scripted producer looks like
The SDK exposes the writer side of the protocol directly, so you do not have to hand-assemble Server-Sent Event frames. A route handler that emits scripted text is about a dozen lines:
import { createUIMessageStream, createUIMessageStreamResponse } from 'ai'
export async function POST() {
const stream = createUIMessageStream({
execute: async ({ writer }) => {
writer.write({ type: 'start' })
writer.write({ type: 'text-start', id: '1' })
writer.write({ type: 'text-delta', id: '1', delta: 'Looking into that...' })
writer.write({ type: 'text-end', id: '1' })
writer.write({ type: 'finish' })
}
})
return createUIMessageStreamResponse({ stream })
}Note the id. A delta is not free-floating text; it belongs to a block that was opened and will be closed. Get that wrong and the surface still renders something, which is worse than rendering nothing, because you will design against the something.
Where the swap is frictionless, and where it is not
“Swap the mock for the real thing without touching a component” is true only for the parts of the protocol your mock actually implemented. It is not a property of the architecture. It is a property of your coverage.
Concretely: a mock that emits start, text-delta and finish gives you a streaming chat surface that will swap in cleanly. The day the real backend starts emitting tool-approval-request, your components have never seen one, and you are writing new UI under exactly the time pressure the kit existed to remove. The mock did not fail. It just never made a promise about that part.
The parts you skip are the parts you will build under a clock
Decide up front which parts of the protocol your kit’s mock covers, and write that list into the kit manifest next to everything else you decided once. The approval events and the step boundaries are the two worth covering even in a demo that does not obviously need them, because they are the two that later prototypes will need and that no component in your kit currently knows how to render.
Two places to put the fake
A route handler is one option and the simplest: your app serves the stream to itself, and swapping means changing the endpoint the transport points at. The other option is interception, and the difference is worth understanding before you pick.
Mock Service Worker describes itself as “an API mocking library for browser and Node.js” that intercepts outgoing requests, observes them, and responds with mocked responses. Its own docs are the source here, and MSW is an open-source project documenting itself — no paid product sits behind the page, which is a weaker bias than the vendor pages this course usually flags, but it is still the tool describing the tool.
The mechanism differs by runtime and the distinction is load-bearing:
- In the browser, MSW registers a Service Worker. Requests genuinely leave your code and are intercepted at the network layer, so anything that makes a request — the SDK’s transport, a raw
fetch, a third-party client you did not write — is covered without knowing MSW exists. - In Node, there is no Service Worker. MSW works by extending the relevant request classes rather than patching modules, so it covers requests made by server code in the same process.
That matters for a Next.js prototype, where a request can originate in the browser or inside a route handler running on the server. “My mock works” is two different claims depending on which side made the call, and a mock installed on only one of them looks identical to a mock installed on both right up until it does not.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
In one sentence: what makes a fake backend “correct”?
Check your answer
It emits the same protocol parts a real backend would emit, in the same order, with the same ids — because the wire is the only thing the frontend can observe, so matching the wire is the whole of the contract.
The corollary is the useful half. Correctness is scoped to the parts you chose to emit, so “is my mock correct” is not a yes or no question. It is a coverage list, and it belongs in the manifest beside every other decision the kit has already made for you.
Hands on
Stand up a producer of the protocol
Done when: The kit has a mock endpoint that streams a scripted multi-step response — text, a tool call and its output, and step boundaries — which a chat surface in the kit renders, plus a written list in the manifest of which protocol parts the mock covers and which it deliberately does not.
- Open the stream protocol page and read the part list end to end before writing anything. You are looking for the sequencing rules, not the names.
- Add a route to the kit that returns a
createUIMessageStreamResponsebuilt from a scripted array. Start with text only, and confirm it renders in a surface usinguseChat. - Extend the script to a run with real structure: a
start-step, some text, a tool call arriving astool-input-startthroughtool-input-available, thentool-output-available, thenfinish-stepandfinish. Do not add timing yet. - Break it on purpose. Emit a
text-deltawith an id that was never opened, and watch what the surface does. Whatever it does is what your prototype will do the day a real backend sequences something differently than you assumed. - Write the coverage list into the kit manifest: the parts the mock emits, the parts it does not, and one line on what a prototype would have to build itself for each uncovered part. That list is the honest version of “swappable.”
- Bring the coverage list into the chat. I will push on any part you marked covered where the script only emits it in one shape, since a single scripted example is a demo rather than coverage.
What this does not cover
Everything above streams instantly and always succeeds, which is not what a model does. Token cadence, the pause between a tool call and its result, and a failure arriving halfway through a run are the three behaviours the fake still owes you, and they are the whole of the next lesson, on faking latency and failure honestly.
This lesson also said nothing about how you find out the mock has stopped resembling the system it stands in for. That is the lesson on when the mock starts lying to you. And the two components the protocol makes possible but nobody builds under a clock — the trace view and the approval gate — get wired into the kit once in the lesson on wiring the trace and the gate once.
Read this next — primary source
AI SDK — Stream ProtocolVercel — fetched 5 September 2026. Vercel writes the AI SDK, publishes this page, and sells the platform the examples deploy to. Vendor documenting its own product.
This lesson takes one thing from it: the enumerated list of stream part types a frontend consumes, which is the entire contract your mock has to honour. Read the page in full anyway, because the part you will need later is the sequencing — which parts open a block, which close it, and which ids tie a delta to the thing it is a delta of. That ordering is what separates a mock that renders correctly from one that renders correctly by accident, and it is not recoverable from a single example.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.