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 |
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 |
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? }.inputmust contain more than whitespace (""and" "are refused before the model is called). Express, Fastify, Hono and Koa share one validator from@cogitator-ai/server-sharedand 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? }withoptionslimited tomaxConcurrency,maxIterationsandcheckpoint. - 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).
Errors
Errors are JSON: { "error": { "message": "...", "code": "..." } } on Express, Fastify, Hono and Koa, and Tetsu's envelope { status, message, error } on Tetsu.
- A
CogitatorErrorkeeps its message, its code and the status mapped to that code — for example429 LLM_RATE_LIMITEDor403 THREAD_ACCESS_DENIED. - Any other error is logged on the server and answered as
500with the messageInternal server error, so internals never reach the client. - Unknown agents, workflows and swarms answer
404; invalid bodies answer400 INVALID_INPUT(422 VALIDATION_FAILEDon Tetsu). - Workflow and swarm routes need
@cogitator-ai/workflowsand@cogitator-ai/swarms; without them they answer501.
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-alivecomment everysseHeartbeatMs(5 seconds by default) while a stream is open, and SSE clients skip comments. Leave it on unless something else keeps the connection alive.0turns it off. - Bun.
Bun.servecloses a connection that stays silent foridleTimeoutseconds, 10 by default. The Hono adapter and the Tetsu controller handle it for you: streams send heartbeats and the JSON routes lift the idle timeout for their own request. If you setidleTimeoutyourself, keep it at 5 seconds or more and abovesseHeartbeatMs. Routes you add next to the adapter need the same care: callserver.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
runanswers only when the run ends. Raise the read or idle timeout above your longest run, or use thestreamroute for long runs. Common defaults: nginxproxy_read_timeout60 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:
{ "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
# 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/sseNext Steps
- Express Adapter — class-based setup with middleware
- Fastify Adapter — native Fastify plugin with schema validation
- Hono Adapter — multi-runtime edge deployment
- Koa Adapter — middleware-chain pattern
- Tetsu Adapter — a Tetsu controller on Bun
- OpenAPI Generation — customizing the generated spec