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
| Method | Unmatched path | Use when |
|---|---|---|
handle(request) | resolves undefined | You compose routes and want to fall through. |
fetch(request) | resolves 404 | Your 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
examples/with-mounted-agent-router— a runnable version with an in-memory backend.- Streaming protocol — the wire contract this router implements.
- HTTP applications — adding your own routes to a Harness-owned server instead.