ToolLoopAgent

Creates a reusable AI agent capable of generating text, streaming responses, and using tools over multiple steps (a reasoning-and-acting loop). ToolLoopAgent is ideal for building autonomous, multi-step agents that can take actions, call tools, and reason over the results until a stop condition is reached.

Unlike single-step calls like generateText(), an agent can iteratively invoke tools, collect tool results, and decide next actions until completion or user approval is required.

import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model: "xai/grok-4.6",
instructions: 'You are a helpful assistant.',
tools: {
weather: weatherTool,
calculator: calculatorTool,
},
});
const result = await agent.generate({
prompt: 'What is the weather in NYC?',
});
console.log(result.text);

For agents, runtimeContext is the shared runtime state that flows through the loop. For guidance on runtimeContext, toolsContext, tool context, and sensitive context filtering, see Runtime and Tool Context. Pass experimental_sandbox to generate() or stream() when tools need access to a command or code execution environment.

To see ToolLoopAgent in action, check out these examples.

Import

import { ToolLoopAgent } from "ai"

Constructor

Parameters

model:

LanguageModel

instructions?:

Instructions

allowSystemInMessages?:

boolean

tools?:

Record<string, Tool>

toolChoice?:

ToolChoice

stopWhen?:

StopCondition | StopCondition[]

activeTools?:

ActiveTools<TOOLS>

toolOrder?:

ToolOrder<TOOLS>

toolApproval?:

ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>

experimental_toolCallers?:

Experimental_ToolCallers<TOOLS>

output?:

Output

prepareStep?:

PrepareStepFunction

include?:

{ requestBody?: boolean; requestMessages?: boolean; responseBody?: boolean; rawChunks?: boolean }

repairToolCall?:

ToolCallRepairFunction

experimental_refineToolInput?:

ToolInputRefinement<TOOLS>

onStart?:

GenerateTextOnStartCallback
GenerateTextStartEvent

provider:

string

modelId:

string

instructions:

Instructions | undefined

messages:

Array<ModelMessage>

tools:

TOOLS | undefined

toolChoice:

ToolChoice<TOOLS> | undefined

activeTools:

ActiveTools<TOOLS>

toolOrder:

ToolOrder<TOOLS>

maxOutputTokens:

number | undefined

temperature:

number | undefined

topP:

number | undefined

topK:

number | undefined

presencePenalty:

number | undefined

frequencyPenalty:

number | undefined

stopSequences:

string[] | undefined

seed:

number | undefined

maxRetries:

number

timeout:

number | { totalMs?: number; stepMs?: number; firstChunkMs?: number; chunkMs?: number } | undefined

headers:

Record<string, string | undefined> | undefined

providerOptions:

ProviderOptions | undefined

output:

OUTPUT | undefined

abortSignal:

AbortSignal | undefined

include:

{ requestBody?: boolean; requestMessages?: boolean; responseBody?: boolean } | undefined

runtimeContext:

CONTEXT

toolsContext:

InferToolSetContext<TOOLS>

onStepStart?:

GenerateTextOnStepStartCallback
GenerateTextStepStartEvent

provider:

string

modelId:

string

instructions:

Instructions | undefined

messages:

Array<ModelMessage>

tools:

TOOLS | undefined

toolChoice:

LanguageModelV4ToolChoice | undefined

activeTools:

ActiveTools<TOOLS>

toolOrder:

ToolOrder<TOOLS>

steps:

ReadonlyArray<StepResult<TOOLS>>

providerOptions:

ProviderOptions | undefined

timeout:

number | { totalMs?: number; stepMs?: number; firstChunkMs?: number; chunkMs?: number } | undefined

headers:

Record<string, string | undefined> | undefined

stopWhen:

StopCondition<TOOLS> | Array<StopCondition<TOOLS>> | undefined

output:

OUTPUT | undefined

abortSignal:

AbortSignal | undefined

include:

{ requestBody?: boolean; requestMessages?: boolean; responseBody?: boolean } | undefined

runtimeContext:

CONTEXT

toolsContext:

InferToolSetContext<TOOLS>

onToolExecutionStart?:

OnToolExecutionStartCallback
ToolExecutionStartEvent

callId:

string

toolCall:

TypedToolCall<TOOLS>

messages:

Array<ModelMessage>

toolContext:

InferToolContext<TOOLS[toolName]>

onToolExecutionEnd?:

OnToolExecutionEndCallback
ToolExecutionEndEvent

callId:

string

toolCall:

TypedToolCall<TOOLS>

toolExecutionMs:

number

messages:

Array<ModelMessage>

toolContext:

InferToolContext<TOOLS[toolName]>

toolOutput:

ToolOutput<TOOLS>

onStepEnd?:

GenerateTextOnStepEndCallback

onStepFinish?:

GenerateTextOnStepFinishCallback

onEnd?:

GenerateTextOnEndCallback

onFinish?:

GenerateTextOnEndCallback

runtimeContext?:

CONTEXT

toolsContext:

InferToolSetContext<TOOLS>

telemetry?:

TelemetryOptions
TelemetryOptions

includeRuntimeContext?:

{ [KEY in keyof CONTEXT]?: boolean }

includeToolsContext?:

{ [TOOL_NAME in keyof InferToolSetContext<TOOLS>]?: { [KEY in keyof InferToolSetContext<TOOLS>[TOOL_NAME]]?: boolean } }

experimental_download?:

DownloadFunction | undefined

maxOutputTokens?:

number

temperature?:

number

topP?:

number

topK?:

number

presencePenalty?:

number

frequencyPenalty?:

number

stopSequences?:

string[]

seed?:

number

maxRetries?:

number

providerOptions?:

ProviderOptions

headers?:

Record<string, string | undefined>

callOptionsSchema?:

FlexibleSchema<CALL_OPTIONS>

prepareCall?:

PrepareCallFunction

id?:

string

Properties

tools:

Record<string, Tool>

id:

string | undefined

Methods

generate()

Generates a response and triggers tool calls as needed, running the agent loop and returning the final result. Returns a promise resolving to a GenerateTextResult.

const result = await agent.generate({
prompt: 'What is the weather like?',
});

prompt:

string | Array<ModelMessage>

messages:

Array<ModelMessage>

abortSignal?:

AbortSignal

timeout?:

number | { totalMs?: number; stepMs?: number; firstChunkMs?: number; chunkMs?: number }

experimental_sandbox?:

Experimental_SandboxSession

options?:

CALL_OPTIONS

onStart?:

GenerateTextOnStartCallback

onStepStart?:

GenerateTextOnStepStartCallback

onToolExecutionStart?:

OnToolExecutionStartCallback

onToolExecutionEnd?:

OnToolExecutionEndCallback

onStepEnd?:

GenerateTextOnStepEndCallback

onStepFinish?:

GenerateTextOnStepFinishCallback

onEnd?:

GenerateTextOnEndCallback

onFinish?:

GenerateTextOnEndCallback

Returns

The generate() method returns a GenerateTextResult object (see generateText for details).

stream()

Streams a response from the agent, including agent reasoning and tool calls, as they occur. Returns a StreamTextResult.

const stream = agent.stream({
prompt: 'Tell me a story about a robot.',
});
for await (const chunk of stream.textStream) {
console.log(chunk);
}

prompt:

string | Array<ModelMessage>

messages:

Array<ModelMessage>

abortSignal?:

AbortSignal

timeout?:

number | { totalMs?: number; stepMs?: number; firstChunkMs?: number; chunkMs?: number }

experimental_sandbox?:

Experimental_SandboxSession

options?:

CALL_OPTIONS

experimental_transform?:

StreamTextTransform | Array<StreamTextTransform>

onStart?:

GenerateTextOnStartCallback

onStepStart?:

GenerateTextOnStepStartCallback

onToolExecutionStart?:

OnToolExecutionStartCallback

onToolExecutionEnd?:

OnToolExecutionEndCallback

onStepEnd?:

GenerateTextOnStepEndCallback

onStepFinish?:

GenerateTextOnStepFinishCallback

onEnd?:

GenerateTextOnEndCallback

onFinish?:

GenerateTextOnEndCallback

Returns

The stream() method returns a StreamTextResult object (see streamText for details).

Types

ActiveTools

type ActiveTools<TOOLS extends ToolSet> =
| ReadonlyArray<keyof TOOLS & string>
| undefined;

Limits an agent step to the listed tool names. undefined means no tool restriction is applied.

InferAgentUIMessage

Infers the UI message type for the given agent instance. Useful for type-safe UI and message exchanges.

Basic Example

import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
const weatherAgent = new ToolLoopAgent({
model: "xai/grok-4.6",
tools: { weather: weatherTool },
});
type WeatherAgentUIMessage = InferAgentUIMessage<typeof weatherAgent>;

Example with Message Metadata

You can provide a second type argument to customize the metadata for each message. This is useful for tracking rich metadata returned by the agent (such as createdAt, tokens, finish reason, etc.).

import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
import { z } from 'zod';
// Example schema for message metadata
const exampleMetadataSchema = z.object({
createdAt: z.number().optional(),
model: z.string().optional(),
totalTokens: z.number().optional(),
finishReason: z.string().optional(),
});
type ExampleMetadata = z.infer<typeof exampleMetadataSchema>;
// Define agent as usual
const metadataAgent = new ToolLoopAgent({
model: "xai/grok-4.6",
// ...other options
});
// Type-safe UI message type with custom metadata
type MetadataAgentUIMessage = InferAgentUIMessage<
typeof metadataAgent,
ExampleMetadata
>;

Examples

Basic Agent with Tools

import { ToolLoopAgent, isStepCount } from 'ai';
import { weatherTool, calculatorTool } from './tools';
const assistant = new ToolLoopAgent({
model: "xai/grok-4.6",
instructions: 'You are a helpful assistant.',
tools: {
weather: weatherTool,
calculator: calculatorTool,
},
stopWhen: isStepCount(3),
});
const result = await assistant.generate({
prompt: 'What is the weather in NYC and what is 100 * 25?',
});
console.log(result.text);
console.log(result.steps); // Array of all steps taken by the agent

Streaming Agent Response

const agent = new ToolLoopAgent({
model: "xai/grok-4.6",
instructions: 'You are a creative storyteller.',
});
const stream = agent.stream({
prompt: 'Tell me a short story about a time traveler.',
});
for await (const chunk of stream.textStream) {
process.stdout.write(chunk);
}

Agent with Output Parsing

import { z } from 'zod';
const analysisAgent = new ToolLoopAgent({
model: "xai/grok-4.6",
output: {
schema: z.object({
sentiment: z.enum(['positive', 'negative', 'neutral']),
score: z.number(),
summary: z.string(),
}),
},
});
const result = await analysisAgent.generate({
prompt: 'Analyze this review: "The product exceeded my expectations!"',
});
console.log(result.output);
// Typed as { sentiment: 'positive' | 'negative' | 'neutral', score: number, summary: string }

Example: Approved Tool Execution

import { ToolLoopAgent, ModelMessage, ToolApprovalResponse, tool } from 'ai';
import { z } from 'zod';
const agent = new ToolLoopAgent({
model: "xai/grok-4.6",
instructions: 'You are an agent with access to a weather API.',
tools: {
weather: tool({
description: 'Get the weather in a location',
inputSchema: z.object({
location: z.string(),
}),
execute: async ({ location }) => ({
location,
temperature: 72,
}),
}),
},
toolApproval: {
weather: 'user-approval',
},
});
const messages: ModelMessage[] = [
{ role: 'user', content: 'Is it raining in Paris today?' },
];
const result = await agent.generate({ messages });
const approvals: ToolApprovalResponse[] = [];
for (const part of result.content) {
if (part.type === 'tool-approval-request') {
approvals.push({
type: 'tool-approval-response',
approvalId: part.approvalId,
approved: true,
});
}
}
messages.push(...result.responseMessages);
messages.push({ role: 'tool', content: approvals });
const approvedResult = await agent.generate({ messages });
console.log(approvedResult.text);