Wire the trace and the gate once
The trace view and the human review gate are the two components every agentic prototype needs and nobody has time to build under a clock — wiring them into the kit against the mock protocol means no future prototype ever wires them again.
Two components turn up in every agentic prototype worth demoing. One shows what the agent did — steps, tool calls, arguments, results. The other stops it before something consequential happens and asks a person to approve. Neither is exotic, and you have built the second one before: HouseWarm’s approve-or-correct step, with the source crop next to the extracted value and nothing downstream firing until a human said yes.
Both get cut under a clock, every time, because at hour two they look like polish. Then the demo has no answer to the only two questions the room actually asks: what did it do, and who signs off.
Both components read the same stream
The reason these two can be wired once, against a fake, is that neither of them talks to a backend. They read protocol parts, the same parts a real backend emits.
- The trace view renders
start-stepandfinish-stepboundaries, thetool-input-startthroughtool-input-availablesequence, andtool-output-availableortool-output-denied. That vocabulary is the whole of its input. - The gate renders a
tool-approval-requestand sends back atool-approval-response. The request carries anapprovalId, thetoolCallbeing gated, areason, and anisAutomaticflag. The response carries the matchingapprovalId, anapprovedboolean, and an optionalreason.
Nothing in either list is backend-specific, which is the point. Wire them against the mock and they are wired against the real thing, because the wire is all they can see.
The gate, on the API as it stands today
For the real backend you will eventually swap in, tool approval moved in version 7 and the move is worth knowing before you write anything. The SDK’s own migration guide states that the needsApproval property on tool() and dynamicTool() is deprecated for generateText, streamText and ToolLoopAgent. Approval is now a setting on the call rather than a property of the tool:
// Deprecated: approval declared on the tool definition
const deleteFile = tool({
needsApproval: async ({ path }) => !path.startsWith('/tmp/'),
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }) => { /* ... */ }
})
// Current: approval declared on the call
await streamText({
model,
tools: { deleteFile },
toolApproval: { deleteFile: 'user-approval' }
})Per tool, toolApproval takes one of four values: 'not-applicable', the default, meaning no gate; 'approved' or 'denied', decided automatically; or 'user-approval', which emits the request and waits. It also accepts a function, so the decision can depend on the arguments — gate a delete outside /tmp, wave through everything inside it.
The move is not cosmetic. Approval stopped being a fact about a tool and became a fact about the context the tool is being called in, which is the correct place for it: the same tool is dangerous in one prototype and harmless in another.
The SDK’s own cookbook still shows the deprecated pattern
The human-in-the-loop cookbook page demonstrates needsApproval set directly on the tool definition — the shape the migration guide deprecates. Both pages are Vercel’s, both were live on 5 September 2026, and they disagree. Copy the cookbook verbatim today and you ship the old API.
This is the maintenance-tax lesson happening in front of you, one module later and one library over. There it was toDataStreamResponse against toUIMessageStream. Here it is the tool property against the call setting. The general rule that falls out of both: when a vendor’s tutorial and its migration guide conflict, the migration guide is the newer document by construction — it exists because something changed — and tutorials are what nobody gets paid to update.
The mock does not need any of that
Here is the split that makes this lesson cheap. Everything above is producer-side configuration for a real model loop. Your fake backend does not evaluate approval policy. It emits a tool-approval-request part with an approvalId, waits, and continues when the matching tool-approval-response arrives.
So the gate component you build against the mock is the finished gate component. Nothing about it changes when a real streamText call with toolApproval starts producing those same parts. You learn the producer-side API now so you can recognise a stale example when you meet one, and so the day you connect a real backend you configure it in a minute rather than rebuilding a component in an hour.
What “wired once” has to mean
A component that exists in the kit but has to be connected in every prototype was not wired once. Three conditions, and all three are testable from a cold clone:
- It is on by default. A fresh clone shows the trace and hits the gate without configuration. Opt out, not in.
- It reads the protocol, not your mock. No import from the mock module, no dependency on a scripted fixture. If the component knows the fake exists, swapping the backend breaks it.
- The gate blocks something visible. An approval step that everything proceeds past regardless is theatre, and a demo audience will find that out by clicking deny.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
Name the one line of code that has to change when you swap the mock for a real backend, assuming the trace and the gate were wired correctly.
Check your answer
The endpoint the chat transport points at. That is the whole swap, when the components read protocol parts rather than anything mock-shaped.
Producer-side there is real configuration to write — a model, a tool set, a toolApproval entry for anything consequential. That work is genuine and it is not component work. The claim being made here is narrow and it is the only one worth defending: no component changes. If one does, it had a dependency on the fake that nobody had written down.
Hands on
Wire both, once, and prove it from a cold clone
Done when: A fresh clone of the kit renders a trace of a scripted multi-step run with no configuration, hits an approval gate that genuinely blocks the run, and completes or denies based on the response — with neither component importing anything from the mock module.
- Extend the mock script so a run emits
start-step, a tool call, atool-approval-requestcarrying anapprovalId, then pauses. It should not proceed until a matchingtool-approval-responsecomes back. - Build the gate against those two parts alone. Approve and deny must produce visibly different outcomes downstream — deny should reach
tool-output-denied, not silently continue. - Render the trace from the same stream. Steps, tool inputs, tool outputs, in order. Resist styling it. The pass condition is that it shows what happened.
- Grep both components for any import from the mock module. If either has one, the coupling is real and the swap you are relying on does not exist yet.
- Read the migration guide and write a
toolApprovalconfiguration for one real tool into the kit as a commented example. You are not running it. You are making sure the current shape is the one recorded in your kit rather than the one in the cookbook. - Run the cold-clone drill from the maintenance-tax lesson again, and extend its pass condition: a running app, the mock responding, a trace rendering, and the gate blocking. Record the new elapsed time. That number is where the timed runs in the practising-against-the-clock module start from.
- Bring the drill result into the chat with the elapsed time. I will push on any step that needed a decision you thought you had already made, since each of those is a kit item you have not finished writing.
What this does not cover
What a trace should actually show — how to make a tool-calling loop legible, what belongs on a timeline, how to render a failed step so someone can act on it — is deliberately out of scope here and belongs to the observability course. This module took a component and fed it a protocol. It did not design one.
The kit is now complete enough to be tested, and it has never been tested. Everything so far is a claim: that the mock is convincing, that the trace and the gate are genuinely reusable, that a prototype now starts hours ahead. The evidence is timed runs against unfamiliar domains with a written cut list, and that starts in the next module, with the three-hour-rule lesson.
Read this next — primary source
AI SDK — Migration guide: 6.x to 7.0Vercel — fetched 5 September 2026. Vercel writes the SDK and publishes the guide. Vendor documenting its own product, though a vendor has no incentive to overstate how much its own upgrade breaks.
This lesson takes one migration from it — tool approval moving off the tool definition and onto the call — and you should read the rest, because the shape of the document is the lesson. It is a list of things that used to be true, written by the people who made them untrue, and it is the fastest way to tell whether any AI SDK example you find online predates it. Read it next to the cookbook page cited below, which has not caught up, and you have a live worked example of a vendor disagreeing with itself.
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.