Building Custom Tools
Tools give agents the ability to take real-world actions — fetch data, run code, call APIs, write files. This recipe covers both the fluent ToolBuilder API and the lower-level ToolDefinition format.
ToolBuilder (Recommended)
Section titled “ToolBuilder (Recommended)”The fluent ToolBuilder catches misconfiguration at build time:
import { ToolBuilder } from "@reactive-agents/tools";
const searchTool = new ToolBuilder("web-search") .description("Search the web for current information") .param("query", "string", "The search query", { required: true }) .param("maxResults", "number", "Max results to return", { default: 5 }) .riskLevel("low") .timeout(15_000) .returnType("SearchResult[]") .category("search") .handler(async (query: string, maxResults: number = 5) => { // your implementation return { results: [] }; }) .build();Register it on the agent:
const agent = await ReactiveAgents.create() .withProvider("anthropic") .withTools({ tools: [searchTool.definition] }) .build();Parameter Types
Section titled “Parameter Types”new ToolBuilder("file-processor") .description("Process a file") .param("path", "string", "Absolute file path", { required: true }) .param("encoding", "string", "File encoding", { default: "utf-8", enum: ["utf-8", "ascii", "base64"], // restricts LLM to these values }) .param("maxBytes", "number", "Maximum bytes to read") .param("lines", "array", "Specific line numbers to extract") .param("options", "object", "Advanced options") .build();Risk Levels and Approval Gates
Section titled “Risk Levels and Approval Gates”const deleteFileTool = new ToolBuilder("delete-file") .description("Permanently delete a file from disk") .param("path", "string", "File path to delete", { required: true }) .riskLevel("high") // "low" | "medium" | "high" | "critical" .requiresApproval() // sets definition.requiresApproval = true .timeout(5_000) .build();requiresApproval() stores a boolean flag on the ToolDefinition. The framework does not automatically pause agent execution — the flag is metadata that your application code can read to implement its own approval gate.
The flag is visible in listTools() output and on the definition returned by build(), so you can check it in a custom execution pipeline:
// Example: check the flag before passing a tool to ToolServiceconst { definition, handler } = new ToolBuilder("delete-file") .description("Permanently delete a file from disk") .param("path", "string", "File path to delete", { required: true }) .riskLevel("high") .requiresApproval() .build();
if (definition.requiresApproval) { const approved = await askUser(`Approve execution of "${definition.name}"?`); if (!approved) throw new Error("User denied approval");}// proceed to register / executeTool Categories
Section titled “Tool Categories”Categories help the agent reason about which tools to use:
new ToolBuilder("send-email") .description("Send an email message") .category("communication") // "search" | "file" | "code" | "communication" | "data" | "compute" .build();Low-Level ToolDefinition
Section titled “Low-Level ToolDefinition”For integrating with existing tool registries or when you need full control:
import type { ToolDefinition } from "@reactive-agents/tools";
const calculator: ToolDefinition = { name: "calculator", description: "Evaluate a mathematical expression", parameters: [ { name: "expression", type: "string", description: "Math expression to evaluate (e.g., '2 + 2 * 3')", required: true, }, ], riskLevel: "low", timeoutMs: 1_000, requiresApproval: false, source: "function", returnType: "number",};Tools with Side Effects
Section titled “Tools with Side Effects”For tools that modify state, use riskLevel("high") and return structured results so the agent can reason about success/failure:
const writeFileTool = new ToolBuilder("write-file") .description("Write content to a file, creating it if it doesn't exist") .param("path", "string", "Destination file path", { required: true }) .param("content", "string", "Content to write", { required: true }) .param("append", "boolean", "Append instead of overwrite", { default: false }) .riskLevel("medium") .timeout(10_000) .handler(async (path: string, content: string, append = false) => { const { writeFile, appendFile } = await import("fs/promises"); const fn = append ? appendFile : writeFile; await fn(path, content, "utf-8"); return { success: true, path, bytesWritten: content.length }; }) .build();Restricting Available Tools
Section titled “Restricting Available Tools”Give the agent a focused set of tools for a specific task — prevents distraction and reduces token usage:
const agent = await ReactiveAgents.create() .withProvider("anthropic") .withTools({ allowedTools: ["web-search", "read-file"], // LLM only sees these }) .build();Tool Result Compression
Section titled “Tool Result Compression”Large tool outputs (e.g., full file contents, long API responses) are automatically compressed to fit the context window. Configure the compression behavior:
const agent = await ReactiveAgents.create() .withProvider("anthropic") .withTools({ resultCompression: { maxChars: 2_000, // truncate results longer than this strategy: "preview", // "preview" | "truncate" | "summarize" }, }) .build();MCP Tools
Section titled “MCP Tools”Connect to any Model Context Protocol server to get its tools automatically:
const agent = await ReactiveAgents.create() .withProvider("anthropic") .withMCP({ name: "filesystem", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"], }) .build();The agent discovers and uses all tools advertised by the MCP server.