{"componentChunkName":"component---src-gatsby-theme-chronoblog-templates-note-js","path":"/notes/ai-agents-vercel-ai-sdk/","result":{"data":{"mdx":{"parent":{"__typename":"File","fields":{"gitLogLatestDate":"2026-06-08 23:19:52 +0200"}},"id":"44593b13-9b03-58f6-acde-02b7a58fc102","excerpt":"The  Vercel AI SDK  treats agents as  tool-calling loops : the model generates text or invokes tools, the SDK runs those tools, and the loop…","frontmatter":{"title":"Building AI agents with Vercel AI SDK","date":"2026-06-09 00:01:00 UTC","job_ad":null,"job_ad_id":null,"job_ad_url":null,"tags":["vercel","ai","llm","node","openai","agents"],"cover":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAABJ0lEQVQoz32R2XLCMAxF+f+f5IFMIXtsHO1SOik0LA3Vkz2joyNfH5a9cncz898y01L4/AVNTaqPtkPEEnG/3A5mLiK2lpu5qoows40Dp0ncn+A3Z0SI0JPWmGnlWYeeUtLNtMJDp5cz9p0gWNtydcptg353OgARrfA4aNtQU2NO9oBh1pxoLipipcjQz9dMKU1pnOszVadCGOYGYDlRmpDI/1nbI9a0VBSB+34SUV8fGgAL0UvzS2BbztuVmRHJTOcix2OuKiRURNw3/6Rtz7OY2T2WJYhC+N4Q4R9hf/oQEVHV61X7HroOENw9uhYAfAeOiDe5qs7FxpHHgQnDPdoW57IH/5VHeM5aX6i+EEJ4WPiHtTf+NoKISilEWgqVQkxr+LeMvwEAA8GnmYu1hQAAAABJRU5ErkJggg==","aspectRatio":1.641025641025641,"src":"/static/56028d1a0b2f080f29ffb583d7f63bff/c4ecb/cover.png","srcSet":"/static/56028d1a0b2f080f29ffb583d7f63bff/57ab0/cover.png 192w,\n/static/56028d1a0b2f080f29ffb583d7f63bff/f4739/cover.png 384w,\n/static/56028d1a0b2f080f29ffb583d7f63bff/c4ecb/cover.png 768w,\n/static/56028d1a0b2f080f29ffb583d7f63bff/4eab0/cover.png 1152w,\n/static/56028d1a0b2f080f29ffb583d7f63bff/f5271/cover.png 1184w","srcWebp":"/static/56028d1a0b2f080f29ffb583d7f63bff/dd090/cover.webp","srcSetWebp":"/static/56028d1a0b2f080f29ffb583d7f63bff/ae504/cover.webp 192w,\n/static/56028d1a0b2f080f29ffb583d7f63bff/fef30/cover.webp 384w,\n/static/56028d1a0b2f080f29ffb583d7f63bff/dd090/cover.webp 768w,\n/static/56028d1a0b2f080f29ffb583d7f63bff/44170/cover.webp 1152w,\n/static/56028d1a0b2f080f29ffb583d7f63bff/cabcb/cover.webp 1184w","sizes":"(max-width: 768px) 100vw, 768px","presentationWidth":768,"presentationHeight":470},"resize":{"src":"/static/56028d1a0b2f080f29ffb583d7f63bff/c4ecb/cover.png"}}}},"fields":{"slug":"/notes/ai-agents-vercel-ai-sdk/","readingTime":{"text":"8 min read"}},"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Building AI agents with Vercel AI SDK\",\n  \"date\": \"2026-06-09 00:01:00 UTC\",\n  \"cover\": \"./cover.png\",\n  \"tags\": [\"vercel\", \"ai\", \"llm\", \"node\", \"openai\", \"agents\"],\n  \"canonical_url\": \"https://sevic.dev/notes/ai-agents-vercel-ai-sdk/\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, \"The \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://ai-sdk.dev/\"\n  }), \"Vercel AI SDK\"), \" treats agents as \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"tool-calling loops\"), \": the model generates text or invokes tools, the SDK runs those tools, and the loop continues until the model answers or a \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"stop condition\"), \" fires.\"), mdx(\"p\", null, \"This post builds a \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"support triage agent\"), \" that looks up customers and invoices, searches an internal knowledge base, and either opens a ticket or escalates to a human. It builds on the \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/llm-integration-vercel-ai-sdk/\"\n  }), \"LLM integration with Vercel AI SDK\"), \" post and focuses on \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"multiple tools\"), \", \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"stopWhen\")), \", and \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"stepCountIs\")), \".\"), mdx(\"p\", null, \"For external tools exposed over MCP instead of SDK-native \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tool()\"), \" handlers, see the \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/mcp-server-nodejs/\"\n  }), \"MCP server with Node.js\"), \" post.\"), mdx(\"h3\", {\n    \"id\": \"prerequisites\"\n  }, \"Prerequisites\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"OpenAI account\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Generated API key\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Enabled billing\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Node.js version 26\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"ai\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"@ai-sdk/openai\"), \", and \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"zod\"), \" installed (\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"npm i ai @ai-sdk/openai zod\"), \")\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Client setup from the \", mdx(\"a\", _extends({\n    parentName: \"li\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/llm-integration-vercel-ai-sdk/\"\n  }), \"Vercel AI SDK integration post\"))), mdx(\"h3\", {\n    \"id\": \"mental-model---steps-and-the-tool-loop\"\n  }, \"Mental model - steps and the tool loop\"), mdx(\"p\", null, \"A \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"step\"), \" is one model generation. In that step the model either:\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"returns \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"text\"), \" (the loop ends), or\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"returns \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"tool calls\"), \" (the SDK executes them and starts another step with the results)\")), mdx(\"p\", null, \"Typical flow for the support triage agent: user question \\u2192 model calls lookup tools (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"getCustomer\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"getInvoice\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"searchKnowledgeBase\"), \") \\u2192 model creates a ticket or escalates \\u2192 final answer. \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stopWhen\"), \" can end the loop before or after the write tools run.\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stepCountIs(5)\"), \" means \\\"stop after \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"5 steps\"), \"\\\" (five model generations), not five individual tool calls. A single step can include multiple parallel tool calls.\"), mdx(\"p\", null, \"When you pass \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tools\"), \" without \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stopWhen\"), \", the SDK defaults to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stepCountIs(20)\"), \" as a safety cap.\"), mdx(\"h3\", {\n    \"id\": \"support-triage-scenario\"\n  }, \"Support triage scenario\"), mdx(\"p\", null, mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Example prompt:\")), mdx(\"blockquote\", null, mdx(\"p\", {\n    parentName: \"blockquote\"\n  }, \"Customer cus_1042 says they were charged twice for invoice inv_8891. What should we do?\")), mdx(\"p\", null, \"A realistic chain:\"), mdx(\"ol\", null, mdx(\"li\", {\n    parentName: \"ol\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"getCustomer\"), \" - plan tier, open ticket count\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"getInvoice\"), \" - amount, status, payment IDs\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"searchKnowledgeBase\"), \" - duplicate-charge and refund policy\"), mdx(\"li\", {\n    parentName: \"ol\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"createSupportTicket\"), \" or \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"escalateToHuman\"), \" - write action or sentinel stop\")), mdx(\"p\", null, \"The demo uses in-memory fixtures (customers, invoices, knowledge-base articles) so scripts run without a database.\"), mdx(\"h3\", {\n    \"id\": \"defining-multiple-tools\"\n  }, \"Defining multiple tools\"), mdx(\"p\", null, \"Register tools with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tool()\"), \" and Zod \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"inputSchema\"), \". Clear \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"description\"), \" values help the model pick the right tool.\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { tool } from 'ai';\\nimport { z } from 'zod';\\n\\nconst getCustomer = tool({\\n  description: 'Look up a customer account by ID',\\n  inputSchema: z.object({\\n    customerId: z.string().describe('Customer ID, e.g. cus_1042'),\\n  }),\\n  execute: async ({ customerId }) => {\\n    const customer = customers.find((item) => item.id === customerId);\\n    if (!customer) {\\n      return { found: false, customerId, error: 'Customer not found' };\\n    }\\n    return { found: true, customer };\\n  },\\n});\\n\\nconst getInvoice = tool({\\n  description: 'Look up an invoice by ID, including payment IDs and status',\\n  inputSchema: z.object({\\n    invoiceId: z.string().describe('Invoice ID, e.g. inv_8891'),\\n  }),\\n  execute: async ({ invoiceId }) => {\\n    const invoice = invoices.find((item) => item.id === invoiceId);\\n    if (!invoice) {\\n      return { found: false, invoiceId, error: 'Invoice not found' };\\n    }\\n    return { found: true, invoice };\\n  },\\n});\\n\\nconst searchKnowledgeBase = tool({\\n  description: 'Search internal support articles by keyword',\\n  inputSchema: z.object({\\n    query: z.string().describe('Search terms, e.g. duplicate charge refund'),\\n  }),\\n  execute: async ({ query }) => {\\n    // keyword match against mocked articles\\n    return { query, articles: matches };\\n  },\\n});\\n\")), mdx(\"p\", null, \"Add write tools for outcomes:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const createSupportTicket = tool({\\n  description: 'Create a support ticket after gathering customer and policy context',\\n  inputSchema: z.object({\\n    customerId: z.string(),\\n    subject: z.string().min(3),\\n    priority: z.enum(['low', 'medium', 'high']),\\n    summary: z.string().min(10),\\n  }),\\n  execute: async (input) => {\\n    const ticket = createTicket(input);\\n    return { created: true, ticket };\\n  },\\n});\\n\\nconst escalateToHuman = tool({\\n  description: 'Escalate when policy requires manual review',\\n  inputSchema: z.object({\\n    customerId: z.string(),\\n    reason: z.string().min(10),\\n    urgency: z.enum(['normal', 'high']),\\n  }),\\n  execute: async (input) => ({\\n    escalated: true,\\n    queue: input.urgency === 'high' ? 'billing-urgent' : 'billing-standard',\\n    ...input,\\n  }),\\n});\\n\")), mdx(\"p\", null, \"Return structured objects from \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"execute\"), \". The SDK serializes them as tool results for the next step. Return explicit errors (for example \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"{ found: false, error: '...' }\"), \") so the model can recover instead of throwing.\"), mdx(\"h3\", {\n    \"id\": \"multi-step-triage-with-generatetext\"\n  }, \"Multi-step triage with \", mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"generateText\")), mdx(\"p\", null, \"Pass all tools and a \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"system\"), \" prompt with triage rules:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { generateText, stepCountIs } from 'ai';\\n\\nconst { text, steps } = await generateText({\\n  model: openai('gpt-5.5'),\\n  system: `You are a billing support triage agent.\\n- Look up customer and invoice before recommending refunds.\\n- Search the knowledge base for policy guidance.\\n- Create a ticket when you can resolve within policy.\\n- Call escalateToHuman when manual review is required.`,\\n  tools: {\\n    getCustomer,\\n    getInvoice,\\n    searchKnowledgeBase,\\n    createSupportTicket,\\n    escalateToHuman,\\n  },\\n  stopWhen: stepCountIs(8),\\n  prompt:\\n    'Customer cus_1042 says they were charged twice for invoice inv_8891. What should we do?',\\n});\\n\\nconsole.log('steps:', steps.length);\\nconsole.log(text);\\n\")), mdx(\"p\", null, \"Use a model that supports tool calling (same requirement as web search in the \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/llm-integration-vercel-ai-sdk/\"\n  }), \"Vercel AI SDK post\"), \").\"), mdx(\"h3\", {\n    \"id\": \"stopwhen---when-the-loop-stops\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"stopWhen\"), \" - when the loop stops\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stopWhen\"), \" defines stopping conditions for the tool loop. Conditions are evaluated \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"only when the last step contains tool results\"), \".\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"A single condition stops when that condition returns \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"true\")), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"An array stops when \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"any\"), \" condition returns \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"true\"), \" (OR logic)\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Without \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"stopWhen\"), \", the SDK applies \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"stepCountIs(20)\"))), mdx(\"p\", null, \"The loop also ends naturally when the model returns text without further tool calls.\"), mdx(\"h3\", {\n    \"id\": \"stepcountis---cap-the-number-of-steps\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"stepCountIs\"), \" - cap the number of steps\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stepCountIs(n)\"), \" stops once \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"steps.length\"), \" reaches \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"n\"), \". Use it on every production agent to prevent runaway loops and unbounded API cost.\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Use case\"), mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Suggested cap\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Single tool, then answer\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"2 (tool step + text step)\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Chat with occasional tool use\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"3-5\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Task agents (triage, research)\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"8-15\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Long autonomous workflows\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"15-20 (with monitoring)\")))), mdx(\"p\", null, \"Tight vs relaxed cap on the same prompt:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { generateText, stepCountIs } from 'ai';\\n\\n// Stops after 3 steps even if the model still wants more context\\nconst capped = await generateText({\\n  model: openai('gpt-5.5'),\\n  tools: supportTools,\\n  stopWhen: stepCountIs(3),\\n  prompt: '...',\\n});\\n\\n// Allows a fuller investigation chain\\nconst relaxed = await generateText({\\n  model: openai('gpt-5.5'),\\n  tools: supportTools,\\n  stopWhen: stepCountIs(8),\\n  prompt: '...',\\n});\\n\")), mdx(\"h3\", {\n    \"id\": \"combining-hastoolcall-with-stepcountis\"\n  }, \"Combining \", mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"hasToolCall\"), \" with \", mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"stepCountIs\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"hasToolCall('toolName')\"), \" stops when the model invokes a specific tool in the latest step. Pair it with \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stepCountIs\"), \" for a hard cap plus a sentinel tool:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { generateText, stepCountIs, hasToolCall } from 'ai';\\n\\nconst { text, steps } = await generateText({\\n  model: openai('gpt-5.5'),\\n  system: TRIAGE_INSTRUCTIONS,\\n  tools: supportTools,\\n  stopWhen: [stepCountIs(10), hasToolCall('escalateToHuman')],\\n  prompt:\\n    'Customer cus_2201 on the starter plan reports a duplicate $190 charge on invoice inv_9104.',\\n});\\n\")), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"escalateToHuman\"), \" works well as a sentinel: the loop stops as soon as the model decides the case needs a human, without waiting for a final text-only step.\"), mdx(\"h3\", {\n    \"id\": \"inspecting-steps-and-usage\"\n  }, \"Inspecting \", mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"steps\"), \" and usage\"), mdx(\"p\", null, \"The \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"steps\"), \" array on the result contains per-step tool calls, tool results, finish reason, and usage. Use it for debugging and cost tracking:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const { text, steps, totalUsage } = await generateText({\\n  model: openai('gpt-5.5'),\\n  tools: supportTools,\\n  stopWhen: stepCountIs(8),\\n  prompt: '...',\\n});\\n\\nfor (const [index, step] of steps.entries()) {\\n  console.log(`step ${index + 1}`);\\n  console.log('  toolCalls:', step.toolCalls?.map((c) => c.toolName));\\n  console.log('  usage:', step.usage);\\n}\\n\\nconsole.log('totalUsage:', totalUsage);\\n\")), mdx(\"p\", null, \"With \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"streamText\"), \", pass \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"onStepFinish\"), \" to log each step as it completes.\"), mdx(\"h3\", {\n    \"id\": \"toolloopagent---reusable-agent-definition\"\n  }, mdx(\"inlineCode\", {\n    parentName: \"h3\"\n  }, \"ToolLoopAgent\"), \" - reusable agent definition\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ToolLoopAgent\"), \" wraps the same loop for reuse across scripts and API routes. It accepts the same settings as \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"generateText\"), \" (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tools\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stopWhen\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"instructions\"), \").\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { ToolLoopAgent, stepCountIs } from 'ai';\\n\\nconst supportTriageAgent = new ToolLoopAgent({\\n  model: openai('gpt-5.5'),\\n  instructions: TRIAGE_INSTRUCTIONS,\\n  tools: supportTools,\\n  stopWhen: stepCountIs(8),\\n});\\n\\nconst result = await supportTriageAgent.generate({\\n  prompt:\\n    'Customer cus_1042 says they were charged twice for invoice inv_8891. What should we do?',\\n  onStepFinish: async ({ stepNumber, usage, toolCalls }) => {\\n    console.log(`step ${stepNumber + 1}`, {\\n      tokens: usage.totalTokens,\\n      tools: toolCalls?.map((call) => call.toolName),\\n    });\\n  },\\n});\\n\\nconsole.log(result.text);\\n\")), mdx(\"p\", null, \"Use \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \".stream()\"), \" for streaming. For Next.js chat UIs, see \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"createAgentUIStreamResponse\"), \" in the \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://ai-sdk.dev/docs/agents/building-agents\"\n  }), \"AI SDK agents docs\"), \".\"), mdx(\"h3\", {\n    \"id\": \"streaming-with-tools\"\n  }, \"Streaming with tools\"), mdx(\"p\", null, mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"streamText\"), \" supports the same \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"tools\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stopWhen\"), \" settings:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"import { streamText, stepCountIs } from 'ai';\\n\\nconst result = streamText({\\n  model: openai('gpt-5.5'),\\n  system: TRIAGE_INSTRUCTIONS,\\n  tools: supportTools,\\n  stopWhen: stepCountIs(8),\\n  prompt: 'Customer cus_1042 says they were charged twice for invoice inv_8891.',\\n  onStepFinish: async ({ stepNumber, toolCalls }) => {\\n    console.error(`step ${stepNumber + 1}:`, toolCalls?.map((c) => c.toolName));\\n  },\\n});\\n\\nfor await (const part of result.textStream) {\\n  process.stdout.write(part);\\n}\\n\")), mdx(\"p\", null, \"Text streams incrementally. Tool calls run between text segments as the loop progresses.\"), mdx(\"h3\", {\n    \"id\": \"production-notes\"\n  }, \"Production notes\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Always set \", mdx(\"inlineCode\", {\n    parentName: \"strong\"\n  }, \"stopWhen\")), \" - do not rely on the default \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"stepCountIs(20)\"), \" in production without monitoring\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Cost\"), \" - each step is another model call; log \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"steps\"), \" or \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"onStepFinish\"), \" usage\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Tool errors\"), \" - return structured errors from \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"execute\"), \" instead of throwing when the model should retry or escalate\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Instructions\"), \" - keep policy rules in \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"system\"), \" / \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"instructions\"), \", not only in the user prompt\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"Same patterns elsewhere\"), \" - PR review (\", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"listPRs\"), \" \\u2192 \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"getChecks\"), \" \\u2192 \", mdx(\"inlineCode\", {\n    parentName: \"li\"\n  }, \"submitReview\"), \") or job-fit scoring use the same loop mechanics with different tools\")), mdx(\"h3\", {\n    \"id\": \"demo\"\n  }, \"Demo\"), mdx(\"p\", null, \"Runnable scripts for each section live in the \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"vercel-ai-sdk-agents-demo\"), \" folder. Get access via \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/demos\"\n  }), \"code demos\"), \".\"));\n}\n;\nMDXContent.isMDXComponent = true;"}},"pageContext":{"id":"44593b13-9b03-58f6-acde-02b7a58fc102"}},"staticQueryHashes":["1961101537","2542493696"]}