Skip to content

Building AI Agentswith the Vercel AI SDK

From the agent loop to a production-grade, multi-agent system — in TypeScript.

What you'll build

A complete, runnable masterclass in building AI agents — starting from a single LLM call and ending with a tested, deployed, multi-agent system. Every concept is backed by real, runnable TypeScript under src/agents/, and every code sample on this site is imported directly from those source files so the docs can never drift from the code.

The diagram above is the agent loop: an LLM in a loop that can call tools, inspect their results, and decide what to do next until it produces a final answer.

The curriculum

The course is organized into four parts:

  • Part I — Foundations: what an agent is, your first agent loop, the anatomy of a tool.
  • Part II — Core Capabilities: streaming & memory, structured output, the ToolLoopAgent class, and RAG.
  • Part III — Production Hardening: robust error handling, human-in-the-loop approval, security, observability & cost, and testing/evaluating agents.
  • Part IV — Architecture & Scale: multi-agent orchestration, a capstone walkthrough, building your own agent, and deploying to production.

Who this is for

TypeScript developers who want to build real, production-grade AI agents — not just call a chat API. You should be comfortable with async/await, ES modules, and the basics of the terminal. See Prerequisites for the full list, then head to Getting Started.

A taste of the code

Here is a real shared tool from the repo (src/tools.ts), imported live into this page:

ts
/**
 * tools.ts
 * ----------------------------------------------------------------------------
 * TUTORIAL: What is a "tool"?
 *
 * An LLM on its own can only produce text. A *tool* is a function you give the
 * model so it can take real actions — fetch data, do math, call an API, etc.
 *
 * In the Vercel AI SDK you define a tool with the `tool()` helper. Each tool has:
 *   - description: plain-English explanation. The model reads this to decide
 *                  WHEN to call the tool, so write it clearly.
 *   - inputSchema: a Zod schema describing the arguments. The SDK validates the
 *                  model's output against this before your code runs, and it
 *                  tells the model exactly what shape of data to produce.
 *   - execute:     the async function that actually runs. Whatever you return
 *                  is fed back into the model as the "tool result", and the
 *                  agent loop continues.
 *
 * We export these tools and reuse them in BOTH tutorial agents.
 * ----------------------------------------------------------------------------
 */

import { tool } from "ai";
import { z } from "zod";

/**
 * A fake "weather" tool. In a real app you'd call a weather API here; we return
 * mock data so the tutorial runs without extra API keys. The point is to show
 * how the model passes structured input (a city) and receives structured output.
 */
// #region weather-tool
export const getWeather = tool({
  description:
    "Get the current weather for a given city. Use this whenever the user asks about weather.",
  inputSchema: z.object({
    city: z.string().describe("The city to get the weather for, e.g. 'Paris'"),
  }),
  execute: async ({ city }) => {
    // Deterministic pseudo-random data so the same city gives the same result.
    const conditions = ["sunny", "cloudy", "rainy", "snowy", "windy"];
    const seed = [...city.toLowerCase()].reduce((a, c) => a + c.charCodeAt(0), 0);
    const condition = conditions[seed % conditions.length];
    const temperatureC = (seed % 30) - 2; // range: -2°C .. 27°C

    return { city, condition, temperatureC, unit: "°C" };
  },
});
// #endregion weather-tool


/**
 * A small, SAFE calculator tool.
 *
 * IMPORTANT: never use eval() on model output. Here we accept only a tiny
 * arithmetic grammar (numbers, + - * / ( ) and decimals) and parse it ourselves.
 * This demonstrates the model deciding to "compute" rather than guess at math.
 */
// #region calculator-tool
export const calculator = tool({
  description:
    "Evaluate a basic arithmetic expression (supports + - * / parentheses and decimals). " +
    "Use this for any math instead of doing it yourself.",
  inputSchema: z.object({
    expression: z
      .string()
      .describe("A math expression, e.g. '23 * 19' or '(2 + 3) / 4'"),
  }),
  execute: async ({ expression }) => {
    const result = safeEvaluate(expression);
    return { expression, result };
  },
});
// #endregion calculator-tool


/**
 * A minimal recursive-descent parser/evaluator for arithmetic.
 * Only digits, '.', whitespace, + - * / and parentheses are allowed.
 *
 * Exported so it can be unit-tested directly (see `tools.spec.test.ts`).
 * Exporting does not change its runtime behavior.
 */
// #region safe-evaluate
export function safeEvaluate(input: string): number {
  if (!/^[\d.\s+\-*/()]+$/.test(input)) {
    throw new Error("Expression contains invalid characters.");
  }



  let pos = 0;

  const peek = () => input[pos];
  const skipSpaces = () => {
    while (pos < input.length && input[pos] === " ") pos++;
  };

  // expr := term (('+' | '-') term)*
  function parseExpr(): number {
    let value = parseTerm();
    skipSpaces();
    while (peek() === "+" || peek() === "-") {
      const op = input[pos++];
      const rhs = parseTerm();
      value = op === "+" ? value + rhs : value - rhs;
      skipSpaces();
    }
    return value;
  }

  // term := factor (('*' | '/') factor)*
  function parseTerm(): number {
    let value = parseFactor();
    skipSpaces();
    while (peek() === "*" || peek() === "/") {
      const op = input[pos++];
      const rhs = parseFactor();
      value = op === "*" ? value * rhs : value / rhs;
      skipSpaces();
    }
    return value;
  }

  // factor := number | '(' expr ')'
  function parseFactor(): number {
    skipSpaces();
    if (peek() === "(") {
      pos++; // consume '('
      const value = parseExpr();
      skipSpaces();
      if (peek() !== ")") throw new Error("Missing closing parenthesis.");
      pos++; // consume ')'
      return value;
    }

    const start = pos;
    while (pos < input.length && /[\d.]/.test(input[pos]!)) pos++;
    const numStr = input.slice(start, pos);
    if (numStr === "") throw new Error("Expected a number.");
    return Number(numStr);
  }

  const result = parseExpr();
  skipSpaces();
  if (pos !== input.length) throw new Error("Unexpected trailing characters.");
  return result;
}
// #endregion safe-evaluate

/**
 * Bundle the tools into a single object. The keys ("getWeather", "calculator")
 * are the names the model uses when it decides to call a tool.
 */
// #region tools-bundle
export const tools = {
  getWeather,
  calculator,
};
// #endregion tools-bundle