Skip to specification

Flue — the harness as a framework

The Astro team's answer to a question this site's eleven primitives raise but never ask out loud: if the slots are a library, what is the framework? Flue runs the same pi loop torn down one sheet earlier, fills nearly every slot pi refused — sandbox, sub-agents, MCP, receipts — and deletes the one thing pi assumed. There is no human at the terminal.

Identification

Repository
withastro/flue
Steward
Astro core team, at Cloudflare
Runtime
TypeScript · Vite + Hono · Apache-2.0
Harness
pi, via @earendil-works/pi-ai
Sandboxes
virtual · local · 11 remote providers
Targets
Node · Cloudflare · GH Actions · GitLab CI · +6

LineageBuilt inside the Astro monorepo to run that project's own GitHub workflows, then generalized — Schott credits Ben Giori getting his hands on it as the moment they realized every agent wanted the same thing. The repository opened 7 February 2026, went public on 1 May, and reached 1.0 Beta on 16 June; the Astro team had joined Cloudflare that January. One detail is worth recording because the site's whole vocabulary turns on it: the launch announcement and the README both say The Agent Harness Framework, while the repository's own one-line description now reads "The sandbox agent framework." Both are current. The category is still being named.

Read it as

Ductwork designed into the house

Not assembled room by room once the walls are up. It arrives with the structure — which is what makes it dependable, and what makes it decide where your rooms can go.

The drawing, in one paragraph

The launch closed with a question — are agents ready for their library to framework moment? — and the software is the argument for yes. An agent is a function: a module marked with a 'use agent' directive, an exported capitalized function whose name becomes its durable identity, and a returned string that is the system prompt. Capabilities mount through hooks inside it — useModel, useSandbox, useTool, useSkill, useSubagent, useMcpConnection, usePersistentState — and the function re-renders on every turn, so instructions are recomputed from live state rather than fixed at boot. Underneath sits pi, imported by name for its provider layer; on top sits everything pi argued against. The second move matters more than the first. Schott's stated design premise is that Flue carries "no baked in assumption like requiring a human operator to function" — so work arrives by dispatch() from a verified Slack or GitHub webhook, runs in a sandbox nobody is watching, and survives the process that started it. pi hands the day to the operator. Flue removes the day.

intake left · capabilities right · the durable record underneath

FIG. 3 — the harness as a framework. Work enters on the left from systems rather than from a person — the operator is the one part struck through. Capabilities mount into the agent function through hooks; underneath, the durable stream records every turn so a crash resumes rather than restarts.

The examination — eleven slots, one harness

Built in
Ships in the core; works with no configuration.
Via config or extension
The slot is real but you fill it — config, extension, or convention.
Deliberately omitted
Left out on purpose, with a stated argument for the gap.
  1. The agent is the function and the function returns the prompt. Because it re-renders every turn, instructions are rebuilt from current state instead of accumulating — a conditional branch in TypeScript is a conditional system prompt. Most of the substance still lives in markdown: skills, context, and the AGENTS.md read out of the sandbox working directory and folded in.

    Built in

    'use agent' · prompt is a return value · AGENTS.md

  2. Attaching a sandbox is what hands the model material: read, write, edit, bash, grep, and glob arrive together, and with them the workspace context — working directory, directory listing, AGENTS.md contents — injected into the system prompt. Skills are advertised in the same breath as one catalog line each.

    Built in

    sandbox tools + cwd listing in the prompt

  3. Threshold compaction fires when used tokens exceed the context window minus reserveTokens (model-aware, at most 20,000); keepRecentTokens (8,000 by default) survives verbatim while older history folds into a summary, and compaction.model can push that summarizing onto a cheaper model. Passing compaction: false disables only the automatic trigger — overflow recovery and explicit harness.compact() still fire when the conversation stops fitting. The numbers are worth noting against the other sheets: this is OpenCode's pair exactly.

    Built in

    reserve <=20k · keep 8k · cheaper compaction model

  4. defineTool with a top-level Valibot object schema, parsed before run executes, mounted by useTool. Three registers above the plain case: harness tools reach the sandbox and the model itself through harness.prompt(), durable tools get a step surface, and conditional tools appear and disappear with agent state — the runtime narrates the delta to the model. MCP is accepted rather than refused: useMcpConnection mounts a remote server's tools as mcp__<server>__<tool>. The inversion is exact, and it is built on pi's own loop.

    Built in

    defineTool · Valibot · useMcpConnection

  5. The slot pi left deliberately empty is the one Flue puts in its own repository description. useSandbox() attaches at most one per agent, in three tiers: a virtual in-memory sandbox on just-bash where network access is opt-in per URL prefix; local(), which binds the host shell and whose docs refuse to overclaim — it is for development and CI, not an isolation boundary — passing through only a short allowlist of PATH, HOME, USER, LANG, TERM, TMPDIR and "never API keys, tokens, or cloud credentials"; and eleven remote providers behind adapters, from E2B and Daytona to Cloudflare's own. The model does not avoid the secrets. It never gets them.

    Built in

    useSandbox() · virtual | local() | 11 remote

  6. The deepest mechanism on the sheet, and the reason the framework exists. Each agent instance owns one canonical Durable Stream — an append-only record of every prompt, model response, tool call and result, compaction, and recovery fact. A submission is recorded before any model work begins, and the contract is that it reaches exactly one terminal outcome, completed or failed or aborted, however many crashes intervene; the stated discipline is at-least-once execution over exactly-once recording. Defaults are 10 attempts and one hour. step.do(name, fn) records a step's return value so recovery replays it instead of re-running it, and usePersistentState writes land in the same stream. Storage is in-memory SQLite until a db.ts names Postgres, libSQL, MySQL, MongoDB, or Redis.

    Built in

    Durable Streams · 10 attempts / 1h · step.do

  7. This is where the operator was removed. Agents are addressable: POST /:id over a Hono sub-app from createAgentRouter(), or dispatch() fire-and-forget with a receipt. Intake is the distinctive part — seventeen first-party channels (Slack, GitHub, Stripe, Linear, Teams, Twilio and the rest) verify each delivery's signature against the exact raw bytes and enforce replay windows before a handler ever runs. Work arrives from the systems where it already happens, not from a person typing.

    Built in

    dispatch() · POST /:id · 17 verified channels

  8. A task tool is added to every agent; useSubagent and defineSubagent name the delegates. A delegate renders fresh in isolation and inherits the sandbox, model, and thinking level but not the parent's history, tools, skills, or state — only its final message returns, and a batch of task calls runs in parallel. Depth is capped at four. pi refused this slot on the grounds that hidden delegation cannot be watched; Flue's answer is not to argue but to make the child's every turn a typed runtime event.

    Built in

    task tool · fresh context · depth 4

  9. The open Agent Skills format, with progressive disclosure enforced by the runtime: only name and description ride in the prompt until an activate_skill call loads the body. Three ways in — import a SKILL.md directly as a module, declare one inline with defineSkill(), or drop it in .agents/skills/ for auto-discovery. The deployment detail is the good one: supporting files travel in the application bundle and are served read-only at virtual paths, so a skill ships with the code instead of needing a filesystem prepared for it.

    Built in

    SKILL.md · activate_skill · bundled files

  10. A runtime event stream runs alongside the conversation stream and carries what an operator would otherwise have watched: agent_start and agent_end, submission_settled, turn events with token usage and cost estimated from the model catalog's rates, tool and task events, structured logs. observe() subscribes; instrument() registers span producers. OpenTelemetry, Sentry, and Braintrust are first-party, Vitest Evals sits in the ecosystem, and on Cloudflare the spans land in Workers Logs and Traces unwired. Adapters capture content by default — content: false or a transform redacts it.

    Built in

    observe() · instrument() · OTel · Vitest evals

  11. The harness does not learn; the repository accretes — and the mechanism assumes an agent is doing the accreting. flue add fetches "a blueprint implementation guide for a coding agent to follow" rather than installing a package, and flue docs bundles the documentation offline and searchable for the same reader. Skills, tools, and channels then live as TypeScript and markdown beside the code, versioned with it. Evolution is a commit, written by the thing being evolved.

    Via config or extension

    flue add — blueprints for an agent · flue docs offline

Where this design runs out of road

The trade is the bargain every framework offers: the slots arrive pre-shaped. Compaction has a threshold before you have a workload; durability gives you a stream you must now operate and a database to put it in; the sandbox is one per agent and a delegate cannot open its own. The durability itself is narrower than it first reads — Flue is explicit that it does not checkpoint arbitrary TypeScript and resume from the last completed line, so any side effect outside a step.do still needs an idempotency guard you write. And the era is young: 1.0 is still Beta, multi-node is stated as not yet shipped, and the project's own one-line description has already drifted from harness to sandbox. The ductwork is excellent. It was also installed before you drew the floor plan.

06Side by sideThe comparison table — one row per primitive, one column per harness

Read from — every finding traces to one of these

UnitT03
Fills10 built in · 1 adapted · 0 omitted
Read againstThe Harness — eleven primitives