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 HandoffsinputGuardrails/toolGuardrails/outputGuardrails/onApprovalRequired— see GuardrailsmodelTimeoutMs/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:
- Starts a
runIdand atrace(see Tracing) - Runs any
inputGuardrailsagainst the prompt — a block throws before any model call happens - Builds the message history (
systeminstructions + priorsessionhistory, if any, + the newprompt) and calls the model - If the model responds with tool calls, runs each tool (through
toolGuardrailsandonApprovalRequiredif configured) and feeds the results back to the model - Repeats step 3–4 until the model responds with plain text (or
maxStepsis hit), then runsoutputGuardrailson the final answer
maxSteps bounds how many model↔tool round trips a single .generate() /
.stream() call can take before it gives up.