Skip to content

Chapter 18 · Agent Introduction: ToolLoopAgent and Loop Control ​

Chapter goals:

  • Understand the three要素 of an Agent: LLM, Tools, and Loop, and how ToolLoopAgent encapsulates them
  • Learn to create Agents: model/tool/instructions config and three usage modes (generate/stream/UI stream)
  • Master loop control: stopWhen stop conditions (isStepCount/hasToolCall/isLoopFinished) and custom conditions
  • Use prepareStep for 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:

ts
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 agent

This 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.

  • 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:

ts
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:

ts
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):

ts
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:

ts
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:

ts
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:

ts
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:

ts
// 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.',
});
ts
// 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():

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

console.log(result.text);

Streaming response — stream():

ts
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):

ts
// 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:

ts
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, and runtimeContext;
  • onStepStart: Called before each step (LLM call);
  • onToolExecutionStart / onToolExecutionEnd: Called before/after tool execute; the latter带着 toolExecutionMs and a discriminated union toolOutput (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):

ts
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:

ts
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 execute function;
  • 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:

ts
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" —任一满足即停:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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内置消息裁剪能力:

ts
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:

ts
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:

ts
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:

  1. Define the schema — declare accepted input via callOptionsSchema;
  2. Configure prepareCall — modify agent settings用 these inputs;
  3. Pass at runtime — provide options when calling generate() or stream().

基础示例: inject user context into the prompt at runtime:

ts
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:

ts
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; ToolLoopAgent encapsulates 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 with pruneMessages, narrow activeTools by 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 ​

  1. Build a weather query agent with ToolLoopAgent (mock weather tool + F-to-C conversion tool); call with both generate() and stream(); observe the tool call chain in each result.steps.
  2. Build a research agent: stopWhen: [isStepCount(10), hasToolCall('submit_report')]; then add a custom StopCondition that中断s when cumulative token cost exceeds a threshold.
  3. Write a phased agent: first two steps only allow the search tool with toolChoice: 'required'; then switch to an analysis model and use pruneMessages tocompact early tool results.

Next: Chapter 19 · Approvals, Memory, and Subagents