HarnessServer
Framework-agnostic HTTP handlers wrapping the runtime — mount the routes into Next.js, Express, Hono, or any HTTP layer.
HarnessServer is one surface in the frontend integration
thematic island —
siblings of react, api-routes,
query, and 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.
@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 (a single Web-standard
POST handler). This page documents the fuller handler set for
reference and for the day the subpath is promoted.
// 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 is one
mount. The route paths and shapes match — picking between them is
about which framework owns your HTTP layer.
Configuration
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
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
Every handler returns one of two shapes:
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
| 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
| 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:
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
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
Next.js App Router
// 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
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
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
API routes
The Next.js reference handlers with the full route catalog and SSE wire format.
React
The client-side hooks that consume these handlers over HTTP + SSE.
Query
Server-side read API over persisted harness data — pairs with these write-path handlers.
DevTools
Browser-console surface for inspecting what the server returns.