Koa Adapter
Integrate Cogitator into your Koa application using the middleware-chain pattern with @koa/router.
The @cogitator-ai/koa package provides cogitatorApp, a factory function that returns a @koa/router instance with all Cogitator routes, middleware, and error handling pre-configured. It follows Koa's middleware-chain philosophy — body parsing, context injection, auth, and error handling are composed as middleware layers.
Installation
pnpm add @cogitator-ai/koa koa @koa/routerBasic Setup
import Koa from 'koa';
import { Cogitator, Agent, tool } from '@cogitator-ai/core';
import { cogitatorApp } from '@cogitator-ai/koa';
import { z } from 'zod';
const app = new Koa();
const cogitator = new Cogitator({
llm: {
defaultModel: 'openai/gpt-6.1-sol',
providers: {
openai: { apiKey: process.env.OPENAI_API_KEY! },
},
},
});
const translator = tool({
name: 'translate',
description: 'Translate text to a target language',
parameters: z.object({
text: z.string(),
targetLanguage: z.string(),
}),
execute: async ({ text, targetLanguage }) => ({
translated: `[${targetLanguage}] ${text}`,
}),
});
const agent = new Agent({
name: 'translator',
description: 'Translates text between languages',
model: 'openai/gpt-6.1-sol',
instructions: 'You translate text between languages. Use the translate tool.',
tools: [translator],
});
const cogitatorRouter = cogitatorApp({
cogitator,
agents: { translator: agent },
enableSwagger: true,
});
app.use(cogitatorRouter.routes());
app.use(cogitatorRouter.allowedMethods());
app.listen(3000, () => {
console.log('Koa server running on http://localhost:3000');
console.log('Swagger UI: http://localhost:3000/docs');
});App Options
interface CogitatorAppOptions {
cogitator: Cogitator;
agents?: Record<string, Agent>;
workflows?: Record<string, Workflow>;
swarms?: Record<string, SwarmConfig>;
auth?: AuthFunction;
enableSwagger?: boolean;
swagger?: SwaggerConfig;
bodyLimit?: number; // max JSON body size in bytes, default 1 MiB
sseHeartbeatMs?: number; // SSE heartbeat interval in ms, default 5000, 0 turns it off
}WebSocket support is attached to the HTTP server with setupWebSocket, not through router options.
Request Handling
- Bodies are validated before anything reaches the runtime:
inputmust contain more than whitespace (""and" "are refused before the model is called),contextan object,threadIda non-empty string, swarmtimeouta positive number of at mostMAX_RUN_TIMEOUT_MS(2147483647, the longest delay a timer can hold). Invalid bodies return400 INVALID_INPUT. - Workflow
optionsare limited tomaxConcurrency,maxIterations(positive integers) andcheckpoint(boolean); other options are dropped and wrongly typed ones return400 INVALID_INPUT. The body itself is optional. - Thread messages accept the roles
user,assistantandsystem;metadatais stored with the entry. - Unknown agents, workflows and swarms return
404 NOT_FOUND; workflow and swarm routes return501 UNIMPLEMENTEDwhen@cogitator-ai/workflowsor@cogitator-ai/swarmsis not installed.GET /swarms/:name/blackboardreturns the configuredblackboard.sections, or400when the blackboard is disabled. - The thread routes use
cogitator.getMemory()and return503 UNAVAILABLEwithout memory; another user's thread returns403 THREAD_ACCESS_DENIED. CogitatorErrors keep their message, HTTP status and code (for example429 LLM_RATE_LIMITED). Other errors with a 4xxstatuskeep it (413 PAYLOAD_TOO_LARGE, otherwise codeHTTP_ERROR); everything else is logged and returned as500with the messageInternal server error. A workflow that ends with an error is answered like the error itself.- Disconnecting clients abort the running agent, workflow or swarm.
GET /agentsexposes each agent'sdescription, never itsinstructions;GET /toolsreturns parameters as JSON Schema.
Middleware Chain
The returned router applies middleware in this order:
- Error handler — catches errors and returns structured JSON responses
- Body parser — parses JSON request bodies up to
bodyLimit(413above it) and reuses a body already parsed by upstream middleware such askoa-bodyparser - Context middleware — injects
CogitatorStateintoctx.state - Auth middleware — optional, runs your auth function
- Route handlers — health, agents, threads, tools, workflows, swarms
- Swagger routes — optional,
/openapi.jsonand/docs, generated per request by the shared@cogitator-ai/server-sharedgenerator
Authentication
The auth function receives a Koa Context and returns an AuthContext. Throwing rejects the request with 401 UNAUTHORIZED (errors with a status >= 500 are treated as server errors instead); returning undefined lets the request through anonymously.
const cogitatorRouter = cogitatorApp({
cogitator,
agents: { translator: agent },
auth: async (ctx) => {
const token = ctx.headers.authorization?.replace('Bearer ', '');
if (!token) throw new Error('Missing token');
const user = await verifyToken(token);
return { userId: user.id, roles: user.roles, permissions: user.permissions };
},
});The auth result is stored in ctx.state.auth for use in downstream middleware, and its userId reaches agent and swarm runs and scopes the thread routes. Auth runs on every Cogitator route, including /health and /ready. Errors thrown by routes after a successful auth check are handled by the error handler, never reported as 401.
Mounting Under a Prefix
Use @koa/router's prefix feature to nest the Cogitator routes:
import Router from '@koa/router';
const root = new Router();
root.get('/', (ctx) => {
ctx.body = { message: 'Welcome' };
});
const cogitatorRouter = cogitatorApp({
cogitator,
agents: { translator: agent },
enableSwagger: true,
});
const prefixed = new Router({ prefix: '/api/v1' });
prefixed.use(cogitatorRouter.routes());
prefixed.use(cogitatorRouter.allowedMethods());
app.use(root.routes());
app.use(prefixed.routes());
app.use(prefixed.allowedMethods());All Cogitator routes are now under /api/v1/agents/..., /api/v1/docs, etc.
Streaming
The KoaStreamWriter manages SSE responses through Koa's raw ctx.res object:
const response = await fetch('http://localhost:3000/agents/translator/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
input: 'Translate "good morning" to Japanese, French, and Spanish.',
}),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(decoder.decode(value));
}WebSocket
WebSocket support requires the ws package. After creating your Koa app, call setupWebSocket with the HTTP server, the route context and the WebSocket options:
import { createServer } from 'http';
import { setupWebSocket } from '@cogitator-ai/koa';
const httpServer = createServer(app.callback());
await setupWebSocket(
httpServer,
{ runtime: cogitator, agents: { translator: agent }, workflows: {}, swarms: {} },
{
path: '/ws',
pingInterval: 30_000,
pingTimeout: 10_000,
maxPayloadSize: 1024 * 1024,
auth: (req) => {
if (req.headers.authorization !== `Bearer ${process.env.API_TOKEN}`) {
throw new Error('Unauthorized');
}
return { userId: 'api' };
},
}
);
httpServer.listen(3000);The second argument is a RouteContext — { runtime, agents, workflows, swarms } — independent of the router, and path is matched against the full request path: the router's prefix does not apply to it. pingInterval (default 30 s) and pingTimeout (default pingInterval) drop clients that stop answering pings; maxPayloadSize defaults to 1 MiB. setupWebSocket resolves to the WebSocketServer, or null when ws is not installed.
The router's auth option does not apply to WebSocket connections; the auth option of setupWebSocket receives the upgrade request and rejects the handshake with 401 when it throws (500 when the error carries a status of 500 or more). Upgrades on other paths are left to other upgrade listeners, or rejected with 404 when there are none.
Clients send:
runwithpayload: { type: 'agent' | 'workflow' | 'swarm', name, input, context?, threadId? }— a workflow gets{ input }as its input state;resumewithpayload: { name, threadId, decisions?, defaultDecision? }to continue an agent run paused for approval;stopto abort the active run, andping.
The server answers with event frames (token, reasoning, tool-call, tool-result for agents, then complete with the result or cancelled), error frames ({ type: 'error', id, error }) and pong. Each connection runs one thing at a time: a second run or resume answers A run is already in progress. There are no subscribe channels. Closing the connection aborts its active run.
State Type
The router is typed with CogitatorState, which provides full type safety in custom middleware:
import type { CogitatorState } from '@cogitator-ai/koa';
import Router from '@koa/router';
const custom = new Router<CogitatorState>();
custom.get('/custom', async (ctx) => {
const { cogitator: routeCtx } = ctx.state;
const { agents, workflows, swarms } = routeCtx;
ctx.body = { agentCount: Object.keys(agents).length };
});Exported Utilities
import {
cogitatorApp,
createContextMiddleware,
createAuthMiddleware,
createBodyParser,
createErrorHandler,
createHealthRoutes,
createAgentRoutes,
createThreadRoutes,
createToolRoutes,
createWorkflowRoutes,
createSwarmRoutes,
createSwaggerRoutes,
KoaStreamWriter,
setupSSEHeaders,
setupWebSocket,
} from '@cogitator-ai/koa';Each create*Routes() function returns a @koa/router instance that you can mount individually.
Hono Adapter
Deploy Cogitator agents to any JavaScript runtime — Node.js, Bun, Deno, and Cloudflare Workers — using the lightweight Hono adapter.
Tetsu Adapter
Serve Cogitator agents, threads, workflows and swarms from a Tetsu application on Bun — one controller with validated requests, SSE, WebSocket and an OpenAPI document.