# Server Adapters

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

> Expose your Cogitator agents, workflows, and swarms as REST APIs with built-in streaming, WebSocket, and OpenAPI support.

Cogitator ships five server adapters that turn your agent runtime into an HTTP API. They share the same routes, request bodies and SSE streaming protocol; they differ in how they are mounted, how OpenAPI is generated and what their WebSocket endpoint can do. Pick the one that matches your stack.

## Available Adapters

| Package                 | Framework | Min Version | Style                            |
| ----------------------- | --------- | ----------- | -------------------------------- |
| `@cogitator-ai/express` | Express   | 4.18+       | Class-based `CogitatorServer`    |
| `@cogitator-ai/fastify` | Fastify   | 5.x         | Plugin `cogitatorPlugin`         |
| `@cogitator-ai/hono`    | Hono      | 4.x         | Factory `cogitatorApp`           |
| `@cogitator-ai/koa`     | Koa       | 2.15+       | Factory `cogitatorApp`           |
| `@cogitator-ai/tetsu`   | Tetsu     | 0.6+ (Bun)  | Controller `cogitatorController` |

## Common Endpoints

Every adapter registers these routes. Express (`config.basePath`) and Fastify (`prefix`) mount them under `/cogitator` by default; Hono, Koa and Tetsu serve them where you mount the app, router or controller.

| Method   | Path                       | Description                                                                |
| -------- | -------------------------- | -------------------------------------------------------------------------- |
| `GET`    | `/health`                  | Health check: `{ status, uptime, timestamp }`                              |
| `GET`    | `/ready`                   | Readiness probe: `{ status: 'ok' }`                                        |
| `GET`    | `/agents`                  | List agents: name, `description`, tool names (never `instructions`)        |
| `POST`   | `/agents/:name/run`        | Run an agent (JSON response)                                               |
| `POST`   | `/agents/:name/stream`     | Run an agent (SSE stream)                                                  |
| `POST`   | `/agents/:name/resume`     | Resume a run paused for [approvals](/docs/tools/approvals)                 |
| `GET`    | `/threads/:id`             | Get thread messages                                                        |
| `POST`   | `/threads/:id/messages`    | Add a message to a thread                                                  |
| `DELETE` | `/threads/:id`             | Delete a thread                                                            |
| `GET`    | `/tools`                   | List the tools of all agents (parameters as JSON Schema)                   |
| `GET`    | `/workflows`               | List workflows                                                             |
| `POST`   | `/workflows/:name/run`     | Execute a workflow                                                         |
| `POST`   | `/workflows/:name/stream`  | Stream workflow execution                                                  |
| `GET`    | `/swarms`                  | List swarms                                                                |
| `POST`   | `/swarms/:name/run`        | Execute a swarm                                                            |
| `POST`   | `/swarms/:name/stream`     | Stream swarm execution                                                     |
| `GET`    | `/swarms/:name/blackboard` | The swarm's configured blackboard sections (only when it is enabled)       |
| `GET`    | `/openapi.json`, `/docs`   | OpenAPI spec and Swagger UI — see [OpenAPI](/docs/server-adapters/openapi) |

Tetsu also serves `POST /agents/:name/resume/stream`. Fastify serves its spec at `/docs/json` instead of `/openapi.json`, and Tetsu's document comes from `docs()` of `@tetsujs/openapi`.

Request bodies:

* Agent run and stream: `{ input, context?, threadId? }`. `input` must contain more than whitespace (`""` and `"   "` are refused before the model is called). Express, Fastify, Hono and Koa share one validator from `@cogitator-ai/server-shared` and Tetsu applies the same rule in its schema, so every adapter refuses the same bodies.
* Resume: `{ threadId, decisions?, defaultDecision? }`, where a decision is `{ approved: boolean, reason?: string }` keyed by tool call id.
* Workflow: `{ input?, options? }` with `options` limited to `maxConcurrency`, `maxIterations` and `checkpoint`.
* Swarm: `{ input, context?, threadId?, timeout? }`.
* Thread message: `{ role: 'user' | 'assistant' | 'system', content, metadata? }`.

`POST /agents/:name/run` answers `{ output, threadId, usage, toolCalls, reasoning?, status, pendingApprovals? }`; a run paused for approval has `status: 'paused'`, and its checkpoint stays on the server. `usage` carries `inputTokens`, `outputTokens` and `totalTokens`, plus `reasoningTokens`, `cachedInputTokens` and `cacheWriteTokens` when the model reported them, on every adapter. The `finish` event of an agent stream carries the same `usage`. Neither includes the run's cost or duration.

## Threads and Users

The thread routes use the runtime's memory (`cogitator.getMemory()`); without a memory adapter they answer `503`. A thread belongs to the user who created it — through a run with a `threadId` or `POST /threads/:id/messages` — and another user's thread answers `403 THREAD_ACCESS_DENIED`. The user is the `userId` your `auth` function returns, so set one whenever several people share a server ([Multiple Users](/docs/advanced/multi-user)).

## Errors

Errors are JSON: `{ "error": { "message": "...", "code": "..." } }` on Express, Fastify, Hono and Koa, and Tetsu's envelope `{ status, message, error }` on Tetsu.

* A `CogitatorError` keeps its message, its code and the status mapped to that code — for example `429 LLM_RATE_LIMITED` or `403 THREAD_ACCESS_DENIED`.
* Any other error is logged on the server and answered as `500` with the message `Internal server error`, so internals never reach the client.
* Unknown agents, workflows and swarms answer `404`; invalid bodies answer `400 INVALID_INPUT` (`422 VALIDATION_FAILED` on Tetsu).
* Workflow and swarm routes need `@cogitator-ai/workflows` and `@cogitator-ai/swarms`; without them they answer `501`.

In a stream, the same errors end the stream with an `error` event instead of `finish`.

## Streaming Protocol

Every adapter implements the same SSE protocol from `@cogitator-ai/server-shared`. The response is `text/event-stream` with one JSON event per `data:` line, ending with `data: [DONE]`:

```
data: {"type":"start","messageId":"msg_abc123"}

data: {"type":"text-start","id":"txt_def456"}

data: {"type":"text-delta","id":"txt_def456","delta":"Let me check."}

data: {"type":"text-end","id":"txt_def456"}

data: {"type":"tool-call-start","id":"call_ghi789","toolName":"get_weather"}

data: {"type":"tool-call-delta","id":"call_ghi789","argsTextDelta":"{\"city\":\"Paris\"}"}

data: {"type":"tool-call-end","id":"call_ghi789"}

data: {"type":"tool-result","id":"res_jkl012","toolCallId":"call_ghi789","result":"Sunny"}

data: {"type":"text-start","id":"txt_mno345"}

data: {"type":"text-delta","id":"txt_mno345","delta":"It is sunny in Paris."}

data: {"type":"text-end","id":"txt_mno345"}

data: {"type":"finish","messageId":"msg_abc123","usage":{"inputTokens":50,"outputTokens":120,"totalTokens":170,"reasoningTokens":64,"cachedInputTokens":32}}

data: [DONE]
```

| Event                                                   | Fields                                               |
| ------------------------------------------------------- | ---------------------------------------------------- |
| `start`                                                 | `messageId`                                          |
| `text-start` / `text-delta` / `text-end`                | `id`, `delta`                                        |
| `reasoning-start` / `reasoning-delta` / `reasoning-end` | `id`, `delta`                                        |
| `tool-call-start` / `tool-call-delta` / `tool-call-end` | `id` (the tool call id), `toolName`, `argsTextDelta` |
| `tool-result`                                           | `id`, `toolCallId`, `result`                         |
| `approval-required`                                     | `threadId`, `approvals`                              |
| `error`                                                 | `message`, `code?`                                   |
| `finish`                                                | `messageId`, `usage?`                                |
| `workflow` / `swarm`                                    | `event`, `data`                                      |

The `finish` event of an agent stream (and of a resumed one) carries the run's `usage`, the same object as in the JSON answer of `POST /agents/:name/run`: `inputTokens`, `outputTokens`, `totalTokens`, and `reasoningTokens`, `cachedInputTokens` and `cacheWriteTokens` when the model reported them. Workflow and swarm streams finish without `usage`. `createFinishEvent` builds it with `toRunUsage`, the converter of the JSON answer, so a run's `RunResult.usage` can be passed as is.

Text and reasoning blocks are closed before a tool call starts. Agents with `reasoning: { summary: true }` stream their reasoning as its own part before the text it leads to. A run that pauses for approval sends `approval-required` with the pending calls (`toolCallId`, `toolName`, `arguments`, `description`, `sideEffects?`) right before `finish`; resume it with `POST /agents/:name/resume`.

Every stream also writes a heartbeat comment, `: keep-alive`, every 5 seconds while it is open (`sseHeartbeatMs` on each adapter, `0` turns it off). SSE clients skip comments, and the heartbeat keeps a stream that waits on a slow tool or model from being closed by an idle timeout: `Bun.serve` closes a connection silent for 10 seconds, nginx one silent for 60.

Workflow streams send `workflow` events named `node_started`, `node_completed`, `node_error`, `node_progress` and `workflow_completed`; swarm streams send `swarm` events named `agent_start`, `agent_complete`, `agent_error`, `message`, the swarm's own events, and `swarm_completed`. Disconnecting the client aborts the run.

The event types (`StreamEvent`, `TextDeltaEvent`, …), their factories (`createTextDeltaEvent`, `createFinishEvent`, …) and the `encodeSSE` / `encodeDone` helpers are exported from `@cogitator-ai/server-shared`, and the adapters re-export the event types.

## Long-Running Runs

An agent waiting on a slow tool or a reasoning model can stay silent for tens of seconds. Anything between the client and the server that closes idle connections then cuts the answer: the client sees the connection close (`terminated`, `other side closed`, `socket hang up` and the like) and a stream ends without `finish` and `data: [DONE]`.

* **SSE streams.** Every adapter writes a `: keep-alive` comment every `sseHeartbeatMs` (5 seconds by default) while a stream is open, and SSE clients skip comments. Leave it on unless something else keeps the connection alive. `0` turns it off.
* **Bun.** `Bun.serve` closes a connection that stays silent for `idleTimeout` seconds, 10 by default. The [Hono](/docs/server-adapters/hono#long-runs-on-bun) adapter and the [Tetsu](/docs/server-adapters/tetsu#long-runs-on-bun) controller handle it for you: streams send heartbeats and the JSON routes lift the idle timeout for their own request. If you set `idleTimeout` yourself, keep it at 5 seconds or more and above `sseHeartbeatMs`. Routes you add next to the adapter need the same care: call `server.timeout(request, 0)` in a long JSON route, or send heartbeats in your own streams.
* **Reverse proxies and load balancers.** Heartbeats keep streams open through them, but a JSON `run` answers only when the run ends. Raise the read or idle timeout above your longest run, or use the `stream` route for long runs. Common defaults: nginx `proxy_read_timeout` 60 seconds, AWS Application Load Balancer idle timeout 60 seconds, Cloudflare 100 seconds. For SSE also turn off response buffering in the proxy (nginx: `proxy_buffering off;`), or events arrive in bursts.

## WebSocket

All adapters offer a WebSocket endpoint, but not the same one:

| Adapter | How to enable                                               | Default path      | Client messages                                             | Runs over WebSocket       |
| ------- | ----------------------------------------------------------- | ----------------- | ----------------------------------------------------------- | ------------------------- |
| Express | `enableWebSocket: true`, then `attachWebSocket(httpServer)` | `<basePath>/ws`   | `run`, `resume`, `stop`, `ping`, `subscribe`, `unsubscribe` | agents                    |
| Fastify | `enableWebSocket: true` with `@fastify/websocket` installed | `<prefix>/ws`     | `run`, `resume`, `stop`, `ping`, `subscribe`, `unsubscribe` | agents                    |
| Hono    | `enableWebSocket: true` and `websocket.upgradeWebSocket`    | `/ws` under mount | `run`, `resume`, `stop`, `ping`                             | agents, workflows, swarms |
| Koa     | `setupWebSocket(httpServer, ctx, options)`                  | `/ws`             | `run`, `resume`, `stop`, `ping`                             | agents, workflows, swarms |
| Tetsu   | `websocket: true`                                           | `/ws` under group | `run`, `resume`, `stop`, `ping`                             | agents, workflows, swarms |

The shared part of the protocol:

```json
{ "type": "run", "id": "r1", "payload": { "type": "agent", "name": "assistant", "input": "Hello" } }
{ "type": "resume", "id": "r2", "payload": { "name": "assistant", "threadId": "thread_1", "decisions": { "call_1": { "approved": true } } } }
{ "type": "stop" }
{ "type": "ping", "id": "p1" }
```

The server answers with `{ "type": "event", "id": "r1", "payload": { ... } }` frames whose payload `type` is `token` (`delta`), `reasoning` (`delta`), `tool-call`, `tool-result`, and finally `complete` (with the `result`) or `cancelled`; with `{ "type": "error", "id", "error" }` for failures (Tetsu adds a `code`); and with `pong`. Runs get the connection's `userId`, and closing the socket aborts its run.

One run at a time per connection: Fastify, Hono, Koa and Tetsu refuse a second `run` with `A run is already in progress`, while Express aborts the running one and starts the new one. `subscribe` / `unsubscribe` exist only on Express and Fastify — see their pages.

## Authentication

Each adapter takes an `auth` function that receives the framework's request object and returns an `AuthContext` (`{ userId?, roles?, permissions?, metadata? }`). Throwing rejects the request with `401 UNAUTHORIZED`. Returning `undefined` lets the request through anonymously on Express, Fastify, Hono and Koa; on Tetsu it answers `401`. The `userId` reaches every agent and swarm run (tools see it as `context.userId`) and scopes the thread routes. Rate limiting is built into Express and Fastify; CORS into Express.

## Choosing an Adapter

| Use case                                  | Recommended             |
| ----------------------------------------- | ----------------------- |
| Existing Express codebase                 | `@cogitator-ai/express` |
| High-performance, schema validation       | `@cogitator-ai/fastify` |
| Edge / multi-runtime (Workers, Bun, Deno) | `@cogitator-ai/hono`    |
| Koa-based middleware stack                | `@cogitator-ai/koa`     |
| Bun app built on Tetsu controllers        | `@cogitator-ai/tetsu`   |

## Quick Install

```bash
# pick one
pnpm add @cogitator-ai/express express
pnpm add @cogitator-ai/fastify fastify
pnpm add @cogitator-ai/hono hono
pnpm add @cogitator-ai/koa koa @koa/router
bun add @cogitator-ai/tetsu @tetsujs/core @tetsujs/sse
```

## Next Steps

* [Express Adapter](/docs/server-adapters/express) — class-based setup with middleware
* [Fastify Adapter](/docs/server-adapters/fastify) — native Fastify plugin with schema validation
* [Hono Adapter](/docs/server-adapters/hono) — multi-runtime edge deployment
* [Koa Adapter](/docs/server-adapters/koa) — middleware-chain pattern
* [Tetsu Adapter](/docs/server-adapters/tetsu) — a Tetsu controller on Bun
* [OpenAPI Generation](/docs/server-adapters/openapi) — customizing the generated spec
