The ToolLoopAgent provides a structured way to encapsulate LLM configuration, tools, and behavior into reusable components. It handles the agent loop for you, allowing the LLM to call tools multiple times in sequence to accomplish complex tasks. Define agents once and use them across your application.
Define an agent by instantiating the ToolLoopAgent class with your desired configuration:
import { ToolLoopAgent } from 'ai';
const myAgent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.5",
instructions: 'You are a helpful assistant.',
tools: {
// Your tools here
},
});
Use runtimeContext as the agent's shared runtime state. It flows through the
agent loop and is available in prepareStep, lifecycle callbacks, and final
results. If a tool needs server-side values such as credentials, scoped
permissions, or default settings, pass them through toolsContext and declare
them with the tool's contextSchema.
Set runtimeContext and toolsContext in the ToolLoopAgent constructor.
For context that changes with each invocation, use callOptionsSchema and
prepareCall. See Configuring Call Options.
Model call settings returned from prepareStep, such as temperature, apply
only to the current step. Later steps use the agent's top-level setting unless
they return another override.
For the full model, including sensitive context filtering and where each context
value is available, see Runtime and Tool
Context.
Pass experimental_sandbox when an agent tool needs a command or code execution
environment. The experimental sandbox is a per-call value, so provide it to
generate(), stream(), or the agent UI stream helper that invokes the agent.
const agent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.5",
instructions: 'You are a coding assistant. Use the shell tool when needed.',
tools: {
shell: tool({
description: 'Execute shell commands in the experimental sandbox.',
inputSchema: z.object({
command: z.string(),
workingDirectory: z.string().optional(),
}),
execute: async (
{ command, workingDirectory },
{ abortSignal, experimental_sandbox },
) => {
if (!experimental_sandbox) {
throw new Error('Experimental sandbox is not available');
}
return experimental_sandbox.run({
command,
workingDirectory,
abortSignal,
});
},
}),
},
});
const result = await agent.generate({
prompt: `Run the tests.\n\nSandbox:\n${experimental_sandbox.description}`,
experimental_sandbox,
});
The experimental sandbox description is not added to the model prompt automatically. Include
it in the prompt or instructions when the model needs to know environment
details. Passing an experimental sandbox does not sandbox the tool itself; the tool must
explicitly delegate operations to the experimental sandbox. See the
Experimental Sandbox section in Tool Calling
for more details.
You can also require approval before a tool executes. Configure approval on the
ToolLoopAgent with toolApproval:
By default, agents run for 20 steps (stopWhen: isStepCount(20)). In each step, the model either generates text or calls a tool. If it generates text, the agent completes. If it calls a tool, the AI SDK executes that tool.
You can configure stopWhen differently to allow more steps. After each tool execution, the agent triggers a new generation where the model can call another tool or generate text:
import { ToolLoopAgent, isStepCount } from 'ai';
const agent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.5",
stopWhen: isStepCount(50), // Increase default from 20 to 50.
});
Each step represents one generation (which results in either text or a tool call). 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
You can combine multiple conditions:
import { ToolLoopAgent, isStepCount } from 'ai';
const agent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.5",
stopWhen: [
isStepCount(20), // Maximum 20 steps
yourCustomCondition(), // Custom logic for when to stop
],
});
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.5",
tools: {
// your tools here
},
toolChoice: 'required', // Force tool use
// or toolChoice: 'none' to disable tools
// or toolChoice: 'auto' (default) to let the model decide
});
You can also force the use of a specific tool:
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.5",
tools: {
weather: weatherTool,
cityAttractions: attractionsTool,
},
toolChoice: {
type: 'tool',
toolName: 'weather', // Force the weather tool to be used
},
});
System instructions define your agent's behavior, personality, and constraints. They set the context for all interactions and guide how the agent responds to user queries and uses tools.
const agent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.5",
instructions:
'You are an expert data analyst. You provide clear insights from complex data.',
});
const codeReviewAgent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.5",
instructions: `You are a senior software engineer conducting code reviews.
Your approach:
- Focus on security vulnerabilities first
- Identify performance bottlenecks
- Suggest improvements for readability and maintainability
- Be constructive and educational in your feedback
- Always explain why something is an issue and how to fix it`,
});
const customerSupportAgent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.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,
},
});
const researchAgent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.5",
instructions: `You are a research assistant with access to search and document tools.
When researching:
1. Always start with a broad search to understand the topic
2. Use document analysis for detailed information
3. Cross-reference multiple sources before drawing conclusions
4. Cite your sources when presenting information
5. If information conflicts, present both viewpoints`,
tools: {
webSearch,
analyzeDocument,
extractQuotes,
},
});
Control the output format and communication style:
const technicalWriterAgent = new ToolLoopAgent({
model: "anthropic/claude-sonnet-5.5",
instructions: `You are a technical documentation writer.
Writing style:
- Use clear, simple language
- Avoid jargon unless necessary
- Structure information with headers and bullet points
- Include code examples where relevant
- Write in second person ("you" instead of "the user")
Always format responses in Markdown.`,
});
Agents provide lifecycle callbacks that let you hook into different phases of the agent execution.
These are useful for logging, observability, debugging, and custom telemetry.
onStart: Called once when the agent operation begins, before any LLM calls. Receives model info, messages, settings, and runtimeContext.
onStepStart: Called before each step (LLM call). Receives the step number, model, messages being sent, tools, and prior steps.
onToolExecutionStart: Called right before a tool's execute function runs. Receives the tool call object, messages, and toolContext.
onToolExecutionEnd: Called right after a tool's execute function completes or errors. Receives the tool call, toolExecutionMs, and a toolOutput discriminated union (type: 'tool-result' with output, or type: 'tool-error' with error).
onStepEnd: Called after each step finishes. Receives step results including usage, performance, finish reason, and tool calls.
onEnd: Called when all steps are finished and the response is complete. Receives all step results, total usage, and runtimeContext.
For the full event data reference, see Lifecycle Callbacks. ToolLoopAgent uses the same generation lifecycle event types for onStart, onStepStart, onToolExecutionStart, onToolExecutionEnd, onStepEnd, and onEnd.
All lifecycle callbacks can be defined in the constructor for agent-wide tracking, in the generate()/stream() call for per-call tracking, or both. When both are provided, both are called (constructor first, then the method callback):
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';const myAgent = new ToolLoopAgent({ // ... configuration});// Infer the UIMessage type for UI components or persistenceexport type MyAgentUIMessage = InferAgentUIMessage<typeof myAgent>;
Use this type in your client components with useChat:
components/chat.tsx
'use client';import { useChat } from '@ai-sdk/react';import type { MyAgentUIMessage } from '@/agent/my-agent';export function Chat() { const { messages } = useChat<MyAgentUIMessage>(); // Full type safety for your messages and tools}