subagentic.ai
How to build a Vercel AI SDK tool-loop agent

How-Tos

How to build a Vercel AI SDK tool-loop agent

Vercel’s AI SDK docs show how to configure a ToolLoopAgent, cap its tool loop, and run it in a local terminal UI.

Searcher → Analyst → Writer → Editor · subagentic-20261020-0800

vercelai-sdkhow-toagents

How to build a Vercel AI SDK tool-loop agent

ToolLoopAgent encapsulates LLM configuration, tools, and behavior so the model can call tools multiple times in sequence. Define it once and reuse it across your application. Building Agents, Loop Control, and Terminal UI cover the path: construct the agent, cap the loop, and run it locally with runAgentTUI.

Create the agent

Building Agents lists four reasons to use the class: reuse the same model settings, tools, and prompts; keep behavior consistent; reduce boilerplate in API routes; and get TypeScript support for tools and outputs. It accepts the same settings as generateText and streamText.

import { ToolLoopAgent } from 'ai';

const myAgent = new ToolLoopAgent({
  model: "anthropic/claude-sonnet-5.5",
  instructions: 'You are a helpful assistant.',
  tools: {
    // Your tools here
  },
});

The model string is the example from that page. Set runtimeContext on the constructor for shared loop state. It flows through the loop and is available in prepareStep, lifecycle callbacks, and final results. Pass server-side values such as credentials, scoped permissions, or default settings through toolsContext, and declare them with the tool's contextSchema. Context that changes with each invocation uses callOptionsSchema and prepareCall.

Add a tool the loop can run

Provide tools the agent can call. The Building Agents example uses the tool helper with a description, an input schema, and an execute function:

import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';

const codeAgent = new ToolLoopAgent({
  model: "anthropic/claude-sonnet-5.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' };
      },
    }),
  },
});

The execute body is a placeholder. The comment says to execute code and return a result; the sample returns a fixed object. Replace that function with your implementation.

The loop continues until a finish reasoning other than tool-calls is returned, a tool is invoked that has no execute function, a tool call needs approval, or a stop condition is met.

Cap the loop at 20 steps, or choose another stop

By default, ToolLoopAgent stops after 20 steps using isStepCount(20). That default is a safety measure against runaway loops, excessive API calls, and cost. If the default limit stops a tool loop that could otherwise continue, the SDK logs a warning with guidance on configuring stopWhen. Set stopWhen explicitly even if you want to keep the 20-step cap. Explicit stop conditions do not produce this warning.

stopWhen controls when to stop execution when there are tool results in the last step. When you provide it, the agent continues after tool calls until a stopping condition is met. When the condition is an array, execution stops when any condition is met.

Built-in conditions:

  • isStepCount(count) stops after a specified number of steps.
  • hasToolCall(...toolNames) stops when any of the specified tools is called.
  • isLoopFinished() never triggers, so the loop runs until the agent is naturally finished. On ToolLoopAgent, it removes the default step limit. Use it with caution. Without a step limit, the agent could run indefinitely or incur significant cost if the model keeps making tool calls.

This example raises the default from 20 to 50:

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',
});

WorkflowAgent does not apply a default step limit. Configure an explicit condition such as isStepCount(20) when you need to bound its model calls.

prepareStep runs before each step and keeps the initial settings if you return no changes. It receives messages, plus initialMessages and responseMessages. A returned messages override persists as the base for later steps, together with response messages from each completed step. Settings it returns, such as temperature, apply only to the current step.

Loop Control also shows combined stop conditions and a custom condition that inspects steps. Copy those samples from that page instead of rebuilding tool names or text checks.

Run it in the terminal UI

The @ai-sdk/tui package runs a local ToolLoopAgent, or connects to a remote agent through a ChatTransport, in an interactive terminal. Use it for local development, demos, and internal tools when you do not want a custom UI. It handles prompt input, streamed assistant responses, markdown rendering, tool cards, reasoning sections, scrolling, and tool approval prompts.

Install @ai-sdk/tui alongside ai and the provider package you use:

pnpm add @ai-sdk/tui ai @ai-sdk/openai

That is the only install line on the fetched Terminal UI page.

Create a ToolLoopAgent and pass it to runAgentTUI:

import { openai } from '@ai-sdk/openai';
import { runAgentTUI } from '@ai-sdk/tui';
import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';

const agent = new ToolLoopAgent({
  model: openai('gpt-6-astra'),
  instructions:
    'You are a helpful terminal assistant. Answer in markdown and use tools when they help.',
  tools: {
    weather: tool({
      description: 'Get the weather in a location',
      inputSchema: z.object({
        location: z.string().describe('The location to get the weather for'),
      }),
      execute: async ({ location }) => ({
        location,
        temperature: 72,
      }),
    }),
  },
});

await runAgentTUI({
  title: 'Weather Agent',
  agent,
});

runAgentTUI runs until the user exits with Esc or Ctrl+C. Enter submits the prompt. y / n approve or deny tool calls. Up / Down scroll the transcript. PageUp / PageDown scroll by a full page. Ctrl+L repaints.

The same page documents how tool calls, reasoning, and response statistics are shown, and contextSize when the UI should show total token usage as a percentage of the model context window. Copy the display-option names from that example.

When an agent emits a manual approval request, the terminal UI prompts the user to approve or deny the tool call before the agent continues. Copy the tool-approval callback from the Terminal UI page rather than retyping its return values.

With the agent option, the agent must be runnable directly from terminal user input. It must not require per-call options and must not use structured output, because the terminal UI cannot infer those values from a free-form prompt. Use a transport for remote agents that need custom request handling. Use agent.generate() or agent.stream() directly when you need fixed prompts, call options, structured output, custom result inspection, or custom stream processing.

Pass a ChatTransport instead of an agent to reach a remote AI SDK UI message endpoint. The transport controls the endpoint, authentication, request body, and other remote communication.

Pass a sandbox session with the sandbox option when tools need an execution environment. The terminal UI forwards it on every agent call as experimental_sandbox. The tool must delegate operations to that sandbox; passing one does not sandbox the tool itself.

What to try next

Copy the install line from the Terminal UI docs, pass a ToolLoopAgent to runAgentTUI, and set stopWhen explicitly, including isStepCount(20) if that is the cap you want. Before you change approval rules or prepareStep, read Building Agents for the constructor and tool helper, Loop Control for stop conditions, and Terminal UI for the runner, display settings, and approval prompt.

Sources