本页还没有译文
本页 72 段还没译成Español,下面显示的是英文原文。Loop Control
还没翻译译自英文(ai-sdk.dev)本页 72 段都还没译
You can control both the execution flow and the settings at each step of the agent loop. The loop continues until:
- A finish reasoning other than tool-calls is returned, or
- A tool that is invoked does not have an execute function, or
- A tool call needs approval, or
- A stop condition is met
A steering message that is waiting when the model finishes its answer keeps the loop going for another step.
The AI SDK provides built-in loop control through two parameters: stopWhen for defining stopping conditions and prepareStep for modifying settings (model, tools, messages, and more) between steps.
Stop Conditions
The stopWhen parameter controls when to stop execution when there are tool results in the last step. By default, ToolLoopAgent stops after 20 steps using isStepCount(20). This default is a safety measure to prevent runaway loops that could result in excessive API calls and costs.
If this default limit stops a tool loop that could otherwise continue, the SDK logs a warning with guidance on configuring stopWhen. Set stopWhen explicitly to choose a limit, even if you want to keep isStepCount(20). Explicit stop conditions do not produce this warning.
WorkflowAgent does not apply a default step limit. It continues until the model stops calling tools or another natural termination condition is met. Configure an explicit condition such as isStepCount(20) when you need to bound its model calls. See WorkflowAgent Loop Control for details.
When you provide stopWhen, the agent continues executing after tool calls until a stopping condition is met. When the condition is an array, execution stops when any of the conditions are met.
Use Built-in Conditions
The AI SDK provides several built-in stopping conditions:
isStepCount(count)— stops after a specified number of stepshasToolCall(...toolNames)— stops when any of the specified tools is calledisLoopFinished()— never triggers, letting the loop run until the agent is naturally finished
Run Up to a Maximum Number of Steps
import { ToolLoopAgent, isStepCount } from 'ai';
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.5',
tools: {
// your tools
},
stopWhen: isStepCount(50), // Increase ToolLoopAgent's default from 20 to 50.
});
const result = await agent.generate({
prompt: 'Analyze this dataset and create a summary report',
});Run Until Finished
If you want a ToolLoopAgent to run until the model naturally stops making tool calls, use isLoopFinished(). This removes its default step limit:
import { ToolLoopAgent, isLoopFinished } from 'ai';
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.5',
tools: {
// your tools
},
stopWhen: isLoopFinished(), // No maximum step limit.
});
const result = await agent.generate({
prompt: 'Analyze this dataset and create a summary report',
});Note
Use isLoopFinished() with caution. Without a step limit, the agent could
potentially run indefinitely or incur significant costs if the model keeps
making tool calls.
Combine Multiple Conditions
Combine multiple stopping conditions. The loop stops when it meets any condition:
import { ToolLoopAgent, isStepCount, hasToolCall } from 'ai';
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.5',
tools: {
// your tools
},
stopWhen: [
isStepCount(20), // Maximum 20 steps
hasToolCall('someTool', 'done'), // Stop after calling either tool
],
});
const result = await agent.generate({
prompt: 'Research and analyze the topic',
});Create Custom Conditions
Build custom stopping conditions for specific requirements:
import { ToolLoopAgent, StopCondition, ToolSet } from 'ai';
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: 'anthropic/claude-sonnet-5.5',
tools,
stopWhen: hasAnswer,
});
const result = await agent.generate({
prompt: 'Find the answer and respond with "ANSWER: [your answer]"',
});Custom conditions receive step information across all steps:
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
};Prepare Step
The prepareStep callback runs before each step in the loop and defaults to the initial settings if you don't return any changes. Use it to modify settings, manage context, or implement dynamic behavior based on execution history.
It receives messages for the current step, plus initialMessages and responseMessages when you need to distinguish the original input from assistant/tool messages accumulated in earlier steps. Treat messages as the loop's current message state: if you return a messages override, that override persists as the base for later steps, together with the response messages from each completed step.
Dynamic Model Selection
Switch models based on step requirements:
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: 'openai/gpt-6-luna', // 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: 'anthropic/claude-sonnet-5.5',
};
}
// Continue with default settings
return {};
},
});
const result = await agent.generate({
prompt: '...',
});Model Call Settings
Override provider-agnostic model call settings for an individual step. This can be useful when tool-calling steps need more deterministic sampling than the final response:
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.5',
temperature: 0.7,
tools: {
// your tools
},
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
return {
temperature: 0,
maxOutputTokens: 300,
};
}
return {};
},
});
const result = await agent.generate({
prompt: '...',
});prepareStep can override maxOutputTokens, temperature, topP, topK,
presencePenalty, frequencyPenalty, stopSequences, seed, and
reasoning. These overrides apply only to the current step. When a setting is
omitted or undefined, the top-level value is used for that step. Defined
falsy values such as temperature: 0, seed: 0, and an empty
stopSequences array are preserved.
Context Management
Long-running agents can accumulate large tool results, reasoning parts, and assistant messages. Use prepareStep to mutate the message state that will be used by later steps. This is useful for compaction, and you decide when compaction should happen.
The messages value contains the messages that will be sent for the current step. When you return a messages override from prepareStep, that changed list becomes the base for later steps. New assistant and tool response messages are appended to it as the loop continues. If you need to rebuild a step from the discrete pieces instead of the persisted message state, use initialMessages for the original input and responseMessages for the model/tool response messages accumulated so far.
The pruneMessages helper provides a built-in way to remove selected messages. You can use it inside prepareStep when you want a simple compaction strategy.
import { ToolLoopAgent, pruneMessages, type ModelMessage } from 'ai';
const COMPACTION_THRESHOLD = 100_000;
const estimateTokens = (messages: ModelMessage[]) => {
return JSON.stringify(messages).length / 4;
};
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.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',
}),
};
}
},
});
const result = await agent.generate({
prompt: '...',
});The token estimator above is intentionally simple and only demonstrates one way to decide when to compact. The main point is that prepareStep can return a new messages array, and that array becomes the message state for following steps. The same pattern also works with generateText and streamText.
Tool Selection
Control which tools are available at each step:
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.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',
};
},
});
const result = await agent.generate({
prompt: '...',
});You can also force a specific tool to be used:
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
// Force the search tool to be used first
return {
toolChoice: { type: 'tool', toolName: 'search' },
};
}
if (stepNumber === 5) {
// Force the summarize tool after analysis
return {
toolChoice: { type: 'tool', toolName: 'summarize' },
};
}
return {};
};Message Modification
Transform messages before sending them to the model. Returned messages carry forward to later steps, so later messages values include your transformed messages plus the assistant/tool response messages from completed steps:
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.5',
tools: {
// your tools
},
prepareStep: async ({ messages, stepNumber }) => {
// Summarize tool results to reduce token usage
const processedMessages = messages.map(msg => {
if (msg.role === 'tool' && msg.content.length > 1000) {
return {
...msg,
content: summarizeToolResult(msg.content),
};
}
return msg;
});
return { messages: processedMessages };
},
});
const result = await agent.generate({
prompt: '...',
});Experimental Sandbox Selection
Switch the experimental sandbox used for tool execution in a single step:
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.5',
tools: {
runCommand,
},
experimental_sandbox: defaultSandbox,
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
return {
experimental_sandbox: setupSandbox,
};
}
return {};
},
});
const result = await agent.generate({
prompt: '...',
});Unlike runtimeContext and toolsContext, an experimental sandbox returned from prepareStep
only applies to tool execution in that step. Later steps use the top-level
experimental_sandbox unless they return their own experimental sandbox override.
Steering
Warning
Steering is an experimental feature.
Steering adds a user message to a running loop. Use it when a user sends a correction or a follow-up while the agent is still working. The message is added at the next step boundary, after the current step and its tool calls are done, so running tools are never interrupted.
Create a steering object with experimental_createSteering(), pass it with the experimental_steering option, and call steer() while the loop is running:
import { experimental_createSteering, isStepCount, streamText } from 'ai';
const steering = experimental_createSteering();
const result = streamText({
model: 'anthropic/claude-sonnet-5.5',
tools: { weather },
stopWhen: isStepCount(10),
prompt: 'What is the weather in Paris?',
experimental_steering: steering,
});
// e.g. when the user sends another message while the agent is running:
const outcome = await steering.steer('I meant Paris, Texas.');Steering works the same way with generateText and ToolLoopAgent, because you create the steering object before the call starts:
const steering = experimental_createSteering();
const run = agent.generate({
prompt: 'What is the weather in Paris?',
experimental_steering: steering,
});
await steering.steer('I meant Paris, Texas.');
const result = await run;How Steering Messages Are Delivered
- Messages are added in the order they are sent. Each message is its own user message.
- If the model finishes its answer while a message is waiting, the loop continues for another step.
- Stop conditions still apply.
generateTextandstreamTextstop after one step by default, so setstopWhento steer them. Agents run up to 20 steps by default. - Messages that are sent before the loop starts are added to the first step.
prepareStepandonStepStartreceive the steering messages as part ofmessages.- A steering object can only be used for one call. Create a new one with
experimental_createSteering()for each call. - For agents, steering is supported by
ToolLoopAgent.createAgentUIStreamthrows when it gets a steering object with another agent.
Warning
messages already contains the steering messages in the right place. If
prepareStep returns a messages override that is rebuilt from other fields,
e.g. initialMessages and responseMessages, the steering messages are
dropped and steer() reports them as not delivered with the reason
removed-by-prepare-step. Build overrides from messages, or add the
steering messages of every step back in order.
prepareStep: ({ initialMessages, steps, experimental_steeringMessages }) => ({
messages: [
...initialMessages,
...steps.flatMap(step => [
...(step.experimental_steeringMessages ?? []),
...step.response.messages,
]),
// steering messages for the current step:
...(experimental_steeringMessages ?? []),
],
}),Handling the Outcome
steer() resolves with the outcome of the message and never rejects:
const outcome = await steering.steer('I meant Paris, Texas.', {
id: messageId,
});
if (outcome.type === 'delivered') {
console.log(`added to step ${outcome.stepNumber}`);
} else {
// the message was not added, e.g. send it as a new message instead:
console.log(outcome.reason, outcome.message);
}A message is delivered when it is part of the messages that are sent to the model, after prepareStep. It is not delivered when the loop ends before the next step boundary, or when prepareStep removes it. The reason is one of:
finished: the loop ended before the message could be added.removed-by-prepare-step:prepareStepreturned messages without it.stop-condition: a stop condition ended the loop.waiting-for-tool-results: the loop ended to wait for tool results, e.g. a tool approval.aborted: the loop was aborted.error: the loop failed.
Pass an id to make retries safe. Sending a message again with the same id returns the outcome of the first message.
Saving Steering Messages
Steering messages are not part of responseMessages. Each step lists the steering messages that were added to it in experimental_steeringMessages, so you can rebuild the full conversation in order:
const messages = [
...initialMessages,
...result.steps.flatMap(step => [
...(step.experimental_steeringMessages ?? []),
...step.response.messages,
]),
];Access Step Information
Both stopWhen and prepareStep receive detailed information about the current execution:
prepareStep: async ({
model, // Current model configuration
stepNumber, // Current step number (0-indexed)
steps, // All previous steps with their results
messages, // Messages to be sent to the model
}) => {
// Access previous tool calls and results
const previousToolCalls = steps.flatMap(step => step.toolCalls);
const previousResults = steps.flatMap(step => step.toolResults);
// Make decisions based on execution history
if (previousToolCalls.some(call => call.toolName === 'dataAnalysis')) {
return {
toolChoice: { type: 'tool', toolName: 'reportGenerator' },
};
}
return {};
},Forced Tool Calling
You can force the agent to always use tools by combining toolChoice: 'required' with a done tool that has no execute function. This pattern ensures the agent uses tools for every step and stops only when it explicitly signals completion.
import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-5.5',
tools: {
search: searchTool,
analyze: analyzeTool,
done: tool({
description: 'Signal that you have finished your work',
inputSchema: z.object({
answer: z.string().describe('The final answer'),
}),
// No execute function - stops the agent when called
}),
},
toolChoice: 'required', // Force tool calls at every step
});
const result = await agent.generate({
prompt: 'Research and analyze this topic, then provide your answer.',
});
// extract answer from done tool call
const toolCall = result.staticToolCalls[0]; // tool call from final step
if (toolCall?.toolName === 'done') {
console.log(toolCall.input.answer);
}Key aspects of this pattern:
toolChoice: 'required': Forces the model to call a tool at every step instead of generating text directly. This ensures the agent follows a structured workflow.donetool withoutexecute: A tool that has noexecutefunction acts as a termination signal. When the agent calls this tool, the loop stops because there's no function to execute.- Accessing results: The final answer is available in
result.staticToolCalls, which contains tool calls that weren't executed.
This pattern is useful when you want the agent to always use specific tools for operations (like code execution or data retrieval) rather than attempting to answer directly.
Manual Loop Control
For scenarios requiring complete control over the agent loop, you can use AI SDK Core functions (generateText and streamText) to implement your own loop management instead of using stopWhen and prepareStep. This approach provides maximum flexibility for complex workflows.
Implementing a Manual Loop
Build your own agent loop when you need full control over execution:
import { generateText, ModelMessage } from 'ai';
const messages: ModelMessage[] = [{ role: 'user', content: '...' }];
let step = 0;
const maxSteps = 10;
while (step < maxSteps) {
const result = await generateText({
model: 'anthropic/claude-sonnet-5.5',
messages,
tools: {
// your tools here
},
});
messages.push(...result.responseMessages);
if (result.text) {
break; // Stop when model generates text
}
step++;
}This manual approach gives you complete control over:
- Message history management
- Step-by-step decision making
- Custom stopping conditions
- Dynamic tool and model selection
- Error handling and recovery