Vercel AI SDK 7 Migration: Interrupted Streams, Cloudflare 524 Boundaries, and Tool-Call Recovery
Model-layer results use provider-free Node.js 22.18.0 fixtures with [email protected] and [email protected]. Disconnect results use real localhost HTTP/SSE and TCP sockets. The 524 result is a local reverse-proxy simulation, not a public Cloudflare edge test. The article does not measure model quality, provider latency, token cost, or a Cloudflare Ray ID.
Vercel AI SDK 7 Migration: Interrupted Streams, Cloudflare 524 Boundaries, and Tool-Call Recovery
Most AI SDK migrations look deceptively small in a pull request.
A few option names change. A stream property gets renamed. TypeScript points at several callbacks. The application builds again, the chat page produces tokens, and the migration is declared complete.
That definition is acceptable for a prototype. It is dangerously incomplete for a production system that calls tools, persists conversations, retries failures, resumes interrupted work, or performs external side effects.
The real migration question is not:
Does the same prompt still produce a sentence?
It is:
Does the application still understand the run correctly when tools, partial state, cancellation, retries, and timeouts are involved?
I built an isolated XBSTACK migration lab to answer that question without provider randomness. It contains two independent fixtures:
Both run on Node.js 22.18.0. Both use strict TypeScript. Both execute the same deterministic workflows through mock language models from ai/test. No API key is required. No paid request is made. No result depends on model quality, network latency, or a vendor outage.
The repository is structured as a standalone technical asset:
xbstack-ai-sdk-7-migration-demo
├── v6/
├── v7/
├── migration-diff/
├── benchmarks/
├── docs/
├── scripts/
└── LICENSE
One command performs both type checks, both runtime suites, and regenerates a machine-readable comparison:
npm test
The result changed my migration recommendation in an important way.
I recommend upgrading to AI SDK 7, but only after separating application state from SDK state.
The upgrade is not primarily about syntax. It is about semantics. In the same successful two-step tool workflow:
AI SDK 6
top-level toolCalls: 0
raw response messages: 3
AI SDK 7
top-level toolCalls: 1
raw response messages: 1
finalStep.toolCalls: 0
The final answer was identical. The number of model steps was identical. The tool was identical. The application-facing shape around that answer was not.
That is the kind of change that passes a visual smoke test and still breaks persistence, analytics, audit logs, or recovery logic.
This article explains what changed, why those differences matter, and how I would migrate a production application without treating the SDK response object as the system of record.
The Production Workflow Behind the Test
The baseline fixture uses a deliberately small workflow:
user asks for order A-100
↓
model emits lookupOrder tool call
↓
tool returns ready_for_pickup
↓
model produces final response
The final text in both versions is:
Order A-100 is ready for pickup.
A second scenario forces the tool to throw. A third streams text. Additional scenarios cover:
- persistence normalization;
- page close;
- network disconnect;
- manual cancellation;
- a tool that fails once and succeeds on retry;
- v6 cancellation through an application-owned timer;
- v7 timeout through first-class timeout configuration.
The fixtures intentionally test structural behavior rather than throughput. A provider-free benchmark cannot tell us which version is faster with a real model. It can tell us whether two versions represent the same workflow differently, whether an abort reaches the provider, and whether our persistence invariants still hold.
Those are the questions that matter before a staged rollout.
The Visible API Diff Is the Easy Part
Before changing option names, verify two runtime requirements that a codemod cannot solve:
- AI SDK 7 requires Node.js 22 or newer;
- AI SDK 7 is ESM-only and no longer supports CommonJS
require().
The application, CI workers, serverless runtime, and local development environment must all satisfy those requirements. A minimal package boundary looks like this:
{
"type": "module",
"engines": {
"node": ">=22"
}
}
These are release requirements, not style preferences. Vercel lists Node.js 22 and ESM among the breaking prerequisites in the official AI SDK 7 release and migration overview.
The most obvious changes are straightforward.
Instructions:
const result = await generateText({
model,
- system: 'Use tools before answering.',
+ instructions: 'Use tools before answering.',
prompt,
});
Lifecycle callback:
const result = await generateText({
model,
prompt,
- onFinish({ steps }) {
+ onEnd({ steps }) {
finalizeRun(steps);
},
});
Full event stream:
const result = streamText({ model, prompt });
-for await (const part of result.fullStream) {
+for await (const part of result.stream) {
persistStreamPart(part);
}
Timeout configuration becomes more explicit in v7:
const result = await generateText({
model,
prompt,
timeout: {
totalMs: 30_000,
stepMs: 15_000,
},
});
Streaming can add a chunk budget:
const result = streamText({
model,
prompt,
timeout: {
totalMs: 30_000,
stepMs: 15_000,
chunkMs: 5_000,
},
});
A codemod helps with part of this surface. In the isolated sample I ran, it changed system to instructions and fullStream to stream. It left onFinish unchanged. The pinned v7 package still accepted the compatibility alias during type checking.
That is useful, but it also demonstrates the limit of migration automation.
A type-compatible alias can keep a build green. It cannot prove that finalization logic behaves correctly after a tool failure, an abort, or a timeout. A codemod cannot infer whether result.toolCalls means “all calls in the run” or “calls in the final step” to your business logic. It cannot redesign your database or add idempotency to an email-sending tool.
Treat the codemod as a syntax assistant, not a migration strategy.
Tool Calling: Full Run and Final Step Are Different Questions
The first important behavioral difference appears in the successful two-step workflow.
The v6 fixture observed:
result.steps.length = 2
result.toolCalls.length = 0
result.toolResults.length = 0
result.steps[0].toolCalls.length = 1
result.steps[0].toolResults.length = 1
The v7 fixture observed:
result.steps.length = 2
result.toolCalls.length = 1
result.toolResults.length = 1
result.finalStep.toolCalls.length = 0
result.finalStep.toolResults.length = 0
The v7 shape is coherent once the scope is explicit:
- top-level tool fields describe the complete run;
finalStepdescribes only the last step;- the last step contains text, not a tool call.
The migration risk comes from old code that never named the scope.
Consider a classifier used for analytics:
const usedTools = result.toolCalls.length > 0;
In this fixture, that expression is false in v6 and true in v7, even though both runs used the tool and produced the same answer.
Or consider a final-step branch:
if (result.toolCalls.length === 0) {
markFinalStepAsTextOnly();
}
That happened to align with the v6 fixture because top-level calls were empty. It becomes incorrect in v7 because the full run contains a tool call even though the final step is text-only.
The fix is not a compatibility shim. The fix is to name the scope:
const runToolCalls = result.toolCalls;
const finalStepToolCalls = result.finalStep.toolCalls;
Then update every consumer to state which question it is answering.
Use full-run tool calls for:
- audit history;
- cost and usage analysis;
- tool adoption metrics;
- run summaries;
- security review;
- detecting whether any external operation occurred.
Use final-step tool calls for:
- deciding how to render the final step;
- validating terminal state;
- determining whether another continuation is required;
- final-step-specific policy checks.
A migration checklist should search for every read of:
toolCalls
toolResults
steps
response.messages
finishReason
The relevant question is not whether the property still exists. It is whether its scope still matches the consumer.
Failure Semantics: A Failed Call Is Not a Successful Result
The failure fixture makes the model call an unstableTool. The tool throws an error. The next model step produces a compact explanation.
Both versions include a tool-error part in the first step and a text part in the second step. That continuity is helpful: a tool exception can remain part of the run and be explained to the user.
The top-level aggregation still differs:
v6 top-level tool calls/results: 0 / 0
v7 top-level tool calls/results: 1 / 0
The v7 result communicates a useful distinction:
- one tool invocation occurred during the run;
- no successful tool result was produced.
That distinction should exist in the application model even if the SDK representation changes again.
A logical tool call should have its own status:
pending
running
succeeded
failed
aborted
unknown
A successful tool result should not be manufactured from an error message. The final assistant explanation is not the tool result. It is a model-generated interpretation of the failure.
This matters for observability. If a dashboard only counts successful results, it can miss high failure volume. If it only counts top-level tool calls, a v6 and v7 deployment may report different usage for the same traffic. Normalize the events before analytics.
Persistence Is the Real Migration Boundary
The most consequential result in the lab is not the renamed callback. It is the raw response message count.
For the same successful workflow:
AI SDK 6 response.messages.length = 3
AI SDK 7 response.messages.length = 1
The v6 fixture produced:
assistant message containing the tool call
tool message containing the tool result
assistant message containing the final text
The v7 fixture produced one raw response message containing the final assistant text, while tool information was available through full-run aggregation and step data.
Nothing was functionally missing. Both versions completed the same workflow. But a database that assumes the raw response message array is the durable event history now has a version problem.
This is why I do not recommend persisting the SDK object as the primary schema.
A production application needs a version-neutral model that represents business facts independently from one SDK release.
A useful baseline is:
conversation
↓
message
↓
run
↓
step
↓
tool_call
↓
tool_attempt
↓
tool_result
↓
final_response
Conversations
The conversation is the long-lived user boundary.
id
user_id or tenant_id
created_at
metadata
It should not care whether the active runtime is v6, v7, or another orchestration layer.
Messages
Messages represent user-visible or application-visible communication.
id
conversation_id
role
status
content_json
sdk_payload_json (optional)
The status field matters:
pending
partial
completed
failed
aborted
A final assistant response that never completed should not look identical to a completed message.
Runs
A run represents one orchestration attempt triggered by a message.
id
message_id
status
sdk_version
abort_reason
idempotency_key
started_at
finished_at
Recommended states include:
queued
running
succeeded
failed
aborted
timed_out
The sdk_version is evidence, not identity. It helps compare migrations and investigate incidents without making the rest of the schema version-specific.
Steps
Steps preserve execution order and model/tool boundaries:
id
run_id
sequence
status
finish_reason
The application can normalize v6 and v7 step data into this table even when their top-level aggregation differs.
Logical Tool Calls
A tool call is the operation the model requested:
id
tool_call_id
run_id
step_id
tool_name
input_json
status
idempotency_key
The idempotency key is critical for any side effect.
Tool Attempts
Attempts record how many times the application tried to execute one logical tool call:
id
tool_call_id
attempt_number
status
started_at
finished_at
error_code
error_message
This separation becomes essential during retries. Two attempts do not mean the model asked for two operations.
Tool Results
The final result belongs to the logical tool call:
id
tool_call_id
status
output_json
The application can retain the original SDK payload in an audit column or object store. Raw payloads remain valuable for debugging, replay investigation, provider support, and migration analysis. They should not be the only representation of durable state.
The adapter layer becomes explicit:
function normalizeV6Result(result: V6Result): ApplicationEvents {
// Read step-level tool calls and v6 response messages.
}
function normalizeV7Result(result: V7Result): ApplicationEvents {
// Read full-run aggregation and finalStep explicitly.
}
The database receives the same application events from both adapters.
That is the architecture that makes an SDK upgrade reversible.
Cancellation: Page Close Is Not Rollback
Production AI applications often treat client disconnects as a transport detail. For tool-using systems, a disconnect is a state transition.
The lab tests three cancellation reasons:
page_closed
network_disconnected
manual_cancel
Each scenario follows the same sequence:
persist user message
↓
model emits tool call
↓
execute tool
↓
persist tool result
↓
start final model step
↓
abort before final response
The assertions are identical in both fixtures:
partialMessagesSaved = true
finalResponseSaved = false
toolExecutions = 1
duplicateToolExecution = false
The important conclusion is simple:
Cancellation does not undo an external side effect.
Imagine that the tool creates a CRM lead. The lead is created successfully. While the model is generating a summary, the user closes the browser tab. If the application deletes the incomplete run, it loses evidence that the lead already exists. If the user retries, the same logical operation may execute again.
The correct flow is:
- persist the user message before model execution;
- record the tool call before the external operation;
- commit the tool result immediately after success;
- mark the run aborted when cancellation arrives;
- do not create a completed final response;
- recover from persisted state rather than replaying the entire request.
Propagate AbortSignal Through the Whole Chain
A cancellation API is only effective when every layer cooperates:
browser request signal
↓
server route
↓
run coordinator
↓
AI SDK call
↓
provider adapter
↓
tool execution
↓
HTTP/database clients used by the tool
If one layer ignores the signal, the user may see “cancelled” while work continues in the background.
That creates several classes of incidents:
- duplicate writes after a retry;
- token spend after the request is gone;
- background jobs with no visible owner;
- a run marked aborted while its tool is still running;
- final responses persisted after the UI has moved on.
Cancellation needs both propagation and a state policy.
Idempotency Is the Recovery Mechanism
For a side-effecting tool, derive an idempotency key from stable application identifiers:
conversation_id + run_id + tool_call_id
If the external API accepts an idempotency key, pass it through. If it does not, maintain an execution record locally:
pending → running → succeeded
Before executing:
const existing = await toolExecutions.findByIdempotencyKey(key);
if (existing?.status === 'succeeded') {
return existing.output;
}
The application can then resume the final model step without repeating the external operation.
This is more reliable than trying to infer from a missing final message whether the tool ran.
Real HTTP/SSE Disconnects: Request-Bound, Detached, and Idempotent Recovery
The deterministic abort fixture verifies the state machine, but it does not open a network connection. I added a second AI SDK 7 test that places the same persistence model behind actual localhost HTTP/SSE endpoints.
The environment is deliberately narrow and reproducible:
- Node.js
22.18.0; [email protected];- Node’s native
fetchclient; - Node’s native HTTP server;
- real localhost TCP and SSE transport;
- a deterministic order lookup tool backed by an in-memory ledger;
- no external model provider;
- no public network.
“Real” here means that the client and server communicate over an actual socket and the client genuinely closes the response stream. It does not mean that the test measures a model provider, a serverless platform, or public-internet reliability.
Request-Bound Execution
The first route binds the run to the request lifecycle:
client opens SSE
↓
tool_call = planned / executing
↓
tool completes and tool_result is committed
↓
client receives tool_result and disconnects
↓
request close aborts the run
↓
run = aborted; final_response is absent
The state after disconnect was:
| Field | Observed value |
|---|---|
run.status | aborted |
toolExecutions | 1 |
toolResultSaved | true |
finalResponseSaved | false |
disconnectObserved | true |
The important result is not that the request aborted. It is that the completed tool result remained independently committed. An application that saves the tool result only when the final assistant message finishes can lose the evidence required to recover safely.
Resumption Reuses the Result Instead of Repeating the Tool
The recovery request uses the same run_id and the same idempotency key:
idempotency_key = direct-run:lookupOrder:A-100
Before executing the tool, the coordinator checks the ledger. It finds the completed result, appends a reused event, and continues to the final model step without running the side effect again.
The assertions after resumption were:
tool executions before resume = 1
tool executions after resume = 1
duplicate executions = 0
final response saved = true
This is also why consumeStream() is not an idempotency mechanism. The official AI SDK message-persistence documentation explains that consumeStream() can keep consuming a stream on the server after a client disconnects and allow completion handling to run. The same documentation recommends storing in-progress/completed request state and using resumability for more robust disconnect handling. Stream consumption does not know whether “create order,” “send email,” or “write CRM record” has already produced an external side effect.
Detached Execution
A second route does not bind the run’s cancellation signal to the client connection. The client still disconnects after receiving the tool result, but the background operation continues and persists the final response:
| Field | Observed value |
|---|---|
disconnectObserved | true |
run.status | completed |
toolExecutions | 1 |
finalResponseSaved | true |
This proves a control-flow distinction; it does not prove that an unowned void promise is durable production infrastructure. Process restarts, deployments, instance reclamation, and long approval pauses still require a queue, worker, or durable workflow runtime to own the task.
Vercel’s AI SDK 7 release describes WorkflowAgent as durable execution that persists state across steps so runs can survive deployments, process restarts, interruptions, and delayed approvals. That durability still does not define business idempotency, reconciliation, or authorization for an external tool.
Cloudflare 524 and chunkMs Are Different Control Planes
The fixture also includes a local reverse proxy with a compressed 70 ms inactivity budget:
- an origin that sends no first byte before the budget receives a simulated
524; - an origin that emits a heartbeat every
35 mscompletes with200.
Observed result:
slow first byte → 524
periodic heartbeat → 200
This is explicitly a local simulation. It is not a real Cloudflare edge test, and it did not produce a public Ray ID or production screenshot.
Cloudflare’s official 524 documentation says the status means Cloudflare connected to the origin but did not receive a timely HTTP response before the default 120-second Proxy Read Timeout. For long-running HTTP work, Cloudflare recommends patterns such as status polling. That proxy boundary is separate from AI SDK timeouts:
| Control plane | What it governs | What it cannot replace |
|---|---|---|
| Browser/request | Page close and network disconnect | Knowledge of whether a tool completed |
AI SDK chunkMs | Silence between stream chunks | Cloudflare’s proxy timeout |
AI SDK toolMs | One tool execution budget | Side-effect reconciliation |
| Cloudflare 524 | Proxy waiting for origin response | Run/step persistence |
Four Failure Paths Exposed by the Additional Test
The first failure is treating request cancellation and task cancellation as the same state. In the request-bound route, closing the SSE connection produced a close event and the run became aborted. If the application also waits until the final-response callback to commit its database transaction, the tool may have completed while the entire transaction rolls back because final text never arrived. On the next visit, the database has no tool result and the application can only replay the request. For a read tool that wastes work; for order creation, email, publishing, or CRM writes it can create a duplicate side effect.
The commit boundary must be narrower. Persist the user message first. Persist the logical tool call as planned before execution and change it to running when work begins. Commit the tool result as soon as it becomes completed. The final assistant message is a later step: it may fail, time out, or be cancelled, but it must not erase a tool that already finished. A tool result and final response should not share one all-or-nothing transaction.
The second failure is assuming that a detached promise is a background job system because it completed in the localhost test. The client disconnected while the process remained alive, so the operation could persist its final response. A deployment, Node process exit, serverless instance reclamation, or container restart would leave that promise without an owner. The test proves that response and execution lifecycles can be separated; it does not prove durability. Production still needs a stored job_id, current step, lease, retry count, and heartbeat, with a queue, worker, or WorkflowAgent capable of reclaiming the work.
The third failure is checking idempotency after a recovered request has already called the tool. The lookup must happen before the side effect. Derive a stable key from conversation_id + run_id + tool_call_id, read any existing execution, and reuse a succeeded result. Only an absent result should attempt to acquire an execution lease. A live running lease should make the recovery request wait or report progress. An unknown result should trigger reconciliation against the external system rather than another blind execution.
The fourth failure is treating heartbeat bytes as a universal long-task solution. In the local proxy test, regular heartbeat output kept the connection inside the 70 ms read budget. That proves only that the proxy observed bytes; it does not prove business progress. Endless empty chunks consume connections, can hide a stalled tool, and may falsely reassure the UI. A useful heartbeat should carry auditable run_id, step_id, status, and update time, plus a total deadline. Once the total SLA is exceeded, the task should move to durable background execution or failure recovery rather than hold one HTTP connection indefinitely.
Together, these failures show why streaming-agent reliability cannot belong to one consumeStream() call, one AbortSignal, or one timeout value. The request layer owns the connection, the run layer owns state, the tool layer owns side effects, the proxy owns transport budgets, and persistence owns recovery evidence. Separating those records is what makes it possible to choose among continuation, reuse, retry, reconciliation, and human intervention after an interruption.
When work may exceed the HTTP proxy budget, the safer architecture is:
POST creates job → return job_id immediately
↓
queue / WorkflowAgent
↓
persist each step and tool result
↓
poll or subscribe to status
The full event record is generated at:
benchmarks/http-interruption.json
It contains the pre- and post-resume run states, tool execution count, idempotent reuse event, detached completion result, and the explicit proxy-simulation boundary.
Timeout: Better Configuration, Same Responsibility
The v6 fixture models timeout with an application-owned AbortController and timer:
const controller = new AbortController();
const timer = setTimeout(
() => controller.abort(new Error('external timeout')),
20,
);
try {
await generateText({
model,
prompt,
abortSignal: controller.signal,
maxRetries: 0,
});
} finally {
clearTimeout(timer);
}
The v7 fixture uses first-class timeout configuration:
await generateText({
model,
prompt,
maxRetries: 0,
timeout: {
totalMs: 20,
},
});
With an abort-aware mock provider, the v7 fixture observed:
TimeoutError
The official AI SDK 7 release overview also describes default and per-tool timeout budgets. This article pins its reproducible runtime assertion to [email protected] and directly tests totalMs; it uses the public API reference for stepMs and chunkMs. It does not present an untested tool timeout as a fixture result. Before adopting toolMs or a per-tool timeout in production, verify the type definitions for the exact version in your lockfile and add a matching fixture.
The three verified budgets below solve different failure modes.
Total Timeout
totalMs is the maximum time for the entire run.
It protects the request from an agent that keeps taking additional steps or waiting across multiple tools.
Step Timeout
stepMs limits one orchestration step.
It helps identify a single provider call or tool round trip that is unhealthy without waiting for the total budget to expire.
Chunk Timeout
chunkMs limits silence between stream chunks.
It addresses a connection that remains open but stops producing data.
These are useful controls, but they do not replace cancellation design.
A timeout is not rollback. When the timeout fires:
- a completed tool result remains completed;
- an in-flight tool needs a known cancellation policy;
- the run should become
timed_out; - the UI should distinguish system timeout from user cancel;
- ambiguous side effects may require reconciliation.
The mock provider also revealed an important testing detail. A fake implementation that merely sleeps without observing abortSignal does not accurately test cancellation. The v7 timeout fixture keeps a real timer active and rejects when the signal aborts. That verifies propagation rather than only checking that an outer promise returned an error.
Production provider adapters and tools need the same cooperative behavior.
Retry: One Logical Tool Call, Multiple Attempts
The retry fixture executes one logical job:
JOB-7
The tool behavior is deterministic:
attempt 1 → transient failure
attempt 2 → success
The final run succeeds. Both fixtures record the attempt sequence:
attempt 1 running
attempt 1 failed
attempt 2 running
attempt 2 succeeded
The key modeling decision is that this remains one logical tool call.
The state hierarchy is:
run: succeeded
step 1: completed
logical tool call: succeeded_after_retry
tool attempt 1: failed
tool attempt 2: succeeded
step 2: completed
final response: completed
If the database stores two tool calls, it implies that the model requested the operation twice. If it only stores the final success, it hides instability and makes retry-rate monitoring impossible.
Provider Retry and Tool Retry Are Different
A provider retry may repeat a model request because of a rate limit or temporary network failure.
A tool retry may repeat an external side effect.
Those are not equivalent risks.
Safe automatic tool retries usually include:
- idempotent reads;
- writes with a verified idempotency key;
- cache refresh operations;
- temporary errors with unambiguous failure semantics.
Unsafe default retries include:
- payment capture;
- sending an email;
- creating an order;
- publishing content;
- deleting a resource;
- any request where the response may be lost after the side effect succeeded.
For ambiguous writes, the correct next state may be:
unknown → reconciliation
The application should query the external system before deciding whether to retry or compensate.
No SDK-level maxRetries value can infer that business rule.
A Production Migration Sequence That Can Be Rolled Back
I would not migrate the SDK and the persistence schema in the same release. That makes incident diagnosis and rollback unnecessarily difficult.
A safer sequence is incremental.
Phase 1: Build the Fixture Before Touching Production
Create deterministic scenarios for the workflows that matter:
- successful tool call;
- tool exception;
- structured output failure, when relevant;
- streaming;
- cancellation;
- real HTTP/SSE client disconnect;
- request-bound versus detached execution;
- idempotent resume after a completed tool result;
- timeout;
- reverse-proxy inactivity versus heartbeat;
- retry;
- persistence snapshots;
- result-shape snapshots.
Assert more than final text. Record:
step count
tool call count by scope
tool result count by scope
raw response message count
content part types
finish reasons
abort state
attempt state
This catches structural drift that a UI test will miss.
Phase 2: Introduce Version-Neutral Persistence While Still on v6
Add conversations, runs, steps, logical tool calls, attempts, and results before changing the SDK.
Write a v6 adapter that produces normalized events. Keep raw v6 payloads for audit.
At this point, production behavior is still on the known SDK version, so persistence bugs can be isolated from SDK changes.
Phase 3: Add Cancellation and Idempotency
Propagate AbortSignal through the route, coordinator, provider, and tools.
Add idempotency keys to every side-effecting tool. Define recovery behavior for:
- completed tool and missing final response;
- in-flight tool at disconnect;
- unknown external outcome;
- timed-out provider step.
Phase 4: Implement a v7 Adapter
The v7 adapter should deliberately choose scopes:
const runToolCalls = result.toolCalls;
const finalStep = result.finalStep;
It should emit the same application events as the v6 adapter.
Run both fixtures in CI whenever the AI SDK or a provider package changes.
Phase 5: Migrate a Low-Risk Workflow
Start with an internal, read-only tool. Avoid payments, publishing, email, deletion, or irreversible external writes.
Compare normalized event records from v6 and v7 rather than only comparing final text.
Observe:
- tool calls per run;
- tool attempts per call;
- abort rate;
- timeout rate;
- unknown outcomes;
- duplicate idempotency keys;
- missing final responses;
- step duration.
Phase 6: Use a Feature Flag, Not a Package Reinstall, as Rollback
Keep both adapters available during rollout:
const runtime = flags.aiSdk7
? createV7Runtime()
: createV6Runtime();
A rollback should be a controlled traffic switch. It should not require an emergency dependency edit and rebuild.
The database remains stable because both adapters write the same normalized model.
Phase 7: Expand by Risk Class
A reasonable order is:
read-only internal tools
↓
low-risk user tools
↓
small production cohort
↓
reversible writes
↓
high-impact side effects with approval
Remove the v6 path only after the observation window is complete and historical recovery no longer depends on it.
What Should Be in the Migration Pull Request
A production migration PR should include more than dependency changes.
Code Changes
- renamed instructions and stream properties;
- explicit
onEndlifecycle handling; - scope-specific variable names for run and final-step tool data;
- timeout budgets;
- adapters for normalized persistence;
- cancellation propagation;
- idempotency enforcement;
- retry classification.
Tests
- deterministic v6 and v7 fixtures;
- success and failure snapshots;
- page close, network disconnect, and manual cancel;
- timeout propagation;
- one-failure-then-success retry;
- no duplicate tool execution;
- partial-state persistence;
- final response absence after abort.
Operational Changes
- metrics for run, step, tool call, and attempt status;
- logs containing run ID, tool call ID, and idempotency key;
- feature flag and rollback procedure;
- reconciliation queue for unknown side effects;
- alert thresholds for timeout and retry rates.
Documentation
- data model diagram;
- scope definitions for top-level and final-step fields;
- tool retry policy;
- timeout policy;
- rollout and rollback checklist.
If the PR only contains package.json, renamed options, and updated snapshots of final text, it has not covered the production migration.
Migration Anti-Patterns That Look Safe but Are Not
Several migration approaches can produce a green build while leaving the application operationally unsafe. They are worth identifying explicitly because they tend to survive code review: each individual change looks reasonable, but the combined system has no reliable state boundary.
Anti-Pattern 1: Update Types Until the Compiler Stops Complaining
TypeScript is an essential gate, but it only verifies the contracts expressed in the type system. It cannot detect that an analytics event now counts full-run tool calls instead of final-step calls. It cannot tell whether a completed external write was lost when an aborted response was removed from storage.
A passing build should be followed by behavioral assertions that compare:
normalized run events
step ordering
tool-call scope
tool-result scope
message persistence
abort state
retry attempts
timeout classification
The migration lab keeps those results in JSON so the comparison can be reviewed as data rather than inferred from console output.
Anti-Pattern 2: Add a Compatibility Wrapper That Preserves Old Names
It is tempting to create a wrapper such as:
function generateTextCompat(options) {
return generateText({
...options,
instructions: options.system,
});
}
That can reduce churn, but it also preserves old mental models. If the wrapper exposes toolCalls without documenting whether it means the complete run or the final step, the most important semantic change remains hidden.
A migration adapter should normalize application events, not merely restore old property names. The adapter is successful when v6 and v7 produce the same business records, even if their native response objects differ.
Anti-Pattern 3: Persist Only After the Final Response
A single transaction at the end appears clean:
const result = await generateText(...);
await saveEverything(result);
It fails under cancellation. A tool may complete before the final model step. If the user disconnects after that side effect, saveEverything never runs and the database loses the evidence required for recovery.
Persist boundaries as they occur:
user message committed
tool call committed
tool result committed
final response committed when available
That creates more writes, but it creates an explainable system.
Anti-Pattern 4: Retry the Entire Run After Any Error
Replaying the whole run is simple when every tool is a read. It becomes dangerous as soon as tools write to external systems. The model may emit the same logical operation again, or it may emit a different call after seeing partial context.
Recovery should begin from persisted state:
- determine whether the logical tool call exists;
- inspect its latest attempt;
- reconcile unknown external outcomes;
- reuse a successful result;
- continue from the next incomplete step.
Retry is a state-machine transition, not a recursive call to the original endpoint.
Anti-Pattern 5: Treat Client Disconnect as a Frontend Concern
The browser owns the initial signal, but the backend owns the consequences. The server must decide what status to persist, which work to cancel, which work cannot be cancelled, and what can be resumed.
A reliable API records an explicit reason:
user_cancelled
client_disconnected
request_timed_out
server_shutdown
provider_cancelled
Those reasons support different product messages and operational responses. Combining them into a generic error removes information that is valuable during incident analysis.
Anti-Pattern 6: Compare Latency Before Establishing Equivalent Semantics
A benchmark is misleading when one version performs or records a different amount of work. Before comparing latency or cost, verify that both versions have equivalent:
- tool inputs;
- tool execution count;
- retry count;
- stopping conditions;
- timeout budgets;
- persistence writes;
- final response requirements.
The XBSTACK fixture deliberately avoids speed claims. Its job is to prove structural equivalence first. Real-provider latency and cost tests should come later, with repeated runs and clearly documented provider settings.
CI Acceptance Criteria for the Upgrade
The migration fixture becomes more valuable when it is a permanent CI contract rather than a one-time experiment.
A practical pipeline should fail when any of these invariants change unexpectedly:
both fixtures type-check
the successful workflow has two steps
the normalized persistence flow matches
every abort scenario executes the tool once
no abort scenario saves a final response
the retry scenario has one logical call and two attempts
the v7 timeout is classified as TimeoutError
comparison artifacts can be regenerated from raw results
Version-specific values should be reviewed rather than universally forced to match. For example, the raw message count is expected to differ. The normalized event flow is expected to match.
That distinction is the core of a useful migration test: it protects application invariants while allowing the SDK implementation to evolve.
When a future patch changes result shape, CI should show exactly which native field changed and whether the normalized model still holds. The team can then decide whether the change is harmless, requires an adapter update, or blocks rollout.
Should You Upgrade?
Upgrade Early When
The application already has:
- strict TypeScript;
- deterministic integration fixtures;
- application-owned persistence;
- run and step observability;
- propagated cancellation;
- idempotent tools;
- retry classification;
- feature flags;
- a rollback path.
AI SDK 7 is particularly attractive for systems that need:
- multi-step tool orchestration;
- full-run tool history;
- explicit final-step inspection;
- long-running streaming;
- separate total, step, and chunk timeouts;
- resumable or auditable agent runs.
Delay the Upgrade When
The application currently:
- persists
response.messagesas the only history; - cannot explain what happened after a client disconnect;
- has side-effecting tools without idempotency;
- uses one retry setting for every failure class;
- lacks deterministic tests;
- cannot roll back without reinstalling dependencies;
- only monitors final response success.
In that situation, the next engineering task is not “upgrade the SDK.” It is “establish a runtime boundary.”
AI SDK 7 will not automatically fix weak persistence or unsafe tools. It will make some of those assumptions easier to see.
Final Recommendation
I would migrate, but I would define success differently from a normal library upgrade.
Migration is not complete when:
npm install succeeds
TypeScript passes
tokens appear in the browser
It is complete when:
the same logical tool never runs twice unintentionally
partial state remains explainable after cancellation
retry attempts are auditable
timeouts actually stop cooperative work
historical data does not depend on one SDK response shape
full-run and final-step semantics are explicit
production traffic can switch back safely
The most valuable outcome of moving to AI SDK 7 is not fewer lines of code. It is the opportunity to stop treating an SDK return object as the architecture of the application.
The SDK should orchestrate model and tool interactions. The application should own identity, persistence, status, idempotency, recovery, and rollout.
Once that boundary exists, AI SDK 7 becomes a controlled infrastructure upgrade instead of a production gamble.
Reproducible Assets
The complete provider-free fixture, raw JSON evidence, migration diff, persistence architecture, and primary documentation are available here:
- XBSTACK AI SDK 7 Migration Demo
- Vercel: AI SDK 7 release and migration overview
- AI SDK: tools, tool calling, and response messages
- AI SDK: timeout and AbortSignal settings
For the surrounding production-agent boundaries, continue with:
- AI agent production governance: permissions, audits, cost, and rollback
- LangGraph failure recovery: tool errors, timeouts, and retry strategy
- AI agent framework selection: AI SDK 7, LangGraph, ADK, and Microsoft Agent Framework
Future production experiments around AI SDK, MCP, LangGraph, tool reliability, and agent recovery will be distributed through:
XBSTACK AI Engineering Weekly
The newsletter is not a news digest. Its structure is built around engineering evidence:
- What changed
- What broke
- What I tested
- Tools worth trying
- XBSTACK updates
The article provides the durable explanation. GitHub provides executable evidence. The newsletter provides a repeatable distribution channel. Together they form one technical asset rather than three disconnected content pieces.
Connect the migration article to reproducible experiments
Use the AI Tools Lab to review migration diffs, tool-call behavior, persistence, abort, retry and timeout evidence instead of relying on release-note summaries.
Next Reading
View Hub →2026 AI Development in Practice: Deconstructing the Physical Architecture of the LangChain + Next.js Template
A deep dive into the official LangChain Next.js template, revealing how to leverage Edge Runtime for millisecond-level streaming responses and resolve physical timeout issues during Vercel deployment.
LangGraph Multi-Agent Failure Recovery: Tool Errors, Timeouts, and Retry Strategies
A practical guide to designing failure recovery in LangGraph multi-agent systems, covering tool errors, timeouts, retries, fallbacks, human review, Checkpointer-based recovery, Supervisor/Worker collaboration, and production error logging. Helps developers build recoverable and auditable AI Agent systems.
AI Agent Framework Guide 2026: LangGraph, AI SDK 7, Google ADK, and Microsoft Agent Framework
A 2026 production comparison of native APIs, AI SDK 7, LangGraph, Google ADK 2.0, Microsoft Agent Framework, AutoGen, and CrewAI across state, durability, human approval, MCP, TypeScript, managed hosting, observability, and lock-in.
What’s the Difference Between ChatGPT Work, Chat, and Codex? I Ran a Real Website Task to Find Out
How do ChatGPT Work, Chat, and Codex divide responsibilities? I ran a complete workflow on a real operational task for XBSTACK after it hadn’t been updated in three days, and reviewed why the first version—1,499 words long and technically built successfully—was still rejected.

Xiaobai
Full-Stack AI Engineer
Xiaobai, a full-stack AI engineer building production Agent systems, product tools and independent software assets.
About Xiaobai & XBSTACK →Liked this article?
Join the newsletter
Every issue condenses production AI engineering changes, real failures, reproducible experiments, useful tools and new XBSTACK assets. No generic news digest and no filler.