Interactive agent tools can rely on a human watching the terminal. Flue cannot. A headless agent invoked by an app, CI job, webhook, or remote caller needs run identity and inspection routes because there may be no person staring at the transcript.
PR #130 is the source event behind this chapter. It added the run registry and public/admin OpenAPI surfaces that make headless inspection work. The important design move is not “more routes.” It is the inspection plane replacing the missing terminal operator.
The source pin for this chapter is withastro/flue@dbaa9effa305561c627c6836559f8a0cbce67875.
Invocation and inspection are separate surfaces; the registry lets a bare runId find its owning run store.
Domain Word
In Flue, a run is a concrete execution record. A run registry is the lookup surface that maps a run ID back to the agent instance and run store that own it.
The invariant is: a caller with only runId can inspect a headless run without knowing the original agent name and instance ID.
The twelve-factor pressure is logs and port binding. A service runtime needs HTTP-visible inspection and event streams, not only in-process callbacks.
Public Route Shape
At the pinned source, packages/runtime/src/runtime/flue-app.ts mounts:
GET /openapi.json
POST /agents/:name/:id
GET /runs/:runId
GET /runs/:runId/events
GET /runs/:runId/stream
The route distinction matters:
| Route | Job |
|---|---|
POST /agents/:name/:id | Invoke a concrete agent instance. |
GET /runs/:runId | Fetch run detail by bare run ID. |
GET /runs/:runId/events | Fetch run events as a bounded list. |
GET /runs/:runId/stream | Replay history, then tail live events with SSE. |
Before the run registry, a caller often had to remember agent and instance routing information. The bare run route changes that contract. Run ID becomes the stable inspection handle.
Registry Lookup
POST /agents/:name/:id │ ├─ creates run record in run store ├─ registers runId -> { agentName, instanceId } └─ returns runId Later: GET /runs/:runId │ ▼ run registry lookup │ returns owning agent + instance ▼ handle-run-routes.ts │ reads run store and event store ▼ detail, events, or replay-then-tail stream
flue-app.ts normalizes the request path after registry lookup, then delegates to handleRunRouteRequest(...) with the resolved agentName, id, runId, runStore, and runSubscribers.
That keeps the public route simple while preserving the internal knowledge needed to find the run.
Run Store vs Run Registry
Run store and run registry are separate concepts.
| Surface | Owns | Does not own |
|---|---|---|
| Run store | Run records, statuses, event records, detail lookup, event list | Global bare-run routing |
| Run registry | Pointer from runId to agentName and instanceId | Event bodies or run details |
| Run subscribers | In-process live event fanout | Durable history |
This split is small but load-bearing. If the registry also became the event store, it would conflate lookup with observation. If the event store had to know global route ownership, every target would need to duplicate dispatch logic.
Events List vs Stream
handle-run-routes.ts supports two different observation modes.
GET /runs/:runId/events returns a list. It accepts query parameters such as after, types, and limit. That is a polling and audit shape.
GET /runs/:runId/stream is SSE. It can replay from a stored event index and then tail live events through the in-process subscriber registry. The stream path also honors Last-Event-ID, dedupes by eventIndex, buffers events that arrive while replay catches up, and caps that replay-time buffer.
The stream route needs both storage and live subscribers. Storage gives replay. Subscribers give tailing. Without both, a reconnecting client either misses history or cannot follow active work.
Run Events Are The Headless Transcript
A run is not the same thing as a session. The session owns scoped conversation and history. The run owns execution identity and event observation.
That distinction lets Flue answer different questions:
| Question | Surface |
|---|---|
| What conversation context will the next prompt see? | Session history and active path. |
| What happened during this invocation? | Run store and run events. |
| How can a remote caller find this invocation later? | Run registry. |
| How can a browser or SDK watch it live? | /runs/:runId/stream. |
The headless runtime needs all four because there is no terminal operator filling the gaps.
Admin Surface
packages/runtime/src/runtime/admin-app.ts exports admin(). It mounts its own OpenAPI route and read-oriented inspection routes including:
GET /openapi.json
GET /agents
GET /agents/:name/instances
GET /agents/:name/instances/:id/runs
GET /runs
GET /runs/:runId
This is intentionally separate from the public flue() app. The source comments warn when admin() is invoked before runtime configuration exists, and the intended use is for the user to mount it deliberately in their own app.
Do not describe admin routes as public by default. The right wording is: Flue provides an admin app; the application decides where and how to mount it, including auth.
Cloudflare Registry
The Cloudflare target cannot depend on one in-process registry across all requests. The pinned source includes Cloudflare-specific registry code and a registry-do.ts Durable Object. The build plugin wires Cloudflare runtime configuration with per-request registry and store factories; when the request is routed through Durable Object storage, those factories use DO-backed stores, with generated in-memory fallbacks for paths that do not have DO storage.
The concept is the same as Node; the backing mechanism changes:
| Target | Registry shape |
|---|---|
| Node | In-memory registry for local process runtime. |
| Cloudflare | Durable Object based registry and stores. |
That target split is why the run registry is an abstraction instead of a global map hidden in flue-app.ts.
What Breaks If This Boundary Drifts
| Drift | Failure |
|---|---|
| Caller must remember agent and instance after invocation | runId is no longer a stable inspection handle. |
| Run store and registry collapse | Lookup and event persistence become hard to vary by target. |
| Stream route only tails live events | Reconnecting clients miss prior run history. |
| Stream route only replays stored events | Active runs cannot be followed live. |
| Admin app is treated as public default | Operational inspection can be exposed without application auth. |
| Session and run become one word | Readers confuse conversation state with execution identity. |
What To Copy
The copyable pattern is bare identity plus pointer registry. Let invocations produce a stable run ID, register where that run lives, and make every later inspection route start from the run ID alone.
For headless systems, this is not polish. It is the substitute for a human watching the terminal.
Verify In Source
flue-app.tsmountsPOST /agents/:name/:id.flue-app.tsmountsGET /runs/:runId,GET /runs/:runId/events, andGET /runs/:runId/stream.run-registry.tsstores run pointers separately from run details.handle-run-routes.tshandles detail, events, and stream actions.handle-run-routes.tsimplements replay-then-tail SSE behavior for streams.admin-app.tsexportsadmin()separately fromflue().- Cloudflare runtime code uses a target-specific registry/store shape rather than Node’s in-process registry.