Tools are how agents act on the world. The model decides when to call them; Vibes validates the arguments, runs the function, and feeds results back into the conversation.
Language models are powerful reasoners but they live in a text box. They cannot roll a die, look up your database, or call an API — unless you give them tools.A tool is a function you register with an agent. The model receives a description of the tool and its parameters. During a run, whenever the model decides it needs information or wants to take an action, it emits a structured tool call. Vibes validates the arguments, executes your function, and appends the result to the message history so the model can continue reasoning.This is more powerful than stuffing everything into the system prompt. Tools are called on demand, only when needed, with typed and validated arguments. The model can call them zero times or ten times in a single run — it decides.
Let’s build something concrete. Imagine a game where the agent rolls dice and asks for the player’s name. Two tools, two different styles.
import { Agent } from "jsr:@vibesjs/sdk";import { plainTool, tool } from "jsr:@vibesjs/sdk";import { anthropic } from "@ai-sdk/anthropic";import { z } from "zod";// (1) plainTool — no context needed, pure functionconst rollDice = plainTool({ name: "roll_dice", description: "Roll an N-sided die and return the result", parameters: z.object({ sides: z.number().int().min(2).describe("Number of sides on the die"), }), execute: async ({ sides }) => { const result = Math.floor(Math.random() * sides) + 1; return `Rolled a ${result}`; },});// (2) tool — receives RunContext so it can read injected dependenciestype Deps = { playerStore: { getName(): Promise<string> } };const getPlayerName = tool<Deps>({ name: "get_player_name", description: "Look up the current player's name", parameters: z.object({}), execute: async (ctx) => { // ctx.deps is typed as Deps return ctx.deps.playerStore.getName(); },});const agent = new Agent({ model: anthropic("claude-sonnet-4-6"), systemPrompt: "You are a game host. Greet the player by name and roll them a 20-sided die.", tools: [rollDice, getPlayerName], // (3) register both tools});const result = await agent.run("Start the game", { deps: { playerStore: { getName: async () => "Alex" } },});console.log(result.output);// "Hello Alex! You rolled a 14 on your d20. Not bad!"
plainTool is the simplest factory. execute receives only the validated args — no context object. Use it for pure functions.
tool<Deps> is the full-featured factory. execute receives a RunContext<Deps> as its first argument, then the args second. The ctx gives you access to injected dependencies, current token usage, the run ID, and more.
Tools are passed as a plain array. Vibes sends the full list to the model on every turn unless you use prepare to conditionally exclude a tool (see below).
The model called both tools in a single turn (it can do that), received both results, then produced the final text. Vibes handles all the serialization and routing. You only write the execute functions.
Use tool<TDeps>() whenever your tool needs to call a database, an API client, or any other injected service. The TDeps type parameter tells TypeScript what shape ctx.deps will have.
The deps are injected at call time via agent.run("...", { deps: { db: myDb } }). This keeps your tools testable — pass a mock in tests, a real connection in production.ctx also exposes:
ctx.usage — token counts accumulated so far in this run
ctx.runId — a unique ID for this run (useful for logging)
ctx.toolName — the name of the currently executing tool
ctx.retryCount — how many times this result has been retried
ctx.metadata — per-run metadata supplied by the caller
ctx.attachMetadata(toolCallId, meta) — attach structured metadata for a tool call that callers can inspect after the run
plainTool() is a convenience wrapper for tools that are pure functions. The execute receives only the validated arguments — there is no RunContext parameter.
import { plainTool } from "jsr:@vibesjs/sdk";import { z } from "zod";const formatDate = plainTool({ name: "format_date", description: "Format a Unix timestamp as a human-readable date", parameters: z.object({ timestamp: z.number().describe("Unix timestamp in seconds"), format: z.enum(["short", "long"]).default("short"), }), execute: async ({ timestamp, format }) => { const d = new Date(timestamp * 1000); return format === "long" ? d.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" }) : d.toLocaleDateString("en-US"); },});
plainTool also supports maxRetries and argsValidator. It does not support prepare, requiresApproval, or sequential — those require access to the run context. Use tool() if you need those features.
Sometimes you want the model to fill in a structured result and stop. outputTool() creates a tool that ends the run. When the model calls it, the return value becomes the agent’s final output and no further turns occur.
import { Agent, outputTool } from "jsr:@vibesjs/sdk";import { anthropic } from "@ai-sdk/anthropic";import { z } from "zod";// (1) Declare the structured output shapeconst submitSummary = outputTool({ name: "submit_summary", description: "Submit the final structured summary", parameters: z.object({ title: z.string(), keyPoints: z.array(z.string()), sentiment: z.enum(["positive", "neutral", "negative"]), }), execute: async (_ctx, args) => args, // (2) return the args as-is});const agent = new Agent({ model: anthropic("claude-sonnet-4-6"), systemPrompt: "Summarize the text the user provides.", tools: [submitSummary], outputMode: "tool", // (3) tell the agent to expect a tool call for output});const result = await agent.run("Vibes is a great SDK for building AI agents.");console.log(result.output);// { title: "Vibes SDK", keyPoints: ["..."], sentiment: "positive" }
The output schema is defined inline in the tool’s parameters.
execute typically returns args directly — the model has already structured the data.
outputMode: "tool" tells Vibes to treat the tool call as the output mechanism.
When you already have a JSON Schema — from an OpenAPI spec, a schema registry, or a third-party library — use fromSchema() to avoid rewriting it in Zod. There is no TypeScript inference for args, so you’ll need to cast.
import { fromSchema } from "jsr:@vibesjs/sdk";const createOrder = fromSchema({ name: "create_order", description: "Create a new order in the system", jsonSchema: { type: "object", properties: { productId: { type: "string" }, quantity: { type: "integer", minimum: 1 }, }, required: ["productId", "quantity"], }, execute: async (_ctx, args) => { const { productId, quantity } = args as { productId: string; quantity: number }; return await placeOrder(productId, quantity); },});
Every tool is sent to the model on every turn by default. The prepare function lets you change that. It is called once per turn before the tool list is sent. Return null or undefined to exclude the tool from that turn; return the tool definition (or a modified version of it) to include it.
import { tool } from "jsr:@vibesjs/sdk";import { z } from "zod";type Deps = { user: { isPremium: boolean }; db: { isConnected(): boolean } };const advancedSearch = tool<Deps>({ name: "advanced_search", description: "Run an advanced query with filters", parameters: z.object({ query: z.string(), filters: z.record(z.string()) }), execute: async (ctx, { query, filters }) => ctx.deps.db.advancedSearch(query, filters), // (1) Only expose when the DB is connected AND the user has a premium plan prepare: async (ctx) => { if (!ctx.deps.db.isConnected()) return null; // (2) null = hide this turn if (!ctx.deps.user.isPremium) return null; return undefined; // (3) undefined = include as-is },});
prepare receives the RunContext, giving it access to deps and all run metadata.
Returning null hides the tool. The model won’t know it exists for this turn.
Returning undefined (or the tool definition itself) includes it normally.
You can also return a modified tool definition from prepare to dynamically update the description or parameters based on runtime state:
prepare: async (ctx) => ({ ...advancedSearch, description: `Run an advanced query. Available filters: ${ctx.deps.db.getAvailableFilters().join(", ")}`,}),
Zod validates the shape and types of each argument. But sometimes you need cross-field validation — for example, ensuring a start date is before an end date. That’s what argsValidator is for.
import { tool } from "jsr:@vibesjs/sdk";import { z } from "zod";const getEvents = tool({ name: "get_events", description: "Fetch calendar events in a date range", parameters: z.object({ start: z.string().describe("ISO 8601 start date"), end: z.string().describe("ISO 8601 end date"), }), argsValidator: ({ start, end }) => { if (new Date(start) > new Date(end)) { throw new Error("start must be before end"); // (1) throw to reject } }, execute: async (_ctx, { start, end }) => fetchEvents(start, end),});
Throwing inside argsValidator rejects the call. The error message is sent back to the model without consuming a retry — it’s treated as a validation failure, not an execution failure.
Tool execution errors are surfaced back to the model by default. If you want Vibes to automatically retry before giving up, set maxRetries:
import { tool } from "jsr:@vibesjs/sdk";import { z } from "zod";const fetchPrice = tool({ name: "fetch_price", description: "Look up the current price for a ticker symbol", parameters: z.object({ ticker: z.string() }), maxRetries: 2, // (1) retry up to 2 times on failure execute: async (_ctx, { ticker }) => { const res = await fetch(`https://api.example.com/price/${ticker}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); // (2) throw triggers a retry const data = await res.json() as { price: number }; return `$${data.price}`; },});
maxRetries: 2 means up to 3 total attempts (1 initial + 2 retries).
Any thrown error triggers a retry. After all attempts are exhausted, the final error is propagated.
maxRetries on a tool retries the execution. It is independent of result validation retries (configured on the agent via maxRetries).