Chapter 18 · Agent Introduction: ToolLoopAgent and Loop Control
Chapter goals:
- Understand the three要素 of an Agent: LLM, Tools, and Loop, and how
ToolLoopAgentencapsulates them- Learn to create Agents: model/tool/instructions config and three usage modes (generate/stream/UI stream)
- Master loop control:
stopWhenstop conditions (isStepCount/hasToolCall/isLoopFinished) and custom conditions- Use
prepareStepfor dynamic model switching, per-step setting overrides, context compaction, and phased tool selection- Understand call options (
callOptionsSchema+prepareCall) for per-request Agent customization
18.1 What Is an Agent
An Agent is a large language model (LLM) that完成任务 in a loop using tools. Three components work together:
- LLM: Processes input and decides the next action;
- Tools: Extend capabilities beyond text generation (read files, call APIs, write to databases);
- Loop: Orchestrates execution, including — context management (maintaining conversation history and deciding what each step's model sees), stop conditions (judging when the task is complete).
ToolLoopAgent Class
The ToolLoopAgent class encapsulates these three components. Below is an agent that loops through multiple tools to完成 a task:
import { ToolLoopAgent, tool, createGateway } from 'ai';
import { z } from 'zod';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const weatherAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
weather: tool({
description: 'Get the weather in a location (in Fahrenheit)',
inputSchema: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
}),
convertFahrenheitToCelsius: tool({
description: 'Convert temperature from Fahrenheit to Celsius',
inputSchema: z.object({
temperature: z.number().describe('Temperature in Fahrenheit'),
}),
execute: async ({ temperature }) => {
const celsius = Math.round((temperature - 32) * (5 / 9));
return { celsius };
},
}),
},
});
const result = await weatherAgent.generate({
prompt: 'What is the weather in San Francisco in celsius?',
});
console.log(result.text); // agent's final answer
console.log(result.steps); // steps taken by the agentThis agent自动: ① calls the weather tool to get the Fahrenheit temperature; ② calls convertFahrenheitToCelsius to convert; ③ generates a final text answer with the result. ToolLoopAgent handles the loop, context management, and stop conditions.
Why ToolLoopAgent Is Recommended
- Less boilerplate —托管 the loop and message array;
- Improved reusability — define once, use across the app;
- Simplified maintenance — update config in a single location.
Start with ToolLoopAgent for most场景; only for complex structured workflows requiring explicit per-step control should you使用 the core functions directly (generateText, streamText). For deterministic, repeatable output, use the structured workflow模式 (conditional branches + standard functions + error handling + explicit control flow).
18.2 Creating and Configuring an Agent
Instantiating ToolLoopAgent完成s the definition:
import { ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const myAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
instructions: 'You are a helpful assistant.',
tools: {
// Your tools here
},
});It accepts the same settings as generateText / streamText. Provide tools for the agent to完成 the task:
import { ToolLoopAgent, tool, createGateway } from 'ai';
import { z } from 'zod';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const codeAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
runCode: tool({
description: 'Execute Python code',
inputSchema: z.object({
code: z.string(),
}),
execute: async ({ code }) => {
// Execute code and return result
return { output: 'Code executed successfully' };
},
}),
},
});Loop Control
By default, the agent runs for 20 steps (stopWhen: isStepCount(20)). In each step, the model either generates text (agent completes) or calls a tool (AI SDK executes the tool then starts a new generation round):
import { ToolLoopAgent, isStepCount, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
stopWhen: isStepCount(50), // Increase default from 20 to 50.
});You can combine multiple conditions:
import { ToolLoopAgent, isStepCount, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
stopWhen: [
isStepCount(20), // Maximum 20 steps
yourCustomCondition(), // Custom logic for when to stop
],
});Tool Choice
Control how the agent uses tools: toolChoice: 'required' forces tool use, 'none' disables tools, 'auto' (default) lets the model decide; you can also force使用 a specific tool:
import { ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
weather: weatherTool,
cityAttractions: attractionsTool,
},
toolChoice: {
type: 'tool',
toolName: 'weather', // Force the weather tool to be used
},
});Structured Output
Define structured output schema via Output:
import { ToolLoopAgent, Output, createGateway } from 'ai';
import { z } from 'zod';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const analysisAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
output: Output.object({
schema: z.object({
sentiment: z.enum(['positive', 'neutral', 'negative']),
summary: z.string(),
keyPoints: z.array(z.string()),
}),
}),
});
const { output } = await analysisAgent.generate({
prompt: 'Analyze customer feedback from the last quarter',
});18.3 System Instructions Define Behavior
System instructions决定 the agent's behavior, personality, and constraints. Several typical patterns:
// Basic role设定
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
instructions:
'You are an expert data analyst. You provide clear insights from complex data.',
});// Detailed behavioral guidelines
const customerSupportAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
instructions: `You are a customer support specialist for an e-commerce platform.
Rules:
- Never make promises about refunds without checking the policy
- Always be empathetic and professional
- If you don't know something, say so and offer to escalate
- Keep responses concise and actionable
- Never share internal company information`,
tools: {
checkOrderStatus,
lookupPolicy,
createTicket,
},
});You can also写 tool usage guidelines (e.g., "search broadly first, then read precisely; cross-validate sources") and format/style requirements (e.g., "output in Markdown, avoid jargon").
18.4 Three Ways to Use an Agent
One-shot text generation — generate():
const result = await myAgent.generate({
prompt: 'What is the weather like?',
});
console.log(result.text);Streaming response — stream():
const result = await myAgent.stream({
prompt: 'Tell me a story',
});
for await (const chunk of result.textStream) {
console.log(chunk);
}UI-oriented API response — createAgentUIStreamResponse() (for chat app routes):
// In your API route (e.g., app/api/chat/route.ts)
import { createAgentUIStreamResponse } from 'ai';
export async function POST(request: Request) {
const { messages } = await request.json();
return createAgentUIStreamResponse({
agent: myAgent,
uiMessages: messages,
});
}18.5 Lifecycle Callbacks
Agents provide lifecycle callbacks for logging, observability, debugging, and telemetry:
const result = await myAgent.generate({
prompt: 'Research and summarize the latest AI trends',
onStart({ modelId }) {
console.log('Agent started', { modelId });
},
onStepStart({ stepNumber, modelId }) {
console.log(`Step ${stepNumber} starting`, { modelId });
},
onToolExecutionStart({ toolCall }) {
console.log(`Tool call starting: ${toolCall.toolName}`);
},
onToolExecutionEnd({ toolCall, toolExecutionMs, toolOutput }) {
console.log(
`Tool call finished: ${toolCall.toolName} (${toolExecutionMs}ms)`,
{
success: toolOutput.type === 'tool-result',
},
);
},
onStepEnd({ stepNumber, usage, performance, finishReason, toolCalls }) {
console.log(`Step ${stepNumber} completed:`, {
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
outputTokensPerSecond: performance.effectiveOutputTokensPerSecond,
stepTimeMs: performance.stepTimeMs,
finishReason,
toolsUsed: toolCalls?.map(tc => tc.toolName),
});
},
onEnd({ usage, steps }) {
console.log('Agent finished:', {
totalSteps: steps.length,
totalTokens: usage.totalTokens,
});
},
});Available callbacks overview:
onStart: Called once at操作开始时 (before any LLM call); receives model info, messages, settings, andruntimeContext;onStepStart: Called before each step (LLM call);onToolExecutionStart/onToolExecutionEnd: Called before/after toolexecute; the latter带着toolExecutionMsand a discriminated uniontoolOutput(success含output, failure含error);onStepEnd: Called after each step; includes usage, performance, finish reason, tool calls;onEnd: Called after all steps结束; includes all step results and total usage.
Callbacks can be done at the agent level in the constructor or per-call in generate()/stream(); when both are provided, they调用 sequentially (constructor first):
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
onStepEnd: async ({ stepNumber, usage }) => {
// Agent-wide logging
console.log(`Agent step ${stepNumber}:`, usage.totalTokens);
},
});
// Method-level callback runs after constructor callback
const result = await agent.generate({
prompt: 'Hello',
onStepEnd: async ({ stepNumber, usage }) => {
// Per-call tracking (e.g., for billing)
await trackUsage(stepNumber, usage);
},
});Additionally, use InferAgentUIMessage to infer the agent's UIMessage type for UI components or persistence:
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
// export type MyAgentUIMessage = InferAgentUIMessage<typeof myAgent>;18.6 Deep Dive: Loop Control and Stop Conditions
The loop continues until one of the following occurs:
- The returned finish reason is not tool-calls;
- A called tool has no
executefunction; - A tool call requires approval;
- A stop condition is met.
Built-in Stop Conditions
isStepCount(count)— stops after reaching the specified step count;hasToolCall(...toolNames)— stops when any specified tool is called;isLoopFinished()— never triggers; lets the loop run其自然 to completion.
Increase the step上限 to 50:
import { ToolLoopAgent, isStepCount, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
// your tools
},
stopWhen: isStepCount(50), // Increasing the default of 20 to 50.
});
const result = await agent.generate({
prompt: 'Analyze this dataset and create a summary report',
});⚠️ Use isLoopFinished() with caution — without a step上限, if the model keeps发起 tool calls, it could run无限 and产生高额 cost.
Multiple conditions are "OR" —任一满足即停:
import { ToolLoopAgent, isStepCount, hasToolCall, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
// your tools
},
stopWhen: [
isStepCount(20), // Maximum 20 steps
hasToolCall('someTool', 'done'), // Stop after calling either tool
],
});Custom Stop Conditions
Build custom conditions for特定需求, e.g., detecting an answer marker:
import { ToolLoopAgent, StopCondition, ToolSet, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const tools = {
// your tools
} satisfies ToolSet;
const hasAnswer: StopCondition<typeof tools> = ({ steps }) => {
// Stop when the model generates text containing "ANSWER:"
return steps.some(step => step.text?.includes('ANSWER:')) ?? false;
};
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools,
stopWhen: hasAnswer,
});
const result = await agent.generate({
prompt: 'Find the answer and respond with "ANSWER: [your answer]"',
});Custom conditions can access跨-step信息, e.g., implementing a budget circuit breaker:
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 },
);
const costEstimate =
(totalUsage.inputTokens * 0.01 + totalUsage.outputTokens * 0.03) / 1000;
return costEstimate > 0.5; // Stop if cost exceeds $0.50
};18.7 prepareStep: Per-Step Execution Customization
The prepareStep callback runs before each loop step; returning无更改沿用初始设置. Can be used to modify settings, manage context, and实现 dynamic behavior基于执行历史.
Dynamic Model Switching
Switch models based on step needs:
import { ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-4o-mini'), // Default model
tools: {
// your tools
},
prepareStep: async ({ stepNumber, messages }) => {
// Use a stronger model for complex reasoning after initial steps
if (stepNumber > 2 && messages.length > 10) {
return {
model: gateway('openai/o4-mini'),
};
}
// Continue with default settings
return {};
},
});Per-Step Model Call Setting Override
Override provider-agnostic call settings for a single step — e.g., tool-calling steps需要 more deterministic sampling than the final answer:
import { ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
temperature: 0.7,
tools: {
// your tools
},
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
return {
temperature: 0,
maxOutputTokens: 300,
};
}
return {};
},
});prepareStep can override maxOutputTokens, temperature, topP, topK, presencePenalty, frequencyPenalty, stopSequences, seed, reasoning. Overrides only affect the current step; omitting or undefined uses the顶层 value; falsy values (e.g., temperature: 0, seed: 0, empty array) are retained.
Context Management and Compaction
Long-running agents accumulate大量 tool results, reasoning parts, and assistant messages. Use prepareStep to change the message state used by subsequent steps to实现 compaction (when to compact is up to you). The returned messages become the basis for subsequent steps; new responses append on top; initialMessages is the原始 input, responseMessages是已积累的模型/工具响应, used to rebuild.
The pruneMessages helper provides内置消息裁剪能力:
import { ToolLoopAgent, pruneMessages, type ModelMessage, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const COMPACTION_THRESHOLD = 100_000;
const estimateTokens = (messages: ModelMessage[]) => {
return JSON.stringify(messages).length / 4;
};
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
// your tools
},
prepareStep: async ({ messages }) => {
if (estimateTokens(messages) > COMPACTION_THRESHOLD) {
return {
messages: pruneMessages({
messages,
reasoning: 'all',
toolCalls: 'before-last-3-messages',
emptyMessages: 'remove',
}),
};
}
},
});The same pattern也 applies to generateText and streamText.
Phased Tool Selection
Control which tools are available each step:
import { ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
search: searchTool,
analyze: analyzeTool,
summarize: summarizeTool,
},
prepareStep: async ({ stepNumber, steps }) => {
// Search phase (steps 0-2)
if (stepNumber <= 2) {
return {
activeTools: ['search'],
toolChoice: 'required',
};
}
// Analysis phase (steps 3-5)
if (stepNumber <= 5) {
return {
activeTools: ['analyze'],
};
}
// Summary phase (step 6+)
return {
activeTools: ['summarize'],
toolChoice: 'required',
};
},
});You can also强制 use a specific tool at a step:
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
// Force the search tool to be used first
return {
toolChoice: { type: 'tool', toolName: 'search' },
};
}
// ...
return {};
},18.8 Call Options: Per-Request Agent Customization
Call options let you pass类型安全的结构化输入 to the agent, dynamically modifying任意 settings based on the具体请求. Three steps:
- Define the schema — declare accepted input via
callOptionsSchema; - Configure
prepareCall— modify agent settings用 these inputs; - Pass at runtime — provide options when calling
generate()orstream().
基础示例: inject user context into the prompt at runtime:
import { ToolLoopAgent, createGateway } from 'ai';
import { z } from 'zod';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const supportAgent = new ToolLoopAgent({
model: gateway('openai/gpt-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 +
`\nUser context:
- Account type: ${options.accountType}
- User ID: ${options.userId}
Adjust your response based on the user's account level.`,
}),
});
// Call the agent with specific user context
const result = await supportAgent.generate({
prompt: 'How do I upgrade my account?',
options: {
userId: 'user_123',
accountType: 'free',
},
});At this point, the options parameter is required and type-checked — missing or wrongly-typed values导致 TypeScript errors.
Dynamic Model Selection
Select a model based on request characteristics:
import { ToolLoopAgent, createGateway } from 'ai';
import { z } from 'zod';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-4o-mini'), // Default model
callOptionsSchema: z.object({
complexity: z.enum(['simple', 'complex']),
}),
prepareCall: ({ options, ...settings }) => ({
...settings,
model:
options.complexity === 'simple'
? gateway('openai/gpt-4o-mini')
: gateway('openai/o4-mini'),
}),
});
// Use faster model for simple queries
await agent.generate({
prompt: 'What is 2+2?',
options: { complexity: 'simple' },
});
// Use more capable model for complex reasoning
await agent.generate({
prompt: 'Explain quantum entanglement',
options: { complexity: 'complex' },
});prepareCall only returns settings you want to改; the rest stay unchanged. Approval strategies等 agent settings can同样 be dynamically returned from prepareCall based on call options (see next chapter).
18.9 Chapter Summary
- Agent = LLM + Tools + Loop;
ToolLoopAgentencapsulates all three,自动 handling the loop, context management, and stop conditions; it is the recommended way to build agents; - Three usage modes:
generate()(one-shot),stream()(streaming),createAgentUIStreamResponse()(chat route); - Four loop终止 conditions: non-tool-calls finish, tool without execute, pending-approval tool call, hit
stopWhen; built-in conditions:isStepCount/hasToolCall/isLoopFinished; supports数组组合 and自定义函数 (e.g., budget circuit breaker); prepareStep可 per-step switch models, override sampling settings, compact context withpruneMessages, narrowactiveToolsby phase;- Call options (
callOptionsSchema+prepareCall)让 the same agent注入 user context, dynamically选 model, and改 config per-request,全程类型安全.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What is the default maximum step count for ToolLoopAgent?
2. Which of the following is NOT a loop termination condition?
3. Regarding prepareStep setting overrides, which statement is correct?
4. What is the purpose of the callOptionsSchema + prepareCall combination?
🛠️ Hands-On Exercises
- Build a weather query agent with ToolLoopAgent (mock weather tool + F-to-C conversion tool); call with both
generate()andstream(); observe the tool call chain in eachresult.steps. - Build a research agent:
stopWhen: [isStepCount(10), hasToolCall('submit_report')]; then add a customStopConditionthat中断s when cumulative token cost exceeds a threshold. - Write a phased agent: first two steps only allow the search tool with
toolChoice: 'required'; then switch to an analysis model and usepruneMessagestocompact early tool results.