The AI SDK v6 landed in late 2025 with a fundamentally different model for building agents. Where v4 and v5 felt like a toolkit of primitives you had to stitch together, v6 ships first-class agent loops, multi-step tool calling that pauses and resumes cleanly, and a provider-agnostic gateway as the default. If you've been gluing together generateText calls inside a while loop, this release pays back your refactor time in the first afternoon.
This article walks through the v6 agent pattern end-to-end: a TypeScript agent that reasons over tools, handles multi-step workflows, and streams progress to the client. Everything below runs on Node.js 24 and uses the "provider/model" string form that routes through the Vercel AI Gateway, so you can swap providers without touching code.
The Agent Loop
The core idea is simple: give the model a set of tools, a task, and a stopping condition. The SDK handles the loop — executes tool calls, feeds results back, and stops when the model produces a final answer or hits the step limit. No more hand-rolled while loops checking for tool_calls in the response.
lib/agents/research-agent.ts
import { generateText, tool, stepCountIs } from 'ai';
import { z } from 'zod';
const searchWeb = tool({
description: 'Search the web for current information',
inputSchema: z.object({
query: z.string().describe('The search query'),
}),
execute: async ({ query }) => {
const res = await fetch(
`https://api.tavily.com/search?q=${encodeURIComponent(query)}`,
{ headers: { Authorization: `Bearer ${process.env.TAVILY_KEY}` } },
);
const data = await res.json();
return data.results.slice(0, 5);
},
});
const fetchUrl = tool({
description: 'Fetch the contents of a URL',
inputSchema: z.object({ url: z.string().url() }),
execute: async ({ url }) => {
const res = await fetch(url);
return (await res.text()).slice(0, 8000);
},
});
export async function research(question: string) {
const { text, steps } = await generateText({
model: 'openai/gpt-4o',
tools: { searchWeb, fetchUrl },
stopWhen: stepCountIs(8),
system: 'You are a research assistant. Cite sources.',
prompt: question,
});
return { text, stepCount: steps.length };
}
A few things to notice. The tool() helper uses inputSchema (v5 called it parameters) and gets full type inference through to execute. The stopWhen: stepCountIs(8) is new — it replaces the old maxSteps number with a composable condition, so you can also stop on custom predicates like hasToolCall('finish') or combine multiple conditions.
Streaming Agent Progress
The second big v6 change is that streamText emits intermediate steps — tool calls, tool results, and text deltas — as part of the stream. On the client, the useChat hook surfaces these as typed message parts, so rendering an agent's reasoning is a matter of iterating over message.parts.
app/api/agent/route.ts
import { streamText, convertToModelMessages, stepCountIs } from 'ai';
import { searchWeb, fetchUrl } from '@/lib/agents/tools';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: 'anthropic/claude-sonnet-4-6',
tools: { searchWeb, fetchUrl },
stopWhen: stepCountIs(6),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
app/agent/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
export default function AgentPage() {
const { messages, sendMessage, status } = useChat({ api: '/api/agent' });
return (
<div>
{messages.map(m => (
<div key={m.id}>
{m.parts.map((p, i) => {
if (p.type === 'text') return <p key={i}>{p.text}</p>;
if (p.type.startsWith('tool-'))
return <pre key={i}>{JSON.stringify(p, null, 2)}</pre>;
})}
</div>
))}
</div>
);
}
Structured Output Without JSON Gymnastics
Agents often need to return typed data, not free-form text. v6's generateObject takes a Zod schema and returns a fully typed result — the SDK handles the prompt engineering, validation, and retry-on-schema-mismatch.
lib/agents/extract.ts
import { generateObject } from 'ai';
import { z } from 'zod';
const invoiceSchema = z.object({
vendor: z.string(),
total: z.number(),
currency: z.string().length(3),
lineItems: z.array(z.object({
description: z.string(),
amount: z.number(),
})),
});
export async function parseInvoice(text: string) {
const { object } = await generateObject({
model: 'openai/gpt-4o',
schema: invoiceSchema,
prompt: `Extract invoice data from this email:\n\n${text}`,
});
return object;
}
Pro Tip: Pair
generateObjectwith.describe()on each Zod field. The SDK folds descriptions into the prompt, and it noticeably improves extraction accuracy on ambiguous fields — especially dates, currencies, and enums.
Provider-Agnostic via the Gateway
v6's default pattern uses plain "provider/model" strings — "openai/gpt-4o", "anthropic/claude-sonnet-4-6", "google/gemini-2-pro" — which route through the Vercel AI Gateway. You no longer install provider-specific packages unless you need a feature the gateway doesn't proxy.
The practical win is failover and cost routing. One env var (AI_GATEWAY_API_KEY), and you can switch primary and fallback models from the Vercel dashboard rather than a redeploy. For an agent that fires dozens of tool calls per request, a 5% cheaper fallback can pay for itself in a week.
Migration Notes from v5
parametersis nowinputSchemaontool()maxSteps: 5is nowstopWhen: stepCountIs(5)- Provider packages (
@ai-sdk/openai, etc.) still work, but string model IDs are the default useChatnow consumesUIMessageparts — update your renderer to iteratemessage.partsinstead ofmessage.toolInvocations- The codemod (
npx @ai-sdk/codemod upgrade) handles about 80% of the mechanical changes
