This series expands the hub post, Flue Under the Hood, into a source-level reading guide. The hub gives the architectural judgment: Flue owns the harness layer and rents the model loop. This chapter turns that claim into a map you can use while reading or changing the code.
The source pin for every claim in this chapter is withastro/flue@dbaa9effa305561c627c6836559f8a0cbce67875, the merge commit for PR #130. At that pin, packages/runtime/package.json says @flue/runtime is 0.5.3, and the model-loop dependencies are @mariozechner/pi-ai and @mariozechner/pi-agent-core.
The first debugging move is to identify which runtime boundary owns the behavior before opening files.
Domain Word
In Flue, harness means the aggregate root for runtime capabilities around an agent: sessions, filesystem access, environment execution, sandbox adaptation, child sessions, tools, compaction, and deployed inspection routes.
The invariant is simple: user agent files declare work; the harness owns the runtime behavior that makes that work repeatable, inspectable, and deployable.
The twelve-factor pressure is build, release, run. A headless agent framework cannot stop at a library call. It has to create the same agent from source, wire it into a runtime, invoke it over a protocol, and inspect it after the process that started it is no longer the only thing holding the story.
The One-Page Map
.flue/agents/*.ts │ user handler declares intent ▼ ctx.init(...) │ constructs a harness for this agent instance ▼ Harness │ owns sessions, fs, env, shell, child-session creation ▼ Session │ owns SessionHistory, active path, role/model/tool scoping ├── pi-agent-core Agent │ └── pi-ai provider/model transport ├── agent.ts built-in tool contracts ├── sandbox.ts SessionEnv/SandboxApi adapters └── compaction.ts threshold + overflow recovery Deployment surface ├── flue-app.ts public OpenAPI and run routes ├── admin-app.ts separately mounted inspection app └── CLI build plugins for Node and Cloudflare
That map is the spine. If you are debugging an agent file, you start at the top. If you are debugging provider payloads, you go to session.ts and runtime/providers.ts. If you are debugging command execution, you go to agent.ts, sandbox.ts, and the adapter for the target. If you are debugging how a caller finds an old run, you skip straight to runtime/flue-app.ts, run-store.ts, run-registry.ts, and handle-run-routes.ts.
The useful habit is to ask which boundary owns the behavior before opening files.
Agent Files Declare Intent
The user-authored agent file is intentionally thin. It is where the handler names the work, accepts payload, and calls ctx.init(...). The agent file should not become the place where session persistence, provider transport, run registry lookup, or sandbox mechanics leak in.
That is the first design bet. A framework becomes sticky when application code can stay small while the runtime absorbs the cross-cutting behavior. In Flue, that runtime begins at ctx.init(...).
Harness Is The First Runtime Boundary
packages/runtime/src/harness.ts defines Harness. It exposes session(), sessions.get(...), sessions.create(...), sessions.delete(...), fs, and shell(...). It also holds the in-process map of open Session objects.
The interesting detail is the storage key. When a session opens, the harness derives a storage key and an affinity key from the agent instance, agent name, and session name. That gives a session durable identity without making the user agent file manage persistence.
The harness also owns child session creation for tasks. When a task tool delegates work, the parent session does not fake a subagent inside the same transcript. The harness creates another Session with its own name, storage key, role, cwd override, task depth, and parent environment boundary.
Session Is The Core
packages/runtime/src/session.ts is the load-bearing file. It is where Flue turns stored history into model-visible messages, scopes roles and model overrides per call, constructs the rented Agent, synchronizes produced messages back into SessionHistory, and triggers compaction after turns.
This is the key ownership split:
| Behavior | Owner |
|---|---|
| Active session tree and leaf | Flue SessionHistory |
| Provider model loop | @mariozechner/pi-agent-core |
| Provider/model payload semantics | @mariozechner/pi-ai |
| Role, model, thinking override precedence | Flue Session |
| Tool schema list and custom tool collision checks | Flue Session and agent.ts |
| Compaction state transition | Flue Session and compaction.ts |
The constructor makes the split visible. Flue prepares initialState with system prompt, model, tools, previous messages, and thinking level. It passes getApiKey, onPayload, toolExecution: 'parallel', and sessionId to new Agent(...). After the rented loop runs, Flue syncs messages, saves history, aggregates usage, emits events, and possibly compacts.
Tools Cross Into Runtime Reality
packages/runtime/src/agent.ts defines the model-visible tools: read, write, edit, bash, grep, glob, and task. The schema alone is not the contract. The contract is schema plus runtime behavior.
That is why packages/runtime/src/sandbox.ts matters. A model can request a command timeout through the bash tool, but the effect is only real if the tool passes timeout into SessionEnv.exec(...) and the sandbox adapter honors it. The pinned source deliberately pushes timeout both as a native provider hint and as an abort signal where possible.
This is the boundary behind the hub post’s PR #25 example. The bug was not “a bash option was missing.” The bug was that the model-visible schema promised a capability the runtime failed to enforce.
Compaction Is A Runtime Transition
packages/runtime/src/compaction.ts is not a prompt snippet. It has settings, token estimation, cut-point selection, message serialization, summary generation, split-turn handling, file-operation tracking, and usage aggregation.
The session calls it in two cases:
| Mode | Trigger | Retry? |
|---|---|---|
| Threshold | provider-reported assistant usage exceeds contextWindow - reserveTokens | No |
| Overflow | isContextOverflow(...) detects provider context overflow | Yes |
Overflow recovery proves why the session tree matters. The session removes the failed assistant leaf, saves the adjusted history, appends a compaction entry, rebuilds model-visible context, and continues the underlying agent. A flat transcript would make that recovery much harder to reason about.
Runtime Routes Make It A Framework
packages/runtime/src/runtime/flue-app.ts turns the harness into a service surface. At the pinned source it mounts:
GET /openapi.json
POST /agents/:name/:id
GET /runs/:runId
GET /runs/:runId/events
GET /runs/:runId/stream
That route set is the difference between “call a library” and “run a headless agent service.” A caller can invoke an agent by name and instance ID, then inspect by bare run ID later. The run registry is what removes the need for the caller to remember which agent instance owns a run.
admin-app.ts is separate. It exports admin() so a user can mount an inspection API deliberately, usually behind their own auth middleware. Do not blur public and admin routes in prose or diagrams.
Build Targets Are Runtime Shapes
The CLI build plugins complete the framework picture. packages/cli/src/lib/build.ts discovers agents, roles, optional app entry, and target configuration. The Node plugin generates a Hono server with in-memory registries and local session env support. The Cloudflare plugin generates a Worker entry, Durable Object classes, request-scoped registry/store factories, Workers AI provider registration, and Wrangler config output.
The source agent remains one thing. The runtime shape changes because the host changes which stores, bindings, registries, and sandbox adapters exist.
| Target | What changes | What should stay stable |
|---|---|---|
| Node | Hono server, local env, in-memory run registry | Agent handler, session language, public routes |
| Cloudflare | Worker entry, Durable Objects, bindings, request-scoped stores, Workers AI provider | Agent handler, session language, public routes |
| Custom app | User owns app composition and middleware | flue() remains the public sub-app |
The Eight Chapters
| Chapter | Boundary | Reader outcome |
|---|---|---|
| 01 Session Tree | session history vs transcript | Can reason about active path, leaf, deletion, and task sessions. |
| 02 Pi-ai Seam | rented model loop | Can change provider behavior without confusing ownership. |
| 03 Compaction | context as state transition | Can debug threshold compaction and overflow retry. |
| 04 Tools/Sandbox | schema vs runtime promise | Can verify tool behavior across adapters. |
| 05 Run Registry | headless inspection | Can explain run lookup, events, streams, and admin routes. |
| 06 Build Targets | generated runtime shapes | Can predict Node vs Cloudflare differences. |
| 07 Extending Flue | safe extension seams | Can add providers, tools, sandboxes, and MCP tools without seam drift. |
What Breaks If This Boundary Drifts
| Drift | Failure |
|---|---|
| Agent files start owning persistence | Every app invents its own session/run story. |
| Provider retention is called Flue memory | Replay becomes dependent on external hosted state instead of session history. |
| Runs and sessions collapse into one word | Callers cannot tell execution identity from conversation state. |
| Build targets are called examples | Deployment behavior stops being treated as a tested framework surface. |
| Admin and public APIs blur | Operators accidentally expose inspection routes without their own auth boundary. |
Verify In Source
packages/runtime/package.json:@flue/runtimeis0.5.3, and dependencies include@mariozechner/pi-aiand@mariozechner/pi-agent-core.packages/runtime/src/harness.ts:Harnessexposes sessions,fs, andshell(...), and creates child sessions throughcreateTaskSession.packages/runtime/src/session.ts:Sessionconstructsnew Agent(...), syncs messages back toSessionHistory, and triggers compaction.packages/runtime/src/agent.tsandpackages/runtime/src/sandbox.ts: built-in tool schemas cross intoSessionEnvandSandboxApi.packages/runtime/src/runtime/flue-app.ts: public route methods match the list above.packages/cli/src/lib/build-plugin-node.tsandbuild-plugin-cloudflare.ts: generated entries configure the runtime before mountingflue().