📄 ai-sdk/cookbook/node/manual-agent-loop

File: manual-agent-loop.md | Updated: 11/15/2025

Source: https://ai-sdk.dev/cookbook/node/manual-agent-loop

AI SDK

Menu

Guides

RAG Agent

Multi-Modal Agent

Slackbot Agent Guide

Natural Language Postgres

Get started with Computer Use

Get started with Gemini 2.5

Get started with Claude 4

OpenAI Responses API

Google Gemini Image Generation

Get started with Claude 3.7 Sonnet

Get started with Llama 3.1

Get started with GPT-5

Get started with OpenAI o1

Get started with OpenAI o3-mini

Get started with DeepSeek R1

Next.js

Generate Text

Generate Text with Chat Prompt

Generate Image with Chat Prompt

Stream Text

Stream Text with Chat Prompt

Stream Text with Image Prompt

Chat with PDFs

streamText Multi-Step Cookbook

Markdown Chatbot with Memoization

Generate Object

Generate Object with File Prompt through Form Submission

Stream Object

Call Tools

Call Tools in Multiple Steps

Model Context Protocol (MCP) Tools

Share useChat State Across Components

Human-in-the-Loop Agent with Next.js

Send Custom Body from useChat

Render Visual Interface in Chat

Caching Middleware

Node

Generate Text

Generate Text with Chat Prompt

Generate Text with Image Prompt

Stream Text

Stream Text with Chat Prompt

Stream Text with Image Prompt

Stream Text with File Prompt

Generate Object with a Reasoning Model

Generate Object

Stream Object

Stream Object with Image Prompt

Record Token Usage After Streaming Object

Record Final Object after Streaming Object

Call Tools

Call Tools with Image Prompt

Call Tools in Multiple Steps

Model Context Protocol (MCP) Tools

Manual Agent Loop

Web Search Agent

Embed Text

Embed Text in Batch

Intercepting Fetch Requests

Local Caching Middleware

Retrieval Augmented Generation

Knowledge Base Agent

API Servers

Node.js HTTP Server

Express

Hono

Fastify

Nest.js

React Server Components

Copy markdown

Manual Agent Loop

==========================================================================================

When you need complete control over the agentic loop and tool execution, you can manage the agent flow yourself rather than using prepareStep and stopWhen. This approach gives you full flexibility over when and how tools are executed, message history management, and loop termination conditions.

This pattern is useful when you want to:

  • Implement custom logic between tool calls
  • Handle tool execution errors in specific ways
  • Add custom logging or monitoring
  • Integrate with external systems during the loop
  • Have complete control over the conversation history

Example


import { openai } from '@ai-sdk/openai';import { ModelMessage, streamText, tool } from 'ai';import 'dotenv/config';import z from 'zod';
const getWeather = async ({ location }: { location: string }) => {  return `The weather in ${location} is ${Math.floor(Math.random() * 100)} degrees.`;};
const messages: ModelMessage[] = [  {    role: 'user',    content: 'Get the weather in New York and San Francisco',  },];
async function main() {  while (true) {    const result = streamText({      model: openai('gpt-4o'),      messages,      tools: {        getWeather: tool({          description: 'Get the current weather in a given location',          inputSchema: z.object({            location: z.string(),          }),        }),        // add more tools here, omitting the execute function so you handle it yourself      },    });
    // Stream the response (only necessary for providing updates to the user)    for await (const chunk of result.fullStream) {      if (chunk.type === 'text-delta') {        process.stdout.write(chunk.text);      }
      if (chunk.type === 'tool-call') {        console.log('\\nCalling tool:', chunk.toolName);      }    }
    // Add LLM generated messages to the message history    const responseMessages = (await result.response).messages;    messages.push(...responseMessages);
    const finishReason = await result.finishReason;
    if (finishReason === 'tool-calls') {      const toolCalls = await result.toolCalls;
      // Handle all tool call execution here      for (const toolCall of toolCalls) {        if (toolCall.toolName === 'getWeather') {          const toolOutput = await getWeather(toolCall.input);          messages.push({            role: 'tool',            content: [              {                toolName: toolCall.toolName,                toolCallId: toolCall.toolCallId,                type: 'tool-result',                output: { type: 'text', value: toolOutput }, // update depending on the tool's output format              },            ],          });        }        // Handle other tool calls      }    } else {      // Exit the loop when the model doesn't request to use any more tools      console.log('\\n\\nFinal message history:');      console.dir(messages, { depth: null });      break;    }  }}
main().catch(console.error);

Key Concepts


Message Management

The example maintains a messages array that tracks the entire conversation history. After each model response, the generated messages are added to this history:

const responseMessages = (await result.response).messages;messages.push(...responseMessages);

Tool Execution Control

Tool execution is handled manually in the loop. When the model requests tool calls, you process each one individually:

if (finishReason === 'tool-calls') {  const toolCalls = await result.toolCalls;
  for (const toolCall of toolCalls) {    if (toolCall.toolName === 'getWeather') {      const toolOutput = await getWeather(toolCall.input);      // Add tool result to message history      messages.push({        role: 'tool',        content: [          {            toolName: toolCall.toolName,            toolCallId: toolCall.toolCallId,            type: 'tool-result',            output: { type: 'text', value: toolOutput },          },        ],      });    }  }}

Loop Termination

The loop continues until the model stops requesting tool calls. You can customize this logic to implement your own termination conditions:

if (finishReason === 'tool-calls') {  // Continue the loop} else {  // Exit the loop  break;}

Extending This Example


Custom Loop Control

Implement maximum iterations or time limits:

let iterations = 0;const MAX_ITERATIONS = 10;
while (iterations < MAX_ITERATIONS) {  iterations++;  // ... rest of the loop}

Parallel Tool Execution

Execute multiple tools in parallel for better performance:

const toolPromises = toolCalls.map(async toolCall => {  if (toolCall.toolName === 'getWeather') {    const toolOutput = await getWeather(toolCall.input);    return {      role: 'tool' as const,      content: [        {          toolName: toolCall.toolName,          toolCallId: toolCall.toolCallId,          type: 'tool-result' as const,          output: { type: 'text' as const, value: toolOutput },        },      ],    };  }  // Handle other tools});
const toolResults = await Promise.all(toolPromises);messages.push(...toolResults.filter(Boolean));

This manual approach gives you complete control over the agentic loop while still leveraging the AI SDK's powerful streaming and tool calling capabilities.

On this page

Manual Agent Loop

Example

Key Concepts

Message Management

Tool Execution Control

Loop Termination

Extending This Example

Custom Loop Control

Parallel Tool Execution

Deploy and Scale AI Apps with Vercel.

Vercel delivers the infrastructure and developer experience you need to ship reliable AI-powered applications at scale.

Trusted by industry leaders:

  • OpenAI
  • Photoroom
  • leonardo-ai Logoleonardo-ai Logo
  • zapier Logozapier Logo

Talk to an expert