Skip to content

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.

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();
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();
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 ToolService
const { 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 / execute

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();

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",
};

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();

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();

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();

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.