
Ai Sdk Agents
- 5 installs
- 5 repo stars
- Updated August 5, 2026
- bjornmelin/dev-skills
Ai-sdk-agents is a Claude Code skill providing expert guidance for building autonomous agents with the AI SDK v6+ ToolLoopAgent.
About
Ai-sdk-agents is a Claude Code skill giving expert guidance for building autonomous agents with the AI SDK v6+ ToolLoopAgent. It covers when to use ToolLoopAgent versus core generateText/streamText, loop control with stopWhen and hasToolCall, structured output, streaming, and type-safe client integration. It also documents runtime configuration via callOptionsSchema, prepareCall, and prepareStep, plus multi-agent workflow patterns. Developers use it when creating agents, tool loops, or agent workflows with the Vercel AI SDK.
- Guidance for building autonomous agents with ToolLoopAgent in AI SDK v6+
- Covers stopWhen, prepareStep, callOptionsSchema, prepareCall, and tool loops
- Includes workflow patterns: sequential, routing, evaluator-optimizer, orchestrator-worker
Ai Sdk Agents by the numbers
- 5 all-time installs (skills.sh)
- Ranked #13,065 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
ai-sdk-agents capabilities & compatibility
Free skill; requires a model provider API key (e.g. Anthropic or OpenAI) to run the agents it builds.
- Capabilities
- orchestration · api development
- Works with
- anthropic · openai · vercel
- Use cases
- orchestration · api development
- Pricing
- Bring your own API key
What ai-sdk-agents says it does
Build autonomous agents with ToolLoopAgent: reusable model + tools + loop control.
Set `stopWhen` (default: `stepCountIs(20)`) for safety.
npx skills add https://github.com/bjornmelin/dev-skills --skill ai-sdk-agentsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 5 |
| Last updated | August 5, 2026 |
| Repository | bjornmelin/dev-skills ↗ |
What it does
Build autonomous AI agents with the AI SDK ToolLoopAgent, controlling the tool loop, structured output, and multi-agent workflows.
Who is it for?
Building autonomous multi-step agents and workflows with ToolLoopAgent and loop control
Skip if: Deterministic single-shot generation flows better served by generateText or streamText
When should I use this skill?
Creating agents, configuring stopWhen/prepareStep, tool loops, or agent workflows with the AI SDK
What you get
A working ToolLoopAgent with correct stopWhen, structured output, streaming, and workflow patterns.
- ToolLoopAgent agent implementations
- agent API routes and workflow patterns
By the numbers
- default stopWhen is stepCountIs(20)
- 7 reference files bundled
Files
AI SDK Agents
Build autonomous agents with ToolLoopAgent: reusable model + tools + loop control.
Quick Start
Assume Zod v4.3.5 for schema typing.
import { ToolLoopAgent, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const weatherAgent = new ToolLoopAgent({
model: anthropic('claude-sonnet-4-20250514'),
tools: {
weather: tool({
description: 'Get the weather in a location (F)',
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => ({ location, temperature: 72 }),
}),
},
});
const result = await weatherAgent.generate({
prompt: 'What is the weather in San Francisco?',
});When to Use ToolLoopAgent vs Core Functions
- Use ToolLoopAgent for dynamic, multi-step tasks where the model decides which tools to call.
- Use generateText/streamText for deterministic flows or strict ordering.
Essential Patterns
Structured Output
import { ToolLoopAgent, Output } from 'ai';
import { z } from 'zod';
const analysisAgent = new ToolLoopAgent({
model: 'openai/gpt-4o',
output: Output.object({
schema: z.object({
sentiment: z.enum(['positive', 'neutral', 'negative']),
summary: z.string(),
}),
}),
});Streaming Agent
const stream = myAgent.stream({ prompt: 'Summarize this report' });
for await (const chunk of stream.textStream) {
process.stdout.write(chunk);
}API Route
import { createAgentUIStreamResponse } from 'ai';
export async function POST(request: Request) {
const { messages } = await request.json();
return createAgentUIStreamResponse({ agent: myAgent, messages });
}Type-Safe Client Integration
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
const myAgent = new ToolLoopAgent({ model, tools });
export type MyAgentUIMessage = InferAgentUIMessage<typeof myAgent>;Loop Control Checklist
- Set
stopWhen(default:stepCountIs(20)) for safety. - Use
hasToolCall('finalAnswer')to stop on terminal actions. - Use
prepareStepto swap models, compress messages, or limit tools per step.
Runtime Configuration
- Use
callOptionsSchemato define type-safe runtime options. - Use
prepareCallto select model/tools or inject RAG context once per call. - Use
prepareStepfor per-step decisions (budget limits, dynamic tools).
Reference Files
| Reference | When to Use |
|---|---|
references/fundamentals.md | ToolLoopAgent basics, Output types, streaming |
references/loop-control.md | stopWhen, hasToolCall, prepareStep patterns |
references/configuration.md | callOptionsSchema, prepareCall vs prepareStep |
references/workflow-patterns.md | multi-agent workflows and routing |
references/real-world.md | RAG, multimodal, file processing |
references/production.md | monitoring, safety, cost control |
references/migration.md | v6 migration notes |
Call Options Configuration Reference
Type-safe runtime inputs to dynamically configure agent behavior.
DefaultstopWhenisstepCountIs(20). Override explicitly if needed.
Why Use Call Options
- Add dynamic context (retrieved docs, user preferences, session data)
- Select models dynamically based on request complexity
- Configure tools per request (user location, API keys)
- Customize provider options (reasoning effort, temperature)
Basic Example
import { ToolLoopAgent } from 'ai';
import { z } from 'zod';
const supportAgent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-4.5',
callOptionsSchema: z.object({
userId: z.string(),
accountType: z.enum(['free', 'pro', 'enterprise']),
}),
instructions: 'You are a helpful customer support agent.',
prepareCall: ({ options, ...settings }) => ({
...settings,
instructions: `${settings.instructions}\n\nUser context:\n- Account type: ${options.accountType}\n- User ID: ${options.userId}`,
}),
});
const result = await supportAgent.generate({
prompt: 'How do I upgrade my account?',
options: { userId: 'user_123', accountType: 'free' },
});Dynamic Model Selection
const agent = new ToolLoopAgent({
model: 'openai/gpt-4o',
callOptionsSchema: z.object({ complexity: z.enum(['simple', 'complex']) }),
prepareCall: ({ options, ...settings }) => ({
...settings,
model: options.complexity === 'simple'
? 'openai/gpt-4o-mini'
: 'openai/o1-mini',
}),
});Dynamic Tool Configuration
import { openai } from '@ai-sdk/openai';
const newsAgent = new ToolLoopAgent({
model: 'openai/gpt-4o',
callOptionsSchema: z.object({
userCity: z.string().optional(),
userRegion: z.string().optional(),
}),
tools: { web_search: openai.tools.webSearch() },
prepareCall: ({ options, ...settings }) => ({
...settings,
tools: {
web_search: openai.tools.webSearch({
searchContextSize: 'low',
userLocation: {
type: 'approximate',
city: options.userCity,
region: options.userRegion,
country: 'US',
},
}),
},
}),
});Provider-Specific Options
import { OpenAIProviderOptions } from '@ai-sdk/openai';
const agent = new ToolLoopAgent({
model: 'openai/o3',
callOptionsSchema: z.object({
taskDifficulty: z.enum(['low', 'medium', 'high']),
}),
prepareCall: ({ options, ...settings }) => ({
...settings,
providerOptions: {
openai: {
reasoningEffort: options.taskDifficulty,
} satisfies OpenAIProviderOptions,
},
}),
});RAG Pattern (Async prepareCall)
const ragAgent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-4.5',
callOptionsSchema: z.object({ query: z.string() }),
prepareCall: async ({ options, ...settings }) => {
const documents = await vectorSearch(options.query);
return {
...settings,
instructions: `Answer questions using the following context:\n\n${documents.map(doc => doc.content).join('\n\n')}`,
};
},
});prepareStep vs prepareCall
| Callback | Timing | Use Case |
|---|---|---|
prepareCall | Once before agent starts | Model selection, inject context, validate options |
prepareStep | Before each step | Budget limits, dynamic tools, step-specific context |
Combined Example
const agent = new ToolLoopAgent({
model: 'openai/gpt-4o-mini',
callOptionsSchema: z.object({ isPremium: z.boolean() }),
prepareCall: ({ options, ...settings }) => ({
...settings,
model: options.isPremium ? 'openai/gpt-4o' : settings.model,
}),
prepareStep: async ({ stepNumber, steps }) => {
const totalTokens = steps.reduce(
(acc, s) => acc + (s.usage?.totalTokens ?? 0),
0
);
if (totalTokens > 10000) return { toolChoice: 'none' };
if (totalTokens > 5000) return { model: 'openai/gpt-4o-mini' };
return {};
},
});AI SDK Agents - Fundamentals
Core concepts for building agents with the ToolLoopAgent class in AI SDK v6+.
ToolLoopAgent Class
ToolLoopAgent encapsulates model, tools, and loop control into a reusable agent. It runs a reasoning-and-acting loop (multi-step tool calling) and exposes generate() / stream().
Basic Agent Creation
import { ToolLoopAgent } from 'ai';
import { openai } from '@ai-sdk/openai';
const myAgent = new ToolLoopAgent({
model: openai('gpt-4o'),
instructions: 'You are a helpful assistant.',
tools: {
// tools here
},
});Complete Example
import { ToolLoopAgent, tool, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const codeAgent = new ToolLoopAgent({
model: openai('gpt-4o'),
instructions: 'You are an expert software engineer.',
stopWhen: stepCountIs(20),
tools: {
runCode: tool({
description: 'Execute Python code',
inputSchema: z.object({ code: z.string() }),
execute: async ({ code }) => ({ output: `ran: ${code.length} chars` }),
}),
},
toolChoice: 'auto',
});Instructions vs System
- ToolLoopAgent uses
instructions(renamed fromsystemin v6 beta). generateText/streamTextstill usesystem.
const agent = new ToolLoopAgent({
model: openai('gpt-4o'),
instructions: 'Be concise and use bullets.',
});Agent Outputs
ToolLoopAgent can return text or structured outputs via Output.
import { Output, ToolLoopAgent } from 'ai';
import { z } from 'zod';
const analysisAgent = new ToolLoopAgent({
model: openai('gpt-4o'),
output: Output.object({
schema: z.object({
sentiment: z.enum(['positive', 'neutral', 'negative']),
summary: z.string(),
}),
}),
});Streaming
const stream = myAgent.stream({ prompt: 'Summarize this report.' });
for await (const chunk of stream.textStream) {
process.stdout.write(chunk);
}Type-safe UI Integration
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
const myAgent = new ToolLoopAgent({ model, tools });
export type MyAgentUIMessage = InferAgentUIMessage<typeof myAgent>;Loop Control Reference
Use stop conditions and per-step configuration to control ToolLoopAgent execution.
Stop Conditions
stepCountIs(n)- Stop after n steps (default: 20)hasToolCall('toolName')- Stop when a specific tool is called
import { ToolLoopAgent, stepCountIs, hasToolCall } from 'ai';
const agent = new ToolLoopAgent({
model: 'openai/gpt-4o',
tools: { /* ... */ },
stopWhen: [stepCountIs(20), hasToolCall('finalAnswer')],
});Custom Stop Condition
import type { StopCondition, ToolSet } from 'ai';
const tools = { /* ... */ } satisfies ToolSet;
const budgetExceeded: StopCondition<typeof tools> = ({ steps }) => {
const totalTokens = steps.reduce((acc, s) => acc + (s.usage?.totalTokens ?? 0), 0);
return totalTokens > 10000;
};prepareStep
prepareStep runs before each step and can override settings.
Model Switching
prepareStep: async ({ stepNumber, messages }) => {
if (stepNumber > 2 && messages.length > 10) {
return { model: 'anthropic/claude-sonnet-4.5' };
}
return {};
}Context Management
prepareStep: async ({ messages }) => {
if (messages.length > 20) {
return { messages: messages.slice(-10) };
}
return {};
}Phase-Based Tooling
prepareStep: async ({ stepNumber }) => {
if (stepNumber <= 2) return { activeTools: ['search'], toolChoice: 'required' };
if (stepNumber <= 5) return { activeTools: ['analyze'] };
return { activeTools: ['summarize'], toolChoice: 'required' };
}Force Specific Tool
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) return { toolChoice: { type: 'tool', toolName: 'search' } };
if (stepNumber === 5) return { toolChoice: { type: 'tool', toolName: 'summarize' } };
return {};
}AI SDK Agents v6 Migration Guide
Migrate from AI SDK v6 beta to v6 stable release.
Breaking Changes Summary
| Before (v6 Beta) | After (v6 Stable) |
|---|---|
Experimental_Agent | ToolLoopAgent |
system parameter | instructions |
Default stepCountIs(1) | Default stepCountIs(20) |
Experimental_InferAgentUIMessage | InferAgentUIMessage |
Experimental_AgentSettings | ToolLoopAgentSettings |
Automated Migration
Step 1: Run Codemod
npx @ai-sdk/codemod v6Note: The codemod handles some type renames but NOT the agent class rename (Experimental_Agent to ToolLoopAgent).
Step 2: Update Packages
pnpm add ai@^6.0.3Step 3: Manual Fixes (Required)
Agent Class Rename
// Before
import { Experimental_Agent } from 'ai';
const agent = new Experimental_Agent({ ... });
// After
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({ ... });Parameter Rename: system to instructions
// Before
new Experimental_Agent({
system: 'You are a helpful assistant.',
model: openai('gpt-4o'),
tools: { ... },
});
// After
new ToolLoopAgent({
instructions: 'You are a helpful assistant.',
model: openai('gpt-4o'),
tools: { ... },
});Note: The system parameter in generateText() and streamText() is unchanged. This rename only affects the ToolLoopAgent class.
Default stopWhen Changed
// v6 Beta default: stepCountIs(1) - single step only
// v6 Stable default: stepCountIs(20) - multi-step agent loop
// If you relied on single-step behavior, explicitly set:
new ToolLoopAgent({
model: openai('gpt-4o'),
stopWhen: stepCountIs(1), // Restore v6 beta behavior
tools: { ... },
});This change reflects that agents are designed for multi-step reasoning. Most agents need multiple steps to call tools and generate responses.
Type Renames
// Before
import {
Experimental_AgentSettings,
Experimental_InferAgentUIMessage
} from 'ai';
type MySettings = Experimental_AgentSettings;
type MyMessage = Experimental_InferAgentUIMessage<typeof myAgent>;
// After
import {
ToolLoopAgentSettings,
InferAgentUIMessage
} from 'ai';
type MySettings = ToolLoopAgentSettings;
type MyMessage = InferAgentUIMessage<typeof myAgent>;Step 4: Search/Replace Patterns
Run these replacements in your codebase:
| Search | Replace |
|---|---|
Experimental_Agent | ToolLoopAgent |
system: (in agent constructors) | instructions: |
Experimental_InferAgentUIMessage | InferAgentUIMessage |
Experimental_AgentSettings | ToolLoopAgentSettings |
Regex patterns for IDE search:
# Agent class rename
Experimental_Agent\b -> ToolLoopAgent
# System to instructions (in object literals)
(\s+)system:\s*(['"`]) -> $1instructions: $2
# Type renames
Experimental_InferAgentUIMessage -> InferAgentUIMessage
Experimental_AgentSettings -> ToolLoopAgentSettingsStep 5: Type Check
pnpm type-checkVerify no TypeScript errors remain after migration.
Complete Migration Example
Before (v6 Beta)
import { Experimental_Agent, Experimental_InferAgentUIMessage, stepCountIs, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const myAgent = new Experimental_Agent({
model: openai('gpt-4o'),
system: 'You are a helpful assistant.',
stopWhen: stepCountIs(5),
tools: {
search: tool({
description: 'Search the web',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => ({ results: [] }),
}),
},
});
export type MyMessage = Experimental_InferAgentUIMessage<typeof myAgent>;
const result = await myAgent.generate({
prompt: 'Search for AI news',
});After (v6 Stable)
import { ToolLoopAgent, InferAgentUIMessage, stepCountIs, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const myAgent = new ToolLoopAgent({
model: openai('gpt-4o'),
instructions: 'You are a helpful assistant.',
stopWhen: stepCountIs(5),
tools: {
search: tool({
description: 'Search the web',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => ({ results: [] }),
}),
},
});
export type MyMessage = InferAgentUIMessage<typeof myAgent>;
const result = await myAgent.generate({
prompt: 'Search for AI news',
});New v6 Stable Features
ToolLoopAgent Constructor Options
new ToolLoopAgent({
// Required
model: LanguageModel,
// Optional - Agent behavior
instructions: string, // Renamed from 'system'
tools: Record<string, Tool>,
stopWhen: StopCondition, // Default: stepCountIs(20)
toolChoice: 'auto' | 'required' | 'none' | { type: 'tool', toolName: string },
output: Output, // Output.object(), Output.array(), etc.
activeTools: string[],
// Optional - Dynamic configuration
prepareStep: PrepareStepFunction,
prepareCall: PrepareCallFunction,
callOptionsSchema: ZodSchema,
// Optional - Callbacks
onStepFinish: Callback,
onFinish: Callback,
// Optional - Model settings
temperature: number,
maxOutputTokens: number,
// ... other LanguageModelSettings
});hasToolCall Stop Condition
import { ToolLoopAgent, hasToolCall, stepCountIs } from 'ai';
const agent = new ToolLoopAgent({
model: openai('gpt-4o'),
tools: {
search: searchTool,
submit: submitTool,
},
// Stop when submit tool is called OR after 20 steps
stopWhen: [stepCountIs(20), hasToolCall('submit')],
});Custom Stop Condition Signature
import { StopCondition, ToolSet } from 'ai';
const tools = { ... } satisfies ToolSet;
const customStop: StopCondition<typeof tools> = ({ steps, stepNumber }) => {
// Access typed step information
const hasFoundAnswer = steps.some(step =>
step.text?.includes('FINAL ANSWER:')
);
return hasFoundAnswer;
};MCP Integration (Now Stable)
import { createMCPClient } from '@ai-sdk/mcp';
// HTTP transport
const mcpClient = createMCPClient({
transport: {
type: 'http',
url: 'https://mcp-server.example.com',
},
});
// SSE transport with OAuth
const mcpClient = createMCPClient({
transport: {
type: 'sse',
url: 'https://mcp-server.example.com/sse',
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
});
// Use MCP tools in agent
const agent = new ToolLoopAgent({
model: openai('gpt-4o'),
tools: await mcpClient.tools(),
});Troubleshooting
Agent runs forever / too many steps
The default changed from stepCountIs(1) to stepCountIs(20). If your agent previously ran once and stopped, it may now run multiple times.
Fix: Explicitly set stopWhen: stepCountIs(1) or adjust to appropriate limit.
Type errors with InferAgentUIMessage
Ensure you're importing from 'ai' not a subpath:
// Correct
import { InferAgentUIMessage } from 'ai';
// Incorrect
import { InferAgentUIMessage } from 'ai/react';"system" property does not exist
The system parameter was renamed to instructions for ToolLoopAgent only.
Fix: Replace system: with instructions: in agent constructors.
Verification Checklist
- [ ] Updated
aipackage to^6.0.3 - [ ] Ran
npx @ai-sdk/codemod v6 - [ ] Replaced
Experimental_AgentwithToolLoopAgent - [ ] Replaced
systemwithinstructionsin agent constructors - [ ] Replaced
Experimental_InferAgentUIMessagewithInferAgentUIMessage - [ ] Replaced
Experimental_AgentSettingswithToolLoopAgentSettings - [ ] Reviewed
stopWhendefaults (now 20 steps instead of 1) - [ ] Ran
pnpm type-checkwith no errors - [ ] Tested agent behavior in development
Production Agent Patterns
Best practices for deploying AI agents to production.
Note: ToolLoopAgent defaults tostopWhen: stepCountIs(20). For production, always explicitly setstopWhenwith appropriate limits and cost controls.
Token Budget Management
Control costs with token-aware stopping:
import { ToolLoopAgent, StopCondition, ToolSet } from 'ai';
const tools = { /* ... */ } satisfies ToolSet;
const budgetExceeded: StopCondition<typeof tools> = ({ steps }) => {
const totalUsage = steps.reduce(
(acc, step) => ({
inputTokens: acc.inputTokens + (step.usage?.inputTokens ?? 0),
outputTokens: acc.outputTokens + (step.usage?.outputTokens ?? 0),
}),
{ inputTokens: 0, outputTokens: 0 },
);
// Estimate cost (adjust rates per model)
const costEstimate =
(totalUsage.inputTokens * 0.01 + totalUsage.outputTokens * 0.03) / 1000;
return costEstimate > 0.50; // Stop if cost exceeds $0.50
};
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-4.5',
tools,
stopWhen: [stepCountIs(20), budgetExceeded],
});Cost-Aware Model Selection
const agent = new ToolLoopAgent({
model: 'openai/gpt-4o-mini', // Start cheap
tools: { /* ... */ },
prepareStep: async ({ stepNumber, steps }) => {
// Calculate running cost
const totalTokens = steps.reduce(
(sum, step) => sum + (step.usage?.inputTokens ?? 0) + (step.usage?.outputTokens ?? 0),
0
);
// If early and cheap, try stronger model for complex reasoning
if (stepNumber > 2 && totalTokens < 5000) {
return { model: 'anthropic/claude-sonnet-4.5' };
}
// If budget is running low, switch to cheaper model
if (totalTokens > 10000) {
return { model: 'openai/gpt-4o-mini' };
}
return {};
},
});Context Window Management
Prune messages to stay within limits:
const agent = new ToolLoopAgent({
model: 'openai/gpt-4o',
tools: { /* ... */ },
prepareStep: async ({ messages }) => {
// Estimate token count (rough: 4 chars ≈ 1 token)
const estimatedTokens = messages.reduce(
(sum, msg) => sum + (typeof msg.content === 'string' ? msg.content.length / 4 : 100),
0
);
// If approaching limit, summarize older messages
if (estimatedTokens > 100000) { // 128k context window
const systemMsg = messages[0];
const recentMsgs = messages.slice(-10);
// Summarize middle messages
const { text: summary } = await generateText({
model: 'openai/gpt-4o-mini',
prompt: `Summarize these conversation messages concisely:
${JSON.stringify(messages.slice(1, -10))}`,
});
return {
messages: [
systemMsg,
{ role: 'system', content: `Previous conversation summary: ${summary}` },
...recentMsgs,
],
};
}
return {};
},
});Error Recovery
Implement retry logic and fallbacks:
import { ToolLoopAgent, AI_APICallError } from 'ai';
async function runAgentWithRetry(
agent: ToolLoopAgent,
prompt: string,
maxRetries = 3
) {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await agent.generate({ prompt });
} catch (error) {
lastError = error as Error;
if (error instanceof AI_APICallError) {
// Rate limit - wait and retry
if (error.statusCode === 429) {
await sleep(Math.pow(2, attempt) * 1000); // Exponential backoff
continue;
}
// Server error - retry with different model
if (error.statusCode >= 500) {
// Try fallback model
const fallbackAgent = new ToolLoopAgent({
...agent.config,
model: 'openai/gpt-4o', // Fallback
});
return await fallbackAgent.generate({ prompt });
}
}
throw error; // Non-retryable error
}
}
throw lastError;
}Type-Safe UI Integration
Use InferAgentUIMessage for typed messages:
import { ToolLoopAgent, InferAgentUIMessage, tool } from 'ai';
import { z } from 'zod';
const myAgent = new ToolLoopAgent({
model: 'openai/gpt-4o',
tools: {
getWeather: tool({
description: 'Get weather',
inputSchema: z.object({ city: z.string() }),
execute: async ({ city }) => ({ temp: 72, condition: 'sunny' }),
}),
},
});
// Type-safe message type for this specific agent
type MyAgentMessage = InferAgentUIMessage<typeof myAgent>;
// Use in React component
function ChatMessage({ message }: { message: MyAgentMessage }) {
return (
<div>
{message.parts.map((part, i) => {
switch (part.type) {
case 'text':
return <p key={i}>{part.text}</p>;
case 'tool-getWeather':
if (part.state === 'output-available') {
return <WeatherCard key={i} {...part.output} />;
}
return <Loading key={i} />;
default:
return null;
}
})}
</div>
);
}createAgentUIStreamResponse
Stream agent responses to UI:
// app/api/chat/route.ts
import { createAgentUIStreamResponse } from 'ai';
import { myAgent } from '@/ai/agents/my-agent';
export async function POST(request: Request) {
const { messages, options } = await request.json();
return createAgentUIStreamResponse({
agent: myAgent,
messages,
options,
onFinish({ steps, usage }) {
// Log for monitoring
console.log('Agent completed', {
stepCount: steps.length,
totalTokens: usage?.totalTokens,
});
},
});
}Monitoring and Observability
const agent = new ToolLoopAgent({
model: 'openai/gpt-4o',
tools: { /* ... */ },
experimental_telemetry: {
isEnabled: true,
functionId: 'my-agent',
metadata: {
version: '1.0.0',
environment: process.env.NODE_ENV,
},
},
onStepFinish({ stepNumber, usage, toolCalls }) {
// Send to monitoring service
metrics.track('agent_step', {
stepNumber,
inputTokens: usage?.inputTokens,
outputTokens: usage?.outputTokens,
toolsUsed: toolCalls.map(tc => tc.toolName),
});
},
});Request Timeouts
const result = await agent.generate({
prompt: 'Complex research task...',
abortSignal: AbortSignal.timeout(60000), // 60 second timeout
});Best Practices Summary
1. Budget limits: Always set token/cost budgets 2. Step limits: Use stepCountIs to prevent infinite loops 3. Fallback models: Have backup models for failures 4. Context pruning: Manage message history size 5. Retry logic: Implement exponential backoff 6. Monitoring: Log all agent runs with telemetry 7. Timeouts: Set request timeouts for all calls 8. Type safety: Use InferAgentUIMessage for UI
Real-World Agent Patterns
Production-ready patterns for common agent use cases.
RAG Agent
Retrieval-Augmented Generation for knowledge-based agents:
import { ToolLoopAgent, tool, embed, cosineSimilarity } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
// Vector store (simplified - use Pinecone, Qdrant, etc. in production)
const documentStore: { content: string; embedding: number[] }[] = [];
const ragAgent = new ToolLoopAgent({
model: 'openai/gpt-4o',
instructions: `You are a helpful assistant with access to a knowledge base.
Use the getInformation tool to search for relevant information before answering.
Always cite your sources.`,
tools: {
addResource: tool({
description: 'Add a resource to the knowledge base',
inputSchema: z.object({
content: z.string().describe('The content to add'),
}),
execute: async ({ content }) => {
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: content,
});
documentStore.push({ content, embedding });
return { success: true, message: 'Resource added to knowledge base' };
},
}),
getInformation: tool({
description: 'Search the knowledge base for relevant information',
inputSchema: z.object({
query: z.string().describe('The search query'),
}),
execute: async ({ query }) => {
const { embedding: queryEmbedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: query,
});
// Find most similar documents
const scored = documentStore.map(doc => ({
content: doc.content,
similarity: cosineSimilarity(queryEmbedding, doc.embedding),
}));
const topResults = scored
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 3);
return { results: topResults };
},
}),
},
});Two-Stage RAG with Reranking
import { embed, embedMany, rerank } from 'ai';
import { cohere } from '@ai-sdk/cohere';
async function twoStageRetrieval(query: string, documents: string[]) {
// Stage 1: Embedding similarity (fast, get top 20)
const { embedding: queryEmb } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: query,
});
const { embeddings: docEmbs } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: documents,
});
const candidates = documents
.map((doc, i) => ({ doc, score: cosineSimilarity(queryEmb, docEmbs[i]) }))
.sort((a, b) => b.score - a.score)
.slice(0, 20)
.map(c => c.doc);
// Stage 2: Reranking (accurate, get top 5)
const { results } = await rerank({
model: cohere.reranker('rerank-v3.5'),
query,
documents: candidates,
topK: 5,
});
return results.map(r => r.document);
}Multi-Modal Agent
Agent that processes images and PDFs:
import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';
import fs from 'fs';
const multiModalAgent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-4.5', // Supports vision
instructions: 'You can analyze images and documents. Ask users to upload files.',
tools: {
analyzeImage: tool({
description: 'Analyze an uploaded image',
inputSchema: z.object({
imagePath: z.string().describe('Path to the image file'),
question: z.string().describe('What to analyze about the image'),
}),
execute: async ({ imagePath, question }) => {
const imageBuffer = fs.readFileSync(imagePath);
const base64 = imageBuffer.toString('base64');
const mimeType = imagePath.endsWith('.png') ? 'image/png' : 'image/jpeg';
const { text } = await generateText({
model: 'anthropic/claude-sonnet-4.5',
messages: [{
role: 'user',
content: [
{ type: 'image', image: `data:${mimeType};base64,${base64}` },
{ type: 'text', text: question },
],
}],
});
return { analysis: text };
},
}),
extractFromPDF: tool({
description: 'Extract text and analyze a PDF document',
inputSchema: z.object({
pdfPath: z.string().describe('Path to the PDF file'),
}),
execute: async ({ pdfPath }) => {
// Use a PDF library to extract text
const text = await extractPDFText(pdfPath);
return { extractedText: text.slice(0, 10000) }; // Limit size
},
}),
},
});Conversational Agent (Slack Bot)
Agent with thread history and context:
import { ToolLoopAgent, tool, stepCountIs } from 'ai';
import { z } from 'zod';
// Store conversation history per thread
const threadHistory = new Map<string, Message[]>();
const slackAgent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-4.5',
callOptionsSchema: z.object({
threadId: z.string(),
channelId: z.string(),
userId: z.string(),
}),
prepareCall: async ({ options, ...settings }) => {
// Load thread history
const history = threadHistory.get(options.threadId) || [];
return {
...settings,
instructions: `You are a helpful Slack assistant.
Thread ID: ${options.threadId}
Channel: ${options.channelId}
User: ${options.userId}
Previous messages in thread:
${history.map(m => `${m.role}: ${m.content}`).join('\n')}`,
};
},
tools: {
searchSlack: tool({
description: 'Search Slack messages',
inputSchema: z.object({
query: z.string(),
channel: z.string().optional(),
}),
execute: async ({ query, channel }) => {
// Call Slack API
const results = await slackClient.search.messages({ query, channel });
return { messages: results.messages.matches.slice(0, 5) };
},
}),
postMessage: tool({
description: 'Post a message to a Slack channel',
inputSchema: z.object({
channel: z.string(),
text: z.string(),
}),
execute: async ({ channel, text }) => {
await slackClient.chat.postMessage({ channel, text });
return { success: true };
},
}),
},
stopWhen: stepCountIs(10),
});
// Handle Slack event
async function handleSlackMessage(event: SlackEvent) {
const result = await slackAgent.generate({
prompt: event.text,
options: {
threadId: event.thread_ts || event.ts,
channelId: event.channel,
userId: event.user,
},
});
// Update thread history
const history = threadHistory.get(event.thread_ts || event.ts) || [];
history.push({ role: 'user', content: event.text });
history.push({ role: 'assistant', content: result.text });
threadHistory.set(event.thread_ts || event.ts, history);
return result.text;
}Customer Support Agent
Agent with ticketing and escalation:
const supportAgent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-4.5',
callOptionsSchema: z.object({
customerId: z.string(),
tier: z.enum(['basic', 'premium', 'enterprise']),
}),
prepareCall: async ({ options, ...settings }) => {
// Fetch customer history
const customer = await db.customers.findOne({ id: options.customerId });
const tickets = await db.tickets.find({ customerId: options.customerId }).limit(5);
return {
...settings,
instructions: `You are a customer support agent.
Customer: ${customer.name} (${options.tier} tier)
Previous tickets: ${tickets.map(t => t.summary).join(', ')}
For ${options.tier} tier:
${options.tier === 'enterprise' ? '- Prioritize their requests\n- Offer phone callback option' : ''}
${options.tier === 'basic' ? '- Suggest upgrade for advanced features' : ''}`,
};
},
tools: {
createTicket: tool({
description: 'Create a support ticket',
inputSchema: z.object({
summary: z.string(),
priority: z.enum(['low', 'medium', 'high']),
}),
execute: async ({ summary, priority }) => {
const ticket = await db.tickets.create({ summary, priority });
return { ticketId: ticket.id };
},
}),
escalateToHuman: tool({
description: 'Escalate to human agent',
inputSchema: z.object({
reason: z.string(),
}),
execute: async ({ reason }) => {
await notifyHumanAgents(reason);
return { escalated: true };
},
}),
},
});MCP Agent (Model Context Protocol)
Connect agents to external tools and services via the standardized Model Context Protocol. MCP allows your agents to use tools from any MCP-compatible server.
Basic MCP Integration
import { ToolLoopAgent, stepCountIs } from 'ai';
import { createMCPClient } from '@ai-sdk/mcp';
import { openai } from '@ai-sdk/openai';
// Create MCP client with HTTP transport
const mcpClient = createMCPClient({
transport: {
type: 'http',
url: 'https://mcp-server.example.com',
},
});
// Use MCP tools in your agent
const mcpAgent = new ToolLoopAgent({
model: openai('gpt-4o'),
instructions: 'You have access to external tools via MCP.',
tools: await mcpClient.tools(),
stopWhen: stepCountIs(20),
});
// Generate with MCP tools
const result = await mcpAgent.generate({
prompt: 'Use the available tools to complete this task.',
});SSE Transport with OAuth
import { createMCPClient } from '@ai-sdk/mcp';
// SSE transport with authentication
const mcpClient = createMCPClient({
transport: {
type: 'sse',
url: 'https://mcp-server.example.com/sse',
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
});
const agent = new ToolLoopAgent({
model: openai('gpt-4o'),
tools: await mcpClient.tools(),
});Combining MCP with Local Tools
import { ToolLoopAgent, tool } from 'ai';
import { createMCPClient } from '@ai-sdk/mcp';
const mcpClient = createMCPClient({
transport: { type: 'http', url: 'https://mcp-server.example.com' },
});
const localTools = {
saveToDatabase: tool({
description: 'Save data to local database',
inputSchema: z.object({ data: z.any() }),
execute: async ({ data }) => {
await db.insert(data);
return { saved: true };
},
}),
};
// Merge MCP tools with local tools
const agent = new ToolLoopAgent({
model: openai('gpt-4o'),
instructions: 'You can use both MCP and local tools.',
tools: {
...localTools,
...(await mcpClient.tools()),
},
});MCP with Resources and Prompts
import { createMCPClient } from '@ai-sdk/mcp';
const mcpClient = createMCPClient({
transport: { type: 'http', url: 'https://mcp-server.example.com' },
});
// Access MCP resources (data sources)
const resources = await mcpClient.resources();
// Access MCP prompts (pre-defined prompt templates)
const prompts = await mcpClient.prompts();
// Use a specific prompt
const promptResult = await mcpClient.getPrompt('analyze-document', {
document: 'path/to/document.pdf',
});Best Practices
1. Persist context: Store conversation history for continuity 2. Rate limiting: Implement per-user rate limits 3. Graceful degradation: Handle API failures with fallbacks 4. Monitoring: Log all agent interactions for debugging 5. Testing: Mock external services for unit tests
Workflow Patterns Reference
Building blocks for reliable AI agent workflows.
Note: ToolLoopAgent usesstopWhen: stepCountIs(20)by default. The patterns below usegenerateText/generateObjectdirectly for simpler workflows. Use ToolLoopAgent for complex multi-step agents.
Pattern Selection Guide
| Pattern | Use Case | Flexibility | Control |
|---|---|---|---|
| Sequential | Pipelines, transformations | Low | High |
| Parallel | Independent tasks | Medium | High |
| Routing | Input-dependent paths | Medium | Medium |
| Orchestrator-Worker | Complex multi-expert tasks | High | Medium |
| Evaluator-Optimizer | Quality-critical outputs | Low | High |
Start simple: Add complexity only when needed.
Sequential Processing (Chains)
Steps executed in order, each output becomes next input:
import { generateText, generateObject } from 'ai';
import { z } from 'zod';
async function generateMarketingCopy(input: string) {
const model = 'openai/gpt-4o';
// Step 1: Generate copy
const { text: copy } = await generateText({
model,
prompt: `Write persuasive marketing copy for: ${input}`,
});
// Step 2: Quality check
const { object: qualityMetrics } = await generateObject({
model,
schema: z.object({
hasCallToAction: z.boolean(),
emotionalAppeal: z.number().min(1).max(10),
clarity: z.number().min(1).max(10),
}),
prompt: `Evaluate this marketing copy: ${copy}`,
});
// Step 3: Improve if needed
if (!qualityMetrics.hasCallToAction || qualityMetrics.emotionalAppeal < 7) {
const { text: improvedCopy } = await generateText({
model,
prompt: `Rewrite with improvements: ${copy}`,
});
return { copy: improvedCopy, qualityMetrics };
}
return { copy, qualityMetrics };
}Routing
Model-driven path selection based on input:
async function handleCustomerQuery(query: string) {
// Step 1: Classify
const { object: classification } = await generateObject({
model: 'openai/gpt-4o',
schema: z.object({
type: z.enum(['general', 'refund', 'technical']),
complexity: z.enum(['simple', 'complex']),
}),
prompt: `Classify this query: ${query}`,
});
// Step 2: Route based on classification
const { text: response } = await generateText({
model: classification.complexity === 'simple'
? 'openai/gpt-4o-mini'
: 'openai/o4-mini',
system: {
general: 'You are a customer service agent.',
refund: 'You specialize in refund requests.',
technical: 'You are a technical support specialist.',
}[classification.type],
prompt: query,
});
return { response, classification };
}Parallel Processing
Independent tasks run simultaneously:
async function parallelCodeReview(code: string) {
const [securityReview, performanceReview, maintainabilityReview] =
await Promise.all([
generateObject({
model: 'openai/gpt-4o',
system: 'You are a security expert.',
schema: z.object({
vulnerabilities: z.array(z.string()),
riskLevel: z.enum(['low', 'medium', 'high']),
}),
prompt: `Review for security: ${code}`,
}),
generateObject({
model: 'openai/gpt-4o',
system: 'You are a performance expert.',
schema: z.object({
issues: z.array(z.string()),
impact: z.enum(['low', 'medium', 'high']),
}),
prompt: `Review for performance: ${code}`,
}),
generateObject({
model: 'openai/gpt-4o',
system: 'You are a code quality expert.',
schema: z.object({
concerns: z.array(z.string()),
qualityScore: z.number().min(1).max(10),
}),
prompt: `Review for quality: ${code}`,
}),
]);
// Aggregate results
const { text: summary } = await generateText({
model: 'openai/gpt-4o',
system: 'You are a technical lead.',
prompt: `Synthesize these reviews: ${JSON.stringify([
securityReview.object,
performanceReview.object,
maintainabilityReview.object,
])}`,
});
return { reviews: [securityReview, performanceReview, maintainabilityReview], summary };
}Orchestrator-Worker
Primary model coordinates specialized workers:
async function implementFeature(featureRequest: string) {
// Orchestrator: Plan the implementation
const { object: plan } = await generateObject({
model: 'anthropic/claude-sonnet-4.5',
schema: z.object({
files: z.array(z.object({
purpose: z.string(),
filePath: z.string(),
changeType: z.enum(['create', 'modify', 'delete']),
})),
estimatedComplexity: z.enum(['low', 'medium', 'high']),
}),
system: 'You are a senior software architect.',
prompt: `Plan implementation for: ${featureRequest}`,
});
// Workers: Execute planned changes
const fileChanges = await Promise.all(
plan.files.map(async file => {
const workerSystem = {
create: 'You implement new files following best practices.',
modify: 'You modify existing code maintaining consistency.',
delete: 'You safely remove code avoiding breaking changes.',
}[file.changeType];
const { object: change } = await generateObject({
model: 'openai/gpt-4o',
schema: z.object({
explanation: z.string(),
code: z.string(),
}),
system: workerSystem,
prompt: `Implement ${file.filePath}: ${file.purpose}`,
});
return { file, implementation: change };
})
);
return { plan, changes: fileChanges };
}Evaluator-Optimizer
Iterative quality improvement loop:
async function translateWithFeedback(text: string, targetLanguage: string) {
let currentTranslation = '';
let iterations = 0;
const MAX_ITERATIONS = 3;
// Initial translation
const { text: translation } = await generateText({
model: 'anthropic/claude-sonnet-4.5',
system: 'You are an expert literary translator.',
prompt: `Translate to ${targetLanguage}: ${text}`,
});
currentTranslation = translation;
// Evaluation-optimization loop
while (iterations < MAX_ITERATIONS) {
// Evaluate
const { object: evaluation } = await generateObject({
model: 'anthropic/claude-sonnet-4.5',
schema: z.object({
qualityScore: z.number().min(1).max(10),
preservesTone: z.boolean(),
culturallyAccurate: z.boolean(),
specificIssues: z.array(z.string()),
}),
system: 'You evaluate literary translations.',
prompt: `Evaluate:
Original: ${text}
Translation: ${currentTranslation}`,
});
// Check if quality meets threshold
if (evaluation.qualityScore >= 8 && evaluation.preservesTone && evaluation.culturallyAccurate) {
break;
}
// Improve based on feedback
const { text: improved } = await generateText({
model: 'anthropic/claude-sonnet-4.5',
system: 'You are an expert literary translator.',
prompt: `Improve this translation:
Issues: ${evaluation.specificIssues.join(', ')}
Original: ${text}
Current: ${currentTranslation}`,
});
currentTranslation = improved;
iterations++;
}
return { translation: currentTranslation, iterations };
}Combining Patterns
Real-world agents often combine multiple patterns:
async function processDocument(document: string) {
// 1. Route based on document type
const { object: docType } = await generateObject({
model: 'openai/gpt-4o',
schema: z.object({ type: z.enum(['contract', 'report', 'email']) }),
prompt: `Classify document type: ${document}`,
});
// 2. Parallel extraction based on type
if (docType.type === 'contract') {
const [parties, terms, risks] = await Promise.all([
extractParties(document),
extractTerms(document),
analyzeRisks(document),
]);
// 3. Evaluate and improve summary
return await evaluateAndImprove({ parties, terms, risks });
}
// ... handle other document types
}Best Practices
1. Start simple: Use single agent + tools before workflows 2. Minimize handoffs: Each step adds latency and error potential 3. Error handling: Implement retries and fallbacks 4. Cost awareness: Complex workflows = more API calls 5. Test components: Unit test each step independently
Related skills
FAQ
When should I use ToolLoopAgent vs core functions?
Use ToolLoopAgent for dynamic, multi-step tasks where the model decides which tools to call, and use generateText/streamText for deterministic flows or strict ordering.
How do I keep an agent loop safe?
Set stopWhen (default stepCountIs(20)) for safety, use hasToolCall('finalAnswer') to stop on terminal actions, and use prepareStep to swap models, compress messages, or limit tools per step.