I/D/E · flue-framework

Runs, Registries, Logs, And APIs

Summary

Why headless Flue agents need run identity, registry lookup, events, streams, OpenAPI, and a separately mounted admin surface.

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.

Bare RunId Resolver

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:

RouteJob
POST /agents/:name/:idInvoke a concrete agent instance.
GET /runs/:runIdFetch run detail by bare run ID.
GET /runs/:runId/eventsFetch run events as a bounded list.
GET /runs/:runId/streamReplay 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

RUN 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.

SurfaceOwnsDoes not own
Run storeRun records, statuses, event records, detail lookup, event listGlobal bare-run routing
Run registryPointer from runId to agentName and instanceIdEvent bodies or run details
Run subscribersIn-process live event fanoutDurable 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:

QuestionSurface
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:

TargetRegistry shape
NodeIn-memory registry for local process runtime.
CloudflareDurable 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

DriftFailure
Caller must remember agent and instance after invocationrunId is no longer a stable inspection handle.
Run store and registry collapseLookup and event persistence become hard to vary by target.
Stream route only tails live eventsReconnecting clients miss prior run history.
Stream route only replays stored eventsActive runs cannot be followed live.
Admin app is treated as public defaultOperational inspection can be exposed without application auth.
Session and run become one wordReaders 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.ts mounts POST /agents/:name/:id.
  • flue-app.ts mounts GET /runs/:runId, GET /runs/:runId/events, and GET /runs/:runId/stream.
  • run-registry.ts stores run pointers separately from run details.
  • handle-run-routes.ts handles detail, events, and stream actions.
  • handle-run-routes.ts implements replay-then-tail SSE behavior for streams.
  • admin-app.ts exports admin() separately from flue().
  • Cloudflare runtime code uses a target-specific registry/store shape rather than Node’s in-process registry.

References

Flue-framework Ch 6/8
  1. 1 Runtime Map 24m
  2. 2 Session Tree, Leaf, And Replay Safety 26m
  3. 3 The Pi-ai Seam 22m
  4. 4 Compaction As Failure Recovery 28m
  5. 5 Tool Contracts And Sandbox Reality 25m
  6. 6 Runs, Registries, Logs, And APIs 27m
  7. 7 Build Targets And Deployment Shape 26m
  8. 8 Extending Flue Safely 24m