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 job | Persistent agent | |
|---|---|---|
| Author with | agent({ run }) (or job({ run })) | createAgent(({ id, env }) => config) |
| Directory | .fabricharness/jobs/ | .fabricharness/agents/ |
| Invoke | POST /jobs/:name → { result, runId } | POST /agents/:name/:id with { message, session? } |
| Lifetime | one run, returns a result | long-lived instance; sessions persist across calls |
| Async / streaming | tracked under /runs | dispatch() + GET /agents/:name/:id WebSocket |
Migration window. Finite jobs may still live in
.fabricharness/agents/andPOST /agents/:name/:idstill invokes them (with aDeprecationheader pointing to/jobs/:name). That alias is removed in a later minor — put new finite jobs injobs/.
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.