Cogitator
Server Adapters

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/router

Basic 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: input must contain more than whitespace ("" and " " are refused before the model is called), context an object, threadId a non-empty string, swarm timeout a positive number of at most MAX_RUN_TIMEOUT_MS (2147483647, the longest delay a timer can hold). Invalid bodies return 400 INVALID_INPUT.
  • Workflow options are limited to maxConcurrency, maxIterations (positive integers) and checkpoint (boolean); other options are dropped and wrongly typed ones return 400 INVALID_INPUT. The body itself is optional.
  • Thread messages accept the roles user, assistant and system; metadata is stored with the entry.
  • Unknown agents, workflows and swarms return 404 NOT_FOUND; workflow and swarm routes return 501 UNIMPLEMENTED when @cogitator-ai/workflows or @cogitator-ai/swarms is not installed. GET /swarms/:name/blackboard returns the configured blackboard.sections, or 400 when the blackboard is disabled.
  • The thread routes use cogitator.getMemory() and return 503 UNAVAILABLE without memory; another user's thread returns 403 THREAD_ACCESS_DENIED.
  • CogitatorErrors keep their message, HTTP status and code (for example 429 LLM_RATE_LIMITED). Other errors with a 4xx status keep it (413 PAYLOAD_TOO_LARGE, otherwise code HTTP_ERROR); everything else is logged and returned as 500 with the message Internal 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 /agents exposes each agent's description, never its instructions; GET /tools returns parameters as JSON Schema.

Middleware Chain

The returned router applies middleware in this order:

  1. Error handler — catches errors and returns structured JSON responses
  2. Body parser — parses JSON request bodies up to bodyLimit (413 above it) and reuses a body already parsed by upstream middleware such as koa-bodyparser
  3. Context middleware — injects CogitatorState into ctx.state
  4. Auth middleware — optional, runs your auth function
  5. Route handlers — health, agents, threads, tools, workflows, swarms
  6. Swagger routes — optional, /openapi.json and /docs, generated per request by the shared @cogitator-ai/server-shared generator

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:

  • run with payload: { type: 'agent' | 'workflow' | 'swarm', name, input, context?, threadId? } — a workflow gets { input } as its input state;
  • resume with payload: { name, threadId, decisions?, defaultDecision? } to continue an agent run paused for approval;
  • stop to abort the active run, and ping.

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.

On this page