Cogitator
Core

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

FieldTypeDescription
namestringUnique tool name (used in LLM function calling)
descriptionstringWhat the tool does (sent to the LLM)
parametersZodTypeZod schema defining accepted parameters
execute(params, context) => Promise<TResult>Implementation function
categoryToolCategory'math', 'text', 'file', 'network', 'system', 'utility', 'web', 'database', 'communication' or 'development'
tagsstring[]Optional tags for filtering
sideEffectsSideEffectType[]'filesystem', 'network', 'database', 'process', 'external'; shown to approvers
requiresApprovalboolean | (params) => booleanRequire human approval before execution (Approvals)
timeoutnumberPer-call timeout in milliseconds
sandboxSandboxConfigRun in an isolated environment (see below)

Tool Context

execute receives the validated parameters and a ToolContext:

FieldDescription
agentId, runIdThe agent and run that called the tool
signalAborted when the run is cancelled, times out, or the tool's timeout passes
threadIdThe conversation
userIdThe user the run acts for (RunOptions.userId)
channelType, channelIdWhere 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 the command argument with sh -c in a container of image, with optional cwd and env arguments, and returns { stdout, stderr, exitCode, timedOut, duration, command }.
  • type: 'wasm' passes the arguments as JSON on stdin to wasmModule and 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.

On this page