Cogitator
Server Adapters

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

PackageFrameworkMin VersionStyle
@cogitator-ai/expressExpress4.18+Class-based CogitatorServer
@cogitator-ai/fastifyFastify5.xPlugin cogitatorPlugin
@cogitator-ai/honoHono4.xFactory cogitatorApp
@cogitator-ai/koaKoa2.15+Factory cogitatorApp
@cogitator-ai/tetsuTetsu0.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.

MethodPathDescription
GET/healthHealth check: { status, uptime, timestamp }
GET/readyReadiness probe: { status: 'ok' }
GET/agentsList agents: name, description, tool names (never instructions)
POST/agents/:name/runRun an agent (JSON response)
POST/agents/:name/streamRun an agent (SSE stream)
POST/agents/:name/resumeResume a run paused for approvals
GET/threads/:idGet thread messages
POST/threads/:id/messagesAdd a message to a thread
DELETE/threads/:idDelete a thread
GET/toolsList the tools of all agents (parameters as JSON Schema)
GET/workflowsList workflows
POST/workflows/:name/runExecute a workflow
POST/workflows/:name/streamStream workflow execution
GET/swarmsList swarms
POST/swarms/:name/runExecute a swarm
POST/swarms/:name/streamStream swarm execution
GET/swarms/:name/blackboardThe swarm's configured blackboard sections (only when it is enabled)
GET/openapi.json, /docsOpenAPI 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? }. 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).

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]
EventFields
startmessageId
text-start / text-delta / text-endid, delta
reasoning-start / reasoning-delta / reasoning-endid, delta
tool-call-start / tool-call-delta / tool-call-endid (the tool call id), toolName, argsTextDelta
tool-resultid, toolCallId, result
approval-requiredthreadId, approvals
errormessage, code?
finishmessageId, usage?
workflow / swarmevent, 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 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 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:

AdapterHow to enableDefault pathClient messagesRuns over WebSocket
ExpressenableWebSocket: true, then attachWebSocket(httpServer)<basePath>/wsrun, resume, stop, ping, subscribe, unsubscribeagents
FastifyenableWebSocket: true with @fastify/websocket installed<prefix>/wsrun, resume, stop, ping, subscribe, unsubscribeagents
HonoenableWebSocket: true and websocket.upgradeWebSocket/ws under mountrun, resume, stop, pingagents, workflows, swarms
KoasetupWebSocket(httpServer, ctx, options)/wsrun, resume, stop, pingagents, workflows, swarms
Tetsuwebsocket: true/ws under grouprun, resume, stop, pingagents, 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 caseRecommended
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/sse

Next Steps

On this page