FabricFabricHarness
Building Agents

Mounting agents in your own server

Serve the persistent-agent HTTP surface from an application that already owns its routes, using createFabricAgentRouter.

Harness serves agents from its own runtimes — fh dev, the Node build artifact, the Cloudflare worker. createFabricAgentRouter() covers the other direction: an application that already owns an HTTP surface, and wants Harness agents on it, under its own paths and behind its own middleware.

import { createFabricAgentRouter } from '@fabric-harness/sdk';

const router = createFabricAgentRouter(backend, { basePath: '/api' });

const handled = await router.handle(request);
if (handled) return handled;
// …your application keeps everything else.

The router is runtime-neutral — Request, Response, URL, and plain objects — so it runs on Node, Cloudflare, Deno, Bun, or any fetch-capable host, and mounts in any framework that can hand it a Request.

Two entry points

MethodUnmatched pathUse when
handle(request)resolves undefinedYou compose routes and want to fall through.
fetch(request)resolves 404Your framework expects a total handler.

What the router owns

Everything a client observes: route parsing, method dispatch, request validation, response shape, status codes, and the 202 admission receipt. The streaming protocol reference documents that wire contract, and the router is the same implementation the built-in runtimes follow — mounting agents yourself does not put you on a different protocol.

What the backend owns

A FabricAgentRouterBackend supplies durable submission, conversation reads, and lifecycle:

import type { FabricAgentRouterBackend } from '@fabric-harness/sdk';

const backend: FabricAgentRouterBackend = {
  async resolveAgent(name) {
    return { found: registry.has(name), exposed: true };
  },
  async admit(input) {
    // Capture the stream position BEFORE admitting.
    const offset = await streams.nextOffset(input);
    const submissionId = await runner.admit(input);
    return { submissionId, offset };
  },
  async readConversation(address) { /* … */ },
  async getSubmission(address) { /* … */ },
  async abort(address) { /* … */ },
};

loadInstance, deleteInstance, streamUpdates, and handleSchedules are optional. A route whose backend method is absent returns 404 rather than a misleading 405, and updatesUrl appears in the receipt only when streamUpdates exists — the router never points a client at a route the host does not serve.

Two invariants worth stating

Capture the offset before admitting. The receipt's offset is the position a client resumes from; reading it must return this delivery and the reply. Returning "0" instead makes every client re-read the whole conversation on every send.

An unexposed agent must answer like a missing one. resolveAgent returning { found: true, exposed: false } produces exactly the 404 that { found: false } does, so probing cannot enumerate private agents.

Errors

A backend method that throws becomes 500 with a generic body. Storage and execution details — connection strings, stack traces, internal identifiers — never reach the caller. Map your own domain failures to responses inside the backend if you need callers to distinguish them.

Mount paths

basePath is stripped before matching, so the backend never learns where the router lives. URLs in the receipt are relative to the router, which means the host composes its own prefix rather than the router guessing at it.

When not to use this

If you are deploying with fh build, you already have this surface — the Node and Cloudflare targets serve it. Reach for the router when Harness is a component inside a larger service you own, not when it is the service.

See also