FabricFabricHarness
Building Agents

Persistent Agents & Dispatch

Long-lived, addressable agent instances with cross-call sessions, async dispatch, and a streaming WebSocket conversation.

Fabric Harness has two authoring surfaces. A job is a finite, run-once execution; a persistent agent is a long-lived, URL-addressable instance whose sessions continue across calls. (Fabric names the finite surface job because workflow is reserved for Temporal durable execution.)

Finite jobPersistent agent
Author withagent({ run }) (or job({ run }))createAgent(({ id, env }) => config)
Directory.fabricharness/jobs/.fabricharness/agents/
InvokePOST /jobs/:name{ result, runId }POST /agents/:name/:id with { message, session? }
Lifetimeone run, returns a resultlong-lived instance; sessions persist across calls
Async / streamingtracked under /runsdispatch() + GET /agents/:name/:id WebSocket

Migration window. Finite jobs may still live in .fabricharness/agents/ and POST /agents/:name/:id still invokes them (with a Deprecation header pointing to /jobs/:name). That alias is removed in a later minor — put new finite jobs in jobs/.

Defining a persistent agent

A persistent agent module default-exports createAgent(...). The initializer receives the instance id and returns runtime configuration (resolved fresh per interaction):

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

export default createAgent(({ id }) => ({
  model: 'anthropic/claude-sonnet-4-6',
  instructions: `You are a long-lived assistant for ${id}.`,
}));

PersistentAgentConfig mirrors the agent-level slice of init()model, tools, skills, sandbox, policy, cwd, thinkingLevel, compaction — plus instructions (the session system prompt) and subagents (named roles).

Direct prompts

POST /agents/:name/:id treats :id as the instance id and resumes the instance's named session, so it continues across calls:

# Same instance "u1" → one continuing conversation
curl -XPOST localhost:4317/agents/assistant/u1 -d '{"message":"remember my name is Ada"}'
curl -XPOST localhost:4317/agents/assistant/u1 -d '{"message":"what is my name?"}'

GET /agents/:name/:id reads the instance's default session. Concurrent prompts to the same (name, instance, session) return 409.

Async dispatch

dispatch() hands an input to an instance for asynchronous processing, returning a receipt immediately:

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

const receipt = await dispatch({ agent: 'assistant', id: 'u1', input: 'summarize today' });
// { dispatchId, acceptedAt }

Over HTTP, POST /agents/:name/:id/dispatch returns 202 + the receipt:

curl -XPOST localhost:4317/agents/assistant/u1/dispatch -d '{"input":"summarize today"}'

Dispatch processing is idempotent by dispatchId (a re-delivered dispatch is applied at most once). The fh dev server uses an in-process queue; with runtime: 'temporal', durable delivery uses temporalDispatchQueue — dispatches survive worker/process restarts.

Streaming conversation (WebSocket)

GET /agents/:name/:id upgrades to a conversational WebSocket:

const ws = new WebSocket('ws://localhost:4317/agents/assistant/u1');
// server → { type: 'ready', target: 'agent', name, instanceId }
ws.send(JSON.stringify({ type: 'prompt', requestId: 'r1', message: 'hello' }));
// server → { type: 'started', requestId }
//        → { type: 'event', requestId, event }   (streamed, repeated)
//        → { type: 'result', requestId, result, session }

Send { type: 'ping' } for a pong heartbeat. Prompts on one connection are serialized.

Relationship to Flue

Persistent agents are Fabric's adoption of the other half of Flue's agent/workflow split. A Flue workflow maps to a Fabric job; a Flue persistent agent maps to a Fabric persistent agent. See Flue migration.