# Welcome to Cogitator URL: https://cogitator.app/docs > The Sovereign AI Agent Runtime — self-hosted, production-grade orchestration for LLM swarms and autonomous agents. ## What is Cogitator? Cogitator is a self-hosted, production-grade AI agent runtime for TypeScript. It provides everything you need to build, orchestrate, and deploy autonomous AI agents — from simple chat assistants to multi-agent swarms with memory, tools, workflows, and sandboxed code execution. ## Key Features * **Multi-Model Support** — Ollama, OpenAI, Anthropic, Google Gemini, Azure OpenAI, AWS Bedrock, vLLM, Mistral, Groq, Together and DeepSeek, plus your own backends * **Agent Framework** — type-safe tools, structured outputs, reasoning, handoffs, tool approvals with pause/resume, vision & audio * **Swarm Intelligence** — 7 coordination strategies (hierarchical, round-robin, consensus, auction, pipeline, debate, negotiation) * **Workflow Engine** — DAG-based orchestration with sagas, human-in-the-loop approvals, timers, scheduling and checkpoints * **Memory & RAG** — Redis, Postgres, SQLite, MongoDB, Qdrant and in-memory adapters, hybrid BM25 + vector search, knowledge graphs, a RAG pipeline * **Channels & Voice** — Telegram, Discord, Slack, WhatsApp and WebChat gateways; STT/TTS and realtime voice agents * **Server Adapters** — Express, Fastify, Hono, Koa, Tetsu and Next.js, with OpenAPI generation * **Sandboxed Execution** — Docker and WASM sandboxes for running untrusted code * **Protocols** — Model Context Protocol (client and server) and Agent-to-Agent (A2A) * **Observability & Evals** — Langfuse and OpenTelemetry exporters, per-run cost tracking, an eval framework ## Quick Start Scaffold a project: ```bash npx create-cogitator-app my-agent cd my-agent npm run dev ``` Or install manually: ```bash npm install @cogitator-ai/core zod ``` ```typescript import { Cogitator, Agent, tool } from '@cogitator-ai/core'; import { z } from 'zod'; const cogitator = new Cogitator({ llm: { providers: { ollama: { baseUrl: 'http://localhost:11434' }, }, }, }); const agent = new Agent({ name: 'assistant', model: 'ollama/llama3.2', instructions: 'You are a helpful assistant.', tools: [ tool({ name: 'greet', description: 'Greet someone', parameters: z.object({ name: z.string() }), execute: async ({ name }) => `Hello, ${name}!`, }), ], }); const result = await cogitator.run(agent, { input: 'Say hello to the world' }); console.log(result.output); await cogitator.close(); ``` Continue with the [Quick Start](/docs/getting-started/quick-start) guide. --- # Installation URL: https://cogitator.app/docs/getting-started > Install Cogitator and set up your development environment. ## Prerequisites * **Node.js 22.12+** — [Download](https://nodejs.org/) * **pnpm** (recommended) — `npm install -g pnpm`; npm, yarn and bun work too * **Docker** (optional) — for Redis, Postgres and sandboxed execution * **Ollama** (for local LLMs) — [Download](https://ollama.com/), or use a hosted provider such as OpenAI, Anthropic or Google ```bash node --version # v22.12.0 or higher ``` ## Scaffolding a Project The fastest way to start is the project scaffolder. It generates the project and installs its dependencies: ```bash npx create-cogitator-app my-agent cd my-agent npm run dev ``` `create-cogitator-app` supports 6 templates: | Template | Description | | ------------ | --------------------------------------------- | | `basic` | Single agent with tools | | `memory` | Agent with persistent memory in Redis | | `swarm` | Team of agents with hierarchical coordination | | `workflow` | Multi-step DAG pipeline of agents | | `api-server` | Express server exposing agents over HTTP | | `nextjs` | Next.js chat app with streaming | Pass flags to skip the prompts: ```bash npx create-cogitator-app my-agent \ --template swarm \ --provider openai \ --pm pnpm ``` See [Project Scaffolding](/docs/getting-started/scaffolding) for every flag. For a config-driven personal assistant with Telegram, Discord or Slack, use the [CLI](/docs/cli) (`cogitator wizard` or `cogitator init`) instead. ## Manual Installation Add Cogitator to an existing project: ```bash pnpm add @cogitator-ai/core zod ``` Optional packages depending on your needs: ```bash # Memory adapters (Redis, Postgres, SQLite, MongoDB, Qdrant) pnpm add @cogitator-ai/memory # RAG pipeline pnpm add @cogitator-ai/rag # Multi-agent swarms pnpm add @cogitator-ai/swarms # Workflow orchestration pnpm add @cogitator-ai/workflows # Server adapters pnpm add @cogitator-ai/express # or fastify, hono, koa, tetsu, next # cogitator.yml and environment loading pnpm add @cogitator-ai/config # MCP protocol pnpm add @cogitator-ai/mcp ``` Some providers and adapters load optional peer dependencies on demand: `@aws-sdk/client-bedrock-runtime` for Bedrock, `pg` for Postgres, `better-sqlite3` for SQLite, `@cogitator-ai/sandbox` for sandboxed tools, `langfuse` for the Langfuse exporter. ## Docker Services Projects scaffolded with Docker Compose (the default answer, or `--docker`) get a `docker-compose.yml` with: * **Redis** (port 6379) — short-term memory, pub/sub, job queues * **Postgres** (port 5432) — long-term memory (user, password and database `cogitator` unless `POSTGRES_*` variables are set) * **Ollama** (port 11434) — local LLM inference, only when Ollama is the chosen provider ```bash docker compose up -d ``` The `postgres:16-alpine` image has no pgvector extension. For semantic search in Postgres, switch the image to `pgvector/pgvector:pg16`. Pull a model when you use Ollama: ```bash ollama pull llama3.2 ``` --- # Quick Start URL: https://cogitator.app/docs/getting-started/quick-start > Build your first AI agent in 5 minutes. ## Your First Agent Create a file `src/agent.ts`: ```typescript import { Cogitator, Agent } from '@cogitator-ai/core'; const cogitator = new Cogitator({ llm: { providers: { ollama: { baseUrl: 'http://localhost:11434' }, }, }, }); const assistant = new Agent({ name: 'assistant', model: 'ollama/llama3.2', instructions: 'You are a helpful assistant. Be concise and friendly.', }); const result = await cogitator.run(assistant, { input: 'Hello! What can you help me with?' }); console.log('Agent:', result.output); console.log('Tokens:', result.usage.totalTokens); await cogitator.close(); ``` Run it (with Ollama running and `ollama pull llama3.2` done): ```bash npx tsx src/agent.ts ``` `run()` takes the agent and a `RunOptions` object; `input` is the user message. The result carries the answer in `output`, token counts and cost in `usage`, every tool call in `toolCalls` and the run's spans in `trace`. ## Adding Tools Tools give your agent the ability to take actions. Define them with Zod schemas for full type safety: ```typescript import { Cogitator, Agent, 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 }) => ({ city, temperature: units === 'celsius' ? 22 : 72, condition: 'sunny', }), }); const weatherBot = new Agent({ name: 'weather-bot', model: 'ollama/llama3.2', instructions: 'You are a weather assistant. Use the get_weather tool to answer questions.', tools: [getWeather], }); const cogitator = new Cogitator(); const result = await cogitator.run(weatherBot, { input: 'What is the weather like in Tokyo?' }); console.log(result.output); console.log(result.toolCalls.map((call) => call.name)); // ['get_weather'] ``` Without any configuration, Cogitator talks to Ollama at `http://localhost:11434`. ## Streaming Responses For real-time output, stream the tokens: ```typescript const result = await cogitator.run(assistant, { input: 'Write a short poem about coding.', stream: true, onToken: (token) => process.stdout.write(token), }); ``` `result.output` still holds the whole answer once the run finishes. ## Conversations Runs with the same `threadId` share a conversation when memory is configured: ```typescript const cogitator = new Cogitator({ llm: { providers: { ollama: { baseUrl: 'http://localhost:11434' } } }, memory: { adapter: 'memory' }, }); await cogitator.run(assistant, { input: 'My name is Alex.', threadId: 'chat-1' }); const reply = await cogitator.run(assistant, { input: 'What is my name?', threadId: 'chat-1' }); ``` See [Memory](/docs/memory) for persistent adapters. ## Multiple Providers Configure several LLM providers and route each agent to the model it needs: ```typescript const cogitator = new Cogitator({ llm: { defaultModel: 'ollama/llama3.2', providers: { ollama: { baseUrl: 'http://localhost:11434' }, openai: { apiKey: process.env.OPENAI_API_KEY! }, anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! }, }, }, }); // runs on llm.defaultModel const localAgent = new Agent({ name: 'local', instructions: '...' }); // per-agent models const smartAgent = new Agent({ name: 'smart', instructions: '...', model: 'openai/gpt-6.1-sol' }); const creativeAgent = new Agent({ name: 'creative', instructions: '...', model: 'anthropic/claude-sonnet-5-5', }); ``` Model strings: * `provider/model-name` — routes to that provider (`openai/gpt-6.1-sol`, `google/gemini-3.5-flash-lite`) * `model-name` — runs on `llm.defaultProvider` (Ollama when unset); so does a string whose prefix is not a known provider, such as `meta-llama/llama-3.3-70b` * no `model` on the agent — uses `llm.defaultModel`; a run fails with `CONFIGURATION_ERROR` when neither is set ## Next Steps * [Project Structure](/docs/getting-started/project-structure) — how projects and the monorepo are organized * [Configuration](/docs/getting-started/configuration) — `cogitator.yml`, environment variables, provider setup * [Core Concepts](/docs/core/cogitator) — deep dive into the Cogitator runtime --- # Project Scaffolding URL: https://cogitator.app/docs/getting-started/scaffolding > Create new Cogitator projects in seconds with create-cogitator-app. `create-cogitator-app` is an interactive CLI that scaffolds a Cogitator project from a template, wired to your chosen LLM provider, and installs its dependencies. ## Quick Start ```bash npx create-cogitator-app ``` The prompts ask for: 1. Project directory (default `my-agents`) 2. Template 3. LLM provider 4. Package manager (pre-selected from the one running the command, otherwise pnpm) 5. Docker Compose (default yes) 6. Git repository (default yes) The target directory must not exist or be empty. The scaffolder then writes the files, runs ` install` (unless you pass `--no-install`) and, if chosen and git is installed, creates a repository. A failed install or git step is reported and left for you to run by hand. ## Templates | Template | What you get | | ------------ | ------------------------------------------------------------------------------------ | | `basic` | One agent with two tools (`src/index.ts`, `src/tools.ts`) | | `memory` | Agent with Redis-backed memory that remembers a conversation across runs | | `swarm` | Hierarchical swarm: a reviewer supervising a researcher and a writer (`src/agents/`) | | `workflow` | DAG workflow of three agent nodes and a function node (`@cogitator-ai/workflows`) | | `api-server` | Express server exposing agents over HTTP (`@cogitator-ai/express`) | | `nextjs` | Next.js App Router chat app with a streaming route and `useCogitatorChat` UI | ## CLI Flags Skip the prompts by passing flags directly: ```bash npx create-cogitator-app my-project \ --template swarm \ --provider openai \ --pm pnpm \ --docker \ --no-git ``` Or accept the defaults for everything you don't pass: ```bash npx create-cogitator-app -y npx create-cogitator-app my-project --provider openai --yes ``` | Flag | Shorthand | Values | | -------------------------- | --------- | -------------------------------------------------------------- | | `[directory]` | | First positional argument; its last segment names the project | | `--template` | `-t` | `basic`, `memory`, `swarm`, `workflow`, `api-server`, `nextjs` | | `--provider` | `-p` | `ollama`, `openai`, `anthropic`, `google` | | `--pm` | | `pnpm`, `npm`, `yarn`, `bun` | | `--docker` / `--no-docker` | | Include Docker Compose | | `--git` / `--no-git` | | Initialize a git repository | | `--no-install` | | Write the files without installing dependencies | | `--yes` | `-y` | Don't prompt; use the defaults for every missing value | An invalid flag value is ignored and asked for interactively; anything not given by a flag is prompted for. With `--yes` nothing is prompted and the defaults fill the gaps: `my-agents`, `basic`, `ollama`, the detected package manager, Docker Compose on, git on. ## Providers | Provider | Default Model | Env Var | | ----------- | ------------------- | ---------------------------- | | `ollama` | `qwen3:8b` | `OLLAMA_BASE_URL` (optional) | | `openai` | `gpt-6.1-sol` | `OPENAI_API_KEY` | | `anthropic` | `claude-sonnet-5-5` | `ANTHROPIC_API_KEY` | | `google` | `gemini-3.8-flash` | `GOOGLE_API_KEY` | The generated code sets `llm.defaultProvider` to the chosen provider, reads the key from the environment variable above and gives each agent the default model. `.env.example` lists the variables to set. Next.js loads `.env*` files on its own; the other templates do not, so export the variables in your shell or run with `tsx --env-file=.env src/index.ts`. ## Generated Structure ``` my-project/ ├── src/ │ ├── index.ts # entry point │ └── tools.ts # tool definitions (files vary by template) ├── package.json # dev/start scripts (tsx), build, typecheck ├── tsconfig.json # not for nextjs, which brings its own ├── cogitator.yml # provider and model, for reference ├── .env.example ├── .gitignore ├── pnpm-workspace.yaml # with --pm pnpm ├── docker-compose.yml # with --docker └── README.md ``` `cogitator.yml` is in [`@cogitator-ai/config`](/docs/getting-started/configuration) format (`llm.defaultProvider`, `llm.defaultModel`, `memory` for the `memory` template, and `deploy.health.path: /api/health` for the `api-server` and `nextjs` templates, the route their app serves health on) and is read by `cogitator run` and `cogitator deploy`. The generated code configures `Cogitator` in `src/` itself and does not load it. `docker-compose.yml` runs Redis 7 and Postgres 16 (credentials from `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, all defaulting to `cogitator`), plus Ollama when it is the provider. Start the project with `npm run dev` (or `pnpm dev`, `yarn dev`, `bun dev`). ## Programmatic API The package also exports its building blocks: ```ts import { scaffold } from 'create-cogitator-app'; const result = await scaffold({ name: 'my-agent', path: '/projects/my-agent', template: 'basic', provider: 'ollama', packageManager: 'pnpm', docker: false, git: true, install: false, // optional, default true }); if (result.install.status === 'failed') console.error(result.install.error); ``` `scaffold()` writes the files, installs dependencies and initializes git like the CLI. Pass `install: false` to only write the files, for example when you embed the scaffolder and install later. It throws when the directory is not empty, and otherwise resolves to a `ScaffoldResult`: the generated `files`, and `install` and `git` steps that are each `{ status: 'done' }`, `{ status: 'skipped' }` or `{ status: 'failed', error }`. A failed step never throws, so the project stays on disk. `parseArgs(argv)` and `collectOptions(args)` turn command-line arguments into these options (prompting for the rest). `getTemplate(name)`, `templateChoices`, `defaultModels`, `providerConfig(provider)`, `providerEnvKey(provider)`, `detectPackageManager()` and `devCommand(pm)` are exported as well. --- # Project Structure URL: https://cogitator.app/docs/getting-started/project-structure > How Cogitator projects and the monorepo are organized. ## Monorepo Architecture Cogitator is a pnpm + Turborepo monorepo. It publishes 32 `@cogitator-ai/*` packages and the `create-cogitator-app` scaffolder; `dashboard` (this site) and `e2e` (end-to-end tests) are private: ``` packages/ ├── types/ # @cogitator-ai/types — shared TypeScript interfaces ├── core/ # @cogitator-ai/core — Cogitator runtime, Agent, tool(), LLM backends, built-in tools ├── models/ # @cogitator-ai/models — model registry with pricing and capabilities ├── config/ # @cogitator-ai/config — cogitator.yml and environment loading ├── memory/ # @cogitator-ai/memory — memory adapters, embeddings, knowledge graphs ├── rag/ # @cogitator-ai/rag — RAG pipeline (loaders, chunkers, retrieval, reranking) ├── workflows/ # @cogitator-ai/workflows — DAG engine, sagas, map-reduce, scheduling ├── swarms/ # @cogitator-ai/swarms — 7 swarm strategies ├── a2a/ # @cogitator-ai/a2a — Agent-to-Agent Protocol v0.3 ├── mcp/ # @cogitator-ai/mcp — Model Context Protocol client and server ├── browser/ # @cogitator-ai/browser — browser automation (Playwright, stealth, vision) ├── sandbox/ # @cogitator-ai/sandbox — Docker and WASM execution isolation ├── wasm-tools/ # @cogitator-ai/wasm-tools — pre-built WASM tools ├── voice/ # @cogitator-ai/voice — voice and realtime agents (STT, TTS, VAD) ├── evals/ # @cogitator-ai/evals — eval framework (metrics, A/B testing, assertions) ├── self-modifying/ # @cogitator-ai/self-modifying — runtime tool generation ├── neuro-symbolic/ # @cogitator-ai/neuro-symbolic — Prolog-style logic, SAT/SMT ├── redis/ # @cogitator-ai/redis — Redis client (standalone + cluster) ├── worker/ # @cogitator-ai/worker — BullMQ distributed job queue ├── server-shared/ # @cogitator-ai/server-shared — shared REST/SSE/WebSocket utilities, OpenAPI ├── express/ # @cogitator-ai/express — Express.js integration ├── fastify/ # @cogitator-ai/fastify — Fastify integration ├── hono/ # @cogitator-ai/hono — Hono integration ├── koa/ # @cogitator-ai/koa — Koa middleware ├── tetsu/ # @cogitator-ai/tetsu — Tetsu controller on Bun (SSE, WebSocket, OpenAPI) ├── next/ # @cogitator-ai/next — Next.js App Router ├── ai-sdk/ # @cogitator-ai/ai-sdk — Vercel AI SDK adapter ├── openai-compat/ # @cogitator-ai/openai-compat — OpenAI Assistants API compatibility ├── channels/ # @cogitator-ai/channels — Telegram, Discord, Slack, WhatsApp, WebChat ├── deploy/ # @cogitator-ai/deploy — Docker and Fly.io deployment ├── cli/ # @cogitator-ai/cli — the `cogitator` command ├── test-utils/ # @cogitator-ai/test-utils — testing utilities ├── create-cogitator-app/ # create-cogitator-app — project scaffolder ├── dashboard/ # private — landing page, docs and cookbook site └── e2e/ # private — end-to-end test suite ``` ## Package Dependency Graph ``` @cogitator-ai/types ← shared interfaces, no dependencies ↑ models · redis → memory ← model registry, Redis client, memory adapters ↑ @cogitator-ai/core ← Cogitator runtime (types + models + memory) ↑ ┌──────┼───────────┬──────────────────┬───────────────────────────┐ │ │ │ │ │ workflows browser, neuro-symbolic channels express, fastify, hono, koa, tetsu ↑ (+ server-shared), next, openai-compat swarms ↑ worker ``` * `@cogitator-ai/core` depends on `types`, `models` and `memory` (which uses `redis`). * `swarms` builds on `workflows`, and `worker` on `swarms`. * `mcp`, `rag`, `sandbox`, `wasm-tools`, `voice`, `a2a`, `evals`, `ai-sdk` and `self-modifying` declare at most `types` as a dependency (some take `core` or `memory` as a peer dependency). * `config` and `deploy` handle configuration and deployment; `cli` combines `core`, `config`, `models`, `memory`, `channels` and `deploy`. ## Typical Project Layout A project scaffolded with `create-cogitator-app` (`basic` template) looks like this: ``` my-agent/ ├── src/ │ ├── index.ts # entry point: Cogitator + Agent │ └── tools.ts # tool definitions ├── cogitator.yml ├── .env.example # API keys to copy into your environment ├── .gitignore ├── docker-compose.yml # Redis + Postgres (+ Ollama), when Docker was chosen ├── pnpm-workspace.yaml # when pnpm is the package manager ├── package.json ├── README.md └── tsconfig.json ``` Other templates add files such as `src/agents/*.ts` (swarm) or `src/agents.ts` (workflow, api-server); see [Project Scaffolding](/docs/getting-started/scaffolding). ## Development Commands Run these from the monorepo root: ```bash pnpm install # install all dependencies pnpm build # build all packages (turbo) pnpm dev # watch mode for all packages pnpm test # unit tests across all packages pnpm typecheck # type-check all packages pnpm lint # lint the whole repo pnpm --filter @cogitator-ai/core test # one package ``` --- # Configuration URL: https://cogitator.app/docs/getting-started/configuration > Configure Cogitator with YAML files, environment variables, and provider settings. ## Configuration File Create a `cogitator.yml` in your project root: ```yaml llm: defaultProvider: ollama defaultModel: ollama/llama3.2 providers: ollama: baseUrl: http://localhost:11434 openai: apiKey: ${OPENAI_API_KEY} anthropic: apiKey: ${ANTHROPIC_API_KEY} google: apiKey: ${GOOGLE_API_KEY} limits: maxConcurrentRuns: 10 defaultTimeout: 120000 memory: adapter: redis redis: url: redis://localhost:6379 embedding: provider: ollama model: nomic-embed-text ``` Load it with `@cogitator-ai/config`: ```typescript import { Cogitator } from '@cogitator-ai/core'; import { loadConfig } from '@cogitator-ai/config'; const config = loadConfig(); const cogitator = new Cogitator(config); ``` `loadConfig()`: * Reads the first of `cogitator.yaml`, `cogitator.yml`, `.cogitator.yaml`, `.cogitator.yml` in the current working directory (none is fine) * Substitutes `${VAR}` references in the YAML * Merges environment variables over the file (see below) * Merges `overrides` over both * Validates the result against Zod schemas and throws `Invalid configuration: ...` listing the offending fields ```typescript const config = loadConfig({ configPath: './config/cogitator.yml', // must exist, or loadConfig throws skipEnv: false, // true: ignore environment variables skipYaml: false, // true: ignore config files overrides: { limits: { maxConcurrentRuns: 4 } }, }); ``` `loadConfig()` does not read `.env` files. Load them first with `process.loadEnvFile()` (Node 20.12+), or parse one with `loadDotenvFile(path)` / `parseDotenv(text)` from the same package. ## Top-Level Sections | Section | What it configures | | ------------- | ------------------------------------------------------------------------------------------------------------------------- | | `llm` | `defaultProvider`, `defaultModel`, `providers`, `retry`, `promptCache`, `plugins` | | `prompts` | `autoDeployWinner` for [prompt A/B tests](/docs/advanced/prompt-versions) | | `limits` | `maxConcurrentRuns`, `defaultTimeout` (ms), `maxTokensPerRun` — see [Cogitator](/docs/core/cogitator#defaults-and-limits) | | `memory` | Adapter, its connection, `embedding` and `contextBuilder` | | `sandbox` | Defaults, container pool, Docker and WASM settings for [sandboxed tools](/docs/deployment/sandbox) | | `reflection` | [Reflection](/docs/advanced/reflection) after tool calls, errors or at the end of a run | | `guardrails` | [Constitutional AI](/docs/advanced/constitutional-ai) input/output/tool filtering | | `costRouting` | [Cost-aware model routing](/docs/advanced/cost-routing) and budgets | | `security` | `promptInjection` detection and `pii` masking ([Security](/docs/advanced/security)) | | `context` | [Context compression](/docs/advanced/context-management) for long conversations | | `deploy` | Target, server, port, registry and resources for `cogitator deploy` ([Deploy](/docs/deployment/deploy-package)) | | `logging` | `level`, `format`, `destination`, `filePath` of the process-wide logger (see below) | ### Memory `memory.adapter` names the thread store, with its settings under the key of the same name. The runtime builds and connects it on the first run (or on `getMemory()`): | `adapter` | Settings | | ---------- | ----------------------------------------------------------------------------------------------------- | | `memory` | `inMemory.maxEntries` (optional) | | `redis` | `redis.url`, or `redis.host` + `redis.port`, or `redis.cluster.nodes`; `keyPrefix`, `ttl`, `password` | | `postgres` | `postgres.connectionString`, `schema`, `poolSize` | | `sqlite` | `sqlite.path` (`':memory:'` for an in-memory database), `walMode` | | `mongodb` | `mongodb.uri`, `database`, `collectionPrefix` | When the section lacks what the store needs (for example `adapter: sqlite` without `sqlite.path`), or the connection fails, the runtime logs a warning, runs without memory and tries again on the next run. `adapter: qdrant` only logs a warning (see below). The Postgres store sizes its vector column to the embedding model in `memory.embedding`. `qdrant` is not a thread store: it holds embeddings. Pick one of the stores above as `adapter` and add `memory.qdrant` next to `memory.embedding` and `memory.contextBuilder`; the context builder then searches Qdrant for semantic context. The runtime only searches it — add embeddings with a `QdrantAdapter` of your own. ```yaml title="cogitator.yml" memory: adapter: redis redis: host: localhost port: 6379 qdrant: url: http://localhost:6333 collection: cogitator dimensions: 768 embedding: provider: ollama model: nomic-embed-text contextBuilder: strategy: hybrid includeSemanticContext: true ``` `memory.qdrant.dimensions` must match the embedding model; a mismatch is logged. `memory.embedding.dimensions` sets the vector size: OpenAI `text-embedding-3-*` and Google models return vectors of that size, and for Ollama it declares the size of a model the runtime does not know. `memory.contextBuilder.graphContextOptions` (`maxNodes`, `maxEdges`, `maxDepth`, `includeInferred`, `entityTypes`) shapes the [knowledge-graph context](/docs/memory#context-building). To use an adapter you built yourself, connect it and hand it over: ```typescript import { Cogitator } from '@cogitator-ai/core'; import { SQLiteAdapter } from '@cogitator-ai/memory'; const memory = new SQLiteAdapter({ provider: 'sqlite', path: './data/memory.db' }); const connected = await memory.connect(); if (!connected.success) throw new Error(connected.error); const cogitator = new Cogitator({ llm: { defaultModel: 'ollama/llama3.2' } }); cogitator.memory = memory; ``` ### Logging `logging` configures the process-wide logger that `getLogger()` returns, when a `Cogitator` is created with it; with several runtimes, the last one created with `logging` wins. `level` is `debug`, `info` (default), `warn`, `error` or `silent`. `format` is `pretty` (default) or `json`. `destination: file` appends one line per entry to `filePath`, as JSON unless `format` says otherwise; without `filePath`, or where there is no file system (edge runtimes), it logs to the console and warns. ```yaml title="cogitator.yml" logging: level: warn destination: file filePath: ./logs/cogitator.log ``` Without `logging`, the logger logs at `info` (or `LOG_LEVEL`), as JSON when `NODE_ENV=production` and pretty-printed otherwise. In code, build the same logger with `createLoggerFromConfig()` or your own with `createLogger()`, and install it with `setLogger()`: ```typescript import { createLogger, createLoggerFromConfig, setLogger } from '@cogitator-ai/core'; setLogger(createLoggerFromConfig({ level: 'debug', destination: 'file', filePath: './debug.log' })); setLogger(createLogger({ level: 'debug', format: 'json' })); ``` ## Environment Variables Cogitator reads environment variables with the `COGITATOR_` prefix: | Variable | Config path | | -------------------------------------------------------------------------------------- | --------------------------------- | | `COGITATOR_LLM_DEFAULT_PROVIDER` | `llm.defaultProvider` | | `COGITATOR_LLM_DEFAULT_MODEL` | `llm.defaultModel` | | `COGITATOR_OLLAMA_BASE_URL`, `COGITATOR_OLLAMA_API_KEY` | `llm.providers.ollama.*` | | `COGITATOR_OPENAI_API_KEY`, `COGITATOR_OPENAI_BASE_URL` | `llm.providers.openai.*` | | `COGITATOR_ANTHROPIC_API_KEY` | `llm.providers.anthropic.apiKey` | | `COGITATOR_GOOGLE_API_KEY` | `llm.providers.google.apiKey` | | `COGITATOR_AZURE_API_KEY`, `_ENDPOINT`, `_API_VERSION`, `_DEPLOYMENT` | `llm.providers.azure.*` | | `COGITATOR_BEDROCK_REGION`, `_ACCESS_KEY_ID`, `_SECRET_ACCESS_KEY` | `llm.providers.bedrock.*` | | `COGITATOR_VLLM_BASE_URL` | `llm.providers.vllm.baseUrl` | | `COGITATOR_MISTRAL_API_KEY`, `_GROQ_API_KEY`, `_TOGETHER_API_KEY`, `_DEEPSEEK_API_KEY` | `llm.providers..apiKey` | | `COGITATOR_LIMITS_MAX_CONCURRENT_RUNS`, `_DEFAULT_TIMEOUT`, `_MAX_TOKENS_PER_RUN` | `limits.*` (digits only) | | `COGITATOR_DEPLOY_TARGET`, `_PORT`, `_REGISTRY` | `deploy.*` | Standard provider variables work too; a `COGITATOR_*` variable wins over its standard counterpart: ```bash OPENAI_API_KEY=sk-xxx OPENAI_BASE_URL=https://proxy.example.com/v1 ANTHROPIC_API_KEY=sk-ant-xxx GOOGLE_API_KEY=xxx # or GEMINI_API_KEY OLLAMA_URL=http://localhost:11434 # or OLLAMA_HOST (scheme optional) OLLAMA_API_KEY=xxx # Ollama Cloud; URL defaults to https://ollama.com unless set in env or YAML AZURE_OPENAI_API_KEY=xxx # with AZURE_OPENAI_ENDPOINT, optional AZURE_OPENAI_DEPLOYMENT AWS_REGION=us-east-1 # AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY for Bedrock MISTRAL_API_KEY=xxx GROQ_API_KEY=xxx TOGETHER_API_KEY=xxx DEEPSEEK_API_KEY=xxx ``` Azure is configured from the environment only when both the key and the endpoint are set. ### Referencing Environment Variables in YAML String values in `cogitator.yml` may reference environment variables. `${VAR}` becomes the value (or an empty string), `${VAR:-default}` falls back when the variable is unset or empty, `${VAR-default}` only when unset, and `$$` is a literal `$`: ```yaml title="cogitator.yml" llm: providers: openai: apiKey: ${OPENAI_API_KEY} baseUrl: ${OPENAI_BASE_URL:-https://api.openai.com/v1} ``` Substituted values are always strings, so numeric fields (for example `deploy.port`) must be written literally. ## Programmatic Configuration You can skip the YAML file and validate a config object directly: ```typescript import { Cogitator } from '@cogitator-ai/core'; import { defineConfig } from '@cogitator-ai/config'; const config = defineConfig({ llm: { defaultModel: 'openai/gpt-6.1-sol', providers: { openai: { apiKey: process.env.OPENAI_API_KEY!, }, }, }, memory: { adapter: 'postgres', postgres: { connectionString: process.env.DATABASE_URL!, }, embedding: { provider: 'openai', apiKey: process.env.OPENAI_API_KEY!, model: 'text-embedding-3-small', }, }, }); const cogitator = new Cogitator(config); ``` `defineConfig()` validates like `loadConfig()` but reads neither files nor the environment. Neither of them is required: `new Cogitator({...})` takes the same object without validation. Options that hold functions, stores or backends exist only in code: `llm.backends`, `llm.retry.onRetry`, `runCheckpoints`, `prompts.versions` / `prompts.abTests` / `prompts.score`, `security.promptInjection.llmBackend`, and callbacks such as `security.promptInjection.onThreat`, `security.pii.onDetect` and `guardrails.onToolApproval`. Plain-data options, including `llm.plugins` and `prompts.autoDeployWinner`, can come from YAML. Add the code-only options next to the loaded config: ```typescript const config = loadConfig(); const cogitator = new Cogitator({ ...config, prompts: { ...config.prompts, score: (result) => (result.output.length > 0 ? 1 : 0) }, }); ``` ## Provider Configuration ### Ollama (Local) ```yaml llm: providers: ollama: baseUrl: http://localhost:11434 # default; https://ollama.com when only apiKey is set apiKey: xxx # optional, Ollama Cloud or an authenticated server ``` ### OpenAI ```yaml llm: providers: openai: apiKey: sk-xxx baseUrl: https://api.openai.com/v1 # optional, for proxies api: responses # optional: responses | chat-completions ``` The official endpoint uses the OpenAI Responses API by default; a `baseUrl` pointing at another OpenAI-compatible server uses Chat Completions. Set `api` to force either wire API. Requests with stop sequences always go through Chat Completions. ### Anthropic ```yaml llm: providers: anthropic: apiKey: sk-ant-xxx ``` ### Google Gemini ```yaml llm: providers: google: apiKey: xxx ``` ### Azure OpenAI ```yaml llm: providers: azure: apiKey: xxx endpoint: https://your-resource.openai.azure.com apiVersion: '2024-02-15-preview' # optional deployment: gpt-6.1-sol # optional ``` ### AWS Bedrock ```yaml llm: providers: bedrock: region: us-east-1 # optional, can use AWS config chain accessKeyId: xxx # optional, uses AWS credentials chain secretAccessKey: xxx # optional ``` Bedrock needs the optional `@aws-sdk/client-bedrock-runtime` package. ### OpenAI-Compatible Providers ```yaml llm: providers: mistral: apiKey: xxx groq: apiKey: xxx together: apiKey: xxx deepseek: apiKey: xxx vllm: baseUrl: http://localhost:8000/v1 ``` ### Retries Failed LLM calls (rate limits, 5xx, timeouts, dropped connections) are retried with exponential backoff and the provider's `Retry-After`. The defaults are below; `retry: false` turns retries off. See [LLM Backends](/docs/core/llm-backends#retries) for how streams and cancellation are handled. ```yaml llm: retry: maxRetries: 2 # attempts after the first one baseDelay: 1000 # ms before the first retry, doubled each time maxDelay: 30000 # backoff cap, ms maxRetryAfter: 60000 # a longer Retry-After fails the call at once ``` ### Prompt Caching Agent runs cache their prompt by default (see [LLM Backends](/docs/core/llm-backends#prompt-caching)): ```yaml llm: promptCache: ttl: 1h # Anthropic keeps the cache for an hour instead of 5 minutes # promptCache: false — turn it off ``` ### PII Masking Keep emails, phones, card numbers and other personal data away from the provider (see [Security](/docs/advanced/security#pii-masking)): ```yaml security: pii: mode: mask # mask | redact | block detect: [email, phone, credit_card, iban, ssn, ip_address, api_key] custom: - type: customer_id pattern: 'CUS-\d{6}' ``` ### Prompt Injection Detection settings live under `security.promptInjection` (see [Security](/docs/advanced/security#prompt-injection-detection)). `patterns` are regular expression sources, compiled without flags, so they are case-sensitive; pass `RegExp` objects in code for flags: ```yaml security: promptInjection: action: block # block | warn | log threshold: 0.7 patterns: - '[Rr]eveal (the|your) system prompt' failMode: secure # secure (default): a throwing classifier fails the check; open: the input passes allowlist: - ignore the previous search ``` ### Sandbox Settings for [sandboxed tools](/docs/deployment/sandbox): ```yaml sandbox: allowNativeFallback: false # default true: Docker tools run on the host when Docker is unavailable defaults: image: cogitator/sandbox:base env: PYTHONUNBUFFERED: '1' mounts: - source: /srv/data target: /workspace/data readOnly: true pool: maxSize: 5 reuseContainers: false # default; true reuses containers, so one execution's files reach the next ``` `sandbox.defaults` also takes `wasmModule`, `wasmFunction` and `wasi` for WASM tools. A custom constitution for [guardrails](/docs/advanced/constitutional-ai#custom-constitutions) can be written in YAML too, under `guardrails.constitution` (`id`, `name`, `version`, `customizable`, `strictMode` and `principles`). ## Config Merging Configs are deep-merged with this priority (highest wins): 1. Programmatic overrides passed to `loadConfig({ overrides })` 2. Environment variables (`COGITATOR_*` and standard provider vars) 3. YAML config file values 4. Built-in defaults Objects merge key by key; arrays and scalar values replace each other. --- # CLI URL: https://cogitator.app/docs/cli > Command-line interface for Cogitator — scaffold assistants, run them as services, chat with agents, manage Ollama models and deploy. Command-line interface for the Cogitator AI agent runtime. Scaffold assistant projects, run them in the foreground or as a background service, chat with agents from the terminal, manage Ollama models and Docker services, and deploy. ## Installation ```bash # Global installation (recommended) pnpm add -g @cogitator-ai/cli # Or use with npx npx @cogitator-ai/cli ``` Requires Node.js 22.12+. ## Quick Start There are two ways to build an assistant: ```bash # A) Config-driven personal assistant (no code) cogitator wizard # answers → cogitator.yml + .env cogitator up # run it # B) Code-first project cogitator init my-assistant cd my-assistant pnpm dev # or: cogitator assistant ``` Quick one-off chat with any model: ```bash cogitator run -m ollama/llama3.1:8b "What is the capital of France?" ``` *** ## Commands | Command | Description | | ------------------------ | -------------------------------------------------------------------------------------------- | | `cogitator init [name]` | Scaffold a code-first assistant project | | `cogitator wizard` | Interactive setup that writes `cogitator.yml` + `.env` | | `cogitator up` | Run the assistant from `cogitator.yml`, or start Docker services | | `cogitator down` | Stop Docker Compose services | | `cogitator assistant` | Run a gateway module (`src/gateway.ts`) with a live dashboard | | `cogitator run [msg]` | Chat with an agent (one-shot or interactive REPL) | | `cogitator build` | Bundle a gateway into a self-starting `dist/cogitator.mjs` | | `cogitator daemon ` | Run in the background / install as a launchd or systemd service | | `cogitator skill ` | Create, validate, install and remove skills | | `cogitator models` | List or pull Ollama models | | `cogitator status` | Show Docker Compose and Ollama status (alias: `ps`) | | `cogitator logs` | View Docker Compose service logs | | `cogitator deploy` | Deploy with Docker or Fly.io (see [`@cogitator-ai/deploy`](/docs/deployment/deploy-package)) | Set `COGITATOR_DEBUG=1` to print stack traces for unexpected errors. *** ### cogitator init Creates a TypeScript project with a [`Gateway`](/docs/channels/gateway) wired to your chosen LLM provider, channels and memory adapter. ```bash cogitator init my-assistant # prompts for provider, model, channels, memory cogitator init my-assistant --no-install ``` The project name must be a valid npm package name (lowercase, `-`, `_`, `.`) and the directory must not exist yet. The prompts cover the provider (Anthropic, OpenAI, Google or Ollama) and its API key, the default model (listed live from the provider), channels (Telegram, Discord, Slack, WebChat — WebChat when none is picked) and the memory adapter. The package manager used for installation is detected from how you invoked the CLI (`pnpm`, `npm`, `yarn` or `bun`, otherwise pnpm). ``` my-assistant/ ├── package.json # @cogitator-ai/* pinned to the versions this CLI ships with ├── tsconfig.json ├── .env # API keys and channel tokens (mode 0600), when any were entered ├── .gitignore ├── pnpm-workspace.yaml # allows native builds (better-sqlite3, esbuild, sharp), pnpm only └── src/ ├── gateway.ts # exports `gateway` — agent, channels, memory adapter └── agent.ts # starts the gateway (pnpm dev / pnpm start) ``` | Memory choice | Generated adapter | Extra dependency | Notes | | ------------- | ----------------- | ---------------- | ----------------------------------- | | SQLite | `SQLiteAdapter` | `better-sqlite3` | Stored in `./data/memory.db` | | In-memory | `InMemoryAdapter` | — | Lost on restart | | PostgreSQL | `PostgresAdapter` | `pg` | Connection string in `DATABASE_URL` | `src/gateway.ts` loads `.env` itself (`process.loadEnvFile`, Node ≥ 20.12), so it works with `pnpm dev`, `cogitator assistant`, `cogitator build` and the daemon alike. *** ### cogitator wizard Interactive setup for the config-driven personal assistant run by `cogitator up`. ```bash cogitator wizard # create cogitator.yml (+ .env) cogitator wizard --edit # edit an existing cogitator.yml, pre-filling current values ``` The wizard asks for the LLM provider and model (models are fetched live from the provider registry or your Ollama server), channels (Telegram, Discord, Slack with owner IDs), capabilities (web search, file system, GitHub, device tools, browser, scheduler, RAG, self-config, self-tools), MCP servers (quoted arguments are supported) and the SQLite memory path. Secrets are merged into `.env` without touching your other variables or comments. In `--edit` mode, leaving a secret blank keeps the current value, and existing MCP servers and advanced settings (security, rate limits, …) are preserved. The wizard does not ask about WhatsApp, WebChat or Postgres memory, but keeps them: `channels.whatsapp` / `channels.webchat` entries stay as they are, and a `memory.adapter: postgres` config is kept (the SQLite path prompt is skipped). *** ### cogitator up / down ```bash cogitator up # ./cogitator.yml (or .yaml) → run the assistant cogitator up -c path/to/assistant.yml cogitator up --no-restart-loop # run in-process, without the restart supervisor ``` When a `cogitator.yml` exists, `up` validates it (errors list the offending fields), loads the `.env` next to it into `process.env` without overriding variables that are already set (so tools that read `process.env`, such as `web_search` and `github_api`, see the wizard's keys) and starts the assistant. Unless `--no-restart-loop` is given, `up` runs the assistant in a child process and starts it again whenever it exits with code `78` — the owner `/restart` command and self-config updates; `SIGINT`/`SIGTERM` are forwarded for a graceful shutdown. With `--no-restart-loop` the assistant runs in the `up` process and simply stops on `/restart`. Once the assistant is up, the summary lists the channels that actually started and warns when none did (for example because their tokens are missing from `.env`). A `cogitator.yml` made only of [`@cogitator-ai/config`](/docs/getting-started/configuration) runtime keys (`llm.defaultModel`, `memory`, `sandbox`, … — the file create-cogitator-app generates) is not an assistant config: your project's code loads it. `up` then starts the project's Docker Compose services, or, without a compose file, exits explaining that you start the project itself (e.g. `pnpm dev`). `up -c` with such a file fails with the same explanation. Without an assistant config, `up` manages the Docker Compose project found in the current or a parent directory (`docker-compose.yml`, `compose.yml`, …): | Option | Default | Description | | -------------- | ------- | ---------------------------------- | | `-d, --detach` | `true` | Run services in background | | `--no-detach` | - | Run services in foreground | | `--pull` | `false` | Pull latest images before starting | After starting, the published ports of every service are printed. ```bash cogitator down # stop services (keep data) cogitator down -v # stop and delete volumes (--volumes) ``` *** ### cogitator assistant Runs a module that exports a `gateway` (as generated by `init`) with a live dashboard. ```bash cogitator assistant # src/gateway.ts cogitator assistant -c src/my-gw.ts cogitator assistant --quiet # no banner, status line or hotkeys (used by the daemon) ``` TypeScript modules are loaded through [`tsx`](https://tsx.is), resolved from your project first. `.env` in the working directory is loaded before the module. Hotkeys (interactive terminals only): `s` sessions, `c` channels, `h` help, `q` quit. *** ### cogitator run Run an agent with a message, or start an interactive REPL when no message is given. ```bash cogitator run "Explain quantum computing in simple terms" cogitator run -m openai/gpt-6.1-sol "Analyze this code..." cogitator run --no-stream "Hello" cogitator run # interactive ``` | Option | Default | Description | | --------------------- | ----------- | ----------------------------------- | | `-c, --config ` | auto | Config file (must exist when given) | | `-m, --model ` | auto-detect | Model, e.g. `ollama/qwen3:8b` | | `-i, --interactive` | `false` | Force interactive mode | | `-s, --stream` | `true` | Stream response tokens | | `--no-stream` | - | Disable streaming | **Config resolution:** `-c` → `COGITATOR_CONFIG` → `cogitator.yml`, `cogitator.yaml`, `cogitator.json`, `.cogitator.yml`, `.cogitator.yaml`. The file is loaded with [`@cogitator-ai/config`](/docs/getting-started/configuration), so `${ENV}` references and `COGITATOR_*` / provider environment variables apply even without a file. **Model resolution:** `-m` → `COGITATOR_MODEL` → `llm.defaultModel` from the config → first installed Ollama model (preferring qwen3:8b, qwen3.5:9b, llama3.1:8b, llama3:8b, gemma3:4b, gemma2:9b, mistral:7b). Ollama is reached at `llm.providers.ollama.baseUrl`, `OLLAMA_URL` or `OLLAMA_HOST`. Interactive commands: `/model [name]`, `/clear`, `/help`, `exit` / `quit` (Ctrl+D also exits). *** ### cogitator build Bundles a gateway module with [esbuild](https://esbuild.github.io) into a single self-starting ES module. The bundle starts the exported `gateway` and stops it gracefully on `SIGINT`/`SIGTERM`. ```bash pnpm add -D esbuild cogitator build # src/gateway.ts → dist/cogitator.mjs cogitator build -c src/gw.ts -o out/bot.mjs --minify --no-sourcemap node dist/cogitator.mjs ``` | Option | Default | | ---------------------- | -------------------- | | `-c, --config ` | `src/gateway.ts` | | `-o, --outfile ` | `dist/cogitator.mjs` | | `--target ` | `node22` | | `--sourcemap` | `true` | | `--minify` | `false` | | `-q, --quiet` | `false` | Native and optional channel dependencies (`better-sqlite3`, `pg`, `grammy`, `discord.js`, `@slack/bolt`, `playwright`, …) stay external. *** ### cogitator daemon Runs your assistant in the background. The entry is auto-detected in this order: `dist/cogitator.mjs` (from `cogitator build`), an assistant `cogitator.yml` / `cogitator.yaml` (via `cogitator up`; a runtime config is skipped), `src/gateway.ts` (via `cogitator assistant --quiet`). Use `-c` to choose explicitly; `-c` with a runtime config fails like `up -c`. ```bash cogitator daemon start [-c ] cogitator daemon status # PID, uptime, memory cogitator daemon logs -f -n 100 # .cogitator/daemon.log (-n defaults to 50) cogitator daemon restart cogitator daemon stop # SIGTERM, then SIGKILL after 10s cogitator daemon install [-c ] # launchd (macOS) or systemd --user (Linux) cogitator daemon uninstall ``` The PID file (`.cogitator/daemon.pid`) records the launched script, so `stop` never signals an unrelated process that reused the PID. Service definitions use absolute paths, the current `PATH`, and start on login (`loginctl enable-linger` keeps a systemd user service running after logout). *** ### cogitator skill ```bash cogitator skill create weather-api --template api # -t: basic | device | api; -g for global cogitator skill validate skills/weather-api # default: the current directory cogitator skill list cogitator skill add ./path/to/skill [-g, --global] cogitator skill remove weather-api [-g, --global] ``` Local skills live in `./skills`, global ones in `~/.cogitator/skills`. Skill names must be kebab-case. `validate` loads `skill.ts`/`skill.js`/`skill.mjs`, checks the `defineSkill` shape and every tool, required environment variables (`env`) and that `dependencies` resolve from the skill's location; it exits non-zero on problems. *** ### cogitator models ```bash cogitator models # list installed models cogitator models --pull qwen2.5:0.5b # pull with progress cogitator models --url http://gpu-box:11434 ``` The Ollama URL defaults to `OLLAMA_URL`, then `OLLAMA_HOST`, then `http://localhost:11434`; `OLLAMA_API_KEY` is sent as a bearer token. Pull errors reported by Ollama (e.g. unknown model) fail the command. *** ### cogitator status / logs ```bash cogitator status # compose services (state, health) + Ollama reachability cogitator logs ollama -f -t -n 50 cogitator logs -n all ``` | `logs` option | Default | Description | | -------------------- | ------- | ----------------------------- | | `[service]` | all | Compose service, e.g. `redis` | | `-f, --follow` | `false` | Follow log output | | `-n, --tail ` | `100` | Number of lines, or `all` | | `-t, --timestamps` | `false` | Show timestamps | `status` still reports Ollama when Docker is not running. *** ### cogitator deploy ```bash cogitator deploy --dry-run # analyze + preflight only cogitator deploy # docker: build image, start compose stack cogitator deploy --target fly --region ord cogitator deploy --registry ghcr.io/acme # build + push (use --no-push to skip) cogitator deploy status cogitator deploy destroy ``` | Option | Description | | ----------------------- | ---------------------------------------- | | `-t, --target ` | `docker` (default) or `fly` | | `-c, --config ` | Config file with a `deploy:` section | | `--registry ` | Container registry to push to | | `--no-push` | Skip pushing even when a registry is set | | `--dry-run` | Show the plan without executing | | `--region ` | Deploy region (Fly.io) | The plan lists detected services, required secrets (read from the environment or the project's `.env`), warnings and preflight checks. See [`@cogitator-ai/deploy`](/docs/deployment/deploy-package) for details. *** ## Environment Variables | Variable | Used by | Description | | ------------------------------------------------------- | ------------------------------------- | -------------------------------------------- | | `COGITATOR_CONFIG` | `run` | Config file path | | `COGITATOR_MODEL` | `run` | Default model | | `OLLAMA_URL` / `OLLAMA_HOST` | `run`, `models`, `status` | Ollama endpoint | | `OLLAMA_API_KEY` | `run`, `models`, `wizard` | Ollama Cloud / authenticated Ollama | | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY` | `run`, `wizard`, projects from `init` | Provider keys (`init` writes them to `.env`) | | `COGITATOR_DEBUG` | all | Print stack traces | See [`@cogitator-ai/config`](/docs/getting-started/configuration) for every `COGITATOR_*` variable. --- # Cogitator Runtime URL: https://cogitator.app/docs/core/cogitator > The main runtime that orchestrates agent execution, LLM routing, memory, and observability. ## Overview The `Cogitator` class is the central runtime for executing AI agents. It manages LLM backend connections, memory persistence, the global tool registry, approvals and observability. You create one instance and use it to run any number of agents. ```typescript import { Cogitator, Agent } from '@cogitator-ai/core'; const cog = new Cogitator({ llm: { defaultProvider: 'openai', providers: { openai: { apiKey: process.env.OPENAI_API_KEY! }, }, }, }); const agent = new Agent({ name: 'assistant', model: 'openai/gpt-6.1-sol', instructions: 'You are a helpful assistant.', }); const result = await cog.run(agent, { input: 'Hello!' }); console.log(result.output); await cog.close(); ``` ## Constructor ```typescript const cog = new Cogitator(config?: CogitatorConfig); ``` The config object is entirely optional. Without it, Cogitator talks to Ollama at `localhost:11434`, and every agent must name its own `model`. Nothing connects in the constructor: backends, memory and other services start on first use. ### Configuration ```typescript interface CogitatorConfig { llm?: { defaultProvider?: LLMProvider; // for model strings without a known provider prefix (default 'ollama') defaultModel?: string; // for agents that set no model providers?: LLMProvidersConfig; // API keys and URLs per provider backends?: Record; // your own backends, by name plugins?: Record; // config for registerLLMBackend() plugins retry?: LLMRetryConfig | false; // default: 2 retries with backoff promptCache?: PromptCacheConfig | false; // default: on }; limits?: { maxConcurrentRuns?: number; // runs over the limit wait in order defaultTimeout?: number; // ms, for agents and runs that set no timeout (default 120000) maxTokensPerRun?: number; // checked before each model call }; memory?: MemoryConfig; runCheckpoints?: RunCheckpointStore; // where paused runs wait prompts?: PromptsConfig; // instruction versions and A/B tests sandbox?: SandboxManagerConfig; reflection?: ReflectionConfig; guardrails?: Partial; costRouting?: CostRoutingConfig; security?: { promptInjection?: Partial; pii?: PiiConfig }; context?: ContextManagerConfig; logging?: LoggingConfig; // level, format and destination of the process-wide logger deploy?: DeployConfig; // read by `cogitator deploy` } ``` The same object can come from `cogitator.yml` and the environment via [`loadConfig()`](/docs/getting-started/configuration). Retries and prompt caching are described in [LLM Backends](/docs/core/llm-backends#retries); `security` in [Security](/docs/advanced/security). ### Defaults and Limits * `llm.defaultModel` is the model of every agent that sets no `model` of its own. `cog.resolveModel(agent)` returns the model a run would use and throws `CONFIGURATION_ERROR` when there is none. * `limits.maxConcurrentRuns` caps how many `run()` calls execute at once on this instance. The rest wait in arrival order; their timeout and abort signal also cover the wait. * `limits.defaultTimeout` applies to runs whose options and agent set no `timeout`. Without it, runs time out after 2 minutes. A run that times out throws a `CogitatorError` with code `RUN_TIMEOUT` (HTTP 504 in the server adapters). * `limits.maxTokensPerRun` is checked before every model call: once the run has used that many input and output tokens, the next call fails with `RUN_TOKEN_LIMIT_EXCEEDED`. The answer of the call that crosses the limit is still returned. * `logging` replaces the process-wide logger (`getLogger()`) when the runtime is created, so with several runtimes the last one created with `logging` wins. `level: 'silent'` turns logging off; `destination: 'file'` appends JSON lines to `filePath`. See [Configuration](/docs/getting-started/configuration#logging). ## Multi-Provider Setup Configure multiple LLM providers and let agents route to whichever they need: ```typescript const cog = new Cogitator({ llm: { defaultProvider: 'ollama', providers: { ollama: { baseUrl: 'http://localhost:11434' }, openai: { apiKey: process.env.OPENAI_API_KEY! }, anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! }, google: { apiKey: process.env.GOOGLE_API_KEY! }, }, }, }); ``` Backends are created lazily -- Cogitator only instantiates a provider's client the first time an agent requests it. The provider is determined by the model string prefix (`openai/gpt-6.1-sol` routes to OpenAI, `anthropic/claude-sonnet-5-5` routes to Anthropic). `cog.route(model)` returns the backend and model name a run would use, and `cog.getLLMBackend(model)` just the backend, already wrapped with retries and PII masking. ## Running Agents The `run()` method executes an agent and returns a `RunResult`: ```typescript const result = await cog.run(agent, { input: 'Search for TypeScript tutorials', threadId: 'session-123', stream: true, onToken: (token) => process.stdout.write(token), onToolCall: (call) => console.log('Tool:', call.name), onToolResult: (result) => console.log('Result:', result), }); ``` ### RunOptions | Option | Type | Description | | -------------------------- | ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | `input` | `string` | User prompt (required) | | `images` | `ImageInput[]` | Images to include with the input ([Vision & Audio](/docs/core/vision-and-audio)) | | `audio` | `AudioInput[]` | Audio files to transcribe and include | | `threadId` | `string` | Conversation to continue; a new one is created when omitted | | `userId` | `string` | The user the run acts for: owns new threads, scopes memory, reaches tools as `context.userId` | | `threadAccess` | `'owner' \| 'shared'` | `'owner'` (default): only the thread's creator may continue it ([Multi-User](/docs/advanced/multi-user)) | | `context` | `Record` | Additional context injected into the system prompt | | `stream` | `boolean` | Enable token streaming | | `signal` | `AbortSignal` | Cancel the run and linked provider/tool work | | `timeout` | `number` | Override the agent timeout (ms) | | `reasoning` | `ReasoningConfig` | Override the agent's `reasoning` for this run | | `parallelToolCalls` | `boolean` | Execute tool calls in parallel (default: sequential) | | `useMemory` | `boolean` | Enable/disable memory for this run (default: on when an adapter is configured) | | `loadHistory` | `boolean` | Load prior memory history for this run (default: true) | | `saveHistory` | `boolean` | Persist run messages to memory (default: true) | | `onApproval` | `(request) => decision \| 'pause'` | Decide calls of tools with `requiresApproval` ([Approvals](/docs/tools/approvals)) | | `onToken` | `(token: string) => void` | Streaming callback | | `onReasoning` | `(delta: string) => void` | Streamed reasoning summary (needs `reasoning.summary`) | | `onToolCall` | `(call: ToolCall) => void` | Called when the agent invokes a tool | | `onToolResult` | `(result: ToolResult) => void` | Called when a tool returns | | `onHandoff` | `(handoff: HandoffEvent) => void` | Called when an agent hands the conversation over ([Handoffs](/docs/core/agents#handoffs)) | | `onRunStart` | `({ runId, agentId, input, threadId }) => void` | Called when the run starts | | `onRunComplete` | `(result: RunResult) => void` | Called with the result | | `onRunError` | `(error: Error, runId: string) => void` | Called when the run fails | | `onMemoryError` | `(error: Error, operation: 'save' \| 'load') => void` | Called when loading or saving history fails (the run goes on) | | `onSpan` | `(span: Span) => void` | Observability callback for tracing | | `channelType`, `channelId` | `string` | Where the message came from; passed on to tools in their context | ### RunResult ```typescript interface RunResult { output: string; // final answer text structured?: unknown; // parsed JSON for json / json_schema agents runId: string; agentId: string; threadId: string; modelUsed?: string; // differs from agent.model under cost routing usage: { inputTokens: number; outputTokens: number; totalTokens: number; cost: number; // USD, from the model registry's prices duration: number; // ms reasoningTokens?: number; cachedInputTokens?: number; cacheWriteTokens?: number; }; reasoning?: string; // reasoning summary, when asked for prompt?: RunPrompt; // instruction version / A/B variant used handoffs?: HandoffEvent[]; finalAgent?: string; // the agent that answered after handoffs status?: 'completed' | 'paused'; pendingApprovals?: ToolApprovalRequest[]; // when paused checkpoint?: RunCheckpoint; // when paused, for cog.resume() toolCalls: ToolCall[]; messages: Message[]; trace: { traceId: string; spans: Span[] }; reflections?: Reflection[]; reflectionSummary?: ReflectionSummary; } ``` All fields are `readonly`. ## Pausing and Resuming A run that reaches a tool call with `requiresApproval` and no `onApproval` decision pauses: it returns with `status: 'paused'`, the waiting calls in `pendingApprovals` and a JSON `checkpoint`. Continue it later by thread id (the checkpoint is kept in `runCheckpoints`, by default in the memory adapter's threads or in process memory) or from the checkpoint itself: ```typescript const paused = await cog.run(agent, { input: 'Refund order 42', threadId, userId }); if (paused.status === 'paused') { const result = await cog.resume(agent, threadId, { userId, decisions: { [paused.pendingApprovals![0].toolCallId]: { approved: true } }, }); } ``` See [Approvals](/docs/tools/approvals) for decisions, stores and servers. ## Prompt Versions `cog.prompts` deploys new instructions for an agent without changing code, rolls them back, and runs A/B tests on live traffic; every run records its outcome against the version it used: ```typescript await cog.prompts.deploy(agent, 'You write release notes. Lead with what changed for the user.'); const result = await cog.run(agent, { input }); result.prompt; // { key: 'assistant', versionId: '...', version: 2 } ``` See [Prompt Versions](/docs/advanced/prompt-versions). ## Cost Estimation Estimate the cost before running an agent: ```typescript const estimate = await cog.estimateCost({ agent, input: 'Analyze this document and summarize key points', options: { assumeIterations: 3, assumeToolCalls: 5 }, }); console.log(`Expected cost: $${estimate.expectedCost.toFixed(4)}`); console.log(`Confidence: ${(estimate.confidence * 100).toFixed(0)}%`); ``` With `costRouting` enabled, `cog.getCostSummary()` returns what runs have spent and `cog.getCostRouter()` the router itself, both before the first run as well ([Cost Routing](/docs/advanced/cost-routing)). A run over budget throws a `CogitatorError` with code `BUDGET_EXCEEDED` (HTTP 429). ## Global Tool Registry Cogitator exposes a shared `tools` registry. Tools registered here are available to every agent run on this instance, next to the agent's own tools (an agent tool with the same name wins): ```typescript cog.tools.register(myGlobalTool); ``` ## Memory The configured memory adapter connects on the first run. To read threads before any agent has run — an endpoint that lists a conversation, an export job — ask for it: ```typescript const memory = await cog.getMemory(); // undefined when memory is not configured ``` `cog.memory` is the same adapter once connected, and `undefined` before. Assigning `cog.memory = adapter` uses an adapter you connected yourself, for example one the config cannot create. ## Other Methods | Method | Returns | | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `getGuardrails()` / `setConstitution(constitution)` | The [Constitutional AI](/docs/advanced/constitutional-ai) instance, or replace its rules; usable before the first run when `guardrails.model` or `llm.defaultModel` is set | | `getInsights(agentId)` / `getReflectionSummary(agentId)` | [Reflection](/docs/advanced/reflection) insights and summary | | `reflectionEngine` | The reflection engine, when reflection is enabled | ## Cleanup Always call `close()` when done to release connections: ```typescript await cog.close(); ``` This disconnects memory adapters, shuts down sandbox containers, and clears backend connections. --- # Agents URL: https://cogitator.app/docs/core/agents > Configure and run AI agents with tools, instructions, and model parameters. ## Overview An `Agent` is a configured LLM instance with a name, system prompt, tools, and model parameters. Agents are stateless blueprints -- the `Cogitator` runtime handles execution, memory, and lifecycle. ```typescript import { Agent } from '@cogitator-ai/core'; const agent = new Agent({ name: 'researcher', model: 'anthropic/claude-sonnet-5-5', instructions: 'You are a research assistant. Find accurate information and cite sources.', }); ``` ## AgentConfig ```typescript interface AgentConfig { id?: string; name: string; description?: string; model?: string; // default: llm.defaultModel of the Cogitator provider?: string; instructions: string; tools?: Tool[]; skills?: Skill[]; // bundles of tools + instructions, merged in temperature?: number; // default: 0.7 topP?: number; maxTokens?: number; stopSequences?: string[]; responseFormat?: ResponseFormat; reasoning?: ReasoningConfig; // effort and summaries for reasoning models handoffs?: Array; // agents it can hand the conversation over to maxIterations?: number; // default: 10 onIterationLimit?: 'answer' | 'stop'; // default: 'answer' timeout?: number; // default: limits.defaultTimeout of the Cogitator, else 120000 (2 min) } ``` ### Model String Format The `model` field uses a `provider/model-name` convention to route to the correct backend: ```typescript model: 'openai/gpt-6.1-sol'; model: 'anthropic/claude-sonnet-5-5'; model: 'ollama/llama3.3:latest'; model: 'google/gemini-3.8-flash'; ``` If you omit the provider prefix (or the prefix is not a known provider, backend or plugin), the whole string goes to the `defaultProvider` from your runtime config, Ollama when unset. If you omit `model` altogether, the agent runs on `llm.defaultModel`; a run fails with a `CONFIGURATION_ERROR` when neither is set. `cog.resolveModel(agent)` returns the model a run would use. ### Provider Override For OpenAI-compatible APIs (OpenRouter, local vLLM, etc.), use the `provider` field to explicitly route: ```typescript const agent = new Agent({ name: 'openrouter-agent', model: 'meta-llama/llama-3.3-70b-instruct', provider: 'openai', instructions: '...', }); ``` ## Agent with Tools ```typescript import { Agent, tool } from '@cogitator-ai/core'; import { z } from 'zod'; const searchWeb = tool({ name: 'search_web', description: 'Search the internet for current information', parameters: z.object({ query: z.string().describe('The search query'), limit: z.number().default(5), }), execute: async ({ query, limit }) => { const results = await searchAPI.search(query, limit); return results.map((r) => ({ title: r.title, url: r.url })); }, }); const researcher = new Agent({ name: 'researcher', model: 'openai/gpt-6.1-sol', instructions: 'You are a research assistant. Use tools to find accurate information.', tools: [searchWeb], maxIterations: 15, }); ``` The agent loops up to `maxIterations` times. Each iteration the LLM may call tools or produce a final response. The loop ends when the LLM responds without tool calls. An empty turn (no text and no tool calls) is not taken as the final response: the run asks the model again, up to twice. When tool calls use up `maxIterations` before the model answers, the run gets one more turn with `toolChoice: 'none'` and an instruction to answer from what it has, so it still ends with an answer and `structured` output can be parsed. Tools the model asks for on that turn are not run. `result.iterationLimitReached` is `true` either way, and `onIterationLimit: 'stop'` ends the run at the last tool turn instead, without an answer. `result.truncated` is `true` when the model's last answer stopped at the output token limit (`maxTokens`): the output may be cut off, or empty when a reasoning model spent the whole limit thinking. Every provider reports it, so it is the way to tell a cut-off answer from a short one. ## Agent with Structured Output Force the LLM to respond with a specific JSON shape using Zod schemas: ```typescript const analyzer = new Agent({ name: 'analyzer', model: 'openai/gpt-6.1-sol', instructions: 'Analyze the given text and extract structured information.', responseFormat: { type: 'json_schema', schema: z.object({ summary: z.string(), sentiment: z.enum(['positive', 'negative', 'neutral']), keyPoints: z.array(z.string()), }), }, }); ``` See [Structured Outputs](/docs/core/structured-outputs) for details on `json` and `json_schema` modes. ## Cloning Agents Create variations of an agent with `clone()`: ```typescript const baseAgent = new Agent({ name: 'coder', model: 'anthropic/claude-sonnet-5-5', instructions: 'You are a senior software engineer.', temperature: 0.3, }); const creativeAgent = baseAgent.clone({ name: 'creative-coder', temperature: 0.9, }); const fastAgent = baseAgent.clone({ model: 'anthropic/claude-haiku-4-5', }); ``` A clone gets a new `id` unless the overrides set one. ## Serialization Agents can be serialized to JSON for storage and restored later: ```typescript import { writeFile } from 'node:fs/promises'; const snapshot = agent.serialize(); await writeFile('agent.json', JSON.stringify(snapshot, null, 2)); ``` Restore with the tool implementations: ```typescript import { readFile } from 'node:fs/promises'; import { Agent } from '@cogitator-ai/core'; import type { AgentSnapshot } from '@cogitator-ai/types'; const saved = JSON.parse(await readFile('agent.json', 'utf-8')) as AgentSnapshot; const restored = Agent.deserialize(saved, { tools: [searchWeb, readUrl], }); ``` Tools are stored by name in the snapshot. During deserialization, each tool name is resolved from `toolRegistry` (any `{ get(name) }`, such as a `ToolRegistry`) when given, otherwise from the `tools` array; a missing tool throws `AgentDeserializationError`. `overrides` replaces any config field of the restored agent. A snapshot keeps the model, instructions, sampling parameters, `reasoning`, `maxIterations`, `onIterationLimit` and `timeout`, but not `handoffs` or `skills` (their tools are kept by name). A `json_schema` response format is saved by name only, so pass the Zod schema back as `overrides.responseFormat`. `Agent.validateSnapshot(value)` checks a parsed snapshot; it rejects snapshots without a `model`, so give agents you serialize an explicit model. ## Skills A skill bundles tools with the instructions for using them. `skills` on an agent adds each skill's tools (unless the agent already has a tool of that name) and appends its instructions to the agent's: ```typescript import { defineSkill } from '@cogitator-ai/core'; const weatherSkill = defineSkill({ name: 'weather', version: '1.0.0', description: 'Current weather and forecasts', tools: [getWeather], instructions: 'Use get_weather for any question about the weather.', env: ['WEATHER_API_KEY'], }); const assistant = new Agent({ name: 'assistant', model: 'openai/gpt-6.1-sol', instructions: 'You are a helpful assistant.', skills: [weatherSkill], }); ``` `validateSkill(skill)` reports missing fields, environment variables and dependencies; the CLI scaffolds and validates skill packages with [`cogitator skill`](/docs/cli#cogitator-skill). ## Agents as Tools Wrap an agent as a tool so other agents can delegate to it: ```typescript import { agentAsTool } from '@cogitator-ai/core'; const researchTool = agentAsTool(cogitator, researcher, { name: 'research', description: 'Delegate a research task to the research agent', }); const orchestrator = new Agent({ name: 'orchestrator', model: 'openai/gpt-6.1-sol', instructions: 'You coordinate tasks. Use the research tool for information gathering.', tools: [researchTool], }); ``` The tool takes a single `task` string and returns `{ output, success, error? }`; `includeUsage` and `includeToolCalls` add the inner run's usage and tool calls, and `timeout` limits it. The inner run inherits the caller's `userId` and abort signal. Calls of the inner agent that need approval are declined unless you pass `onApproval`, since nobody can answer them mid-delegation. See [Agent as Tool](/docs/tools/agent-as-tool). ## Reasoning Reasoning models think before they answer. `reasoning` sets how hard, in one vocabulary for every provider, and can ask for a readable summary of the thinking: ```typescript const analyst = new Agent({ name: 'analyst', model: 'anthropic/claude-opus-5-5', instructions: 'Answer questions about the quarterly numbers.', reasoning: { effort: 'high', summary: true }, }); const result = await cog.run(analyst, { input: 'Why did margins drop in Q3?', stream: true, onReasoning: (delta) => process.stdout.write(delta), // the summary, while it streams }); result.reasoning; // the whole summary result.usage.reasoningTokens; // thinking tokens, billed as output ``` `effort` goes from `none` to `max`; a run can override it with `cog.run(agent, { reasoning })`. How each provider applies it is in [LLM Backends](/docs/core/llm-backends#reasoning). ## Handoffs An agent can hand the conversation over to another one. `handoffs` lists the agents it may pass to; each becomes a `transfer_to_` tool described by the target's `description`. When the model calls it, the rest of the run goes on **as the target agent** — its instructions, tools, model and reasoning — with the whole conversation so far: ```typescript const billing = new Agent({ name: 'billing', description: 'Invoices, payments and refunds', model, instructions, tools: [lookupInvoice], }); const support = new Agent({ name: 'tech_support', description: 'Login problems and outages', model, instructions, tools: [serviceStatus], }); const triage = new Agent({ name: 'triage', model, instructions: 'Greet the customer and hand the conversation to the right specialist.', handoffs: [billing, support], // or { agent: billing, toolName: 'ask_billing', description: '...' } }); const result = await cog.run(triage, { input: 'How much do I owe on INV-204?', onHandoff: console.log, }); result.handoffs; // [{ from: 'triage', to: 'billing', reason: '...' }] result.finalAgent; // 'billing' — send the next message of this thread to it ``` Unlike [agents as tools](#agents-as-tools), where a sub-agent answers the caller and the caller keeps the conversation, a handoff gives the conversation away. Unlike [swarms](/docs/swarms), there is no coordinator: each agent decides when to pass on. A run that pauses for [approval](/docs/tools/approvals) after a handoff resumes in the agent it was handed to. ## Temperature Guidelines | Use Case | Temperature | Notes | | ------------------------- | ----------- | --------------------------- | | Code generation | 0.0 -- 0.2 | Deterministic, correct code | | Planning / classification | 0.2 -- 0.4 | Consistent but flexible | | General assistant | 0.5 -- 0.7 | Balanced (default: 0.7) | | Creative writing | 0.8 -- 1.2 | More varied output | | Brainstorming | 1.0 -- 1.5 | Maximum creativity | --- # Tools URL: https://cogitator.app/docs/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. ```typescript 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 ```typescript function tool(config: ToolConfig): Tool; ``` ### 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` | 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](/docs/tools/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 "" timed out after 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. ```typescript 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: ```typescript 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: `. ### 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)"`: ```typescript 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: ```typescript 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](/docs/tools/built-in) for parameters): ```typescript 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: ```typescript 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: ```typescript 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: ```typescript 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. ```typescript 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](/docs/deployment/sandbox#fallback). ## Tool Caching Wrap tools with `withCache` to avoid repeated expensive calls: ```typescript import { withCache } from '@cogitator-ai/core'; const cachedWeather = withCache(getWeather, { strategy: 'exact', ttl: '5m', maxSize: 1000, storage: 'memory', }); ``` See [Tool Caching](/docs/tools/tool-caching) for TTL strategies, semantic matching, and Redis-backed storage. --- # LLM Backends URL: https://cogitator.app/docs/core/llm-backends > Supported LLM providers, configuration, and provider-prefix routing. ## Overview Cogitator supports 11 LLM providers out of the box. Backends are created lazily -- a provider's client is only instantiated the first time an agent needs it. The provider is determined by the model string prefix. ```typescript const cog = new Cogitator({ llm: { defaultProvider: 'openai', providers: { openai: { apiKey: process.env.OPENAI_API_KEY! }, anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! }, ollama: { baseUrl: 'http://localhost:11434' }, }, }, }); ``` ## Provider Routing Model strings follow the `provider/model-name` format: ```typescript 'openai/gpt-6.1-sol'; // routes to OpenAI 'anthropic/claude-sonnet-5-5'; // routes to Anthropic 'ollama/llama3.3:latest'; // routes to Ollama 'google/gemini-3.8-flash'; // routes to Google ``` If the model string has no prefix, or its prefix is not a built-in provider, a backend in `llm.backends` or a registered plugin, the whole string goes to `defaultProvider` (or `ollama` if unset). That is how `meta-llama/llama-3.3-70b` reaches an OpenAI-compatible default provider unchanged. An agent's `provider` field skips the prefix check and sends the model string as it is. The `parseModel()` utility handles this splitting: ```typescript import { parseModel } from '@cogitator-ai/core'; parseModel('openai/gpt-6.1-sol'); // { provider: 'openai', model: 'gpt-6.1-sol' } parseModel('llama3.3:latest'); // { provider: null, model: 'llama3.3:latest' } ``` ## Supported Providers ### Ollama Local inference with Ollama. No API key required. ```typescript providers: { ollama: { baseUrl: 'http://localhost:11434', // default apiKey: process.env.OLLAMA_API_KEY, // optional: Ollama Cloud (https://ollama.com) or an authenticated server }, } ``` ```typescript model: 'ollama/llama3.3:latest'; model: 'ollama/codellama:34b'; model: 'ollama/mistral:7b-instruct'; ``` ### OpenAI ```typescript providers: { openai: { apiKey: process.env.OPENAI_API_KEY!, baseUrl: 'https://api.openai.com/v1', // optional, for proxies api: 'responses', // optional: 'responses' | 'chat-completions' }, } ``` ```typescript model: 'openai/gpt-6.1-sol'; // default when no model is given model: 'openai/gpt-6-luna'; model: 'openai/gpt-6-astra'; ``` The official OpenAI backend talks to the **Responses API**. GPT-6 models only support tool calling there, and GPT-5.x/GPT-6 reject non-default sampling parameters on Chat Completions. Requests are stateless (`store: false`); encrypted reasoning items are kept on `ToolCall.replay` and sent back with the call on the next turn, so reasoning models keep their chain of thought across tool calls (also after a memory round-trip). | Behaviour | Details | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Wire API selection | `responses` for the official endpoint (no `baseUrl`, or a `baseUrl` on `api.openai.com`); `chat-completions` when `baseUrl` points anywhere else. `api` forces either path | | Stop sequences | The Responses API has no stop parameter, so a request with `stop` sequences is sent through Chat Completions | | Sampling params | Reasoning models (o-series, GPT-5 and later) get no `temperature` / `top_p`; `gpt-4o` / `gpt-4.1` and `-chat` variants keep them | | `maxTokens` | Sent as `max_output_tokens` on the Responses API; on Chat Completions as `max_completion_tokens` for the official endpoint and `max_tokens` otherwise | | Structured output | `responseFormat` maps to `text.format` (`json_object` / `json_schema`); images are sent as `input_image` | | Usage | `usage.cachedInputTokens` and `usage.reasoningTokens` are reported when OpenAI returns them (both already included in `inputTokens` / `outputTokens`) | OpenAI-compatible providers (Azure, Mistral, Groq, Together, DeepSeek, vLLM, any custom `baseUrl`) stay on Chat Completions unchanged. `DEFAULT_OPENAI_MODEL` and `isOpenAIReasoningModel(model)` are exported from `@cogitator-ai/core`. ### Anthropic ```typescript providers: { anthropic: { apiKey: process.env.ANTHROPIC_API_KEY!, }, } ``` ```typescript model: 'anthropic/claude-sonnet-5-5'; // default when no model is given model: 'anthropic/claude-opus-5-5'; model: 'anthropic/claude-haiku-4-5'; ``` The backend adapts each request to what the target Claude model accepts (model ids from the Anthropic API, Vertex AI and Bedrock, including inference-profile prefixes and ARNs, are recognised): | Model family | Behaviour | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Claude 4.7+, 5.x, Fable | `temperature` / `top_p` are omitted (these models reject non-default sampling params), including the agent default `0.7` | | Claude 4.0 – 4.6 | At most one of `temperature` / `top_p` is sent; `temperature` wins when both are set | | Claude 4.5+, Fable | `json_schema` response format uses native structured outputs (`output_config`) instead of a forced tool | | Claude Opus 5.5, Sonnet 5.5, Fable | Forced tool choice (`'required'` or a named function) falls back to `auto` with a system-prompt instruction naming the tool, plus a one-time warning per model | The stop reasons `model_context_window_exceeded` and `pause_turn` are reported as `finishReason: 'length'`, and `refusal` as `'error'`. ### Google (Gemini) ```typescript providers: { google: { apiKey: process.env.GOOGLE_API_KEY!, }, } ``` ```typescript model: 'google/gemini-3.8-flash'; model: 'google/gemini-3.1-pro-preview'; model: 'google/gemini-3.5-flash-lite'; ``` Gemini has no `null` schema type, so `.nullable()` fields in tool parameters and response schemas are sent as `nullable: true`. Gemini attaches a thought signature to function calls; it is stored on `ToolCall.thoughtSignature` (also in memory) and sent back with the call on the next request, which Gemini 3 models require for multi-step tool use. Thought-summary parts are excluded from the response text. ### Azure OpenAI ```typescript providers: { azure: { endpoint: 'https://your-resource.openai.azure.com', apiKey: process.env.AZURE_OPENAI_API_KEY!, apiVersion: '2024-02-01', // optional deployment: 'gpt-6.1-sol', // optional default deployment }, } ``` ### AWS Bedrock ```typescript providers: { bedrock: { region: 'us-east-1', accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, }, } ``` Bedrock needs the optional `@aws-sdk/client-bedrock-runtime` package. Without explicit credentials it uses the AWS credential chain. ```typescript model: 'bedrock/global.anthropic.claude-sonnet-5-5'; model: 'bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0'; ``` Claude models on Bedrock follow the same sampling-parameter and forced-tool-choice rules as the Anthropic backend. `json_object` and `json_schema` response formats are supported. `json_object` is requested through a system-prompt instruction; `json_schema` is enforced with Converse `outputConfig.textFormat` on Claude 4.5 – 4.6 (the only Claude versions Bedrock serves structured outputs for) and otherwise sent as a system-prompt instruction that includes the schema. ### OpenAI-Compatible Providers Mistral, Groq, Together, DeepSeek, and vLLM use the OpenAI SDK under the hood with a provider-specific base URL. Hosted providers only need an API key; vLLM needs the base URL of your OpenAI-compatible server. ```typescript providers: { mistral: { apiKey: process.env.MISTRAL_API_KEY! }, groq: { apiKey: process.env.GROQ_API_KEY! }, together: { apiKey: process.env.TOGETHER_API_KEY! }, deepseek: { apiKey: process.env.DEEPSEEK_API_KEY! }, vllm: { baseUrl: 'http://localhost:8000/v1' }, } ``` ```typescript model: 'mistral/mistral-large-latest'; model: 'groq/llama-3.3-70b-versatile'; model: 'together/meta-llama/Llama-3.3-70B-Instruct-Turbo'; model: 'deepseek/deepseek-chat'; model: 'vllm/mistral-7b'; ``` ### Message Conversion All backends receive the same `Message[]`. Every `system` message (the agent instructions plus any later ones, such as reflection hints or context summaries) is combined into the provider's system prompt for Anthropic, Gemini and Bedrock, and consecutive tool results are sent together in a single turn, as those APIs expect for parallel tool calls. Images in tool results are placed where each API accepts them (see [Images in Tool Results](/docs/core/tools#images-in-tool-results)). ## Custom Backends ### Backend Instances A backend implements `LLMBackend` (`provider`, `chat()`, `chatStream()`), usually by extending `BaseLLMBackend`, or `OpenAICompatibleBackend` for servers that speak the OpenAI API. Its `provider` field is an `LLMBackendProvider`: a built-in provider name or any name of your own (usually the one it is registered under). Routing goes by the name in `llm.backends` or the plugin's provider, not by this field. Pass backends of your own to `llm.backends`. An agent whose model starts with the name, or whose `provider` is the name, runs on it; the rest of the model string is sent as the model name. A name of a built-in provider replaces it, for example with a `withDebug()` wrapper: ```typescript const cog = new Cogitator({ llm: { backends: { local: new MyCustomBackend({ url: 'http://gpu-box:8000' }), openai: withDebug(new OpenAIBackend({ apiKey: process.env.OPENAI_API_KEY! })), }, }, }); const agent = new Agent({ name: 'a', model: 'local/my-finetune', instructions: '...' }); ``` `cog.route('local/my-finetune')` returns the backend and model name a run would use. A run that names a provider nobody provides fails with `CONFIGURATION_ERROR`. `cog.knowsProvider('local')` tells whether a `local/...` prefix routes to that provider (a backend in `llm.backends`, a built-in provider or a registered plugin) or stays part of the model name on `llm.defaultProvider`. ### Plugin System Register custom LLM backends for providers not included by default. The runtime creates a plugin's backend the first time an agent uses its provider name, with the config under that name in `llm.plugins`: ```typescript import { defineBackend, registerLLMBackend } from '@cogitator-ai/core'; const myBackend = defineBackend<{ apiKey: string }>({ provider: 'my-provider', metadata: { name: 'My Provider', // version, description and author are optional version: '1.0.0', }, create: (config) => new MyCustomBackend(config), validateConfig: (config): config is { apiKey: string } => typeof config === 'object' && config !== null && 'apiKey' in config, }); registerLLMBackend(myBackend); const cog = new Cogitator({ llm: { plugins: { 'my-provider': { apiKey: '...' } } } }); const agent = new Agent({ name: 'a', model: 'my-provider/model-x', instructions: '...' }); ``` Registering a provider name twice throws; `unregisterLLMBackend(provider)` removes one. A failing `validateConfig` makes the first run on that provider fail with `Invalid configuration for provider '...'`. List registered plugins: ```typescript import { listLLMPlugins, hasLLMPlugin } from '@cogitator-ai/core'; console.log(listLLMPlugins()); console.log(hasLLMPlugin('my-provider')); ``` ## Debug Wrapper Wrap any backend with debug logging to inspect requests and responses: ```typescript import { withDebug } from '@cogitator-ai/core'; const debugBackend = withDebug(backend, { logRequest: true, logResponse: true, logStream: false, redactApiKeys: true, maxContentLength: 2000, logger: { log: (level, message, data) => console.log(`[LLM:${level}]`, message, data ?? ''), }, }); ``` Without a `logger` it prints timestamped entries to the console. Put the wrapped backend into `llm.backends` under the provider's name to debug every run on it. ## Reasoning `reasoning` on an agent (or a run, or a `ChatRequest`) controls reasoning models the same way on every provider: ```typescript reasoning: { effort: 'high', // none | minimal | low | medium | high | xhigh | max budgetTokens: 8000, // providers that take a thinking budget summary: true, // return a readable summary of the reasoning } ``` | Provider | What `reasoning` becomes | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Anthropic, Bedrock | Claude 4.6+ / 5.x / Fable: `thinking: { type: 'adaptive' }` and the closest `output_config.effort` the model accepts; `none` turns thinking off where the model allows it (`disabled`, or `between_tools` on Sonnet 5.5) and asks for the lowest effort where it does not (Opus 5.5, Fable). Claude 3.7 – 4.5: a thinking budget (`budgetTokens`, or derived from the effort) added on top of `maxTokens`. | | OpenAI (Responses) | `reasoning.effort` and, with `summary`, `reasoning.summary: 'auto'` — reasoning models only | | OpenAI-compatible | `reasoning_effort`; reasoning text comes from `reasoning_content` / `reasoning` (DeepSeek, vLLM, Groq, OpenRouter) | | Google | Gemini 3+: `thinkingConfig.thinkingLevel`; Gemini 2.5: `thinkingBudget` (Pro never below 128); `summary` → `includeThoughts` | | Ollama | `think`: a level for gpt-oss, on or off for other thinking models | The reasoning comes back as `ChatResponse.reasoning` and `delta.reasoning` while streaming; agent runs collect it into `RunResult.reasoning` and stream it to `onReasoning`. Reasoning tokens are reported in `usage.reasoningTokens` and counted in `outputTokens` (Gemini bills them as output too). Summaries are best-effort on some providers: Gemini omits the thought summary from a fair share of streamed responses even when it thought (`reasoningTokens` is still set), while non-streamed responses carry it. **Reasoning across tool calls.** Claude and OpenAI reasoning models keep thinking between tool calls only if their reasoning comes back with the next request. Backends keep it with the tool calls it preceded (`ToolCall.replay`) and send it back while the agent works through a tool loop. Claude's thinking blocks are tied to the conversation that produced them, so they are replayed only after the latest user message; if history was rewritten in between (context compression), the backend retries once without them. ## Prompt Caching Agent runs cache their prompt by default. Anthropic and Bedrock cache only what the request marks, so their backends mark the stable prefix (tools, system prompt, history); OpenAI and Gemini cache on their own. Every provider reports what it served from cache, and the run's cost prices those tokens at the model's cache price: ```typescript const result = await cog.run(agent, { input }); result.usage.cachedInputTokens; // read from the cache (about a tenth of the input price on Claude) result.usage.cacheWriteTokens; // written to it (Anthropic: 1.25× the input price) ``` In an agent loop every iteration resends the previous one's prompt, so after the first call most of the input is a cache read. `llm.promptCache: { ttl: '1h' }` keeps Anthropic's cache for an hour instead of five minutes; `llm.promptCache: false` turns marking off. A single `ChatRequest` takes the same setting as `cache`. ## Retries The runtime retries LLM calls that fail with a retryable error: rate limits (429), server errors (5xx), timeouts and dropped connections. Authentication, bad requests and content filters fail at once. Retries are on by default for every backend an agent runs on, including your own `llm.backends` and plugins: ```typescript const cog = new Cogitator({ llm: { defaultModel: 'anthropic/claude-sonnet-5-5', retry: { maxRetries: 3, // attempts after the first one (default 2) baseDelay: 1000, // first backoff, doubled on each retry (default 1000 ms) maxDelay: 30_000, // backoff cap (default 30000 ms) maxRetryAfter: 60_000, // longest Retry-After worth waiting for (default 60000 ms) onRetry: ({ provider, attempt, delay, error }) => console.warn(`${provider}: retry ${attempt} in ${delay}ms (${error.message})`), }, }, }); ``` * **Retry-After.** When the provider names a wait (`retry-after`, `retry-after-ms` or Gemini's `RetryInfo`), the runtime waits exactly that long. If the wait is longer than `maxRetryAfter`, the call fails at once instead of holding the run. * **Streaming.** A stream is retried only until its first chunk. After that the caller has already seen part of the answer, so the error is passed on. * **Cancellation.** The run's timeout and abort signal stop the waits between attempts too. * **One layer.** Backends created by the runtime make one attempt per call: the OpenAI, Anthropic, Azure and Bedrock SDKs have their own retries switched off, so attempts never multiply. `retry: false` turns retries off. To use a backend on its own with the same behaviour, wrap it with `withLLMRetry(backend, { maxRetries: 3 })`. ## Error Handling All backends throw typed `LLMError` instances with structured error codes: ```typescript import { LLMError, ErrorCode } from '@cogitator-ai/core'; try { await cog.run(agent, { input: 'Hello' }); } catch (e) { if (e instanceof LLMError) { if (e.code === ErrorCode.LLM_RATE_LIMITED) { // still rate limited after the runtime's retries; e.retryAfter is the provider's hint, if any } } } ``` Error codes include `LLM_UNAVAILABLE`, `LLM_RATE_LIMITED`, `LLM_TIMEOUT`, `LLM_INVALID_RESPONSE`, `LLM_CONTEXT_LENGTH_EXCEEDED` and `LLM_CONTENT_FILTERED`; `e.details` carries the provider, model, status code and request id. Rate limit, server and connection errors are marked as retryable, and `retryAfter` holds the wait the provider asked for, when it named one. --- # Model Registry URL: https://cogitator.app/docs/core/model-registry > Dynamic model registry with pricing, capabilities, and multi-provider support. ## Overview The `@cogitator-ai/models` package provides a dynamic model registry that tracks pricing, context windows, and capabilities for LLM models across all major providers. It fetches data from [LiteLLM](https://github.com/BerriAI/litellm) and includes built-in fallbacks. ```typescript import { initializeModels, getModel, getPrice, listModels } from '@cogitator-ai/models'; await initializeModels(); const model = getModel('gpt-6.1-sol'); console.log(model?.contextWindow); // 1050000 console.log(model?.capabilities?.supportsVision); // true const price = getPrice('claude-sonnet-5-5'); console.log(`$${price?.input}/M input, $${price?.output}/M output`); ``` Model ids are matched case-insensitively and `aliases` resolve too (`claude-sonnet-5.5` → `claude-sonnet-5-5`). Lookups return `null` for unknown models. ### Provider-qualified ids The same model is often sold by several providers at different prices, so the registry keeps one entry per provider. Each entry's `id` is the model's name at that provider, `provider` is the registry's provider id, and `catalogId` is the key LiteLLM lists it under: ```typescript await initializeModels(); getModel('openrouter/deepseek/deepseek-v4-pro'); // { id: 'deepseek/deepseek-v4-pro', provider: 'openrouter', // catalogId: 'openrouter/deepseek/deepseek-v4-pro', pricing: { input: 0.2088, output: 0.4176 }, ... } getModel('azure_ai/deepseek-v4-pro')?.provider; // 'azure', at Azure's price getModel('google/gemini-3.8-flash')?.id; // 'gemini-3.8-flash' (also found as gemini/gemini-3.8-flash) ``` A lookup resolves in this order: 1. A provider-qualified id finds that provider's entry. Both Cogitator model strings (`openai/gpt-6.1-sol`, `google/gemini-3.8-flash`, `openrouter/deepseek/deepseek-v4-pro`) and LiteLLM keys (`gemini/...`, `azure_ai/...`) work. 2. When that provider does not list the model, the prefix is dropped and the rest is looked up, so `openai/deepseek/deepseek-v4-pro` (an OpenAI-compatible server) is priced as `deepseek/deepseek-v4-pro`. 3. A bare name (`deepseek-v4-pro`) picks one listing by fixed rules, never by catalogue order: active models before deprecated ones, the vendor's own listing (a built-in model or an unprefixed LiteLLM key) before resellers, then the providers in `BUILTIN_PROVIDERS` order, then the shorter id, then alphabetically. Pass the full model string your agent runs on to get that provider's price. The built-in models follow the same rules before `initializeModels()` runs. ## Pricing and Cost Prices are USD per million tokens. `getPrice()` returns only `input` and `output`; `getPricing()` returns the full `ModelPricing`, including prompt-cache prices: ```typescript import { getPricing, calculateCost } from '@cogitator-ai/models'; getPricing('claude-sonnet-5-5'); // { input: 2, output: 10, inputCached: 0.2, inputCacheWrite: 2.5 } const cost = calculateCost('claude-sonnet-5-5', { inputTokens: 10_000, outputTokens: 1_000, cachedInputTokens: 8_000, // part of inputTokens read from the cache cacheWriteTokens: 0, // part of inputTokens written to the cache }); // 0.0156 (USD), or null when the model's price is unknown ``` Cache reads and writes are charged at `inputCached` / `inputCacheWrite` where the model has them, and at the input price otherwise. `RunResult.usage.cost` adds up the cost of each model call. When the provider reports what a call cost (OpenRouter does, in `usage.cost`), the runtime takes that price, which also reflects which upstream served the call. Other calls are priced with `calculateCost()` on the shared registry, using the full model string the agent runs on, so a model the registry does not know and whose provider reports nothing costs `0`. Call `initializeModels()` at startup to price more than the built-in models. ## ModelRegistry The `ModelRegistry` class manages model data with caching and auto-refresh. ```typescript import { ModelRegistry } from '@cogitator-ai/models'; const registry = new ModelRegistry({ cache: { ttl: 24 * 60 * 60 * 1000, storage: 'file', filePath: './cache/models.json', }, autoRefresh: true, refreshInterval: 24 * 60 * 60 * 1000, fallbackToBuiltin: true, }); await registry.initialize(); ``` ### Options | Option | Default | Description | | ------------------- | -------------------------------- | -------------------------------------------------------------------------------------------- | | `cache.ttl` | 24 hours | Cache time-to-live in milliseconds | | `cache.storage` | `'memory'` | `'memory'` or `'file'` | | `cache.filePath` | `~/.cogitator/models-cache.json` | File path for file-based cache (in memory where there is no file system, e.g. edge runtimes) | | `autoRefresh` | `false` | Automatically refresh data in background | | `refreshInterval` | 24 hours | Refresh interval in milliseconds | | `fallbackToBuiltin` | `true` | Use built-in models when fetch fails | ### Methods `initialize()` loads a fresh cache if there is one (and refreshes it in the background), otherwise fetches LiteLLM data; on failure it falls back to built-in models (or throws when `fallbackToBuiltin` is `false`). Lookups before `initialize()` load the built-in models. ```typescript registry.getModel('gpt-6.1-sol'); registry.getPrice('gpt-6.1-sol'); registry.getPricing('gpt-6.1-sol'); registry.listModels({ provider: 'openai', supportsTools: true }); registry.listProviders(); registry.getProvider('anthropic'); registry.getModelCount(); registry.isInitialized(); await registry.refresh(); registry.shutdown(); ``` ## Filtering Use `ModelFilter` to query models by provider, capabilities, and cost: ```typescript import { listModels } from '@cogitator-ai/models'; const visionModels = listModels({ supportsVision: true, excludeDeprecated: true }); const cheapToolModels = listModels({ supportsTools: true, maxPricePerMillion: 1.0, }); const largeContext = listModels({ minContextWindow: 200_000, provider: 'anthropic', }); ``` ### Filter Options | Filter | Type | Description | | -------------------- | --------- | --------------------------------------- | | `provider` | `string` | Filter by provider ID | | `supportsTools` | `boolean` | Filter by tool/function calling support | | `supportsVision` | `boolean` | Filter by vision/image support | | `minContextWindow` | `number` | Minimum context window size | | `maxPricePerMillion` | `number` | Max average price per million tokens | | `excludeDeprecated` | `boolean` | Exclude deprecated models | ## Built-in Providers The registry includes provider entries (`BUILTIN_PROVIDERS`) for 16 providers: OpenAI (`openai`), Anthropic (`anthropic`), Google (`google`), Ollama (`ollama`), Azure OpenAI (`azure`), AWS Bedrock (`aws`), Mistral AI (`mistral`), Cohere (`cohere`), Groq (`groq`), Together AI (`together`), Fireworks AI (`fireworks`), DeepInfra (`deepinfra`), Perplexity (`perplexity`), Replicate (`replicate`), xAI (`xai`), OpenRouter (`openrouter`). Built-in model data covers OpenAI, Anthropic and Google; models of the other providers, and additional providers, come from LiteLLM data once it is fetched. `getProvider(id).models` lists the model ids loaded for a provider. ## Built-in Models Built-in fallbacks cover current model families for major providers, including OpenAI GPT-6 (`gpt-6-astra`, `gpt-6.1-sol`, `gpt-6-luna`) and GPT-5.6/5.5, Anthropic Claude Opus 5.5 / Sonnet 5.5 / Fable 5.1 / Haiku 4.5, and Google Gemini 3.8 Flash / Gemini 3.5 Flash-Lite / Gemini 3.1 Pro Preview. Retired or retiring models stay in the registry with `deprecated: true` so lookups and pricing keep working for existing configs — among them `o3`, `o3-mini`, `o3-pro`, `o4-mini`, `gpt-4.1-nano`, `gpt-5.4-nano`, `gpt-4`, `gpt-4-turbo`, `claude-sonnet-4`, `claude-opus-4`, `claude-opus-4-1`, the Claude 3.x models, and Gemini 2.5 / 2.0 / 1.5. Use `excludeDeprecated: true` (or filter on `model.deprecated`) to skip them. ```typescript import { BUILTIN_MODELS, OPENAI_MODELS, ANTHROPIC_MODELS, GOOGLE_MODELS, } from '@cogitator-ai/models'; const activeOpenAI = OPENAI_MODELS.filter((model) => !model.deprecated); const cheapToolModels = BUILTIN_MODELS.filter( (model) => model.capabilities?.supportsTools && (model.pricing.input + model.pricing.output) / 2 <= 5 ); ``` ## Caching Models data is cached to avoid repeated network requests: ```typescript // Memory cache (default) new ModelRegistry({ cache: { ttl: 3600000, storage: 'memory' } }); // File cache (persists across restarts) new ModelRegistry({ cache: { ttl: 86400000, storage: 'file', filePath: './models-cache.json' }, }); ``` The cache supports stale-while-revalidate: if a refresh fails, the registry falls back to stale cached data before using built-in models. Fetched data is merged over the built-in entries, so built-in models keep their display names and aliases with fresh prices. `ModelCache` is exported for direct use (`get()`, `getStale()`, `set(models)`, `clear()`). ## LiteLLM Integration For advanced use cases, you can fetch and transform LiteLLM data directly: ```typescript import { fetchLiteLLMData, transformLiteLLMData } from '@cogitator-ai/models'; const rawData = await fetchLiteLLMData(); const models = transformLiteLLMData(rawData); ``` ## Zod Schemas All types have corresponding Zod schemas for runtime validation: ```typescript import { ModelInfoSchema, ModelPricingSchema, ModelCapabilitiesSchema, ProviderInfoSchema, } from '@cogitator-ai/models'; const result = ModelInfoSchema.safeParse(unknownData); if (result.success) { console.log(result.data.id); } ``` ## Global Functions Convenience functions use a shared default registry: ```typescript import { initializeModels, getModel, getPrice, listModels, getModelRegistry, shutdownModels, } from '@cogitator-ai/models'; await initializeModels(); getModel('gpt-6.1-sol'); getPrice('claude-sonnet-5-5'); listModels({ provider: 'google' }); shutdownModels(); ``` These functions auto-initialize with built-in data on first use, so `initializeModels()` is optional but recommended for fetching latest data. The shared registry (`getModelRegistry()`) caches to the file `~/.cogitator/models-cache.json` for 24 hours; `shutdownModels()` stops it and discards it, so the next call creates a new one. --- # Structured Outputs URL: https://cogitator.app/docs/core/structured-outputs > Get typed JSON responses from LLMs using Zod schemas and response format modes. ## Overview Structured outputs let you constrain the LLM to return data in a specific JSON shape. Cogitator supports three response format modes and handles the provider-specific differences for you. ```typescript import { Agent } from '@cogitator-ai/core'; import { z } from 'zod'; const agent = new Agent({ name: 'extractor', model: 'openai/gpt-6.1-sol', instructions: 'Extract structured data from the given text.', responseFormat: { type: 'json_schema', schema: z.object({ title: z.string(), topics: z.array(z.string()), sentiment: z.enum(['positive', 'negative', 'neutral']), }), }, }); ``` ## Response Format Modes ### `text` (default) The LLM responds with free-form text. This is the default when no `responseFormat` is set, and the same as `responseFormat: { type: 'text' }`. `result.structured` stays `undefined`. ### `json` The LLM is instructed to return valid JSON, but without a specific schema. Useful when you need flexible JSON output. ```typescript const agent = new Agent({ name: 'json-agent', model: 'openai/gpt-6.1-sol', instructions: 'Respond with a JSON object containing your analysis.', responseFormat: { type: 'json' }, }); const result = await cog.run(agent, { input: 'Analyze this sentence: "I love TypeScript"' }); const data = result.structured; // the parsed JSON, or undefined if the answer was not JSON ``` ### `json_schema` The LLM is constrained to a specific JSON structure defined by a Zod schema. This is the most powerful mode -- providers with native structured outputs enforce the schema, and every answer is validated against it (see [Reading the Result](#reading-the-result)). ```typescript const sentimentSchema = z.object({ text: z.string(), sentiment: z.enum(['positive', 'negative', 'neutral']), confidence: z.number(), keywords: z.array(z.string()), entities: z.array( z.object({ name: z.string(), type: z.enum(['person', 'organization', 'location', 'other']), }) ), }); const agent = new Agent({ name: 'sentiment-analyzer', model: 'openai/gpt-6.1-sol', instructions: 'Analyze the sentiment and extract entities from the input text.', responseFormat: { type: 'json_schema', schema: sentimentSchema, }, }); ``` ## How Providers Handle It Each LLM provider implements structured outputs differently. Cogitator abstracts these differences away, but understanding them helps pick the right provider. ### OpenAI OpenAI natively supports all three modes. The official OpenAI backend uses the Responses API, where `json_schema` is sent as `text.format` with strict schema enforcement: ```typescript // what Cogitator sends under the hood (Responses API) text: { format: { type: 'json_schema', name: 'response', schema: { /* your Zod schema converted to JSON Schema */ }, strict: true, // when the schema allows it, see below }, } ``` OpenAI-compatible providers (and requests with stop sequences) use Chat Completions, where the same schema goes into `response_format: { type: 'json_schema', json_schema: { ... } }`. When the agent also has tools, OpenAI-compatible servers other than OpenAI and Azure OpenAI (OpenRouter, DeepSeek, Groq, Together, Mistral, vLLM, a custom `OpenAIBackend` with a `baseUrl`) get the schema as an instruction in the system prompt instead of `response_format`. Many of them apply `response_format` from the first turn, so the model answers in JSON without calling its tools, or ignore the schema altogether, while the same models follow a schema in the prompt reliably. Requests without tools keep `response_format`. ### Anthropic * `json` mode: Appends "respond with valid JSON only" to the system prompt * `json_schema` mode on Claude 4.5+ and Fable: uses **native structured outputs** (`output_config.format`). The schema is adapted to the subset Claude accepts: every object gets `additionalProperties: false`, `oneOf` becomes `anyOf`, and unsupported constraints (numeric bounds, string lengths, `maxItems`, unknown formats) are moved into the description so the model still sees them * `json_schema` mode on older models: Cogitator uses a **tool trick** -- it creates a synthetic tool named `__json_response` with your schema as its parameters and forces the LLM to call it. The output is extracted from the tool call arguments. When the agent has tools of its own, forcing that call would keep it from using them, so the schema goes into the system prompt instead Either way you get the same structured response regardless of provider. ### AWS Bedrock * `json` mode: Appends "respond with valid JSON only" to the system prompt * `json_schema` mode: enforced with Converse `outputConfig.textFormat` on Claude 4.5 – 4.6; other models get a system-prompt instruction containing the schema ### Google (Gemini) Google supports structured outputs via `responseMimeType` and `responseSchema`: * `json` mode: Sets `responseMimeType: 'application/json'` * `json_schema` mode: Sets both `responseMimeType` and `responseSchema` (keywords Gemini does not accept are dropped). Gemini has no `null` type, so nullable fields (`.nullable()` in Zod, a `null` in `type`, `anyOf` or `oneOf`) are sent as `nullable: true`, in tool parameter schemas too * Gemini 1.x and 2.x refuse a JSON mime type together with function calling, so for an agent with tools the schema goes into the system instruction instead. Gemini 3 takes both in one request ### Ollama Ollama supports structured outputs via the `format` field: * `json` mode: Sets `format: 'json'` * `json_schema` mode: Passes the full JSON Schema as the `format` value ### Azure OpenAI Azure uses the same format as OpenAI since it's backed by the same API. ## Defining Schemas with Zod Use the full power of Zod to define your output shapes: ```typescript const analysisSchema = z.object({ summary: z.string().describe('A brief summary of the content'), categories: z .array(z.enum(['tech', 'business', 'science', 'politics', 'entertainment'])) .describe('Relevant categories'), metrics: z.object({ readability: z.number().min(0).max(100), complexity: z.enum(['simple', 'moderate', 'complex']), wordCount: z.number().int().positive(), }), references: z .array( z.object({ title: z.string(), url: z.string().url().optional(), relevance: z.number().min(0).max(1), }) ) .optional(), }); ``` Field descriptions (`.describe()`) are included in the JSON Schema sent to the LLM, which improves output quality. A `.describe()` on the root schema becomes the schema's description, and names the schema in a [serialized](/docs/core/agents#serialization) agent snapshot. ## Reading the Result `result.output` is always the raw text of the final answer. For `json` and `json_schema` agents, `result.structured` holds the parsed value: the JSON (a ` ```json ` fence around it is tolerated), run through your Zod schema in `json_schema` mode, so transforms and defaults apply. ```typescript const result = await cog.run(agent, { input: 'Analyze this article...' }); const analysis = result.structured as z.infer | undefined; ``` JSON wrapped in prose or a code fence is still read. When the final answer is not valid JSON or does not match the schema, the run asks the model once more, telling it what was wrong (for example `celsius: expected number, received string`); the rejected answer is not saved to the thread. Streamed runs skip that second attempt, since the client has already seen the first answer. A turn that ends with neither text nor tool calls (some models, Gemini in particular, occasionally stop empty after a tool result) is not treated as an answer: the run asks again, up to twice, and the empty turn never reaches the thread. This applies to every run, with or without a response format, and the retries count towards `maxIterations`. `structured` is `undefined` when even that answer does not fit, so check it before use: `result.structuredError` then says why, in the words the model was given (`the answer is not valid JSON`, `celsius: expected number, received string`). Agents with tools can call them as usual: the format applies to the final answer. Strict mode is requested only when the schema allows it: OpenAI rejects strict schemas with optional fields, so a schema with `.optional()` properties is sent with `strict: false`. ## Common Patterns ### Classification ```typescript const classifier = new Agent({ name: 'classifier', model: 'openai/gpt-6-luna', instructions: 'Classify the support ticket into a category and priority.', temperature: 0, responseFormat: { type: 'json_schema', schema: z.object({ category: z.enum(['billing', 'technical', 'account', 'feature_request', 'other']), priority: z.enum(['low', 'medium', 'high', 'critical']), reasoning: z.string(), }), }, }); ``` ### Data Extraction ```typescript const extractor = new Agent({ name: 'extractor', model: 'anthropic/claude-sonnet-5-5', instructions: 'Extract all contact information from the given text.', responseFormat: { type: 'json_schema', schema: z.object({ contacts: z.array( z.object({ name: z.string(), email: z.string().email().optional(), phone: z.string().optional(), company: z.string().optional(), }) ), }), }, }); ``` --- # Vision & Audio URL: https://cogitator.app/docs/core/vision-and-audio > Multi-modal capabilities including image analysis, image generation, audio transcription, and text-to-speech. ## Overview Cogitator provides four multi-modal tool factories for working with images and audio. Each factory returns a standard tool that can be given to any agent. ```typescript import { createAnalyzeImageTool, createGenerateImageTool, createTranscribeAudioTool, createGenerateSpeechTool, } from '@cogitator-ai/core'; ``` ## Image Analysis `createAnalyzeImageTool` creates a tool that sends images to a vision-capable model for analysis. It supports both URL and base64 image inputs. ```typescript import { Cogitator, Agent, createAnalyzeImageTool, OpenAIBackend } from '@cogitator-ai/core'; const openai = new OpenAIBackend({ apiKey: process.env.OPENAI_API_KEY! }); const analyzeImage = createAnalyzeImageTool({ llm: openai, defaultModel: 'gpt-6.1-sol', }); const agent = new Agent({ name: 'vision-assistant', model: 'openai/gpt-6.1-sol', instructions: 'You can analyze images. Use the analyzeImage tool when given an image URL.', tools: [analyzeImage], }); ``` ### Parameters | Parameter | Type | Description | | --------- | ------------------------------ | ----------------------------------------------- | | `image` | `string \| { data, mimeType }` | Image URL, or base64 data with its MIME type | | `prompt` | `string` | Question or instruction about the image | | `detail` | `'auto' \| 'low' \| 'high'` | Analysis detail level (`high` uses more tokens) | | `model` | `string` | Override the vision model | ### Inline Images via RunOptions You can also pass images directly through `RunOptions` without needing the analyze tool: ```typescript const result = await cog.run(agent, { input: 'What do you see in this image?', images: [ 'https://example.com/photo.jpg', { data: base64EncodedImage, mimeType: 'image/png', }, ], }); ``` The `images` array accepts URLs as strings or objects with `data` (base64) and `mimeType` fields. Supported MIME types: `image/jpeg`, `image/png`, `image/gif`, `image/webp`. ## Image Generation `createGenerateImageTool` creates images with OpenAI image models. The default model is `gpt-image-2.5-flare`. ```typescript const generateImage = createGenerateImageTool({ apiKey: process.env.OPENAI_API_KEY!, }); const agent = new Agent({ name: 'artist', model: 'openai/gpt-6.1-sol', instructions: 'You create images based on user descriptions. Use the generateImage tool.', tools: [generateImage], }); ``` ### Configuration | Option | Type | Default | Description | | --------- | -------- | --------------------------- | ---------------------------------------------------------------- | | `apiKey` | `string` | `OPENAI_API_KEY` | OpenAI API key | | `baseUrl` | `string` | `https://api.openai.com/v1` | API base URL (for OpenAI-compatible image endpoints) | | `model` | `string` | `gpt-image-2.5-flare` | Image model; `dall-e-*` ids keep the legacy DALL-E request shape | Set `model` to a `dall-e-*` id only for OpenAI-compatible servers that still serve DALL-E. In that mode the tool sends `size` (`auto` becomes `1024x1024`), `quality` (`hd` or `standard`), `style` and `response_format: 'url'`, and the result carries a hosted `url` instead of base64 data. ### Parameters | Parameter | Type | Description | | -------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `prompt` | `string` | Detailed description of the image to generate | | `size` | `'auto' \| '1024x1024' \| '1536x1024' \| '1024x1536' \| '1792x1024' \| '1024x1792'` | Image dimensions (default: `auto`, the model chooses) | | `quality` | `'auto' \| 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max' \| 'standard' \| 'hd'` | Rendering quality (default: `auto`); legacy `standard` maps to `medium`, `hd` to `high` | | `style` | `'vivid' \| 'natural'` | Legacy DALL-E style; ignored by `gpt-image-*` models (describe the style in the prompt) | | `outputFormat` | `'png' \| 'jpeg' \| 'webp'` | Image file format (default: `png`) | | `background` | `'auto' \| 'transparent' \| 'opaque'` | Background handling; `transparent` requires `png` or `webp` | ### Result ```typescript { imageBase64: 'iVBORw0KGgo...', // base64-encoded image bytes (gpt-image-* models) mimeType: 'image/png', model: 'gpt-image-2.5-flare', size: 'auto', quality: 'auto', } ``` `url` is optional and present only when a legacy `dall-e-*` compatible endpoint returns a hosted image; `style` is included only in that mode. `revisedPrompt` is set when the API returns one. ## Audio Transcription `createTranscribeAudioTool` transcribes audio to text using OpenAI transcription models (`gpt-transcribe` by default). ```typescript const transcribe = createTranscribeAudioTool({ apiKey: process.env.OPENAI_API_KEY!, defaultModel: 'gpt-transcribe', defaultLanguage: 'en', }); const agent = new Agent({ name: 'transcriber', model: 'openai/gpt-6.1-sol', instructions: 'Transcribe audio files. Use the transcribeAudio tool.', tools: [transcribe], }); ``` ### Parameters | Parameter | Type | Description | | ------------ | ------------------------------------------------------------------------------------ | ----------------------------------------------------- | | `audio` | `string \| { data, format }` | URL or base64-encoded audio data | | `language` | `string` | ISO-639-1 code (`'en'`, `'es'`, `'fr'`, `'ja'`, etc.) | | `model` | `'gpt-transcribe' \| 'whisper-1' \| 'gpt-4o-transcribe' \| 'gpt-4o-mini-transcribe'` | Transcription model | | `timestamps` | `boolean` | Include word-level timestamps (see below) | Supported audio formats: `mp3`, `mp4`, `mpeg`, `mpga`, `m4a`, `wav`, `webm`, `ogg`, `flac` (up to 25MB). The default model is `gpt-transcribe`. Only `whisper-1` returns word timestamps, so when `timestamps: true` is requested without an explicit `model` (and no `defaultModel` is configured), the tool uses `whisper-1`. `words` and `duration` are returned only for `whisper-1` requests with timestamps. ### Result ```typescript { text: 'The transcribed content of the audio file...', language: 'en', duration: 42.5, words: [ { word: 'The', start: 0.0, end: 0.1 }, { word: 'transcribed', start: 0.1, end: 0.5 }, // ... ], } ``` ### Inline Audio via RunOptions Audio files can be passed directly to the run. Each file is transcribed with OpenAI `gpt-transcribe` before the agent sees the input, and the transcripts are prepended to `input` as `[Audio transcription]: ...` (or `[Audio N transcription]: ...` for several files). Transcription uses `llm.providers.openai.apiKey`, falling back to `OPENAI_API_KEY`; the run fails with a clear error when neither is set. Prompt-injection detection and input guardrails run on the combined text. ```typescript const result = await cog.run(agent, { input: 'Summarize what was said in this recording.', audio: ['https://example.com/meeting.mp3', { data: base64Audio, format: 'wav' }], }); ``` ## Text-to-Speech `createGenerateSpeechTool` converts text to natural-sounding speech using OpenAI TTS. The default model is `gpt-4o-mini-tts`; `tts-1` and `tts-1-hd` are legacy models. ```typescript const speak = createGenerateSpeechTool({ apiKey: process.env.OPENAI_API_KEY!, defaultModel: 'gpt-4o-mini-tts', defaultVoice: 'nova', defaultFormat: 'mp3', }); const agent = new Agent({ name: 'narrator', model: 'openai/gpt-6.1-sol', instructions: 'Read text aloud using the generateSpeech tool.', tools: [speak], }); ``` ### Parameters | Parameter | Type | Description | | --------- | ------------------------------------------------------ | ------------------------------------------ | | `text` | `string` | Text to convert (max 4096 characters) | | `voice` | `string` | Voice selection (see below) | | `model` | `'tts-1' \| 'tts-1-hd' \| 'gpt-4o-mini-tts'` | TTS model | | `speed` | `number` | Playback speed, 0.25 to 4.0 (default: 1.0) | | `format` | `'mp3' \| 'opus' \| 'aac' \| 'flac' \| 'wav' \| 'pcm'` | Output format | ### Available Voices `alloy`, `ash`, `ballad`, `coral`, `echo`, `fable`, `nova`, `onyx`, `sage`, `shimmer`, `verse`, `marin`, `cedar` ### Result ```typescript { audioBase64: 'AAAA...', // base64-encoded audio format: 'mp3', voice: 'nova', model: 'gpt-4o-mini-tts', textLength: 256, } ``` ## Utilities Cogitator exports helpers for working with image and audio data: ```typescript import { fetchImageAsBase64, fetchAudioAsBuffer, audioInputToBuffer, isValidAudioFormat, getAudioMimeType, } from '@cogitator-ai/core'; const image = await fetchImageAsBase64('https://example.com/photo.jpg'); // { data: 'base64...', mediaType: 'image/jpeg' } — up to 20 MB, 30 s timeout const audio = await fetchAudioAsBuffer('https://example.com/audio.mp3'); // { buffer: Buffer, format: 'mp3', filename: 'audio.mp3' } — 60 s timeout const input = await audioInputToBuffer({ data: base64Audio, format: 'wav' }); // URL or base64 AudioInput isValidAudioFormat('ogg'); // true getAudioMimeType('mp3'); // 'audio/mpeg' ``` Both fetchers take `{ timeout, signal }` as a second argument. --- # Built-in Tools URL: https://cogitator.app/docs/tools/built-in > Reference for the 26 built-in tools that ship with @cogitator-ai/core, the environment variables they read, and the tool factories next to them. ## Overview Cogitator ships with **26 built-in tools** covering math, data processing, the file system, network, databases, email and GitHub. Every tool has a Zod schema (the runtime validates the model's arguments against it) and returns structured results; most failures come back as `{ error }` data the model can read instead of a thrown exception. ```typescript import { Agent, builtinTools, calculator, fileRead, httpRequest } from '@cogitator-ai/core'; const assistant = new Agent({ name: 'assistant', model: 'openai/gpt-6.1-sol', instructions: 'You are a helpful assistant.', tools: [calculator, fileRead, httpRequest], }); const everything = new Agent({ name: 'power-user', model: 'openai/gpt-6.1-sol', instructions: 'You are a helpful assistant.', tools: builtinTools, }); ``` `builtinTools` is a `Tool[]`, so pass it as `tools` directly or spread it next to your own tools. Prefer picking the tools an agent needs: `builtinTools` includes `exec`, `file_delete` and `send_email`. | Export | Tool name | Export | Tool name | | --------------- | ---------------- | -------------- | --------------- | | `calculator` | `calculator` | `fileRead` | `file_read` | | `datetime` | `datetime` | `fileWrite` | `file_write` | | `uuid` | `uuid` | `fileList` | `file_list` | | `randomNumber` | `random_number` | `fileExists` | `file_exists` | | `randomString` | `random_string` | `fileDelete` | `file_delete` | | `hash` | `hash` | `httpRequest` | `http_request` | | `base64Encode` | `base64_encode` | `exec` | `exec` | | `base64Decode` | `base64_decode` | `webSearch` | `web_search` | | `sleep` | `sleep` | `webScrape` | `web_scrape` | | `jsonParse` | `json_parse` | `sqlQuery` | `sql_query` | | `jsonStringify` | `json_stringify` | `vectorSearch` | `vector_search` | | `regexMatch` | `regex_match` | `sendEmail` | `send_email` | | `regexReplace` | `regex_replace` | `githubApi` | `github_api` | ### Environment Variables | Variable | Read by | | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | | `TAVILY_API_KEY`, `BRAVE_API_KEY`, `SERPER_API_KEY` | `web_search` (first one set wins when `provider` is omitted) | | `DATABASE_URL` | `sql_query`, `vector_search` when no `connectionString` is passed | | `OPENAI_API_KEY`, `OLLAMA_BASE_URL` / `OLLAMA_URL` / `OLLAMA_HOST`, `GOOGLE_API_KEY` | `vector_search` embeddings (auto-detected in that order) | | `RESEND_API_KEY`, `RESEND_FROM_EMAIL` | `send_email` via Resend | | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM` | `send_email` via SMTP | | `GITHUB_TOKEN` | `github_api` | Variables are read on each call. On runtimes without `process.env` (Cloudflare Workers without `nodejs_compat`, Deno without `--allow-env`) they count as unset. ### Optional Packages | Package | Needed by | | ---------------- | ------------------------------------------ | | `pg` | `sql_query` on PostgreSQL, `vector_search` | | `better-sqlite3` | `sql_query` on SQLite | | `nodemailer` | `send_email` via SMTP | They are optional peer dependencies of `@cogitator-ai/core`; a tool that needs a missing one returns an error telling you what to install. *** ## Math & Utilities ### `calculator` Evaluates mathematical expressions without `eval`. Supports `+`, `-`, `*`, `/`, `^` (power), parentheses, and functions: `sqrt`, `sin`, `cos`, `tan`, `log`, `abs`, `round`, `floor`, `ceil`. Constants: `pi`, `e`. | Parameter | Type | Description | | ------------ | -------- | ----------------------------------- | | `expression` | `string` | Math expression, e.g. `"2 + 3 * 4"` | ### `uuid` Generates UUID v4 identifiers with `crypto.randomUUID`. | Parameter | Type | Description | | --------- | -------- | ------------------------------------------------ | | `count` | `number` | Number of UUIDs to generate (1 - 100, default 1) | ### `random_number` Generates a random number within a range. | Parameter | Type | Description | | --------- | --------- | ------------------------------- | | `min` | `number` | Minimum value (default: 0) | | `max` | `number` | Maximum value (default: 1) | | `integer` | `boolean` | Return integer (default: false) | ### `random_string` Generates a cryptographically secure random string using `crypto.randomBytes`. | Parameter | Type | Description | | --------- | -------- | ----------------------------------------------------------------------- | | `length` | `number` | String length (1 - 1000) | | `charset` | `enum` | `alphanumeric` \| `alpha` \| `numeric` \| `hex` (default: alphanumeric) | ### `hash` Computes cryptographic hashes. | Parameter | Type | Description | | ----------- | -------- | --------------------------------------------------------- | | `data` | `string` | String to hash | | `algorithm` | `enum` | `md5` \| `sha1` \| `sha256` \| `sha512` (default: sha256) | | `encoding` | `enum` | `hex` \| `base64` (default: hex) | ### `sleep` Pauses execution for a specified duration. Maximum 60 seconds. | Parameter | Type | Description | | --------- | -------- | --------------------------------- | | `ms` | `number` | Milliseconds to sleep (0 - 60000) | ### `datetime` Returns the current date and time in various formats and timezones. | Parameter | Type | Description | | ---------- | -------- | ---------------------------------------------------------------- | | `timezone` | `string` | IANA timezone, e.g. `"America/New_York"` (default: system zone) | | `format` | `enum` | `iso` \| `unix` \| `readable` \| `date` \| `time` (default: iso) | *** ## Data Processing ### `json_parse` Parses a JSON string into an object. Returns `{ result, valid: true }` on success, `{ error, valid: false }` on failure. | Parameter | Type | Description | | --------- | -------- | -------------------- | | `json` | `string` | JSON string to parse | ### `json_stringify` Converts a value to a JSON string with optional pretty-printing. | Parameter | Type | Description | | --------- | --------- | ------------------------------------ | | `data` | `unknown` | Data to serialize | | `pretty` | `boolean` | Enable indentation (default: false) | | `indent` | `number` | Spaces for indent (0 - 8, default 2) | ### `base64_encode` / `base64_decode` Encode strings to Base64 or decode Base64 back to plain text. Both support the URL-safe variant. | Parameter | Type | Description | | --------- | --------- | ---------------------------------------- | | `data` | `string` | Input string | | `urlSafe` | `boolean` | URL-safe Base64 variant (default: false) | ### `regex_match` Finds matches of a regular expression in text, returning match strings, indices, and named groups. | Parameter | Type | Description | | --------- | -------- | ---------------------------- | | `text` | `string` | Text to search | | `pattern` | `string` | Regular expression pattern | | `flags` | `string` | Regex flags (default: `"g"`) | ### `regex_replace` Replaces regex matches in text. Supports `$1`, `$2` capture group references. | Parameter | Type | Description | | ------------- | -------- | ---------------------------- | | `text` | `string` | Input text | | `pattern` | `string` | Regex pattern | | `replacement` | `string` | Replacement string | | `flags` | `string` | Regex flags (default: `"g"`) | Both regex tools reject texts over 1,000,000 characters and stop matching after 5 seconds. They run on the host event loop; for untrusted patterns prefer the WASM [`regex`](/docs/tools/wasm-tools) tool, which runs in a worker that the timeout terminates. *** ## File System `file_write` and `file_delete` declare `sideEffects: ['filesystem']`. None of the file tools restricts paths — they act with the permissions of your process, so give them only to agents you trust or put the agent behind [approvals](/docs/tools/approvals). ### `file_read` Reads file contents. Use `base64` encoding for binary files. | Parameter | Type | Description | | ---------- | -------- | ------------------------------------ | | `path` | `string` | Path to the file | | `encoding` | `enum` | `utf-8` \| `base64` (default: utf-8) | ### `file_write` Writes content to a file. Creates parent directories by default. | Parameter | Type | Description | | ------------ | --------- | ------------------------------------ | | `path` | `string` | File path | | `content` | `string` | Content to write | | `encoding` | `enum` | `utf-8` \| `base64` (default: utf-8) | | `createDirs` | `boolean` | Create parent dirs (default: true) | ### `file_list` Lists directory contents with file names, paths, types, and sizes. | Parameter | Type | Description | | --------------- | --------- | -------------------------------------------- | | `path` | `string` | Directory path | | `recursive` | `boolean` | Recurse into subdirectories (default: false) | | `includeHidden` | `boolean` | Include dotfiles (default: false) | ### `file_exists` Checks if a file or directory exists and returns its type and size. | Parameter | Type | Description | | --------- | -------- | ------------- | | `path` | `string` | Path to check | ### `file_delete` Deletes a file or directory. Use `recursive: true` for non-empty directories. | Parameter | Type | Description | | ----------- | --------- | -------------------------------------------- | | `path` | `string` | Path to delete | | `recursive` | `boolean` | Delete contents recursively (default: false) | *** ## Network & Web ### `http_request` Makes HTTP requests with custom headers, body, and timeout. Response bodies longer than 100,000 characters are truncated (`truncated: true`). | Parameter | Type | Description | | --------- | ------------------------ | ------------------------------------------------------------------------------------- | | `url` | `string` | Request URL | | `method` | `enum` | `GET` \| `POST` \| `PUT` \| `PATCH` \| `DELETE` \| `HEAD` \| `OPTIONS` (default: GET) | | `headers` | `Record` | Request headers | | `body` | `string` | Request body (for POST/PUT/PATCH) | | `timeout` | `number` | Timeout in ms (1000 - 60000, default: 30000) | ### `web_search` Searches the web via Tavily, Brave Search, or Serper. Without `provider` it uses the first of `TAVILY_API_KEY`, `BRAVE_API_KEY`, `SERPER_API_KEY` that is set. | Parameter | Type | Description | | ------------------- | ------------------------------- | ------------------------------------------------------------------------------- | | `query` | `string` | Search query | | `provider` | `enum` | `tavily` \| `brave` \| `serper` | | `maxResults` | `number` | Max results (1 - 20, default: 5) | | `page` | `number` | Page of results (1 - 10, Brave and Serper only) | | `topic` | `enum` | `general` \| `news` (default: general) | | `recency` | `enum` | `day` \| `week` \| `month` \| `year`: only results from that period | | `dateRange` | `{ from: string, to?: string }` | Only results published within these days (`YYYY-MM-DD`, `to` defaults to today) | | `includeDomains` | `string[]` | Search only these domains, e.g. `["reuters.com"]` | | `excludeDomains` | `string[]` | Leave out results from these domains | | `country` | `string` | Favour results from a country, ISO 3166-1 alpha-2 (`us`, `de`) | | `language` | `string` | Favour results in a language, ISO 639-1 (`en`, `pt-br`) | | `searchDepth` | `enum` | `ultra-fast` \| `fast` \| `basic` \| `advanced` (Tavily only) | | `includeAnswer` | `boolean` | Include AI summary (Tavily only) | | `includeRawContent` | `boolean` | Full page content as markdown in each result (Tavily only) | The filters work on every provider, each through its own parameters: Tavily's `topic`, `time_range`, `start_date`, `include_domains`, `country` and `language`, the news endpoints, `freshness`, `country` and `search_lang` of Brave, and the news endpoint, `tbs`, `gl` and `hl` of Serper. Domain filters become `site:` operators for Brave and Serper. A filter a provider cannot apply comes back as an `error` instead of being dropped: Tavily has no result pages, and it favours a country only in general searches. Results carry `publishedAt` (ISO 8601) when the provider reports a date, Serper's relative dates ("3 hours ago") resolved at the time of the search, `source` for Serper news, and `content` with `includeRawContent`. `createWebSearchTool({ provider, apiKeys })` builds the same tool with a default provider and keys passed in code instead of the environment: ```typescript import { createWebSearchTool } from '@cogitator-ai/core'; const search = createWebSearchTool({ provider: 'tavily', apiKeys: { tavily: config.tavilyKey } }); ``` ### `web_scrape` Fetches an HTML page and extracts its content as text, markdown or raw HTML, optionally limited to a CSS selector, with link and image collection. | Parameter | Type | Description | | --------------- | --------- | ---------------------------------------------------- | | `url` | `string` | URL to scrape | | `selector` | `string` | CSS selector to extract (e.g. `"article"`) | | `format` | `enum` | `text` \| `markdown` \| `html` (default: text) | | `maxLength` | `number` | Max content length (100 - 100000, default: 50000) | | `timeout` | `number` | Request timeout in ms (1000 - 60000, default: 30000) | | `includeLinks` | `boolean` | Extract links (default: false) | | `includeImages` | `boolean` | Extract image URLs (default: false) | Non-HTML responses return an error. *** ## Database & Search ### `sql_query` Executes SQL queries against PostgreSQL (`pg`) or SQLite (`better-sqlite3`). Supports parameterized queries and is read-only by default. | Parameter | Type | Description | | ------------------ | ----------- | ----------------------------------------------------------------- | | `query` | `string` | SQL query | | `database` | `enum` | `postgres` \| `sqlite` (auto-detected from the connection string) | | `connectionString` | `string` | Postgres URL or SQLite file path (defaults to `DATABASE_URL`) | | `params` | `unknown[]` | Parameterized query values | | `maxRows` | `number` | Max rows returned (1 - 1000, default: 100) | | `readOnly` | `boolean` | Only allow read queries (default: true) | In read-only mode the query must be a single `SELECT`/`WITH`/`SHOW`/`DESCRIBE`/`EXPLAIN` statement. On PostgreSQL it additionally runs inside a `BEGIN TRANSACTION READ ONLY` block that is always rolled back, so side-effecting functions (for example `nextval()`) are rejected by the database itself; SQLite databases are opened with `readonly: true`. With `readOnly: false`, statements that return no rows (`INSERT`, `UPDATE`, `DELETE`) return `rows: []` and the number of affected rows in `rowCount`. ### `vector_search` Performs semantic similarity search with pgvector: embeds the query via OpenAI, Ollama, or Google, then finds similar rows in PostgreSQL. | Parameter | Type | Description | | ------------------- | -------- | ------------------------------------------- | | `query` | `string` | Search query (converted to embedding) | | `collection` | `string` | Table name (default: `"documents"`) | | `topK` | `number` | Number of results (1 - 100, default: 5) | | `threshold` | `number` | Minimum similarity 0 - 1 (default: 0.7) | | `filter` | `Record` | Metadata filter as key-value pairs | | `embeddingProvider` | `enum` | `openai` \| `ollama` \| `google` | | `embeddingModel` | `string` | Embedding model override | | `connectionString` | `string` | PostgreSQL URL (defaults to `DATABASE_URL`) | The provider is auto-detected from `OPENAI_API_KEY`, then `OLLAMA_BASE_URL`/`OLLAMA_URL`/`OLLAMA_HOST` (the first one set is also the Ollama URL; default `http://localhost:11434`), then `GOOGLE_API_KEY`. Default models: `text-embedding-3-small` (OpenAI), `nomic-embed-text` (Ollama), `gemini-embedding-001` (Google). The embedding dimension must match your pgvector column. *** ## Communication & Dev ### `send_email` Sends email via the Resend API or SMTP (`nodemailer`). Declares `sideEffects: ['network', 'external']`. | Parameter | Type | Description | | ---------- | -------------------- | --------------------------------------------------- | | `to` | `string \| string[]` | Recipient(s) | | `subject` | `string` | Email subject | | `body` | `string` | Email body (plain text or HTML) | | `html` | `boolean` | Treat body as HTML (default: false) | | `from` | `string` | Sender (default: `RESEND_FROM_EMAIL` / `SMTP_FROM`) | | `replyTo` | `string` | Reply-to address | | `cc` | `string \| string[]` | CC recipients | | `bcc` | `string \| string[]` | BCC recipients | | `provider` | `enum` | `resend` \| `smtp` (auto-detected) | Auto-detection picks Resend when `RESEND_API_KEY` is set, otherwise SMTP when `SMTP_HOST` and `SMTP_USER` are set. Resend sends from `onboarding@resend.dev` when no sender is configured. SMTP needs `SMTP_PASS` too, uses port `SMTP_PORT` (default 587, TLS on 465) and falls back to `SMTP_USER` as sender. ### `github_api` GitHub REST API access with `GITHUB_TOKEN`. One tool, 12 actions: `get_repo`, `list_issues`, `get_issue`, `create_issue`, `update_issue`, `list_prs`, `get_pr`, `create_pr`, `get_file`, `list_commits`, `search_code`, `search_issues`. | Parameter | Type | Description | | --------------------- | ---------- | ------------------------------------------------ | | `action` | `enum` | One of the actions above | | `owner`, `repo` | `string` | Repository | | `number` | `number` | Issue or PR number (get/update) | | `title`, `body` | `string` | For new issues and PRs | | `state` | `enum` | `open` \| `closed` \| `all` (default: open) | | `labels`, `assignees` | `string[]` | For issues | | `path`, `ref` | `string` | File path and ref for `get_file` | | `base`, `head` | `string` | Branches for `create_pr` | | `query` | `string` | For the search actions | | `perPage`, `page` | `number` | Pagination (1 - 100, default 30; default page 1) | ```typescript import { Agent, githubApi } from '@cogitator-ai/core'; const agent = new Agent({ name: 'github-bot', model: 'openai/gpt-6.1-sol', tools: [githubApi], instructions: 'You help manage GitHub repositories.', }); ``` ### `exec` Executes shell commands and returns stdout, stderr and the exit code. Declares `sideEffects: ['process', 'filesystem', 'network']` and `requiresApproval: true`, so every call waits for a decision (see [Approvals](/docs/tools/approvals)). | Parameter | Type | Description | | --------- | -------- | -------------------------------------------- | | `command` | `string` | Shell command to execute | | `cwd` | `string` | Working directory | | `timeout` | `number` | Timeout in ms (100 - 300000, default: 30000) | | `env` | `Record` | Additional environment variables | `exec` declares a Docker sandbox (`cogitator/sandbox:base`, 256 MB, 100 PIDs, no network). Inside a `Cogitator` the command runs in that container through `@cogitator-ai/sandbox`. When the sandbox package is not installed or Docker is unreachable, the runtime logs a warning and runs the command **on the host**, unless `sandbox.allowNativeFallback: false` on the `Cogitator` makes the call fail. See [Sandbox](/docs/deployment/sandbox). *** ## Multimodal Tool Factories Image and audio tools are created with factories rather than shipped in `builtinTools`. See [Vision & Audio](/docs/core/vision-and-audio) for parameters and results. | Factory | Tool | Default model | | --------------------------- | ----------------- | ------------------------------------------------------------------------------------------ | | `createAnalyzeImageTool` | `analyzeImage` | `gpt-6.1-sol` on the `llm` backend you pass in | | `createGenerateImageTool` | `generateImage` | `gpt-image-2.5-flare`; returns base64 `imageBase64`, `size` / `quality` default to `auto` | | `createTranscribeAudioTool` | `transcribeAudio` | `gpt-transcribe`; `whisper-1` when word timestamps are requested without an explicit model | | `createGenerateSpeechTool` | `generateSpeech` | `gpt-4o-mini-tts` | ## Other Tool Factories These build tools around state you provide; they are not part of `builtinTools`. | Factory | Tools | Notes | | ------------------------------------------------------------------------ | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `createMemoryTools({ graphAdapter, agentId, coreFacts?, embeddingFn? })` | `remember`, `recall`, `forget` | Long-term memory in a knowledge graph — see [Smart Memory](/docs/channels/smart-memory) | | `createSchedulerTools({ store, defaultChannel?, defaultUserId? })` | `schedule_task`, `list_tasks`, `cancel_task` | Delayed, dated or cron tasks in a workflows `TimerStore` — see [Scheduler](/docs/channels/scheduler) | | `createCapabilitiesTool(doc)` | `lookup_capabilities` | Searches a capabilities document line by line | | `createDeviceTools()` | `clipboard_read`, `clipboard_write`, `system_info`, `screenshot` | Clipboard via `pbpaste`/`pbcopy` (macOS) or `xclip`; `screenshot` is macOS only | | `createSelfTools({ toolsDir, onToolsChanged? })` | `create_tool`, `test_tool`, `list_custom_tools`, `delete_tool` | Lets the agent write `.mjs` tools into `toolsDir`; `create_tool` requires approval | | `loadCustomTools(toolsDir)` | the tools in `toolsDir` | Loads the `.mjs` default exports `createSelfTools` wrote | Tools created with `createSelfTools` run with full Node.js access in your process. ## Public hosts only A model chooses the URLs `web_scrape` and `http_request` fetch, and the audio URL `transcribe_audio` reads, so a page it read or a prompt it was given can point it at `http://169.254.169.254/` (a cloud's credentials), `http://localhost:8080/admin` or a database on the internal network. These tools reach only the public internet: a URL on a loopback, private, link-local, carrier-grade NAT, multicast or documentation address, `localhost`, `*.local`, `*.internal` or a metadata host is refused, every redirect target is checked again, and a hostname is checked by the address it resolves to when the request connects, so a name that resolves elsewhere a moment later (DNS rebinding) is refused too. A refused URL comes back to the model as an `error` naming it. An agent meant to work on a trusted network opts in, or brings its own client: ```typescript import { createHttpRequestTool, createWebScrapeTool } from '@cogitator-ai/core'; const intranet = createWebScrapeTool({ allowPrivateNetwork: true }); const api = createHttpRequestTool({ fetch: proxiedFetch }); ``` A `fetch` of your own is trusted with every URL, so it should apply your network policy. `createTranscribeAudioTool` takes the same two options. The guard is yours to use too: `fetchPublic` is a `fetch` for public hosts, `createPublicFetch({ allowPrivateNetwork, maxRedirects, resolver, isBlocked })` builds one to your rules, `assertPublicUrl(url)` checks a URL up front, `assertPublicHost(url)` checks the addresses its host resolves to as well and `isPrivateAddress(ip)` classifies an address. They run on `node:http`, in Node and Bun alike, and a refusal throws a `PrivateNetworkError`. With `robots`, the tool checks a URL and the addresses its host resolves to before it reads the site's robots.txt. `RobotsPolicy` reads robots.txt with the global `fetch` unless given another: `new RobotsPolicy({ userAgent, fetch: fetchPublic })` keeps those reads on public hosts too. Requests carry `DEFAULT_USER_AGENT` and `Accept: */*` unless the caller sets its own headers. ## Polite crawling with robots.txt `RobotsPolicy` reads and caches a site's robots.txt and answers whether a URL may be fetched, by RFC 9309: the groups that name your product token (else `*`), longest match wins with `*` and `$` patterns, a 4xx robots.txt allows everything, a 5xx or a network failure allows nothing until the cache (one hour by default) expires. It implements `RobotsChecker`, which `WebLoader` in `@cogitator-ai/rag` and `BrowserSession` in `@cogitator-ai/browser` take as `robots`: ```typescript import { RobotsPolicy } from '@cogitator-ai/core'; const robots = new RobotsPolicy({ userAgent: 'NewsBot/1.0 (+https://example.com/bot)' }); await robots.allows('https://example.com/news/today'); // true or false ``` The token defaults to the User-Agent's first word (`NewsBot`); pass `token` to match another. `robotsRulesFor(text, token)` and `robotsAllowsPath(rules, path)` expose the parser for robots.txt files you already have. `createWebScrapeTool({ userAgent, robots })` builds a `web_scrape` tool with your User-Agent and a robots.txt check on every hop, redirects included (a disallowed page comes back as an `error`). The plain `webScrape` keeps the default User-Agent and checks nothing. --- # WASM Tools URL: https://cogitator.app/docs/tools/wasm-tools > Run tools in isolated WebAssembly sandboxes with Extism for security, speed, and portability. ## Overview The `@cogitator-ai/wasm-tools` package provides tools that execute inside WebAssembly sandboxes powered by [Extism](https://extism.org/). Compared to Docker-based sandboxing, WASM tools offer: * **100-500x faster cold start** than Docker containers * **\~20x lower memory footprint** per tool instance * **Memory-safe execution** in an isolated linear memory space * **No network or filesystem access** -- no hosts are allowed and no directories are pre-opened ```typescript import { Agent } from '@cogitator-ai/core'; import { createCalcTool, createHashTool, createCsvTool } from '@cogitator-ai/wasm-tools'; const agent = new Agent({ name: 'data-processor', model: 'openai/gpt-6.1-sol', instructions: 'You process and verify data.', tools: [createCalcTool(), createHashTool(), createCsvTool()], }); ``` *** ## Pre-built WASM Tools The package ships with **14 ready-to-use** WASM tool factories. Each returns a `Tool` instance backed by a compiled `.wasm` module. | Factory | Tool name | Parameters | Description | | ------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | `createCalcTool()` | `calculate` | `expression` | Evaluate math expressions (+, -, \*, /, %, parens) | | `createJsonTool()` | `process_json` | `json`, `query?` (JSONPath) | Parse JSON and query with JSONPath | | `createHashTool()` | `hash_text` | `text`, `algorithm` (`sha256` \| `sha1` \| `md5`) | Cryptographic hashes | | `createBase64Tool()` | `base64` | `text`, `operation` (`encode` \| `decode`), `urlSafe?` | Encode/decode Base64 (standard and URL-safe) | | `createSlugTool()` | `slug` | `text`, `separator?` (`-`), `lowercase?` (true), `maxLength?` | Convert text to URL-safe slugs with transliteration | | `createValidationTool()` | `validate` | `value`, `type` (`email` \| `url` \| `uuid` \| `ipv4` \| `ipv6`) | Validate common formats | | `createDiffTool()` | `diff` | `original`, `modified`, `format?` (`unified` \| `inline` \| `json`), `context?` (3) | Generate diffs between texts (Myers algorithm) | | `createRegexTool()` | `regex` | `text`, `pattern`, `operation` (`match` \| `matchAll` \| `test` \| `replace` \| `split`), `flags?`, `replacement?`, `limit?` | Regex operations with ReDoS protection | | `createCsvTool()` | `csv` | `data` (string or rows), `operation` (`parse` \| `stringify`), `delimiter?`, `quote?`, `headers?` | Parse/generate CSV data (RFC 4180) | | `createMarkdownTool()` | `markdown` | `markdown`, `options?: { sanitize?, gfm? }` | Convert Markdown to HTML (GFM subset) | | `createXmlTool()` | `xml` | `xml`, `query?` (XPath-like) | Parse XML to JSON with XPath-like queries | | `createDatetimeTool()` | `datetime` | `operation` (`parse` \| `format` \| `add` \| `subtract` \| `diff` \| `now`), `date?`, `format?`, `timezone?`, `amount?`, `unit?`, `endDate?` | Parse, format, manipulate dates (UTC + offsets) | | `createCompressionTool()` | `compression` | `data`, `operation` (`compress` \| `decompress`), `inputEncoding?`, `outputEncoding?`, `level?` (0-9, 6) | Gzip compress/decompress data | | `createSigningTool()` | `signing` | `operation` (`generateKeypair` \| `sign` \| `verify`), `algorithm` (`ed25519`), `message?`, `privateKey?`, `publicKey?`, `signature?`, `encoding?`, `seed?` | Ed25519 keypair generation, sign, and verify | Every factory accepts an optional `{ timeout }` in milliseconds — 5000 by default, 10000 for `compression` and `signing`: ```typescript const calc = createCalcTool({ timeout: 10000 }); ``` The Zod schemas are exported too (`calcToolSchema`, `jsonToolSchema`, …, `signingToolSchema`, with matching `CalcToolInput`-style types), as is `getWasmPath(name)`, the absolute path of a bundled module (`getWasmPath('calc')`). With `@cogitator-ai/sandbox` the package path `@cogitator-ai/wasm-tools/wasm/calc.wasm` works as well: the executor resolves it from your application. `calcToolConfig`, `jsonToolConfig`, `hashToolConfig` and `base64ToolConfig` are ready-made `SandboxConfig`s for those four modules. ### Executing Tools Inside Cogitator, tools run through the `@cogitator-ai/sandbox` WASM executor; only when that package is not installed or cannot start does the runtime call the tool's own `execute()`. A WASM call never falls back to Docker or the host. You can also call them directly -- parameters are validated with the Zod schema, the module runs in an Extism worker thread, the timeout terminates the worker, and `context.signal` aborts the call: ```typescript const result = await createCalcTool().execute( { expression: '(2 + 3) * 4' }, { agentId: 'a', runId: 'r', signal: AbortSignal.timeout(5000) } ); // { result: 20, expression: '(2 + 3) * 4' } ``` Plugin-level errors come back as data (`{ ..., error }`) so the agent can recover. A few behaviours worth knowing: * **json**: JSONPath subset -- `.key`, `['key']`, `[n]` (negative allowed), slices, `*`, `..key`. Multi-value paths return arrays; missing paths return `found: false`. * **xml**: `/root/child` (absolute from the root element), `//name`, `*`, `name[2]`, `/@attr`, `/text()`. A query without matches returns `type: 'empty'`. Malformed XML is rejected; DTD entities are never expanded. * **diff**: `unified` (default) is a standard unified diff with `---`/`+++` headers and `@@` hunks; `inline` and `json` are also available. * **validation**: IPv4 with leading zeros is rejected, IPv6 is normalized to the RFC 5952 canonical form, and only the email domain is lower-cased. * **markdown**: `sanitize` defaults to `true` -- raw HTML is escaped and `javascript:`/`data:`-style URLs become `#`. * **compression**: gzip interoperable with zlib; `decompress` defaults to base64 in / UTF-8 out and verifies CRC32. * **signing**: WASM has no secure RNG, so a 32-byte seed is generated on the host for `generateKeypair` (the seed is the private key). Signatures interoperate with `node:crypto`. * **datetime**: month arithmetic clamps to month end (`Jan 31 + 1 month = Feb 29`), impossible dates are rejected. * **regex**: nested unbounded quantifiers are rejected; any other catastrophic pattern is stopped by the timeout. *** ## Defining Custom WASM Tools Use `defineWasmTool()` to wrap any `.wasm` module as a Cogitator tool: ```typescript import { defineWasmTool } from '@cogitator-ai/wasm-tools'; import { z } from 'zod'; const imageProcessor = defineWasmTool({ name: 'process_image', description: 'Resize and crop images in a WASM sandbox', wasmModule: './plugins/image-proc.wasm', wasmFunction: 'process', parameters: z.object({ imageData: z.string().describe('Base64-encoded image'), operation: z.enum(['resize', 'crop', 'rotate']), width: z.number().optional(), height: z.number().optional(), }), category: 'utility', tags: ['image', 'processing'], timeout: 10000, wasi: true, }); ``` Modules compiled with `extism-js` (like the pre-built ones) embed QuickJS and need `wasi: true`. Extism `>= 2.0.0-rc13` is required for their host functions. ### WasmToolConfig | Field | Type | Description | | -------------- | -------------- | ------------------------------------------ | | `name` | `string` | Tool name for LLM function calling | | `description` | `string` | What the tool does (shown to the LLM) | | `wasmModule` | `string` | Path or URL to the `.wasm` file | | `wasmFunction` | `string` | Exported function to call (default: `run`) | | `parameters` | `ZodType` | Zod schema for input validation | | `category` | `ToolCategory` | Optional category for grouping | | `tags` | `string[]` | Optional tags for discovery | | `timeout` | `number` | Execution timeout in ms (default: 5000) | | `wasi` | `boolean` | Enable WASI (needed for extism-js modules) | *** ## WasmToolManager For dynamic loading, hot-reloading, and managing multiple WASM modules at runtime, use `WasmToolManager`: ```typescript import { WasmToolManager } from '@cogitator-ai/wasm-tools'; const manager = new WasmToolManager({ debounceMs: 100, useWasi: true, timeout: 30000 }); const tool = await manager.load('./plugins/calc.wasm'); const agent = new Agent({ name: 'dynamic-agent', model: 'openai/gpt-6.1-sol', instructions: 'Use the loaded plugins.', tools: manager.getTools(), }); ``` | Option | Default | Description | | ------------ | ------- | ---------------------------------------------- | | `debounceMs` | `100` | Debounce for file change events while watching | | `useWasi` | `false` | Instantiate plugins with WASI | | `timeout` | `30000` | Per-call timeout in ms | A manager tool is named after the file (`calc.wasm` → `calc`), described as `WASM tool: calc`, and takes a free-form object: whatever the model passes is JSON-serialized and handed to the module's `run` export, and the output is parsed as JSON when possible. Loading a second file with the same base name replaces the first. Use `defineWasmTool()` when the model needs a real schema. ### File Watching The manager can watch a glob pattern for `.wasm` files and automatically load, reload, or unload modules as they change on disk: ```typescript await manager.watch('./plugins/*.wasm', { onLoad: (name, path) => console.log(`Loaded: ${name}`), onReload: (name, path) => console.log(`Reloaded: ${name}`), onUnload: (name, path) => console.log(`Unloaded: ${name}`), onError: (name, path, error) => console.error(`Error: ${name}`, error), }); ``` When a `.wasm` file is modified, the manager closes the old plugin instance and loads the new one; deleting the file unloads it. Agents keep the same tool object, so a reload is picked up on the next call. A manager watches one pattern at a time — `watch()` throws if it is already watching. `WasmLoader` and `FileWatcher`, the building blocks of the manager, are exported as well. Plugins run in worker threads: when a call exceeds `timeout` (default 30 s) or its `context.signal` aborts, the call rejects and the plugin is replaced with a fresh instance. ### Manager API ```typescript const tool = await manager.load('./path/to/module.wasm'); const allTools = manager.getTools(); const specific = manager.getTool('calc'); const mod = manager.getModule('calc'); // { name, path, plugin, tool, loadedAt } const modules = manager.getModules(); await manager.close(); ``` *** ## How WASM Sandboxing Works Each WASM tool runs inside an Extism plugin -- a lightweight WebAssembly runtime with strict isolation: 1. **Loading**: The `.wasm` binary is read from disk (compiled once and cached until the file changes) or fetched from a URL, and instantiated via the Extism SDK in a worker thread 2. **Execution**: Validated parameters are JSON-serialized and passed to the exported WASM function via `plugin.call(functionName, input)` 3. **Output**: The WASM function returns a byte buffer, which is parsed as JSON (or returned as raw text if parsing fails) 4. **Isolation**: Each plugin has its own linear memory space -- it cannot access host memory, and with no pre-opened directories or allowed hosts it has no file system or network access even with WASI enabled ``` ┌─────────────────────────────────────┐ │ Host Process (Node.js) │ │ │ │ params ──► JSON.stringify ──────┐ │ │ │ │ │ ┌───────────────────────────────▼─┐ │ │ │ Extism WASM Sandbox │ │ │ │ │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ Own linear memory │ │ │ │ │ │ No host access │ │ │ │ │ │ No network │ │ │ │ │ │ No filesystem │ │ │ │ │ └─────────────────────────┘ │ │ │ │ │ │ │ └───────────────────────────────┬─┘ │ │ │ │ │ result ◄── JSON.parse ◄────────┘ │ └─────────────────────────────────────┘ ``` ### Security Benefits * **No access to the host process** -- the module only sees the JSON input it is given * **Memory bounds checking** -- buffer overflows in WASM trap instead of corrupting host memory * **No ambient authority** -- no file system, network, or secure randomness inside the sandbox (randomness, when needed, is supplied by the host) * **Timeout enforcement** -- the worker thread running a long computation is terminated after the configured timeout * **ReDoS containment** -- catastrophic regexes run in the worker, never on the host event loop; the timeout kills them *** ## WASM vs Docker Sandboxing | Aspect | WASM (Extism) | Docker | | --------------- | ------------------- | ------------------------ | | Cold start | \< 5 ms | 500 ms - 2 s | | Memory overhead | \~2 MB per instance | \~50 MB per container | | Isolation | Linear memory | Full OS-level | | Network access | None | Configurable | | File system | None | Mountable volumes | | Best for | Pure computation | I/O-heavy, multi-process | Use WASM tools when you need fast, lightweight sandboxing for pure computation (hashing, parsing, math, text processing). Use Docker sandboxing when tools need file system access, network calls, or a full OS environment. --- # Creating Custom Tools URL: https://cogitator.app/docs/tools/custom-tools > Build type-safe tools for your agents using the tool() factory, Zod schemas, and structured error handling. ## Overview The `tool()` factory is how you give agents new capabilities. Every tool has a name, description, a Zod schema for parameter validation, and an async `execute` function. The LLM sees the name and description to decide when to call the tool, and the schema ensures parameters are always valid. ```typescript import { tool } from '@cogitator-ai/core'; import { z } from 'zod'; const weatherTool = tool({ name: 'get_weather', description: 'Get 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 }) => { const res = await fetch(`https://api.weather.com/v1/${city}?units=${units}`); return res.json(); }, }); ``` *** ## The `tool()` Factory ### Signature ```typescript function tool(config: ToolConfig): Tool; ``` ### ToolConfig Fields | Field | Type | Required | Description | | ------------------ | --------------------------------------- | -------- | ------------------------------------------------- | | `name` | `string` | Yes | Unique tool name (used in LLM function calling) | | `description` | `string` | Yes | What the tool does (shown to the LLM) | | `parameters` | `ZodType` | Yes | Zod schema for input validation | | `execute` | `(params, context) => Promise` | Yes | The function that runs when the tool is called | | `category` | `ToolCategory` | No | Grouping: `math`, `text`, `file`, `network`, etc. | | `tags` | `string[]` | No | Tags for discovery and filtering | | `sideEffects` | `SideEffectType[]` | No | Declares what the tool affects | | `requiresApproval` | `boolean \| (params) => boolean` | No | Require human approval before execution | | `timeout` | `number` | No | Execution timeout in milliseconds | | `sandbox` | `SandboxConfig` | No | Run in Docker or WASM sandbox | *** ## Defining Parameters with Zod Every tool uses a Zod schema that gets converted to JSON Schema for the LLM. Use `.describe()` on each field -- this text is what the LLM reads to understand each parameter. ### Basic Schema ```typescript const params = z.object({ query: z.string().describe('Search query'), limit: z.number().int().min(1).max(100).default(10).describe('Max results'), }); ``` ### Optional and Default Values ```typescript const params = z.object({ path: z.string().describe('File path'), encoding: z.enum(['utf-8', 'base64']).default('utf-8').describe('File encoding'), createDirs: z.boolean().optional().describe('Create parent directories'), }); ``` ### Complex Schemas ```typescript const params = z.object({ to: z.union([z.string().email(), z.array(z.string().email())]).describe('Recipient email(s)'), headers: z .record(z.string(), z.string()) .optional() .describe('Custom headers as key-value pairs'), filters: z .array( z.object({ field: z.string(), operator: z.enum(['eq', 'gt', 'lt', 'contains']), value: z.unknown(), }) ) .optional() .describe('Filter conditions'), }); ``` Cogitator converts Zod schemas to OpenAPI 3.0-compatible JSON Schema via `z.toJSONSchema()`. This means `z.string()`, `z.number()`, `z.enum()`, `z.object()`, `z.array()`, `z.union()`, `z.record()`, `z.boolean()`, and `z.unknown()` all work correctly with every LLM provider. Types JSON Schema cannot express are sent as "any". Before `execute` runs, the runtime parses the model's arguments with your schema: `execute` receives the parsed values (defaults applied, transforms run), and arguments that do not match are answered with `Invalid arguments: …` so the model can correct itself. A field with `.default()` is optional in the schema the model sees (see [Schema Serialization](#schema-serialization)), and `execute` receives the default when the model leaves it out. *** ## The Execute Function The `execute` function receives validated parameters and a `ToolContext`: ```typescript interface ToolContext { agentId: string; runId: string; signal: AbortSignal; threadId?: string; userId?: string; // the run's userId — scope data access by it channelType?: string; // set when the run comes from a channel (Telegram, Slack, …) channelId?: string; } ``` ### Async Execution All execute functions are async. You can call APIs, read files, query databases, or run any asynchronous operation: ```typescript const searchTool = tool({ name: 'search_docs', description: 'Search internal documentation', parameters: z.object({ query: z.string().describe('Search query'), department: z.enum(['engineering', 'sales', 'support']).optional(), }), execute: async ({ query, department }, context) => { const results = await db.search({ query, department, signal: context.signal, }); return { results, count: results.length }; }, }); ``` ### Using AbortSignal The `context.signal` aborts when the run times out or is cancelled, and when the tool's own `timeout` elapses. Pass it to fetch calls and other cancellable operations so they stop cleanly. A tool that outlives its `timeout` fails with `Tool "" timed out after ms` either way: ```typescript execute: async ({ url }, context) => { const response = await fetch(url, { signal: context.signal }); return response.json(); }; ``` *** ## Return Types Tool results can be any serializable value. Return structured objects so the LLM gets clear, actionable data: ```typescript // structured result return { results: searchResults, count: searchResults.length, query, truncated: searchResults.length > maxResults, }; // error as result (agent can interpret and retry) return { error: 'File not found', path: requestedPath, suggestion: 'Check the file path or list the directory first', }; ``` Avoid returning raw strings when you can return an object. Structured results let the LLM make better decisions about what to do next. To show the model an image, return it base64-encoded in an `image` or `imageBase64` field: the model receives it as an image next to the rest of the result (see [Images in Tool Results](/docs/core/tools#images-in-tool-results)). ```typescript return { width: 1280, height: 720, image: screenshot.toString('base64') }; ``` *** ## Error Handling There are two approaches to errors in tools: ### Recoverable Errors -- Return Them When the error is something the agent can understand and act on, return it as part of the result: ```typescript const fileTool = tool({ name: 'read_config', description: 'Read a configuration file', parameters: z.object({ path: z.string() }), execute: async ({ path }) => { try { const content = await readFile(path, 'utf-8'); return { content, path }; } catch (err) { const error = err as NodeJS.ErrnoException; if (error.code === 'ENOENT') { return { error: `File not found: ${path}`, path }; } return { error: error.message, path }; } }, }); ``` ### Fatal Errors -- Throw Them When something is fundamentally broken and the agent cannot recover, throw: ```typescript execute: async ({ connectionString }) => { if (!connectionString && !process.env.DATABASE_URL) { throw new Error('No database connection configured'); } // ... }; ``` The runtime catches thrown errors: the model receives `{ "error": "" }` as the tool result and the run goes on, so it can still explain the failure to the user. `result.toolCalls` keeps the call. *** ## Side Effects and Approval Declare side effects so the runtime can audit and control tool behavior: ```typescript const deployTool = tool({ name: 'deploy_service', description: 'Deploy a service to production', parameters: z.object({ service: z.string(), version: z.string(), }), sideEffects: ['network', 'external'], requiresApproval: true, execute: async ({ service, version }) => { // ... }, }); ``` `requiresApproval` can also be a function for conditional approval: ```typescript const shell = tool({ name: 'shell', description: 'Run a shell command', parameters: z.object({ command: z.string() }), requiresApproval: (params) => { const dangerous = ['rm', 'sudo', 'chmod', 'kill']; return dangerous.some((cmd) => params.command.includes(cmd)); }, execute: async ({ command }) => runCommand(command), }); ``` The check receives the model's arguments as sent, before schema parsing and defaults; a check that throws counts as "needs approval". A call that needs approval pauses the run until someone decides — see [Approvals](/docs/tools/approvals). ### Side Effect Types | Type | Description | | ------------ | ---------------------------------- | | `filesystem` | Reads or writes files | | `network` | Makes HTTP/network requests | | `database` | Queries or mutates a database | | `process` | Spawns processes or runs commands | | `external` | Calls third-party APIs or services | *** ## Sandbox Configuration Tools can declare a sandbox for isolated execution. A sandboxed tool is not run through its `execute` inside a `Cogitator` — the runtime hands the arguments to `@cogitator-ai/sandbox` instead: * **`type: 'docker'`** runs `sh -c ` in the container, with the `command`, `cwd` and `env` arguments, and returns `{ stdout, stderr, exitCode, timedOut, duration, command }`. Give such a tool a `command` parameter. * **`type: 'wasm'`** passes the arguments as JSON to `wasmFunction` of `wasmModule` and returns its output, parsed as JSON when possible. ```typescript const runPython = tool({ name: 'run_python', description: 'Run a Python one-liner, e.g. python3 -c "print(2 ** 10)"', parameters: z.object({ command: z.string().describe('Shell command that runs python3') }), sandbox: { type: 'docker', image: 'python:3.12-alpine', resources: { memory: '512MB', pidsLimit: 100 }, network: { mode: 'none' }, timeout: 30_000, }, execute: async ({ command }) => ({ error: 'sandbox unavailable', command }), }); ``` When `@cogitator-ai/sandbox` is not installed or cannot start, the runtime logs a warning and calls `execute` on the host instead — make `execute` safe for that case, as above; when Docker is unreachable, the command runs on the host shell. `sandbox.allowNativeFallback: false` on the `Cogitator` makes both fail instead. See [Sandbox](/docs/deployment/sandbox) for the configuration. For lightweight computation, use WASM sandboxing instead (or [`defineWasmTool()`](/docs/tools/wasm-tools), which sets this up): ```typescript sandbox: { type: 'wasm', wasmModule: './plugins/parser.wasm', wasmFunction: 'parse', timeout: 5000, } ``` *** ## Tool Sets A function that builds several related tools can return them with `toolset()`: the result is an ordinary array an agent takes, and each element keeps its own parameter and result types. ```typescript import { Agent, tool, toolset } from '@cogitator-ai/core'; export function createOrderTools(api: OrdersApi) { return toolset( tool({ name: 'find_order', description: 'Find an order by id', parameters: z.object({ id: z.string() }), execute: async ({ id }) => api.find(id), }), tool({ name: 'cancel_order', description: 'Cancel an order', parameters: z.object({ id: z.string(), reason: z.string() }), execute: async (p) => api.cancel(p), }) ); } const tools = createOrderTools(api); const agent = new Agent({ name: 'support', model: 'openai/gpt-6.1-sol', instructions: 'You help customers with their orders.', tools, }); const [findOrder] = tools; await findOrder.execute({ id: 'A-1' }, context); // typed as find_order's parameters ``` The built-in factories (`createMemoryTools`, `createSchedulerTools` and the browser tool sets) return their tools this way. ## Schema Serialization Every tool created with `tool()` has a `toJSON()` method that converts it to the JSON Schema format used by LLM providers: ```typescript const schema = weatherTool.toJSON(); // { // name: 'get_weather', // description: 'Get current weather for a city', // parameters: { // type: 'object', // properties: { // city: { type: 'string', description: 'City name' }, // units: { default: 'celsius', type: 'string', enum: ['celsius', 'fahrenheit'] }, // }, // required: ['city'], // }, // } ``` This conversion happens automatically when the agent sends tools to the LLM backend; `toolToSchema(tool)` does the same for any tool. You never need to write JSON Schema by hand. ## Registering Tools Globally `cogitator.tools` is a `ToolRegistry` (`register`, `registerMany`, `get`, `has`, `getAll`, `getNames`, `getSchemas`, `clear`). Tools registered there are offered to every agent this `Cogitator` runs, next to the agent's own `tools` (an agent tool with the same name wins): ```typescript cog.tools.register(weatherTool); const result = await cog.run(agent, { input: 'Weather in Paris?' }); // agent can call get_weather ``` A standalone `ToolRegistry` also resolves tool names when you restore a serialized agent: ```typescript import { Agent, ToolRegistry } from '@cogitator-ai/core'; const registry = new ToolRegistry(); registry.registerMany([weatherTool]); const restored = Agent.deserialize(snapshot, { toolRegistry: registry }); ``` --- # Approvals URL: https://cogitator.app/docs/tools/approvals > Pause an agent before it runs a sensitive tool, let a person decide — now or hours later — and continue the run where it stopped. ## Overview A tool with `requiresApproval` never runs without a person's say-so. When the model calls it, the run either asks right away (`onApproval`) or **pauses**: it returns with `status: 'paused'` and the calls that wait, keeps its state, and goes on with `cogitator.resume()` once someone decided — a minute later, after a Telegram reply, or after the server restarted. ```typescript const refund = tool({ name: 'refund', description: 'Refund an order', parameters: z.object({ order: z.string(), amount: z.number() }), requiresApproval: ({ amount }) => amount > 100, // small refunds need no one sideEffects: ['external'], execute: async ({ order, amount }) => payments.refund(order, amount), }); const result = await cog.run(support, { input: 'Refund order A-1, it arrived broken', threadId, userId, }); if (result.status === 'paused') { for (const call of result.pendingApprovals!) { console.log(`${call.toolName}(${JSON.stringify(call.arguments)}) needs approval`); } } ``` Nothing of the paused turn has run: if the model asked for several tools at once, all of them wait, so a decision never comes after side effects already happened. `requiresApproval` is `true` (always ask) or a function of the model's arguments as sent — before schema parsing and defaults. A function that throws counts as "needs approval". Each waiting call in `pendingApprovals` is a `ToolApprovalRequest`: `{ toolCallId, toolName, arguments, description, sideEffects? }`, where `description` is the tool's description. ## Resuming The runtime keeps the paused run per thread. Resume it with a decision for each call — declined calls answer the model with the reason, so it can tell the user or try something else: ```typescript const done = await cog.resume(support, threadId, { userId, decisions: { [callId]: { approved: true }, // or { approved: false, reason: 'refunds over $100 need a manager' } }, }); ``` `defaultDecision` answers every waiting call at once (`{ approved: true }` for "approve all"). Calls without a decision keep the run paused. Resuming by thread id checks the caller: only the user the run belongs to may resume it (`THREAD_ACCESS_DENIED` otherwise), and a thread with nothing paused answers `RUN_NOT_PAUSED`. If the user writes something new instead of deciding, the waiting calls are answered as declined ("the user moved on") and the new run starts from a consistent history. ## Deciding Inline When a person can answer while the run waits — a CLI prompt, a confirm dialog over a WebSocket — decide inline. Return `'pause'` to fall back to pausing: ```typescript await cog.run(agent, { input, onApproval: async (call) => { const ok = await confirm(`Run ${call.toolName} with ${JSON.stringify(call.arguments)}?`); return ok ? { approved: true } : { approved: false, reason: 'declined in the CLI' }; }, }); ``` Without `onApproval`, `guardrails.onToolApproval` (`(toolName, args, sideEffects) => Promise`) decides when it is set; otherwise the run pauses. With [guardrails](/docs/advanced/constitutional-ai) `strictMode`, every call of a tool with `sideEffects` goes through the same flow, as if the tool had `requiresApproval`. `onApproval` is a run option, so it also works for `cog.resume()`, where it decides calls the model makes after resuming. Agents wrapped with [`agentAsTool()`](/docs/tools/agent-as-tool) cannot pause: unless you pass `onApproval` to `agentAsTool`, their calls that need approval are declined (as they are when that `onApproval` returns `'pause'`) and the inner agent is told why. ## Where Paused Runs Live | Setup | Paused runs are kept in | | ------------------------- | ----------------------------------------------------------------------------------- | | `memory` configured | the thread's metadata — as durable as the memory (Redis, Postgres, SQLite, MongoDB) | | no memory | process memory | | `runCheckpoints: myStore` | your `RunCheckpointStore` (`save`, `load(threadId)`, `delete(threadId)`) | The checkpoint holds the whole conversation, so it stays on the server. You can also keep it yourself — `result.checkpoint` is plain JSON — and pass it back: `cog.resume(agent, checkpoint, { decisions })`. ## Servers and Chats * **Server adapters** answer a paused run with `status: 'paused'` and `pendingApprovals`, stream an `approval-required` event, and take decisions on `POST /agents/:name/resume` (`{ threadId, decisions?, defaultDecision? }`). * **Channels** ask in the chat ("reply approve or deny") and resume when the user answers — see [Gateway approvals](/docs/channels/gateway#approvals). --- # Agent as Tool URL: https://cogitator.app/docs/tools/agent-as-tool > Wrap agents as tools with agentAsTool() to enable hierarchical delegation between agents. ## Overview The `agentAsTool()` function wraps a Cogitator agent as a tool that other agents can call. This enables hierarchical delegation -- a manager agent can delegate subtasks to specialist agents, each with their own models, instructions, and tool sets. ```typescript import { Cogitator, Agent, agentAsTool, webSearch, webScrape } from '@cogitator-ai/core'; const cog = new Cogitator({ llm: { providers: { openai: { apiKey: process.env.OPENAI_API_KEY! }, }, }, }); const researcher = new Agent({ name: 'researcher', model: 'openai/gpt-6.1-sol', instructions: 'You research topics thoroughly and return detailed findings.', tools: [webSearch, webScrape], }); const writer = new Agent({ name: 'writer', model: 'openai/gpt-6.1-sol', instructions: 'You write clear, well-structured articles from research notes.', }); const manager = new Agent({ name: 'manager', model: 'openai/gpt-6.1-sol', instructions: 'You coordinate research and writing tasks. Delegate research to the researcher and writing to the writer.', tools: [ agentAsTool(cog, researcher, { name: 'research', description: 'Delegate a research task. Provide a clear research question.', }), agentAsTool(cog, writer, { name: 'write_article', description: 'Delegate writing an article. Provide the research notes and topic.', }), ], }); const result = await cog.run(manager, { input: 'Write a comprehensive article about WebAssembly in production', }); ``` *** ## Function Signature ```typescript function agentAsTool( cogitator: Cogitator, agent: Agent, options: AgentAsToolOptions ): Tool<{ task: string }, AgentToolResult>; ``` The returned tool has a single `task` parameter -- a string that becomes the input to the inner agent's `cog.run()` call. Each call is a separate run on a new thread: the inner agent sees only the task, not the parent's conversation, and keeps no memory between calls. The run inherits the parent call's abort signal and `userId`, so cancelling the parent run cancels the delegated one and thread ownership stays with the same user. The tool declares `sideEffects: ['external']`. *** ## Options ```typescript interface AgentAsToolOptions { name: string; description: string; timeout?: number; includeUsage?: boolean; includeToolCalls?: boolean; onApproval?: RunOptions['onApproval']; } ``` | Field | Type | Required | Description | | ------------------ | --------- | -------- | --------------------------------------------------- | | `name` | `string` | Yes | Tool name visible to the parent agent | | `description` | `string` | Yes | What this agent-tool does (shown to the LLM) | | `timeout` | `number` | No | Override timeout for the inner agent run (ms) | | `includeUsage` | `boolean` | No | Include token usage and cost in the result | | `includeToolCalls` | `boolean` | No | Include the list of tool calls the inner agent made | | `onApproval` | function | No | Decides inner tool calls that need approval | ### Approvals Inside Delegated Runs A delegated run cannot pause and wait for a person. Calls of the inner agent that need [approval](/docs/tools/approvals) are therefore declined by default (the inner agent is told "no one can approve calls of a delegated agent" and answers accordingly). Pass `onApproval` to decide them yourself: ```typescript agentAsTool(cog, billingAgent, { name: 'billing', description: 'Handle refunds and invoices.', onApproval: async (call) => call.toolName === 'refund' && Number(call.arguments.amount) <= 50 ? { approved: true } : { approved: false, reason: 'refunds over $50 need a manager' }, }); ``` Returning `'pause'` from this `onApproval` declines the call like the default. If the inner run pauses anyway, the tool reports `success: false` with an `error` naming the tools that waited for approval, never an empty success. To let a person approve delegated work, use [handoffs](/docs/core/agents#handoffs) instead: a handed-over run can pause and resume. ### Writing Good Descriptions The `description` is what the parent agent reads to decide when to delegate. Be specific about what the wrapped agent specializes in and what kind of input it expects: ```typescript agentAsTool(cog, sqlAgent, { name: 'query_database', description: 'Run a natural language query against the database. Provide the question in plain English -- the agent will generate and execute the SQL.', }); ``` *** ## Result Shape The wrapped agent returns an `AgentToolResult`: ```typescript interface AgentToolResult { output: string; success: boolean; error?: string; usage?: { inputTokens: number; outputTokens: number; totalTokens: number; cost: number; duration: number; }; toolCalls?: Array<{ name: string; arguments: unknown }>; } ``` On success, `output` contains the agent's final text response. On failure, `success` is `false` and `error` contains the error message. The parent agent sees this result and can decide how to proceed -- retry, try a different approach, or report the failure. *** ## Patterns ### Manager-Worker Hierarchy One manager agent coordinates multiple specialist workers: ```typescript const codeReviewer = new Agent({ name: 'code-reviewer', model: 'openai/gpt-6.1-sol', instructions: 'Review code for bugs, security issues, and style problems.', }); const testWriter = new Agent({ name: 'test-writer', model: 'openai/gpt-6.1-sol', instructions: 'Write comprehensive unit tests for the provided code.', tools: [fileRead, fileWrite], }); const leadDev = new Agent({ name: 'lead-developer', model: 'openai/gpt-6.1-sol', instructions: 'You are a tech lead. Review code with the reviewer, then have tests written.', tools: [ agentAsTool(cog, codeReviewer, { name: 'review_code', description: 'Get a code review. Send the code or file path to review.', }), agentAsTool(cog, testWriter, { name: 'write_tests', description: 'Generate unit tests for code. Send the code to test.', }), ], }); ``` ### Routing by Capability Use different models for different sub-agents based on task complexity: ```typescript const cheapSummarizer = new Agent({ name: 'summarizer', model: 'openai/gpt-6-luna', instructions: 'Summarize text concisely.', }); const deepAnalyst = new Agent({ name: 'analyst', model: 'anthropic/claude-sonnet-5-5', instructions: 'Perform deep analysis with nuanced reasoning.', }); const router = new Agent({ name: 'router', model: 'openai/gpt-6-luna', instructions: 'Route tasks: use summarize for simple summaries, use analyze for complex analysis.', tools: [ agentAsTool(cog, cheapSummarizer, { name: 'summarize', description: 'Quick text summarization. Use for straightforward content.', }), agentAsTool(cog, deepAnalyst, { name: 'analyze', description: 'Deep analysis with reasoning. Use for complex questions.', includeUsage: true, }), ], }); ``` ### Tracking Inner Agent Usage Enable `includeUsage` and `includeToolCalls` to give the parent agent visibility into what the sub-agent did: ```typescript const researchTool = agentAsTool(cog, researcher, { name: 'research', description: 'Research a topic thoroughly', includeUsage: true, includeToolCalls: true, timeout: 120_000, }); ``` The parent agent receives: ```json { "output": "WebAssembly (Wasm) is a binary instruction format...", "success": true, "usage": { "inputTokens": 2340, "outputTokens": 1856, "totalTokens": 4196, "cost": 0.042, "duration": 8500 }, "toolCalls": [ { "name": "web_search", "arguments": { "query": "WebAssembly production use cases 2025" } }, { "name": "web_scrape", "arguments": { "url": "https://..." } } ] } ``` *** ## Error Handling `agentAsTool` catches all errors from the inner agent run and returns them as structured results instead of throwing. The parent agent always gets a response it can work with: ```typescript // if the inner agent times out or fails { "output": "", "success": false, "error": "Run timed out after 30000ms" } ``` The parent agent sees `success: false` and can decide to retry with simpler instructions, try a different agent, or inform the user. *** ## Timeout Behavior The timeout resolution follows this priority: 1. `timeout` in `AgentAsToolOptions` (if set) -- also used as the tool's own timeout 2. `timeout` in the inner agent's config 3. Cogitator runtime's `limits.defaultTimeout`, then 120 seconds For long-running sub-agents (research, code generation), explicitly set a generous timeout: ```typescript agentAsTool(cog, researcher, { name: 'deep_research', description: 'Extensive multi-source research', timeout: 300_000, }); ``` --- # Tool Caching URL: https://cogitator.app/docs/tools/tool-caching > Cache tool results with TTL, LRU eviction, and semantic similarity matching using in-memory or Redis storage. ## Overview The `withCache()` function wraps any tool with a caching layer. Repeated calls with the same (or semantically similar) parameters return cached results instantly, saving API calls, database queries, and compute time. ```typescript import { Agent, withCache, webSearch } from '@cogitator-ai/core'; const cachedSearch = withCache(webSearch, { strategy: 'exact', ttl: '10m', maxSize: 500, storage: 'memory', }); const agent = new Agent({ name: 'assistant', model: 'openai/gpt-6.1-sol', instructions: 'Answer questions using web search.', tools: [cachedSearch], }); ``` The cached tool is a drop-in replacement -- it has the same name, description, parameters and approval settings as the original, plus a `.cache` object for management. Only its `execute` changes, so caching works the same inside agent runs and when you call the tool directly. Whatever `execute` returns is cached, including error objects such as `{ error: 'API key not found…' }` that many built-in tools return instead of throwing. A thrown error is never cached. If a tool reports transient failures as data, wrap it so those results throw, or keep the TTL short. *** ## `withCache()` API ```typescript function withCache( tool: Tool, config: WithCacheOptions ): CachedTool; ``` ### Configuration ```typescript interface WithCacheOptions { strategy: 'exact' | 'semantic'; ttl: DurationString; maxSize: number; storage: 'memory' | 'redis'; similarity?: number; keyPrefix?: string; embeddingService?: EmbeddingService; redisClient?: RedisClientLike; onHit?: (key: string, params: unknown) => void; onMiss?: (key: string, params: unknown) => void; onEvict?: (key: string) => void; } ``` | Field | Type | Default | Description | | ------------------ | ----------------------- | ------------- | ------------------------------------------------- | | `strategy` | `'exact' \| 'semantic'` | required | Cache matching strategy | | `ttl` | `DurationString` | required | Time-to-live: `"30s"`, `"5m"`, `"2h"`, `"1d"` | | `maxSize` | `number` | required | Maximum number of cached entries | | `storage` | `'memory' \| 'redis'` | required | Storage backend | | `similarity` | `number` | `0.95` | Cosine similarity threshold for semantic matching | | `keyPrefix` | `string` | `"toolcache"` | Key prefix; a trailing `:` is not doubled | | `embeddingService` | `EmbeddingService` | -- | Required for semantic strategy | | `redisClient` | `RedisClientLike` | -- | Required for redis storage (throws without it) | | `onHit` | `function` | -- | Called on an exact or semantic hit | | `onMiss` | `function` | -- | Called before the tool runs on a miss | | `onEvict` | `function` | -- | Called when an entry is invalidated or evicted | `maxSize` caps the number of entries per storage; when it is reached, the least recently used entry is dropped. Expired entries are dropped when they are read. ### Duration Strings TTL values use human-readable duration strings: | Suffix | Meaning | Example | | ------ | ------------ | --------- | | `ms` | Milliseconds | `"500ms"` | | `s` | Seconds | `"30s"` | | `m` | Minutes | `"10m"` | | `h` | Hours | `"2h"` | | `d` | Days | `"1d"` | | `w` | Weeks | `"1w"` | *** ## Cache Strategies ### Exact Matching Generates a deterministic cache key from the key prefix, the tool name and a SHA-256 hash of the parameters (serialized with sorted keys, so property order does not matter). Two calls with identical parameters hit the same cache entry. ```typescript const cachedHash = withCache(hash, { strategy: 'exact', ttl: '1h', maxSize: 1000, storage: 'memory', }); ``` ### Semantic Matching Uses embedding vectors to find cached results for semantically similar inputs. Requires an `embeddingService` (any `EmbeddingService` from `@cogitator-ai/memory`, such as `OpenAIEmbeddingService`, or your own `{ embed, embedBatch, dimensions, model }`). Without one, `semantic` behaves like `exact`. ```typescript const cachedSearch = withCache(webSearch, { strategy: 'semantic', ttl: '30m', maxSize: 200, storage: 'memory', similarity: 0.92, embeddingService: myEmbeddingService, }); ``` With semantic matching, a query like `"TypeScript best practices"` might match a cached result for `"best practices for TypeScript development"` if their cosine similarity exceeds the threshold. The text that gets embedded is the first string among the parameters `query`, `q`, `search`, `text`, `input`, `prompt` and `question`, or the JSON of all parameters when there is none. A semantic hit therefore ignores the other parameters: `{ query: 'react hooks', maxResults: 20 }` can be answered from the entry for `{ query: 'React hooks', maxResults: 5 }`. Use `exact` when the other parameters change the result. The lookup flow: 1. Check for an exact key match first 2. If no exact match, embed the current parameters and search for similar entries above the `similarity` threshold 3. If a similar entry is found, return its cached result 4. Otherwise, execute the tool and cache the result with its embedding Embedding failures are ignored: the lookup falls back to running the tool, and the entry is stored without an embedding. *** ## Storage Backends ### In-Memory (LRU) Entries are stored in a `Map` with LRU eviction -- when `maxSize` is reached, the least recently accessed entry is evicted. Each cached tool gets its own storage. ```typescript const cached = withCache(sqlQuery, { strategy: 'exact', ttl: '5m', maxSize: 500, storage: 'memory', }); ``` In-memory storage is fast and requires no external dependencies, but entries are lost when the process restarts. ### Redis For persistent, shared caching across multiple processes or server instances. Uses Redis sorted sets for LRU tracking and key-value storage for entries. ```typescript import Redis from 'ioredis'; import { withCache, webScrape } from '@cogitator-ai/core'; const redis = new Redis('redis://localhost:6379'); const cached = withCache(webScrape, { strategy: 'exact', ttl: '1h', maxSize: 5000, storage: 'redis', redisClient: redis, keyPrefix: 'myapp:tools', }); ``` An ioredis client matches `RedisClientLike` as it is, and so does a client from [`@cogitator-ai/redis`](/docs/deployment/redis#tool-cache-client): ```typescript import { createRedisClient } from '@cogitator-ai/redis'; const client = await createRedisClient({ url: 'redis://localhost:6379', keyPrefix: 'myapp:' }); const cachedScrape = withCache(webScrape, { strategy: 'exact', ttl: '1h', maxSize: 5000, storage: 'redis', redisClient: client, }); ``` Its `scan` is relative to the client's `keyPrefix` and walks every master in cluster mode, so `cache.clear()` and semantic lookups work in both modes; in a cluster, give the client a `{hashtag}` prefix so the storage's multi-key commands (`mget`, `del`) stay in one slot. Other clients need a small adapter: node-redis, for example, spells the commands `setEx`/`zAdd` and takes `SCAN` options as an object. Entries are written with `SETEX`, so Redis expires them on its own as well. The storage keeps its data under `keyPrefix` (a `:` is appended when it does not end with one): each entry at `:entry:`, the LRU order in the sorted set `:lru` and the entry count in `:counter`. The cache key itself is `::` (again with a single `:` after the prefix), so with `keyPrefix: 'myapp:tools'` (the ioredis example) an entry lives at `myapp:tools:entry:myapp:tools:web_scrape:`. Cached tools that share a `keyPrefix` share one LRU and one `maxSize`, and `cache.clear()` deletes every key under the prefix. A `keyPrefix` set on the Redis client itself comes in front of all of these. The `RedisClientLike` interface requires the methods used by Redis-backed cache storage: ```typescript interface RedisClientLike { get(key: string): Promise; setex(key: string, seconds: number, value: string): Promise; del(...keys: string[]): Promise; mget(...keys: string[]): Promise<(string | null)[]>; zadd(key: string, score: number, member: string): Promise; zrange(key: string, start: number, stop: string): Promise; zrem(key: string, ...members: string[]): Promise; incr(key: string): Promise; decr(key: string): Promise; exists(...keys: string[]): Promise; scan( cursor: number | string, matchToken: 'MATCH', pattern: string, countToken: 'COUNT', count: number | string ): Promise<[cursor: string, keys: string[]]>; } ``` You can also create storage instances directly: ```typescript import { createToolCacheStorage } from '@cogitator-ai/core'; const memoryStorage = createToolCacheStorage('memory', { maxSize: 1000 }); const redisStorage = createToolCacheStorage('redis', { redisClient: redis, keyPrefix: 'cache:', maxSize: 5000, onEvict: (key) => console.log('evicted', key), }); ``` `createToolCacheStorage(type, { maxSize?, redisClient?, keyPrefix?, onEvict? })` returns a `ToolCacheStorage` (`InMemoryToolCacheStorage` or `RedisToolCacheStorage`, both exported; `maxSize` defaults to 1000, the Redis key prefix to `toolcache:`). `onEvict` is called with the key of each entry evicted to make room for a new one. `withCache` creates its own storage from its options, so use these when you build caching around something other than a tool. *** ## Cache Management Every cached tool exposes a `.cache` object for runtime management: ### Stats ```typescript const stats = cachedSearch.cache.stats(); // { // hits: 142, // misses: 38, // size: 38, // evictions: 12, // hitRate: 0.789, // } ``` ### Clear ```typescript await cachedSearch.cache.clear(); ``` `hits`, `misses` and `hitRate` count calls of this cached tool since it was created; `evictions` counts entries dropped to stay under `maxSize`. ### Invalidate Remove a specific entry by its parameters (it resolves to whether an entry existed, and calls `onEvict`): ```typescript const existed = await cachedSearch.cache.invalidate({ query: 'TypeScript tutorials', maxResults: 5, }); ``` ### Warmup Pre-populate the cache with known parameter-result pairs: ```typescript await cachedSearch.cache.warmup([ { params: { query: 'React hooks guide', maxResults: 5 }, result: { query: 'React hooks guide', provider: 'tavily', results: [] }, }, { params: { query: 'Node.js streams', maxResults: 5 }, result: { query: 'Node.js streams', provider: 'tavily', results: [] }, }, ]); ``` *** ## When to Cache Cache tools that: * **Call external APIs** (web search, scraping) -- save quota and reduce latency * **Run expensive queries** (SQL, vector search) -- avoid duplicate database load * **Perform deterministic computation** (hashing, math) -- same input always gives same output * **Have rate limits** -- cache prevents hitting API rate limits on repeated queries Avoid caching tools that: * **Have side effects** (file\_write, send\_email, exec) -- caching would silently skip the action * **Must return real-time data** (datetime, random\_number) -- cached values would be stale * **Depend on external state** that changes frequently between calls *** ## Observability Callbacks Use the `onHit`, `onMiss`, and `onEvict` callbacks to integrate with your monitoring. `onEvict` receives the cache key of each entry removed by `cache.invalidate()` or dropped to stay under `maxSize` (in memory and in Redis); expired entries are dropped silently. ```typescript const cached = withCache(webSearch, { strategy: 'exact', ttl: '10m', maxSize: 500, storage: 'memory', onHit: (key, params) => { metrics.increment('tool_cache.hit', { tool: 'web_search' }); }, onMiss: (key, params) => { metrics.increment('tool_cache.miss', { tool: 'web_search' }); }, onEvict: (key) => { metrics.increment('tool_cache.evict', { tool: 'web_search' }); }, }); ``` ## Helpers | Export | Description | | ------------------------------------------------- | -------------------------------------------------------------- | | `generateCacheKey({ toolName, params, prefix? })` | The exact-match key: `::<16-hex-char hash>` | | `paramsToQueryString(params)` | The text semantic matching embeds | | `cosineSimilarity(a, b)` | Similarity of two vectors | | `parseDuration(duration)` | `DurationString` to milliseconds (throws on an invalid string) | --- # Browser URL: https://cogitator.app/docs/browser > Playwright-based browser automation for AI agents with 33 tools across navigation, interaction, extraction, vision, and network modules. ## What is @cogitator-ai/browser? `@cogitator-ai/browser` gives your agents the ability to browse the web. It wraps Playwright in a session-based API and exposes **33 tools** that agents can call to navigate pages, fill forms, extract data, take screenshots, and intercept network traffic. Everything runs through `BrowserSession` which manages the browser lifecycle, tabs, cookies, and optional stealth mode for anti-detection. Tools are created via `browserTools()` and plug directly into any Cogitator agent. ``` Agent │ ├── browser_navigate("https://example.com") ├── browser_fill_form({ email: "...", password: "..." }) ├── browser_click("#submit") ├── browser_screenshot() └── browser_get_text(".result") ``` ## Installation ```bash pnpm add @cogitator-ai/browser playwright ``` `playwright` comes with the package (it is loaded when the session starts); add it yourself to pin its version. Playwright needs browser binaries — install them after adding the package: ```bash npx playwright install chromium ``` ## Quick Start ```typescript import { Cogitator, Agent } from '@cogitator-ai/core'; import { BrowserSession, browserTools } from '@cogitator-ai/browser'; const session = new BrowserSession({ headless: true }); await session.start(); const agent = new Agent({ name: 'web-researcher', model: 'openai/gpt-6.1-sol', tools: browserTools(session), instructions: 'You browse the web to find information.', }); const cogitator = new Cogitator({ llm: { defaultModel: 'openai/gpt-6.1-sol', providers: { openai: { apiKey: process.env.OPENAI_API_KEY! } }, }, }); const result = await cogitator.run(agent, { input: 'Go to https://news.ycombinator.com and get the top 5 stories', }); console.log(result.output); await session.close(); ``` Tools from `browserTools()` start the session on their first call, so `session.start()` is optional here — it just moves the browser launch out of the first tool call. ## Tool Modules All 33 tools are organized into 5 modules. Use `browserTools(session)` to get all of them, or filter by module. | Module | Tools | Description | | ---------------------------------------------- | ----- | ----------------------------------------------------- | | [Navigation](/docs/browser/tools#navigation) | 7 | URL navigation, history, waiting | | [Interaction](/docs/browser/tools#interaction) | 9 | Clicking, typing, forms, drag & drop | | [Extraction](/docs/browser/tools#extraction) | 7 | Text, HTML, links, tables, structured data | | [Vision](/docs/browser/tools#vision) | 4 | Screenshots, accessibility tree, click-by-description | | [Network](/docs/browser/tools#network) | 6 | Request interception, HAR capture, API call tracking | ### Module Filtering Load only the modules you need: ```typescript const tools = browserTools(session, { modules: ['navigation', 'extraction'], }); ``` ### Individual Tool Import Import module or tool factories when you want fine-grained control. Module factories return typed tuples, so each destructured tool keeps its own parameter and result types: ```typescript import { createNavigateTool, createScreenshotTool, createVisionTools } from '@cogitator-ai/browser'; const tools = [createNavigateTool(session), createScreenshotTool(session)]; const [screenshot, , findByDescription] = createVisionTools(session); ``` Factory-made tools expect a started session — call `await session.start()` before the agent runs. ## Key Features * **Session management** with multi-tab support, cookie persistence, proxy configuration * **Stealth mode** with anti-detection evasions, fingerprint randomization, human-like behavior * **Vision tools** that leverage the accessibility tree for natural language element finding * **Network interception** for blocking resources, modifying requests, and capturing HAR logs * **Smart form filling** that matches fields by name, placeholder, aria-label, or label text ## Next Steps * [BrowserSession](/docs/browser/session) — lifecycle, tabs, cookies, configuration * [Tools Reference](/docs/browser/tools) — all 33 tools with parameters * [Stealth](/docs/browser/stealth) — anti-detection and human-like behavior * [Vision](/docs/browser/vision) — screenshots and accessibility-based interaction * [Network](/docs/browser/network) — request interception and traffic capture --- # BrowserSession URL: https://cogitator.app/docs/browser/session > Manage browser lifecycle, tabs, cookies, proxy, and stealth configuration with BrowserSession. ## Overview `BrowserSession` is the core class that manages a Playwright browser instance. It handles launching the browser, creating contexts with the right configuration, and provides tab and cookie management. Every browser tool receives a session reference and operates on its active page. ```typescript import { BrowserSession } from '@cogitator-ai/browser'; const session = new BrowserSession({ headless: true, browser: 'chromium', stealth: true, }); await session.start(); // ... use tools ... await session.close(); ``` ## Configuration The full `BrowserSessionConfig` interface: ```typescript interface BrowserSessionConfig { headless?: boolean; // default: true browser?: BrowserType; // 'chromium' | 'firefox' | 'webkit', default: 'chromium' stealth?: boolean | StealthConfig; proxy?: string | ProxyConfig; viewport?: { width: number; height: number }; // default: 1280x720 userAgent?: string; locale?: string; // e.g. 'en-US' timezone?: string; // e.g. 'America/New_York' geolocation?: { latitude: number; longitude: number }; persistentContext?: string; // user data dir; profile survives restarts cookies?: BrowserCookie[]; // initial cookies timeout?: number; // navigation timeout, default: 30000ms actionTimeout?: number; // action timeout, default: 10000ms pool?: { maxPages: number }; // max open tabs for newTab(), positive integer robots?: RobotsChecker; // block navigations the site's robots.txt disallows } ``` ### Persistent Profile `persistentContext` launches Playwright's persistent context with the given user data directory. Cookies, `localStorage`, IndexedDB and cache survive across sessions, which keeps logins without exporting cookies. In this mode `session.browser` is `null` and the profile's existing tabs become the session's tabs. ```typescript const session = new BrowserSession({ persistentContext: './.browser-profile' }); await session.start(); ``` ### Tab Limit `pool.maxPages` caps the number of tabs `newTab()` may open. Opening a tab beyond the limit throws `Tab limit reached: pool.maxPages is N`. ```typescript const session = new BrowserSession({ pool: { maxPages: 3 } }); ``` ### robots.txt `robots` takes a checker such as `RobotsPolicy` from `@cogitator-ai/core`. Every page navigation, typed, clicked, redirected or in a frame, is checked against the site's robots.txt, and disallowed ones are blocked. `newTab(url)` and the `browser_navigate` tool throw `robots.txt does not allow visiting `, so an agent learns why. ```typescript import { RobotsPolicy } from '@cogitator-ai/core'; const userAgent = 'NewsBot/1.0 (+https://example.com/bot)'; const session = new BrowserSession({ userAgent, robots: new RobotsPolicy({ userAgent }) }); ``` ### Browser Types Playwright supports three browser engines: ```typescript new BrowserSession({ browser: 'chromium' }); // Chrome, Edge new BrowserSession({ browser: 'firefox' }); // Firefox new BrowserSession({ browser: 'webkit' }); // Safari ``` ### Proxy Pass a URL string or a full `ProxyConfig` with authentication: ```typescript new BrowserSession({ proxy: 'http://proxy.example.com:8080' }); new BrowserSession({ proxy: { server: 'http://proxy.example.com:8080', username: 'user', password: 'pass', }, }); ``` ### User Agent An explicit `userAgent` wins over stealth: the random stealth user agent is skipped, but the evasion scripts still run and report a `navigator.platform` that matches your user agent. ### Viewport and Locale ```typescript new BrowserSession({ viewport: { width: 1920, height: 1080 }, locale: 'de-DE', timezone: 'Europe/Berlin', geolocation: { latitude: 52.52, longitude: 13.405 }, // also grants the geolocation permission }); ``` ## Lifecycle ### `start()` Launches the browser, creates a context with the configured options, opens the first tab, and applies stealth evasions if enabled. If any step fails, everything launched so far is closed and the error is rethrown, so the session can be started again. Calling `start()` while the session is running or starting throws. ```typescript const session = new BrowserSession({ headless: false }); await session.start(); ``` If the browser context closes on its own (crash, the user closing a headed window), the session resets and `started` becomes `false`. ### `ensureStarted()` Starts the session if needed. Concurrent callers share a single launch. Tools returned by `browserTools()` call it before every execution, so a session that was never started, or whose browser was closed or crashed, is (re)launched on the next tool call. ```typescript await Promise.all([session.ensureStarted(), session.ensureStarted()]); // one browser ``` ### `onStart(listener)` Registers a listener that receives the `BrowserContext` after every successful start, and immediately if the session is already running. Returns an unsubscribe function. The network tools use it to record traffic from the first request. ```typescript const unsubscribe = session.onStart((context) => { context.on('request', (req) => console.log(req.url())); }); ``` ### `close()` Closes the browser (or the persistent context) and cleans up all resources. Safe to call multiple times, and waits for a launch in progress first. ```typescript await session.close(); ``` `BrowserSession` is also async-disposable, so `await using` closes it at the end of the scope: ```typescript await using session = new BrowserSession(); await session.start(); ``` ### `page` Returns the currently active `Page` object. Throws if the session hasn't been started. If the active tab was closed, the session falls back to an open tab. `session.started` tells whether a context is running and `session.activeTabIndex` returns the active tab index. ```typescript const page = session.page; console.log(page.url()); ``` ## Tab Management Sessions support multiple tabs. New tabs automatically become the active tab. Pages opened by the site itself (popups, `target="_blank"` links) are tracked in `tabs` too, without changing the active tab. ### `newTab(url?)` Opens a new tab, optionally navigating to a URL. Returns the Playwright `Page` object. If navigation fails the tab is closed and the error is rethrown. Respects `pool.maxPages`. ```typescript const page = await session.newTab('https://example.com'); ``` ### `switchTab(index)` Switches the active tab by index. ```typescript session.switchTab(0); // switch to first tab ``` ### `closeTab(index?)` Closes a tab by index. Defaults to closing the active tab. Cannot close the last remaining tab. ```typescript await session.closeTab(1); ``` ### `tabs` Returns a copy of all open `Page` objects. Tabs closed outside the session are pruned, and `switchTab`/`closeTab` indexes always refer to this pruned list. ```typescript for (const tab of session.tabs) { console.log(tab.url()); } ``` ## Cookie Management ### `getCookies()` Returns all cookies from the current browser context. ```typescript const cookies = await session.getCookies(); ``` ### `setCookies(cookies)` Adds cookies to the browser context. Cookies with a `domain` but no `path` default to `path: '/'`. ```typescript await session.setCookies([{ name: 'session', value: 'abc123', domain: '.example.com' }]); ``` ### `saveCookies(filePath)` / `loadCookies(filePath)` Persist cookies to a JSON file and restore them later. Useful for maintaining login state across sessions. `loadCookies` requires a JSON array and skips entries without `name`, `value`, and `url` or `domain`. ```typescript await session.saveCookies('./cookies.json'); // later... const session2 = new BrowserSession(); await session2.start(); await session2.loadCookies('./cookies.json'); ``` ## Initial Cookies Pass cookies at construction time to have them applied on `start()`: ```typescript const session = new BrowserSession({ cookies: [{ name: 'token', value: 'xyz', domain: '.example.com', path: '/' }], }); await session.start(); ``` ## Accessing Playwright Objects For advanced use cases, you can access the underlying Playwright objects directly: ```typescript session.browser; // Browser | null (null with persistentContext) session.context; // BrowserContext | null session.page; // active Page ``` The resolved configuration is available too: ```typescript session.config; // BrowserSessionConfig with defaults applied session.stealthEnabled; // true when `stealth` is set session.stealthConfig; // StealthConfig merged with defaults, or null ``` ## Next Steps * [Tools Reference](/docs/browser/tools) — all 33 tools that operate on the session * [Stealth](/docs/browser/stealth) — anti-detection configuration --- # Tools Reference URL: https://cogitator.app/docs/browser/tools > Complete reference for all 33 browser tools across navigation, interaction, extraction, vision, and network modules. ## Overview `browserTools(session)` returns all 33 tools. You can filter by module or create tools with the module and per-tool factories. ```typescript import { browserTools } from '@cogitator-ai/browser'; const allTools = browserTools(session); const subset = browserTools(session, { modules: ['navigation', 'extraction'], }); ``` Tools from `browserTools()` call `session.ensureStarted()` before every execution, so the browser launches on the first tool call (and relaunches if it was closed or crashed). An unknown module name throws, listing the available ones. Every tool has `category: 'web'` and the tags `['browser', ]`. ### Typed Factories Each module has a factory — `createNavigationTools`, `createInteractionTools`, `createExtractionTools`, `createVisionTools`, `createNetworkTools` — that returns a typed tuple built with [`toolset()`](/docs/tools/custom-tools). Destructuring keeps each tool's parameter and result types, which is handy when you call tools from your own code: ```typescript import { BrowserSession, createNavigationTools, createExtractionTools, } from '@cogitator-ai/browser'; const session = new BrowserSession(); await session.start(); const [navigate] = createNavigationTools(session); const [getText, , , getLinks] = createExtractionTools(session); const ctx = { agentId: 'script', runId: 'manual', signal: AbortSignal.timeout(30_000) }; const page = await navigate.execute({ url: 'https://example.com' }, ctx); // { url, title, status } const { text } = await getText.execute({ selector: 'main' }, ctx); const { links } = await getLinks.execute({ baseUrl: page.url }, ctx); ``` Tuple order matches the order of the tools on this page. Factory-made tools do not auto-start the session — call `session.start()` first. Every tool also has its own factory (`createNavigateTool`, `createClickTool`, …), and the Zod schemas (`navigateSchema`, `clickSchema`, …) and input types (`NavigateInput`, `ClickInput`, …) are exported. *** ## Navigation 7 tools for page navigation and waiting. ### `browser_navigate` Navigate to a URL. Returns `{ url, title, status }` — the final URL, page title, and HTTP status (`0` when there is no response, e.g. `about:blank`). | Parameter | Type | Description | | ----------- | -------- | ----------------------------------------------------------------------------------- | | `url` | `string` | URL to navigate to; must include a scheme (`https:`, `about:`, `data:`, `file:`, …) | | `waitUntil` | `enum` | `load` (default) \| `domcontentloaded` \| `networkidle` \| `commit` | ### `browser_go_back` Go back to the previous page in browser history. No parameters. Returns `{ navigated, url, title }`; `navigated` is `false` when there was no history entry. ### `browser_go_forward` Go forward to the next page in browser history. No parameters. Returns `{ navigated, url, title }`. ### `browser_reload` Reload the current page. Returns `{ url, title }`. | Parameter | Type | Description | | ----------- | ------ | ------------------------------------------------------------------- | | `waitUntil` | `enum` | `load` (default) \| `domcontentloaded` \| `networkidle` \| `commit` | ### `browser_wait_for_navigation` Wait for the page URL to match a glob pattern. Returns `{ url, title }`. | Parameter | Type | Description | | --------- | -------- | ------------------------------------------------------------------- | | `url` | `string` | URL glob to wait for (e.g. `**/dashboard`); default `**` (any) | | `timeout` | `number` | Timeout in milliseconds; `0` disables it (default: session timeout) | ### `browser_get_current_url` Get the current page URL and title. No parameters. Returns `{ url, title }`. ### `browser_wait_for_selector` Wait for a CSS selector to reach a state. Returns `{ found: true }`, or `{ found: false }` on timeout. | Parameter | Type | Description | | ---------- | -------- | ----------------------------------------------------------- | | `selector` | `string` | CSS selector to wait for | | `state` | `enum` | `visible` (default) \| `hidden` \| `attached` \| `detached` | | `timeout` | `number` | Timeout in milliseconds; `0` disables it | *** ## Interaction 9 tools for clicking, typing, and form manipulation. With stealth `humanLikeMouse`, click, hover and scroll use human-like mouse movement; with `humanLikeTyping`, typing is keystroke by keystroke (see [Stealth](/docs/browser/stealth#human-like-behavior)). ### `browser_click` Click an element by CSS selector. Returns `{ clicked: true }`. | Parameter | Type | Description | | ------------ | ---------- | ------------------------------------- | | `selector` | `string` | CSS selector of element | | `button` | `enum` | `left` \| `right` \| `middle` | | `clickCount` | `number` | Number of clicks (2 for double-click) | | `position` | `{ x, y }` | Position relative to element | ### `browser_type` Type text into an input element. Uses `fill()` for instant input (replacing the value), or keystrokes when `delay` is set (appending unless `clearFirst`). With stealth `humanLikeTyping`, the field is cleared and typed key by key with human-like delays, so the result matches `fill()`. Returns `{ typed: true }`. | Parameter | Type | Description | | ------------ | --------- | -------------------------------------------- | | `selector` | `string` | CSS selector of input | | `text` | `string` | Text to type | | `delay` | `number` | Delay between keystrokes in ms | | `clearFirst` | `boolean` | Clear the field before typing (with `delay`) | ### `browser_select_option` Select an option from a `