# Koa Adapter

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

> 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

```bash
pnpm add @cogitator-ai/koa koa @koa/router
```

## Basic Setup

```typescript
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

```typescript
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`](#websocket), 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`.
* `CogitatorError`s 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.

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
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.
