Guides

Agents

Defining tools, constructing an Agent, and how the tool-calling loop runs.

Defining a tool

tool() is a thin identity helper whose only job is type inference: it lets execute's argument type be derived from inputSchema (a Zod schema), so execute receives a properly typed, validated object instead of Record<string, unknown>.

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

const calculator = tool({
  description: "Performs a single arithmetic operation on two numbers.",
  inputSchema: z.object({
    operation: z.enum(["add", "multiply"]),
    a: z.number(),
    b: z.number(),
  }),
  execute({ operation, a, b }) {
    return operation === "add" ? a + b : a * b;
  },
});

Constructing an Agent

new Agent({ name, instructions, model, tools, maxSteps }) bundles a system prompt (instructions), a LanguageModel, and a tools map into one named, reusable object. Nothing runs yet — .generate() / .stream() are what actually call the model.

import { Agent } from "chai-ai";
import { google } from "@chai-ai/google";

const agent = new Agent({
  name: "calculator-agent",
  instructions:
    "You are a helpful assistant. Always use the calculator tool for arithmetic instead of computing it yourself.",
  model: google("gemini-3.1-pro-preview"),
  tools: { calculator },
});

Other constructor options, covered in their own guides:

  • handoffs / maxHandoffs / description — see Handoffs
  • inputGuardrails / toolGuardrails / outputGuardrails / onApprovalRequired — see Guardrails
  • modelTimeoutMs / toolTimeoutMs — see Reliability

Defaults: maxSteps is 5, maxHandoffs is 5, modelTimeoutMs is 60_000, toolTimeoutMs is 30_000.

Running it

agent.generate({ prompt }) drains the agent's internal run loop silently and returns its final value:

const result = await agent.generate({
  prompt: "What is 47 multiplied by 23?",
});

console.log(result.text);
console.log(result.steps);

result.steps records each step of the loop — every model call and tool call the agent made to arrive at result.text. Internally, .generate() and .stream() share the exact same loop; .stream() just exposes each step live as an event instead of waiting for the final result — see Streaming.

The loop, at a glance

On each call, the agent:

  1. Starts a runId and a trace (see Tracing)
  2. Runs any inputGuardrails against the prompt — a block throws before any model call happens
  3. Builds the message history (system instructions + prior session history, if any, + the new prompt) and calls the model
  4. If the model responds with tool calls, runs each tool (through toolGuardrails and onApprovalRequired if configured) and feeds the results back to the model
  5. Repeats step 3–4 until the model responds with plain text (or maxSteps is hit), then runs outputGuardrails on the final answer

maxSteps bounds how many model↔tool round trips a single .generate() / .stream() call can take before it gives up.

On this page