Skip to main content

Tools & Function Calling

RadarOS agents can call external functions (tools) during a run. Tools are defined with defineTool(), validated with Zod schemas, and executed automatically when the LLM decides to use them.

defineTool()

Use defineTool() to create a type-safe tool definition:
import { defineTool } from "@radaros/core";
import { z } from "zod";

const myTool = defineTool({
  name: "toolName",
  description: "What the tool does (used by the LLM to decide when to call it)",
  parameters: z.object({
    param1: z.string().describe("Description for the LLM"),
    param2: z.number().optional(),
  }),
  execute: async (args, ctx) => {
    // args is typed from the Zod schema
    return "Result string or ToolResult";
  },
});

Tool Definition Anatomy

Unique identifier for the tool. The LLM uses this to invoke the tool. Use camelCase (e.g. getWeather, searchDatabase).
Natural language description of what the tool does. The LLM uses this to decide when to call the tool. Be clear and specific.
A Zod object schema defining the tool’s input. Use .describe() on fields to help the LLM understand each parameter. The schema is converted to JSON Schema for the provider.
Async function that runs when the tool is called. Receives parsed args (typed from the schema) and RunContext. Returns a string or ToolResult (with optional artifacts).

Examples

Weather Tool

import { defineTool } from "@radaros/core";
import { z } from "zod";

const weatherTool = defineTool({
  name: "getWeather",
  description: "Get the current weather for a city",
  parameters: z.object({
    city: z.string().describe("City name"),
  }),
  execute: async ({ city }) => {
    const conditions = ["sunny", "cloudy", "rainy", "snowy"];
    const temp = Math.floor(Math.random() * 30) + 5;
    const condition = conditions[Math.floor(Math.random() * conditions.length)];
    return `${city}: ${temp}°C, ${condition}`;
  },
});

Calculator Tool

const calculator = defineTool({
  name: "calculator",
  description: "Evaluate a math expression",
  parameters: z.object({
    expression: z.string().describe("Math expression to evaluate (e.g. 2 + 3 * 4)"),
  }),
  execute: async ({ expression }) => {
    const result = new Function(`return (${expression})`)();
    return String(result);
  },
});

Multiple Tools on an Agent

Pass an array of tools to the agent:
const agent = new Agent({
  name: "ToolBot",
  model: openai("gpt-4o"),
  tools: [weatherTool, calculator],
  instructions: "You help with weather queries and math. Use tools when needed.",
});
The LLM receives all tool definitions and can call any of them in a single turn when appropriate.

ToolResult Type

Instead of returning a plain string, return a ToolResult to include optional artifacts:
interface ToolResult {
  content: string;   // The text sent back to the LLM
  artifacts?: Artifact[];  // Optional structured data for downstream use
}

interface Artifact {
  type: string;
  data: unknown;
  mimeType?: string;
}
execute: async ({ query }) => {
  return `Found 3 results for "${query}"`;
}

Tool Artifacts

Artifacts are structured data attached to a tool result. The LLM only sees the content string, but your application receives the full artifacts in ToolCallResult. Use artifacts to pass structured data (JSON, images, charts) through the agent pipeline without polluting the LLM context.
const chartTool = defineTool({
  name: "generateChart",
  description: "Generate a sales chart for a given time period",
  parameters: z.object({
    period: z.enum(["week", "month", "quarter"]).describe("Time period"),
  }),
  execute: async ({ period }) => {
    const data = await getSalesData(period);
    const chartUrl = await renderChart(data);

    return {
      content: `Generated ${period}ly sales chart. Total revenue: $${data.total.toLocaleString()}.`,
      artifacts: [
        {
          type: "chart",
          data: { url: chartUrl, period, dataPoints: data.points },
          mimeType: "image/png",
        },
        {
          type: "raw_data",
          data: data.points,
          mimeType: "application/json",
        },
      ],
    };
  },
});

const result = await agent.run("Show me this month's sales chart");
console.log(result.text); // LLM's summary

// Access artifacts from tool calls
for (const tc of result.toolCalls) {
  const artifacts = tc.result?.artifacts ?? [];
  for (const artifact of artifacts) {
    if (artifact.type === "chart") {
      displayChart(artifact.data.url);
    }
  }
}

Parallel Tool Execution

When the LLM returns multiple tool calls in a single turn, RadarOS executes them in parallel (batches of up to 5 by default). Each tool call is validated against its schema; invalid arguments produce an error result for that call only.
// Agent can call getWeather("Tokyo") and calculator("42 * 17") in the same turn
const result = await agent.run("What's the weather in Tokyo? Also, what's 42 * 17?");
// Both tools run in parallel; results are combined and sent back to the LLM.

Tool Caching

Add a cache property to any tool to cache results and avoid redundant executions:
const weatherTool = defineTool({
  name: "getWeather",
  description: "Get current weather for a city",
  parameters: z.object({ city: z.string() }),
  execute: async ({ city }) => { /* ... */ },
  cache: { ttl: 60_000 }, // Cache for 1 minute
});
See Tool Caching for full documentation.

Strict Mode (OpenAI Structured Outputs)

Enable strict on a tool to activate OpenAI’s Structured Outputs for tool calls. When enabled, the model is guaranteed to return valid JSON matching the schema — no malformed arguments.
const weatherTool = defineTool({
  name: "getWeather",
  description: "Get weather for a city",
  parameters: z.object({ city: z.string() }),
  execute: async ({ city }) => `Sunny in ${city}`,
  strict: true,
});
When strict is true, RadarOS automatically:
  1. Strips verbose JSON Schema metadata ($schema, additionalProperties on nested objects)
  2. Sets additionalProperties: false at the top level (required by OpenAI)
  3. Passes strict: true to the OpenAI function definition
Strict mode is only supported by OpenAI models. For other providers, the strict option is ignored.

Sandbox & Approval

Tools support two additional safety features:
  • sandbox — Run the tool in an isolated subprocess with timeout and memory limits. See Sandbox Execution.
  • requiresApproval — Require human approval before executing the tool. See Human-in-the-Loop.
const riskyTool = defineTool({
  name: "deleteUser",
  description: "Delete a user account",
  parameters: z.object({ userId: z.string() }),
  execute: async ({ userId }) => `Deleted ${userId}`,
  sandbox: { timeout: 5_000 },
  requiresApproval: true,
});

Tool Error Handling

When a tool’s execute function throws an error, RadarOS catches it and returns the error message as the tool result. The LLM sees the error and can decide how to respond — often retrying with different arguments or explaining the failure to the user.
const flightTool = defineTool({
  name: "searchFlights",
  description: "Search for available flights",
  parameters: z.object({
    origin: z.string().describe("Airport code (e.g. LAX)"),
    destination: z.string().describe("Airport code (e.g. JFK)"),
    date: z.string().describe("Date in YYYY-MM-DD format"),
  }),
  execute: async ({ origin, destination, date }) => {
    const flights = await flightAPI.search(origin, destination, date);
    if (flights.length === 0) {
      throw new Error(`No flights found from ${origin} to ${destination} on ${date}`);
    }
    return JSON.stringify(flights.slice(0, 5));
  },
});
When the tool throws, the LLM receives:
Error: No flights found from LAX to JFK on 2026-03-15
The LLM can then inform the user or try alternative dates. Tool errors do not abort the run — they are treated as informational results. For schema validation errors (wrong argument types), RadarOS returns a descriptive validation error so the model can self-correct.

Tool Router (Automatic Tool Selection)

When an agent has many tools (e.g., 50+ from an MCP server), sending all tool schemas in every prompt wastes tokens and can confuse the model. The Tool Router automatically selects only the relevant tools for each query using a cheap/fast model.

Basic usage

Pass a toolRouter config to the agent. The router runs before every LLM call and filters the tool list:
import { Agent, openai, anthropic } from "@radaros/core";

const agent = new Agent({
  name: "assistant",
  model: anthropic("claude-sonnet-4-6"),
  tools: allMyTools, // 50+ tools
  toolRouter: {
    model: anthropic("claude-haiku-4-5-20251001"), // cheap model for selection
    maxTools: 8,
  },
});
The main model only sees the 8 most relevant tools instead of all 50+.

Configuration

OptionTypeDefaultDescription
modelModelProviderrequiredCheap/fast model used to select relevant tools
maxToolsnumber8Maximum number of tools to select per query
minToolsnumber0Minimum tools to return; if selection returns fewer, all tools are sent as fallback
temperaturenumber0Temperature for the selection model

How it works

  1. Before each run() or stream() call, the router sends the user query and a compact tool index (name + description) to the selection model.
  2. The selection model returns a JSON array of tool names.
  3. The agent rebuilds its tool set with only the selected tools for that turn.
  4. If selection fails or returns too few tools, all tools are sent as a safe fallback.

When to use it

  • Many tools — MCP servers or large toolkit collections with 10+ tools
  • Cost-sensitive — Reducing prompt tokens directly lowers cost
  • Mixed-domain agents — Agent has tools across shipping, billing, CRM, etc. and most queries only need a few

Choosing a router model

Use the cheapest model that can reliably read tool names and match them to a query. Good choices:
  • anthropic("claude-haiku-4-5-20251001")
  • openai("gpt-4o-mini")
  • google("gemini-2.0-flash")
The router model does not need to be the same provider as the main model.
The toolRouter config is entirely optional. If omitted, the agent sends all tools on every call (the default behavior).

Tool Result Limits

When tools return large payloads (e.g. an MCP server returning 200KB of database records), the entire result is sent back to the LLM on the next roundtrip — causing massive prompt token usage and high costs. toolResultLimit intercepts oversized tool results and either smart-truncates them or summarizes them via a cheap model before they reach the main LLM.

Basic usage

const agent = new Agent({
  name: "assistant",
  model: anthropic("claude-sonnet-4-6"),
  tools: allMyTools,
  toolResultLimit: {
    maxChars: 20000,        // Trigger at 20K chars (~5K tokens)
    strategy: "truncate",   // Smart JSON truncation (default)
  },
});

With summarization

Use a cheap model to summarize large results instead of truncating. This preserves data fidelity while cutting tokens:
const agent = new Agent({
  name: "assistant",
  model: anthropic("claude-sonnet-4-6"),
  tools: allMyTools,
  toolResultLimit: {
    maxChars: 20000,
    strategy: "summarize",
    model: anthropic("claude-haiku-4-5-20251001"),
  },
});

Configuration

OptionTypeDefaultDescription
maxCharsnumber20000Character threshold before the strategy kicks in
strategy"truncate" | "summarize""truncate"How to handle oversized results
modelModelProviderModel for summarization (required when strategy is "summarize")

Strategies

"truncate" (default) — Smart JSON-aware truncation:
  • JSON arrays: keeps the first N items that fit, appends "[Showing 15 of 892 items — 877 more omitted]"
  • JSON objects with array values: truncates each array proportionally
  • Plain text: hard-cut with a note about remaining characters
"summarize" — Sends the full result to a cheap/fast model with instructions to preserve all key data points, totals, IDs, and dates. The summary replaces the original result before the main model sees it. Falls back to truncation if summarization fails.

When to use it

  • MCP tools returning bulk data — API endpoints that return full record sets without pagination
  • Cost-sensitive agents — A 200KB tool result can cost $0.25+ in prompt tokens on a single roundtrip
  • Any agent with maxToolRoundtrips > 0 — Tool results are sent back to the LLM on each roundtrip; large results compound quickly
toolResultLimit only affects text results sent back to the LLM. It does not modify the ToolCallResult stored in RunOutput.toolCalls — your application still has access to the original full result.

Using RunContext in Tools

The execute function receives two arguments: args (the parsed parameters) and ctx (the RunContext). Use ctx to access session info, metadata, state, and dependencies.
const orderTool = defineTool({
  name: "lookup_order",
  description: "Look up an order by ID",
  parameters: z.object({ orderId: z.string() }),
  execute: async (args, ctx) => {
    // Session and user info
    const userId = ctx.userId;
    const sessionId = ctx.sessionId;
    const tenantId = ctx.tenantId;

    // Metadata passed via RunOpts
    const region = ctx.metadata.region as string;

    // Read/write session state (persists across turns)
    const lastOrder = ctx.getState<string>("lastOrderId");
    ctx.setState("lastOrderId", args.orderId);

    // Injected dependencies
    const apiUrl = ctx.dependencies.ORDER_API_URL;

    // Check cancellation
    if (ctx.signal?.aborted) return "Request cancelled";

    return `Order ${args.orderId} for user ${userId} in ${region}`;
  },
});

RunContext properties available in tools

PropertyTypeDescription
ctx.runIdstringCurrent run’s unique ID
ctx.sessionIdstringSession ID
ctx.userIdstring?User ID
ctx.tenantIdstring?Tenant ID
ctx.metadataRecord<string, unknown>Arbitrary metadata from RunOpts
ctx.sessionStateRecord<string, unknown>Mutable key-value state bag
ctx.dependenciesRecord<string, string>Resolved dependency variables
ctx.signalAbortSignal?Cancellation signal
ctx.eventBusEventBusEvent bus for emitting events
See Types Reference for the full RunContext specification.

Sandbox Configuration

Run tools in isolated subprocesses with resource limits. Set at the agent level (applies to all tools) or per-tool.
PropertyTypeDefaultDescription
enabledbooleantrue (when config provided)Explicit on/off
timeoutnumber30000 (30s)Execution timeout in ms
maxMemoryMBnumber256Max heap memory in MB
allowNetworkbooleanfalseAllow outbound network
allowFSboolean | { readOnly?: string[]; readWrite?: string[] }falseFilesystem access. Pass object for granular paths
envRecord<string, string>undefinedEnvironment variables forwarded to the sandbox
// Agent-level sandbox (applies to all tools)
const agent = new Agent({
  model: openai("gpt-4o"),
  sandbox: {
    timeout: 10_000,
    maxMemoryMB: 128,
    allowFS: { readOnly: ["/data"] },
  },
  tools: [safeTool, riskyTool],
});

// Per-tool sandbox (overrides agent-level)
const riskyTool = defineTool({
  name: "run_code",
  description: "Execute arbitrary code",
  parameters: z.object({ code: z.string() }),
  sandbox: {
    timeout: 5000,
    maxMemoryMB: 64,
    allowNetwork: false,
    allowFS: false,
  },
  execute: async (args) => eval(args.code),
});

Conditional Approval

requiresApproval can be a function that decides approval based on the arguments:
const deleteTool = defineTool({
  name: "delete_record",
  description: "Delete a database record",
  parameters: z.object({
    table: z.string(),
    id: z.string(),
  }),
  requiresApproval: (args) => {
    // Only require approval for production tables
    return args.table === "users" || args.table === "orders";
  },
  execute: async (args) => {
    await db.delete(args.table, args.id);
    return `Deleted ${args.id} from ${args.table}`;
  },
});

Runtime Tool Mutation

Add, remove, and replace tools on a running agent:
const agent = new Agent({
  name: "flexible-agent",
  model: openai("gpt-4o"),
  tools: [searchTool],
});

// Add a tool at runtime
agent.addTool(calculatorTool);

// Remove a tool
agent.removeTool("search");

// Replace all tools
agent.setTools([newTool1, newTool2]);

// List current tools
const tools = agent.listTools();
// ["newTool1", "newTool2"]

Dynamic Tool Resolver

For context-dependent tools that change per run, use toolResolver:
const agent = new Agent({
  model: openai("gpt-4o"),
  toolResolver: async (ctx) => {
    const role = ctx.metadata.role as string;
    if (role === "admin") {
      return [readTool, writeTool, deleteTool];
    }
    return [readTool]; // Regular users only get read access
  },
});