# Fastify Adapter

URL: https://cogitator.app/docs/server-adapters/fastify

> Register Cogitator as a native Fastify plugin with automatic schema validation, Swagger UI, rate limiting, and WebSocket support.

The `@cogitator-ai/fastify` package provides `cogitatorPlugin` — a [fastify-plugin](https://github.com/fastify/fastify-plugin) compatible with Fastify 5.x. It leverages Fastify's native ecosystem: `@fastify/swagger` for OpenAPI, `@fastify/rate-limit` for throttling, and `@fastify/websocket` for real-time communication.

## Installation

```bash
pnpm add @cogitator-ai/fastify fastify

# optional — install for Swagger UI, rate limiting, or WebSocket
pnpm add @fastify/swagger @fastify/swagger-ui
pnpm add @fastify/rate-limit
pnpm add @fastify/websocket
```

## Basic Setup

```typescript
import Fastify from 'fastify';
import { Cogitator, Agent, tool } from '@cogitator-ai/core';
import { cogitatorPlugin } from '@cogitator-ai/fastify';
import { z } from 'zod';

const fastify = Fastify({ logger: true });

const cogitator = new Cogitator({
  llm: {
    defaultModel: 'openai/gpt-6.1-sol',
    providers: {
      openai: { apiKey: process.env.OPENAI_API_KEY! },
    },
  },
});

const multiply = tool({
  name: 'multiply',
  description: 'Multiply two numbers',
  parameters: z.object({ a: z.number(), b: z.number() }),
  execute: async ({ a, b }) => ({ result: a * b }),
});

const mathAgent = new Agent({
  name: 'math',
  description: 'Math assistant',
  model: 'openai/gpt-6.1-sol',
  instructions: 'You are a math assistant. Use the multiply tool for multiplication.',
  tools: [multiply],
});

await fastify.register(cogitatorPlugin, {
  cogitator,
  agents: { math: mathAgent },
  prefix: '/cogitator',
  enableSwagger: true,
  enableWebSocket: true,
});

await fastify.listen({ port: 3000 });
```

After startup, all Cogitator endpoints are available under `/cogitator` and Swagger UI is at `/cogitator/docs`. The plugin is wrapped with `fastify-plugin`, so `fastify.cogitator` is visible to the rest of your app, while the hooks it adds (auth, rate limit, error handler) apply only to the Cogitator routes.

## Plugin Options

```typescript
interface CogitatorPluginOptions {
  cogitator: Cogitator;
  agents?: Record<string, Agent>;
  workflows?: Record<string, Workflow>;
  swarms?: Record<string, SwarmConfig>;
  prefix?: string; // default: '/cogitator'
  auth?: AuthFunction;
  rateLimit?: RateLimitConfig;
  enableSwagger?: boolean;
  enableWebSocket?: boolean;
  swagger?: SwaggerConfig;
  websocket?: WebSocketConfig; // { path? }, default '/ws'
  sseHeartbeatMs?: number; // SSE heartbeat interval in ms, default 5000, 0 turns it off
}
```

`enableSwagger` and `enableWebSocket` default to off.

## Authentication

The auth function receives a Fastify request and returns an `AuthContext`. It runs as an `onRequest` hook:

```typescript
await fastify.register(cogitatorPlugin, {
  cogitator,
  agents: { math: mathAgent },
  auth: async (request) => {
    const token = request.headers.authorization?.replace('Bearer ', '');
    if (!token) return undefined;

    const user = await verifyToken(token);
    return { userId: user.id, roles: user.roles };
  },
});
```

Returning `undefined` lets the request through unauthenticated; throwing rejects it with `401 UNAUTHORIZED`. The auth context is available on `request.cogitatorAuth` (with `request.cogitatorRequestId` and `request.cogitatorStartTime`), its `userId` is passed to agent and swarm runs and scopes the thread routes, and — because it is an `onRequest` hook — it also guards the WebSocket upgrade.

## Rate Limiting

When `rateLimit` is provided, the plugin registers `@fastify/rate-limit` automatically:

```typescript
await fastify.register(cogitatorPlugin, {
  cogitator,
  agents: { math: mathAgent },
  rateLimit: {
    max: 100,
    timeWindow: '1 minute',
  },
});
```

`keyGenerator` and `errorResponseBuilder` are passed through to `@fastify/rate-limit`. If it is not installed, the plugin logs a warning and continues without throttling. Limited requests receive `429` with code `RATE_LIMIT_EXCEEDED`.

## Swagger & OpenAPI

When `enableSwagger` is `true`, the plugin registers `@fastify/swagger` and `@fastify/swagger-ui` (a missing package logs a warning and skips them). The spec is built by `@fastify/swagger` from the Fastify route schemas, so it describes the generic routes (`/agents/{name}/run`, …) with their validated bodies rather than one path per agent. Swagger UI is at `<prefix>/docs` and the JSON spec at `<prefix>/docs/json`; there is no `/openapi.json` route.

```typescript
await fastify.register(cogitatorPlugin, {
  cogitator,
  agents: { math: mathAgent },
  enableSwagger: true,
  swagger: {
    title: 'My AI API',
    description: 'Production agent runtime',
    version: '2.0.0',
    contact: { name: 'Team', email: 'team@example.com' },
    servers: [{ url: 'https://api.example.com', description: 'Production' }],
  },
});
```

The `swagger` options fill the document's `info` and `servers`; `servers` defaults to `[{ url: '/' }]`. The document declares six tags: `agents`, `threads`, `tools`, `workflows`, `swarms`, and `health`.

## WebSocket

When `enableWebSocket` is `true`, the plugin registers `@fastify/websocket` and exposes a WebSocket endpoint:

```typescript
await fastify.register(cogitatorPlugin, {
  cogitator,
  agents: { math: mathAgent },
  enableWebSocket: true,
  websocket: {
    path: '/ws', // relative to the prefix: ws://host/cogitator/ws
  },
});
```

Run an agent with `{ "type": "run", "id": "r1", "payload": { "type": "agent", "name": "math", "input": "6 * 7?" } }` (add `threadId` to continue a thread). Events arrive as `{ "type": "event", "id": "r1", "payload": { "type": "token" | "reasoning" | "tool-call" | "tool-result" | "complete" | "cancelled", ... } }`, failures as `{ "type": "error", "id": "r1", "error": "..." }`. Resume a run paused for approval with `{ "type": "resume", "id": "r2", "payload": { "name": "math", "threadId": "...", "decisions": { "<toolCallId>": { "approved": true } } } }`, send `{ "type": "stop" }` to abort the run and `{ "type": "ping" }` to get a `pong`.

One run per connection at a time — a second `run` or `resume` answers `A run is already in progress` — and only agent runs are supported. `{ "type": "subscribe", "channel": "agent:math" }` delivers the events of runs of that agent started from the same user's other connections (with `channel` set on each frame) until `unsubscribe`; up to 64 channels per connection. A subscriber never receives another user's runs; without `auth` all connections share one anonymous user and see every run. Closing the socket aborts its run.

## Streaming

The streaming endpoints use SSE, identical to other adapters: `text-*` blocks closed around tool calls, `tool-call-*` events whose `id` matches `tool-result.toolCallId`, then `finish` and `[DONE]` (or an `error` event). `FastifyStreamWriter` hijacks the reply and keeps headers set by your hooks (CORS, rate limit). Disconnecting aborts the run:

```typescript
const response = await fetch('http://localhost:3000/cogitator/agents/math/stream', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ input: 'What is 42 * 17?' }),
});

const reader = response.body!.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(decoder.decode(value));
}
```

## Accessing the Context

Inside custom Fastify routes, the Cogitator context is available via the `cogitator` decorator:

```typescript
fastify.get('/custom', async (request, reply) => {
  const { runtime, agents, workflows, swarms } = fastify.cogitator;
  const result = await runtime.run(agents.math, { input: '2 + 2' });
  return { answer: result.output };
});
```

## Errors

Schema validation failures and malformed JSON return `400 INVALID_INPUT` (with the validation `details`), unknown agents, workflows and swarms `404 NOT_FOUND`, and a failed workflow `500 WORKFLOW_FAILED`. The thread routes return `503 UNAVAILABLE` when the runtime has no memory and `403 THREAD_ACCESS_DENIED` for another user's thread; workflow and swarm routes return `501 UNIMPLEMENTED` without their packages. `CogitatorError`s keep their message, code and mapped status (for example `LLM_RATE_LIMITED` → 429). Any other error is logged through `request.log` and answered as `500` with the message `Internal server error` (code `INTERNAL_ERROR`).

## Exported Schemas

The package exports JSON Schema objects for request/response validation:

```typescript
import {
  AgentRunRequestSchema,
  AgentRunResponseSchema,
  AgentResumeRequestSchema,
  AddMessageRequestSchema,
  WorkflowRunRequestSchema,
  SwarmRunRequestSchema,
} from '@cogitator-ai/fastify';
```

`AgentRunRequestSchema` and `SwarmRunRequestSchema` require an `input` with more than whitespace (the `RUN_INPUT_SCHEMA` of `@cogitator-ai/server-shared`), `AgentResumeRequestSchema` a `threadId` with optional `decisions` / `defaultDecision` (the swarm `timeout` must be greater than 0 and at most `MAX_RUN_TIMEOUT_MS`), `AddMessageRequestSchema` requires a non-empty `content` and a `user`/`assistant`/`system` role, and `WorkflowRunRequestSchema.options` only allows `maxConcurrency`/`maxIterations` (integers ≥ 1) and `checkpoint` (`additionalProperties: false`). These can be used in custom Fastify route schemas for native validation.
