ToolLoopAgent class overview
The ToolLoopAgent class handles the three components of agents: LLMs, tools, and the orchestration loop. It automatically manages the loop, context management, and stopping conditions to orchestrate tool calls and generate final responses.
toolsContext for per-tool values
Use toolsContext for per-tool values such as API keys or scoped permissions. Each tool receives only its own typed context based on its contextSchema.
runtimeContext for agent shared state
Use runtimeContext as the agent's shared runtime state for server-side data that should not be placed directly in the prompt, such as tenant settings, request IDs, feature flags, credentials, or progress through a task. It flows through the agent loop, is available in prepareStep and lifecycle callbacks, and can be updated between steps.
ToolLoopAgent benefits
The ToolLoopAgent is the recommended approach for building agents with the AI SDK because it reduces boilerplate by managing loops and message arrays, improves reusability by defining the agent once and using it throughout your application, and simplifies maintenance by providing a single place to update agent configuration.
When to use ToolLoopAgent vs core functions
Start with the ToolLoopAgent for most use cases. Use core functions like generateText and streamText when you need explicit control over each step for complex structured workflows.
runAgentTUI function usage
The runAgentTUI function accepts an object with title and agent properties, where title is a string for the terminal UI title and agent is a ToolLoopAgent instance to run.
Weather agent example with ToolLoopAgent
Example showing a ToolLoopAgent with two tools: a 'weather' tool that takes a location string and returns temperature in Fahrenheit, and a 'convertFahrenheitToCelsius' tool that converts Fahrenheit to Celsius. The agent automatically calls these tools in sequence to answer the prompt 'What is the weather in San Francisco in celsius?' and returns the final answer in result.text and the steps taken in result.steps.
Structured workflows for deterministic outcomes
When you need reliable, repeatable outcomes with explicit control flow instead of non-deterministic agent behavior, use core functions with structured workflow patterns combining conditional statements for explicit branching, standard functions for reusable logic, error handling for robustness, and explicit control flow for predictability.
ToolLoopAgent.generate() method
The ToolLoopAgent.generate() method takes a prompt parameter and returns a result object with a text property containing the agent's final answer and a steps property containing the steps taken by the agent.
ToolLoopAgent constructor: model and tools parameters
The ToolLoopAgent constructor takes a model parameter and a tools parameter. The model is the LLM to use, and tools is an object containing tool definitions, where each tool is created using the tool() function with description, inputSchema, and execute properties.
Agent loop orchestration: context management and stopping conditions
The agent loop orchestrates execution through two mechanisms: context management, which maintains conversation history and decides what the model sees at each step, and stopping conditions, which determine when the loop (task) is complete.
Terminal UI with @ai-sdk/tui
Use @ai-sdk/tui to run a ToolLoopAgent in an interactive terminal UI. It is useful for local agent development, demos, and internal tools, providing prompt input, streamed responses, tool cards, reasoning sections, scrolling, and tool approval prompts without building a custom interface.
HarnessAgent class for preconfigured harnesses
Use HarnessAgent when you want to run a preconfigured established harness, such as Claude Code, Codex, or Pi, instead of building the loop yourself around a language model. Harnesses are a separate abstraction from providers and models, but stream into AI SDK-compatible result and UI primitives.
Agents definition: LLMs using tools in a loop
Agents are large language models (LLMs) that use tools in a loop to accomplish tasks. The three core components are: LLMs that process input and decide the next action, tools that extend capabilities beyond text generation (reading files, calling APIs, writing to databases), and a loop that orchestrates execution through context management and stopping conditions.
Provider-specific options example code
import { OpenAILanguageModelResponsesOptions } from '@ai-sdk/openai';
import { ToolLoopAgent } from 'ai';
import { z } from 'zod';
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 OpenAILanguageModelResponsesOptions,
},
}),
});
await agent.generate({
prompt: 'Analyze this complex scenario...',
options: { taskDifficulty: 'high' },
});
RAG with call options example code
import { ToolLoopAgent } from 'ai';
__PROVIDER_IMPORT__;
import { z } from 'zod';
const ragAgent = new ToolLoopAgent({
model: __MODEL__,
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')}`,
};
},
});
await ragAgent.generate({
prompt: 'What is our refund policy?',
options: { query: 'refund policy' },
});
Dynamic model selection with call options
You can select different models at runtime based on call options. Use prepareCall to return a different model property based on options. For example, use a faster model like 'openai/gpt-4o-mini' for simple queries and a more capable model like 'openai/o1-mini' for complex reasoning.
Basic call options example code
import { ToolLoopAgent } from 'ai';
__PROVIDER_IMPORT__;
import { z } from 'zod';
const supportAgent = new ToolLoopAgent({
model: __MODEL__,
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:\n- Account type: ${options.accountType}\n- User ID: ${options.userId}\n\nAdjust your response based on the user's account level.`,
}),
});
const result = await supportAgent.generate({
prompt: 'How do I upgrade my account?',
options: {
userId: 'user_123',
accountType: 'free',
},
});
Dynamic model selection example code
import { ToolLoopAgent } from 'ai';
__PROVIDER_IMPORT__;
import { z } from 'zod';
const agent = new ToolLoopAgent({
model: __MODEL__,
callOptionsSchema: z.object({
complexity: z.enum(['simple', 'complex']),
}),
prepareCall: ({ options, ...settings }) => ({
...settings,
model:
options.complexity === 'simple' ? 'openai/gpt-4o-mini' : 'openai/o1-mini',
}),
});
await agent.generate({
prompt: 'What is 2+2?',
options: { complexity: 'simple' },
});
await agent.generate({
prompt: 'Explain quantum entanglement',
options: { complexity: 'complex' },
});
Dynamic tool configuration with call options
You can configure tool behavior at runtime using call options. Define tools in the agent config, then in prepareCall return a modified tools object with the same tool keys but updated configuration. For example, pass user location to web search tools based on userCity and userRegion options.
Call options for RAG pattern
For Retrieval Augmented Generation, use call options to pass a query that prepareCall can use to fetch relevant documents asynchronously. Since prepareCall can be async, you can await vectorSearch(options.query) and inject the returned documents into instructions.
Combining multiple modifications example code
import { ToolLoopAgent } from 'ai';
__PROVIDER_IMPORT__;
import { z } from 'zod';
const agent = new ToolLoopAgent({
model: __MODEL__,
callOptionsSchema: z.object({
userRole: z.enum(['admin', 'user']),
urgency: z.enum(['low', 'high']),
}),
tools: {
readDatabase: readDatabaseTool,
writeDatabase: writeDatabaseTool,
},
prepareCall: ({ options, ...settings }) => ({
...settings,
model: options.urgency === 'high' ? __MODEL__ : settings.model,
activeTools:
options.userRole === 'admin'
? ['readDatabase', 'writeDatabase']
: ['readDatabase'],
instructions: `You are a ${options.userRole} assistant.\n${options.userRole === 'admin' ? 'You have full database access.' : 'You have read-only access.'}`,
}),
});
await agent.generate({
prompt: 'Update the user record',
options: {
userRole: 'admin',
urgency: 'high',
},
});
prepareCall hook signature and behavior
The prepareCall hook receives an object with options (the runtime options passed in) and settings (the current agent settings). It returns modified settings. Only return the settings you want to change; the rest will be merged. The prepareCall function can be async, enabling you to fetch data before configuring the agent.
callOptionsSchema property
The callOptionsSchema property on agent configuration accepts a Zod schema that defines the shape of options that will be passed at runtime. When defined, the options parameter becomes required and type-checked on generate() and stream() calls.
Call options configuration steps
Define call options in three steps: (1) Define the schema - specify what inputs you accept using callOptionsSchema; (2) Configure with prepareCall - use those inputs to modify agent settings; (3) Pass options at runtime - provide the options when calling generate() or stream().
Combining multiple modifications in prepareCall
You can modify multiple agent settings in a single prepareCall implementation. Common patterns include: changing model based on urgency, limiting activeTools based on user role, and adjusting instructions based on options.
Using call options with createAgentUIStreamResponse
Pass call options through createAgentUIStreamResponse by providing an options property alongside agent and messages. Options extracted from the request can be passed directly to createAgentUIStreamResponse, which will forward them to the agent.
Provider-specific options with call options
Use prepareCall to set provider-specific options dynamically. Return a providerOptions object keyed by provider name. For example, set OpenAI's reasoningEffort based on task difficulty: providerOptions: { openai: { reasoningEffort: options.taskDifficulty } }.
Call options definition and purpose
Call options allow you to pass type-safe structured inputs to your agent. Use them to dynamically modify any agent setting based on the specific request. Without call options, you would need to create multiple agents or handle configuration logic outside the agent.
Using call options with createAgentUIStreamResponse example code
import { createAgentUIStreamResponse } from 'ai';
import { myAgent } from '@/ai/agents/my-agent';
export async function POST(request: Request) {
const { messages, userId, accountType } = await request.json();
return createAgentUIStreamResponse({
agent: myAgent,
messages,
options: {
userId,
accountType,
},
});
}
Dynamic tool configuration example code
import { openai } from '@ai-sdk/openai';
import { ToolLoopAgent } from 'ai';
__PROVIDER_IMPORT__;
import { z } from 'zod';
const newsAgent = new ToolLoopAgent({
model: __MODEL__,
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',
},
}),
},
}),
});
await newsAgent.generate({
prompt: 'What are the top local news stories?',
options: {
userCity: 'San Francisco',
userRegion: 'California',
},
});
Call options use cases
Call options enable: (1) Adding dynamic context by injecting retrieved documents, user preferences, or session data into prompts; (2) Selecting models dynamically based on request complexity; (3) Configuring tools per request such as passing user location to search tools or adjusting tool behavior; (4) Customizing provider options by setting reasoning effort, temperature, or other provider-specific settings.
Custom memory tool implementation patterns
Two common patterns for building custom memory tools: (1) Structured actions — define explicit operations (view, create, update, search) and handle structured input yourself, safe by design since you control every operation. (2) Bash-backed — give the model a sandboxed bash environment to compose shell commands (cat, grep, sed, echo) for flexible memory access, more powerful but requires command validation for safety.
isLoopFinished() purpose with memory tools
isLoopFinished() lets an agent keep running until the tool loop naturally finishes, which is useful when memory tools need to read and write before the final response is generated.
MongoDB memory usage example
import { createMongoDBMemory } from '@mongodb-developer/vercel-ai-memory';
import { openai } from '@ai-sdk/openai';
import { ToolLoopAgent, isLoopFinished } from 'ai';
const mongodbMemory = createMongoDBMemory({
uri: process.env.MONGODB_URI!,
embedder: openai.embedding('text-embedding-3-small'),
});
const agent = new ToolLoopAgent({
model: openai('gpt-4.1'),
tools: mongodbMemory({ userId: 'alice', sessionId: 'sess-001' }),
stopWhen: isLoopFinished(),
});
const result = await agent.generate({
prompt: 'My name is Alice and I love hiking. Remember that.',
});
MongoDB memory session modes
MongoDB memory session supports two modes: tool-driven (the LLM decides when to read/write, good for prototypes) and hook-driven (the runtime persists every turn via prepareCall + onEnd hooks, recommended for production). Other memory tiers (semantic, procedural, episodic, scratchpad) are always LLM-controlled and selective by design.
Hindsight bankId configuration
In Hindsight, the bankId parameter identifies the memory store and is typically a user ID. In multi-user applications, call createHindsightTools inside the request handler so each request gets the correct bank/memory store.
Hindsight usage example
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
import { ToolLoopAgent } from 'ai';
const client = new HindsightClient({ baseUrl: process.env.HINDSIGHT_API_URL });
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: createHindsightTools({ client, bankId: 'user-123' }),
instructions: 'You are a helpful assistant with long-term memory.',
});
const result = await agent.generate({
prompt: 'Remember that my favorite editor is Neovim',
});
MongoDB memory provider features
@mongodb-developer/vercel-ai-memory provides MongoDB Atlas-backed persistent memory with five structured tiers: Session, Semantic, Procedural, Episodic, and Scratchpad. Retrieval is powered by Atlas Vector Search using any AI SDK embedding model. Features include automatic index creation and per-type retention policies. Targets AI SDK v6 and requires peer dependencies: ai ^6.0.0, mongodb ^6.0.0, zod ^3.0.0.
Supermemory usage example
import { supermemoryTools } from '@supermemory/tools/ai-sdk';
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: supermemoryTools(process.env.SUPERMEMORY_API_KEY!),
});
const result = await agent.generate({
prompt: 'Remember that my favorite editor is Neovim',
});
Hindsight memory provider installation and setup
Install Hindsight with: pnpm add @vectorize-io/hindsight-ai-sdk @vectorize-io/hindsight-client. Hindsight provides agents with persistent memory through five tools: retain, recall, reflect, getMentalModel, and getDocument. Can be self-hosted with Docker or used as a cloud service. Works with any AI SDK provider with no vendor lock-in beyond the service itself.
MongoDB memory provider installation
Install MongoDB memory with: pnpm add @mongodb-developer/vercel-ai-memory.
Mem0 memory provider installation and setup
Install Mem0 provider with: pnpm add @mem0/vercel-ai-provider. Mem0 adds a memory layer on top of any supported LLM provider. It automatically extracts memories from conversations, stores them, and retrieves relevant ones for future prompts. Works across multiple LLM providers including OpenAI, Anthropic, Google, Groq, and Cohere.
Letta memory tools integration example
import { lettaCloud } from '@letta-ai/vercel-ai-sdk-provider';
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: lettaCloud(),
tools: {
core_memory_append: lettaCloud.tool('core_memory_append'),
memory_insert: lettaCloud.tool('memory_insert'),
memory_replace: lettaCloud.tool('memory_replace'),
},
providerOptions: {
letta: {
agent: { id: 'your-agent-id' },
},
},
});
const stream = agent.stream({
prompt: 'What do you remember about me?',
});
Mem0 basic usage example
import { createMem0 } from '@mem0/vercel-ai-provider';
import { ToolLoopAgent } from 'ai';
const mem0 = createMem0({
provider: 'openai',
mem0ApiKey: process.env.MEM0_API_KEY,
apiKey: process.env.OPENAI_API_KEY,
});
const agent = new ToolLoopAgent({
model: mem0('gpt-4.1', { user_id: 'user-123' }),
});
const { text } = await agent.generate({
prompt: 'Remember that my favorite editor is Neovim',
});
Letta memory provider installation and basic setup
Install Letta provider with: pnpm add @letta-ai/vercel-ai-sdk-provider. Configure a ToolLoopAgent with lettaCloud as the model and set providerOptions with agent id. Letta handles memory management (core memory, archival memory, recall) transparently. The agent is created on Letta's platform (cloud or self-hosted) before using the AI SDK provider.
Anthropic Memory Tool usage example
import { anthropic } from '@ai-sdk/anthropic';
import { ToolLoopAgent } from 'ai';
const memory = anthropic.tools.memory_20250818({
execute: async action => {
// action contains command, path, and other fields
// Implement your storage backend here.
// Return the result as a string.
},
});
const agent = new ToolLoopAgent({
model: 'anthropic/claude-haiku-4.5',
tools: { memory },
});
const result = await agent.generate({
prompt: 'Remember that my favorite editor is Neovim',
});
Letta basic usage example
import { lettaCloud } from '@letta-ai/vercel-ai-sdk-provider';
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: lettaCloud(),
providerOptions: {
letta: {
agent: { id: 'your-agent-id' },
},
},
});
const result = await agent.generate({
prompt: 'Remember that my favorite editor is Neovim',
});
Mem0 explicit memory management functions
Mem0 provides addMemories and retrieveMemories functions for explicit memory management. addMemories(messages, { user_id: 'user-123' }) stores memories. retrieveMemories(prompt, { user_id: 'user-123' }) retrieves relevant memories for a prompt.
Three approaches to add memory to agents
Memory can be added to an agent using three approaches: (1) Provider-Defined Tools (low effort, medium flexibility, yes provider lock-in), (2) Memory Providers (low effort, low flexibility, depends on memory provider for lock-in), (3) Custom Tool (high effort, high flexibility, no provider lock-in).
Supermemory provider installation and usage
Install Supermemory with: pnpm add @supermemory/tools. Supermemory is a long-term memory platform providing tools that handle saving and retrieving memories automatically through semantic search. Works with any AI SDK provider. Tools provide addMemory and searchMemories operations that handle storage and retrieval.
Agent loop stops when
The agent loop continues until one of these conditions is met: a finish reasoning other than tool-calls is returned, a tool that is invoked does not have an execute function, a tool call needs approval, or a stop condition is met.
stopWhen parameter for loop control
The stopWhen parameter controls when to stop execution when there are tool results in the last step. By default, agents stop after 20 steps using isStepCount(20). 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.
prepareStep callback for loop modification
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.
Combine multiple stopping conditions example
Example showing how to combine multiple stopping conditions with an array, where the loop stops when any condition is met:
```ts
import { ToolLoopAgent, isStepCount, hasToolCall } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
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',
});
```
isLoopFinished stopping condition example
Example showing how to use isLoopFinished() to allow the agent to run until it naturally stops making tool calls with no maximum step limit:
```ts
import { ToolLoopAgent, isLoopFinished } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: {
// your tools
},
stopWhen: isLoopFinished(), // No maximum step limit.
});
const result = await agent.generate({
prompt: 'Analyze this dataset and create a summary report',
});
```
isStepCount stopping condition example
Example showing how to use isStepCount to increase the default step limit from 20 to 50 in a ToolLoopAgent:
```ts
import { ToolLoopAgent, isStepCount } from 'ai';
__PROVIDER_IMPORT__;
const agent = new ToolLoopAgent({
model: __MODEL__,
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',
});
```
Manual loop control with generateText
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 and gives complete control over message history management, step-by-step decision making, custom stopping conditions, dynamic tool and model selection, and error handling and recovery.
Accessing forced tool calling results
When using forced tool calling with a done tool that has no execute function, the final answer is available in result.staticToolCalls, which contains tool calls that weren't executed.
Default step limit safety measure
The default step limit of 20 steps using isStepCount(20) is a safety measure to prevent runaway loops that could result in excessive API calls and costs.