Advanced patterns for building tools: conditional availability with prepare, multimodal returns, sequential execution, role-based toolset composition, and approval workflows.
The tool() factory and the toolset classes compose to cover a wide range of runtime requirements. This page documents five patterns that appear repeatedly in production agents.
Every ToolDefinition accepts an optional prepare function. It runs once per model turn and returns either a (possibly modified) tool definition to include in the turn, or null/undefined to exclude the tool entirely.
prepare on a single tool differs from PreparedToolset (which operates over a full set) in that it targets exactly one tool and can mutate its description or parameters before the turn.
import { tool, Agent } from "jsr:@vibesjs/sdk";import { z } from "npm:zod";type Deps = { featureFlags: Record<string, boolean> };const experimentalTool = tool<Deps>({ name: "beta_analysis", description: "Run beta analysis pipeline", parameters: z.object({ data: z.string() }), prepare: (ctx) => { // Exclude this tool unless the beta flag is enabled if (!ctx.deps.featureFlags["beta_analysis"]) return null; return undefined; // returning undefined means "keep tool as-is" }, execute: async (_ctx, { data }) => runBetaAnalysis(data),});const agent = new Agent<Deps>({ model, tools: [experimentalTool] });
Return undefined (or nothing) from prepare to include the tool unchanged. Return a modified ToolDefinition to alter its name, description, or parameters for that turn. Return null to exclude it.
Tool execute functions can return BinaryContent (raw bytes with a MIME type) in addition to strings and objects. The agent run loop converts the value to an AI SDK image content part and forwards it to the model in the next turn.
A common use case is a screenshot tool that returns an image the model can reason about:
import { tool, Agent } from "jsr:@vibesjs/sdk";import type { BinaryContent } from "jsr:@vibesjs/sdk";import { z } from "npm:zod";const screenshotTool = tool({ name: "take_screenshot", description: "Capture a screenshot of a URL and return it as an image", parameters: z.object({ url: z.string().url() }), execute: async (_ctx, { url }): Promise<BinaryContent> => { const bytes = await captureScreenshot(url); // your implementation return { type: "binary", mimeType: "image/png", data: bytes, }; },});const agent = new Agent({ model, // model must support vision tools: [screenshotTool],});const result = await agent.run( "Go to https://example.com, take a screenshot, and describe what you see.",);
Tools can also return an UploadedFile reference when the file has already been uploaded server-side:
When the model issues multiple tool calls in a single response, Vibes executes them concurrently by default. Set sequential: true on a ToolDefinition to serialize those tools using a run-level mutex — no two sequential tools will run at the same time within one agent run.
import { tool, Agent } from "jsr:@vibesjs/sdk";import { z } from "npm:zod";// These two tools share a mutex — they never execute concurrentlyconst debitAccount = tool({ name: "debit_account", description: "Debit the specified amount from an account", parameters: z.object({ accountId: z.string(), amount: z.number().positive(), }), sequential: true, execute: async (_ctx, { accountId, amount }) => { return db.debit(accountId, amount); },});const creditAccount = tool({ name: "credit_account", description: "Credit the specified amount to an account", parameters: z.object({ accountId: z.string(), amount: z.number().positive(), }), sequential: true, execute: async (_ctx, { accountId, amount }) => { return db.credit(accountId, amount); },});const agent = new Agent({ model, tools: [debitAccount, creditAccount] });
sequential only serializes tools that also have sequential: true. Tools without this flag are unaffected and may run concurrently with each other — but not with a currently-executing sequential tool.
Set requiresApproval on a ToolDefinition to require approval every time the model calls that tool. It accepts a static boolean or a conditional function:
import { tool, Agent, ApprovalRequiredError } from "jsr:@vibesjs/sdk";import { z } from "npm:zod";type Deps = { trustedDomains: string[] };const fetchTool = tool<Deps>({ name: "fetch_url", description: "Fetch content from a URL", parameters: z.object({ url: z.string().url() }), // Require approval only for URLs not in the trusted list requiresApproval: (ctx, args) => { const url = new URL(args.url as string); return !ctx.deps.trustedDomains.includes(url.hostname); }, execute: async (_ctx, { url }) => { const res = await fetch(url); return res.text(); },});const agent = new Agent<Deps>({ model, tools: [fetchTool],});
When the model calls an approval-gated tool, agent.run() throws ApprovalRequiredError. Inspect the pending requests, supply results, and call agent.resume():
import { ApprovalRequiredError } from "jsr:@vibesjs/sdk";import type { DeferredToolResults } from "jsr:@vibesjs/sdk";async function runWithApproval(prompt: string) { try { return await agent.run(prompt, { deps: { trustedDomains: ["example.com"] } }); } catch (err) { if (err instanceof ApprovalRequiredError) { const pending = err.deferred.requests; // Display pending calls to a human reviewer for (const req of pending) { console.log(`Approve ${req.toolName}(${JSON.stringify(req.args)})?`); } const approved = await promptHuman(pending); // your UI / CLI if (!approved) throw new Error("User rejected tool call"); // Resume with explicit results or let the tool re-execute unchanged const results: DeferredToolResults = { results: pending.map((r) => ({ toolCallId: r.toolCallId, // omit `result` to re-execute the tool with original args // or supply `result` to inject a value without re-executing result: "approved", })), }; return agent.resume(err.deferred, results, { deps: { trustedDomains: ["example.com"] }, }); } throw err; }}
DeferredToolResult accepts either result (inject a pre-computed value) or argsOverride (re-execute the tool with modified arguments):
type DeferredToolResult = { toolCallId: string; result?: string | object; // inject this as the tool output argsOverride?: Record<string, unknown>; // re-run with these args instead};
For a complete walkthrough of human-in-the-loop patterns including UI integration, see Human-in-the-Loop.