Tools
Create type-safe tools that give agents the ability to interact with external systems.
Overview
Tools enable agents to take actions -- call APIs, read files, execute code, query databases. Cogitator tools are defined with Zod schemas for full type safety and automatic JSON Schema generation for LLM function calling.
import { tool } from '@cogitator-ai/core';
import { z } from 'zod';
const getWeather = tool({
name: 'get_weather',
description: 'Get the current weather for a city',
parameters: z.object({
city: z.string().describe('City name'),
units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
execute: async ({ city, units }, context) => {
const response = await fetch(`https://api.weather.com/${city}?units=${units}`, {
signal: context.signal,
});
return response.json();
},
});The tool() Factory
function tool<TParams, TResult>(config: ToolConfig<TParams, TResult>): Tool<TParams, TResult>;ToolConfig
| Field | Type | Description |
|---|---|---|
name | string | Unique tool name (used in LLM function calling) |
description | string | What the tool does (sent to the LLM) |
parameters | ZodType | Zod schema defining accepted parameters |
execute | (params, context) => Promise<TResult> | Implementation function |
category | ToolCategory | 'math', 'text', 'file', 'network', 'system', 'utility', 'web', 'database', 'communication' or 'development' |
tags | string[] | Optional tags for filtering |
sideEffects | SideEffectType[] | 'filesystem', 'network', 'database', 'process', 'external'; shown to approvers |
requiresApproval | boolean | (params) => boolean | Require human approval before execution (Approvals) |
timeout | number | Per-call timeout in milliseconds |
sandbox | SandboxConfig | Run in an isolated environment (see below) |
Tool Context
execute receives the validated parameters and a ToolContext:
| Field | Description |
|---|---|
agentId, runId | The agent and run that called the tool |
signal | Aborted when the run is cancelled, times out, or the tool's timeout passes |
threadId | The conversation |
userId | The user the run acts for (RunOptions.userId) |
channelType, channelId | Where the message came from, for channel gateways |
When timeout is set, the runtime aborts context.signal once the limit is reached and reports Tool "<name>" timed out after <ms>ms to the model as the tool error. Long-running tools should pass context.signal to fetch and other cancellable APIs so the work actually stops.
Parameter Schemas
The parameters field accepts any Zod schema. Cogitator converts it to JSON Schema (OpenAPI 3.0 format) before sending it to the LLM.
const createIssue = tool({
name: 'create_github_issue',
description: 'Create a new issue in a GitHub repository',
parameters: z.object({
repo: z.string().describe('Repository in owner/repo format'),
title: z.string().max(256).describe('Issue title'),
body: z.string().describe('Issue description in markdown'),
labels: z.array(z.string()).optional().describe('Labels to apply'),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
}),
execute: async ({ repo, title, body, labels, priority }) => {
const issue = await github.createIssue(repo, { title, body, labels, priority });
return { issueNumber: issue.number, url: issue.html_url };
},
});Use .describe() on each field -- these descriptions are sent to the LLM and significantly improve tool call accuracy.
Arguments the model sends are parsed with the schema before execute runs, so defaults and transforms apply. Arguments that fail validation never reach execute: the model gets Invalid arguments: ... as the tool result and can try again.
Tool Results
The execute function can return any serializable value. Cogitator converts the result to JSON and feeds it back to the LLM as a tool response:
const calculator = tool({
name: 'calculator',
description: 'Evaluate a mathematical expression',
parameters: z.object({
expression: z.string().describe('Math expression to evaluate'),
}),
execute: async ({ expression }) => {
const result = eval(expression);
return { result, expression };
},
});If the execute function throws, the error message is sent back to the LLM (as { "error": "..." }) so it can retry or adjust its approach. A call to a tool the agent does not have gets Tool not found: <name>.
Images in Tool Results
A result object with a base64 image in image or imageBase64 (PNG, JPEG, GIF or WebP, plain or as a data:image/...;base64, URL) reaches the model as an image, so a vision model sees a screenshot or chart instead of its base64 text. The rest of the object is still sent as JSON, with the image field replaced by "(image attached)":
const renderChart = tool({
name: 'render_chart',
description: 'Render a line chart of the given values as a PNG',
parameters: z.object({ values: z.array(z.number()) }),
execute: async ({ values }) => {
const png = await drawChart(values);
return { points: values.length, image: png.toString('base64') };
},
});Each backend places the image where its API allows: inside the tool result for Anthropic, Bedrock and the OpenAI Responses API, after the function responses of the turn for Google, in the tool message's images for Ollama, and in a user message right after the turn's tool messages for Chat Completions (api: 'chat-completions', OpenAI-compatible baseUrl, Azure), whose tool messages take only text. Results of the image-generation tool (imageBase64) and the browser screenshot tools (image) take this path.
Typed Tool Sets
A function that returns several tools as Tool[] loses each tool's own parameter types. toolset() keeps them as a tuple, which is still an array an agent accepts:
import { toolset } from '@cogitator-ai/core';
function createIssueTools() {
return toolset(createIssue, getWeather);
}
const [issues, weather] = createIssueTools();
await weather.execute({ city: 'Tokyo', units: 'celsius' }, context); // typed parameters
const agent = new Agent({
name: 'ops',
model: 'openai/gpt-6.1-sol',
instructions: '...',
tools: createIssueTools(),
});Built-in Tools
Cogitator ships with ready-to-use tools (builtinTools is all of them as one array; see Built-in Tools for parameters):
import {
calculator,
datetime,
uuid,
fileRead,
fileWrite,
fileList,
fileExists,
fileDelete,
httpRequest,
exec,
webSearch,
webScrape,
sqlQuery,
vectorSearch,
sendEmail,
githubApi,
jsonParse,
jsonStringify,
regexMatch,
regexReplace,
hash,
base64Encode,
base64Decode,
sleep,
randomNumber,
randomString,
} from '@cogitator-ai/core';Use them directly:
const agent = new Agent({
name: 'file-assistant',
model: 'openai/gpt-6.1-sol',
instructions: 'You help users manage files.',
tools: [fileRead, fileWrite, fileList, fileDelete],
});Tool Registry
The ToolRegistry class manages collections of tools and provides lookup by name:
import { ToolRegistry } from '@cogitator-ai/core';
const registry = new ToolRegistry();
registry.register(getWeather);
registry.register(createIssue);
registry.registerMany([calculator, datetime]);
const weatherTool = registry.get('get_weather');
const allNames = registry.getNames();
const schemas = registry.getSchemas();
registry.has('get_weather'); // true
registry.getAll(); // Tool[]
registry.clear();Registering a tool under a name that already exists replaces it.
The Cogitator runtime uses ToolRegistry internally to resolve tool calls from the LLM.
Tool Schema Conversion
Under the hood, toolToSchema() converts a Zod-based tool into the JSON Schema format that LLM providers expect:
import { toolToSchema } from '@cogitator-ai/core';
const schema = toolToSchema(getWeather);
// {
// name: 'get_weather',
// description: 'Get the current weather for a city',
// parameters: {
// type: 'object',
// properties: {
// city: { type: 'string', description: 'City name' },
// units: { type: 'string', enum: ['celsius', 'fahrenheit'], default: 'celsius' }
// },
// required: ['city']
// }
// }The conversion uses the schema's input type, so fields with a .default() are optional for the model; the default is applied when the arguments are parsed. tool.toJSON() returns the same schema.
Sandboxed Execution
Tools that run untrusted commands can declare a sandbox. A sandboxed tool does not run its execute function: the runtime executes the call in the sandbox instead (install @cogitator-ai/sandbox):
type: 'docker'runs thecommandargument withsh -cin a container ofimage, with optionalcwdandenvarguments, and returns{ stdout, stderr, exitCode, timedOut, duration, command }.type: 'wasm'passes the arguments as JSON on stdin towasmModuleand returns its stdout, parsed as JSON when possible.
const runPython = tool({
name: 'run_python',
description: 'Run a shell command in a Python 3.12 container, e.g. python -c "print(2 ** 10)"',
parameters: z.object({
command: z.string().describe('Shell command to run'),
}),
execute: async ({ command }) => ({ command }), // not called while the sandbox is available
sandbox: {
type: 'docker',
image: 'python:3.12-slim',
network: { mode: 'none' },
resources: { memory: '256MB', cpus: 0.5 },
timeout: 30_000,
},
});The container is managed by the runtime (configure it with sandbox on the Cogitator). Two fallbacks log a warning and run without isolation: when @cogitator-ai/sandbox cannot be loaded, the runtime calls execute natively, so keep execute harmless; when Docker is not available, the sandbox manager runs the command on the host shell. Both are allowed by sandbox.allowNativeFallback (default true); set it to false on the Cogitator when untrusted commands must never reach the host, and such calls fail instead — see Sandbox.
Tool Caching
Wrap tools with withCache to avoid repeated expensive calls:
import { withCache } from '@cogitator-ai/core';
const cachedWeather = withCache(getWeather, {
strategy: 'exact',
ttl: '5m',
maxSize: 1000,
storage: 'memory',
});See Tool Caching for TTL strategies, semantic matching, and Redis-backed storage.