# HarnessServer (/docs/server)



`HarnessServer` is one surface in the **frontend integration**
[thematic island](/docs/concept-clusters#what-lives-outside-the-cluster-pattern) —
siblings of [react](/docs/react), [api-routes](/docs/api-routes),
[query](/docs/query), and [devtools](/docs/devtools). Wiring
surfaces, not concept triplets.

`HarnessServer` is a set of pure request-to-response handlers that
wrap a `SessionRuntime`. It does not bind a port. Each handler
mounts into whatever HTTP framework already owns your transport —
Next.js route segments, Express middleware, Hono routes, raw
`node:http`.

<Callout type="warn">
  **`@pleach/core/server` is not a published subpath today.**
  `HarnessServer` + `ROUTES` are real classes, but they are an internal
  substrate surface — the `./server` export key is not yet in
  `@pleach/core`'s `package.json`, so the imports below resolve only
  inside the monorepo, not for an external consumer. For a published,
  ready-to-mount route handler use `createPleachRoute` from
  [`@pleach/core/quickstart`](/docs/api-routes) (a single Web-standard
  POST handler). This page documents the fuller handler set for
  reference and for the day the subpath is promoted.
</Callout>

```typescript
// Internal substrate path — not a published @pleach/core subpath yet.
import {
  HarnessServer,
  ROUTES,
  type HarnessServerConfig,
} from "@pleach/core/server";
```

`HarnessServer` is the substrate-level handler set; the Next.js
adapter that ships at [`/docs/api-routes`](/docs/api-routes) is one
mount. The route paths and shapes match — picking between them is
about which framework owns your HTTP layer.

<SourceMeta source="{ label: &#x22;src/server/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/server&#x22; }" />

## Configuration [#configuration]

```typescript
interface HarnessServerConfig {
  provider:      ServerProvider;       // executes messages, yields stream events
  storage:       ServerStorage;        // session CRUD
  checkpointer?: ServerCheckpointer;   // enables checkpoints + rollback routes
  auth?:         ServerAuthProvider;   // surfaces on /health features map
  tools?:        ToolRegistry;         // enables /tools + /tools/:name
  port?:         number;               // informational only
  hostname?:     string;               // informational only
  cors?:         { origins, methods?, headers? };
}
```

`provider` is the seam the execute routes call into — typically a thin
adapter that calls `runtime.executeMessage(...)` and yields each
stream event. `storage` mirrors the `StorageAdapter` shape but with
loosened types so the server stays decoupled from the full
`SessionState` envelope.

Calling `start()` flips an `isRunning()` flag and nothing else; it's
useful for health-check gating but binds no socket.

## The `ROUTES` constant [#the-routes-constant]

`ROUTES` is the canonical path table. Mount handlers against these
strings so a client built from `ROUTES` and a server built from
`ROUTES` stay in lockstep.

| Constant              | Path                                           | Handler                                      |
| --------------------- | ---------------------------------------------- | -------------------------------------------- |
| `ROUTES.HEALTH`       | `/health`                                      | `handleHealth`                               |
| `ROUTES.SESSIONS`     | `/sessions`                                    | `handleCreateSession` / `handleListSessions` |
| `ROUTES.SESSION`      | `/sessions/:sessionId`                         | `handleGetSession` / `handleDeleteSession`   |
| `ROUTES.EXECUTE`      | `/sessions/:sessionId/execute`                 | `handleExecuteMessage` (SSE)                 |
| `ROUTES.EXECUTE_SYNC` | `/sessions/:sessionId/execute/sync`            | `handleExecuteMessageSync` (buffered JSON)   |
| `ROUTES.INTERRUPT`    | `/sessions/:sessionId/interrupts/:interruptId` | `handleResolveInterrupt`                     |
| `ROUTES.CHECKPOINTS`  | `/sessions/:sessionId/checkpoints`             | `handleListCheckpoints`                      |
| `ROUTES.ROLLBACK`     | `/sessions/:sessionId/rollback`                | `handleRollback`                             |
| `ROUTES.TOOLS`        | `/tools`                                       | `handleListTools`                            |
| `ROUTES.TOOL`         | `/tools/:toolName`                             | `handleGetTool`                              |

The Next.js handlers under `/api/harness/*` add an extra `sync` route
for version-vector merge that `HarnessServer` does not ship. If
sync is load-bearing, mount the Next.js adapter directly or proxy
the sync endpoint to your own implementation.

## Handler signatures [#handler-signatures]

Every handler returns one of two shapes:

```typescript
interface HandlerResponse {
  status:   number;
  body:     unknown;
  headers?: Record<string, string>;
}

interface SSEResponse {
  status:  number;
  headers: Record<string, string>;
  stream:  AsyncIterable<string>;   // already-formatted SSE frames
}
```

`HandlerResponse` is for buffered JSON; `SSEResponse` is the streaming
path. The frame format the server emits is one of:

```
event: <type>
data: <json>

event: done
data: {}
```

`event: done` is yielded at end-of-stream so a client can distinguish
clean close from disconnect without inspecting the underlying socket.

### Session handlers [#session-handlers]

| Handler                                    | Input  | Returns                     |
| ------------------------------------------ | ------ | --------------------------- |
| `handleCreateSession({ userId, config? })` | body   | 201 + seeded `SessionState` |
| `handleGetSession({ sessionId })`          | params | 200 + state, or 404         |
| `handleListSessions({ userId?, limit? })`  | query  | 200 + array                 |
| `handleDeleteSession({ sessionId })`       | params | 204                         |

`handleCreateSession` seeds the envelope (`id`, `version: 1`, empty
arrays for messages / tool calls / jobs / artifacts) and merges
`config` last. The id is a fresh `crypto.randomUUID()`.

### Execution handlers [#execution-handlers]

| Handler                    | Response                                          |
| -------------------------- | ------------------------------------------------- |
| `handleExecuteMessage`     | SSE stream of events from `provider.execute`      |
| `handleExecuteMessageSync` | Buffered JSON: `{ events, message, toolResults }` |

`handleExecuteMessageSync` drains the provider stream and extracts the
`message.complete` event into `message` and every `tool.completed`
event into `toolResults`. Use it for non-streaming clients (cron jobs,
batch workers, integration tests).

A buffered call from a batch worker, no SSE wiring:

```typescript
const result = await server.handleExecuteMessageSync(
  { sessionId },
  { message: "Summarize today's queue." },
);

if (result.status === 200) {
  const body = result.body as { message: unknown; toolResults: unknown[] };
  await writeReport(sessionId, body.message, body.toolResults);
}
```

### Checkpoint handlers [#checkpoint-handlers]

Both checkpoint routes return `501` when `checkpointer` is not
configured — the body is `{ error: "Checkpointer not configured" }`,
not a generic 500.

| Handler                                           | Notes                                                                           |
| ------------------------------------------------- | ------------------------------------------------------------------------------- |
| `handleListCheckpoints({ sessionId })`            | Drains the checkpointer's `list` async iterable into an array                   |
| `handleRollback({ sessionId }, { checkpointId })` | Reads the checkpoint, writes `checkpoint.state` back to `storage.updateSession` |

The rollback here is the wire-level operation — it replays the stored
state without the in-process bookkeeping (version-vector bump,
`source: "rollback"` checkpoint write) that
`runtime.checkpoints.rollback` does. If you need that bookkeeping,
mount the Next.js handler or call the runtime method directly behind
your own route.

## Mounting examples [#mounting-examples]

### Next.js App Router [#nextjs-app-router]

```typescript
// app/api/harness/[...path]/route.ts
// Internal substrate path — not a published @pleach/core subpath yet.
import { HarnessServer, ROUTES } from "@pleach/core/server";

const server = new HarnessServer({ provider, storage, checkpointer });
server.start();

export async function POST(req: Request, { params }: { params: { path: string[] } }) {
  const [resource, sessionId, action] = params.path;
  if (resource === "sessions" && !sessionId) {
    const body = (await req.json()) as { userId: string; config?: Record<string, unknown> };
    const r = await server.handleCreateSession(body);
    return Response.json(r.body, { status: r.status });
  }
  if (resource === "sessions" && action === "execute") {
    const body = (await req.json()) as { message: string; options?: Record<string, unknown> };
    const r = await server.handleExecuteMessage({ sessionId }, body);
    return new Response(toReadableStream(r.stream), { status: r.status, headers: r.headers });
  }
  // ... etc
}
```

### Express [#express]

```typescript
import express from "express";
// Internal substrate path — not a published @pleach/core subpath yet.
import { HarnessServer, ROUTES } from "@pleach/core/server";

const app = express();
const server = new HarnessServer({ provider, storage });

app.post(ROUTES.SESSIONS, async (req, res) => {
  const r = await server.handleCreateSession(req.body);
  res.status(r.status).json(r.body);
});

app.post(ROUTES.EXECUTE, async (req, res) => {
  const r = await server.handleExecuteMessage({ sessionId: req.params.sessionId }, req.body);
  res.writeHead(r.status, r.headers);
  for await (const frame of r.stream) res.write(frame);
  res.end();
});
```

### Hono [#hono]

```typescript
import { Hono } from "hono";
import { streamSSE } from "hono/streaming";

const app = new Hono();
app.post(ROUTES.EXECUTE, (c) =>
  streamSSE(c, async (stream) => {
    const r = await server.handleExecuteMessage({ sessionId: c.req.param("sessionId") }, await c.req.json());
    for await (const frame of r.stream) await stream.write(frame);
  }),
);
```

The pattern is the same in every framework: route the framework's
`(req, params, body)` into the matching `handle*` method, then serialize
the `HandlerResponse` or `SSEResponse` back into whatever the framework
expects.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="API routes" href="/docs/api-routes" description="The Next.js reference handlers with the full route catalog and SSE wire format." />

  <Card title="React" href="/docs/react" description="The client-side hooks that consume these handlers over HTTP + SSE." />

  <Card title="Query" href="/docs/query" description="Server-side read API over persisted harness data — pairs with these write-path handlers." />

  <Card title="DevTools" href="/docs/devtools" description="Browser-console surface for inspecting what the server returns." />
</Cards>
