fabri is the agent engine I built; ludexel-gba is the game-build project it runs. There’s a comment in ludexel-gba’s .agent/fabri_agent.yaml that says more about orchestration than any diagram I could draw:
“30 was exhausted mid-run (a 26-step run hit max_steps right after emitting its summary -> outcome=incomplete).”
That’s max_steps: 48 explaining itself. The orchestrator used to cap at 30, and a real build got cut off one beat before fabri recorded it as done — outcome=incomplete for a run that had, in every sense that mattered, finished. A step is one LLM turn, not one tool call — a turn can fire off a whole wave of parallel spawn_subagent calls at once. The gap between 26 and 30 has a mechanical cause: fabri’s step loop only accepts a turn as the final answer when it calls zero tools. A turn that pairs a summary with a tool call gets logged as an ordinary tool step, and the loop keeps waiting for a clean answer-only turn — one that, this time, never came before the ceiling hit. fabri 0.7.5+ closed the gap with a final-step nudge (force a final-answer-only reply on the run’s last allowed step) and only then raised the cap to 48, as headroom, not as the fix. 48 isn’t a round number someone picked; it’s a fossil. So is max_cost_usd: 8.0, and the sub-agent budget, max_steps: 14 / max_cost_usd: 3.0 — every constant in that config exists because something specific broke first.
That’s the part I keep wanting to explain to people who ask me how fabri’s multi-agent orchestration “works.” The interesting question was never “how do agents talk to each other.” It’s “what’s the cheapest role that can do this step” and “how do you stop the bill from leaking when something goes wrong.” Everything else is plumbing.
Per-role, not per-run
fabri doesn’t run one model. It runs four roles — main, decompose, planner, narrator — each pinned to a completely different provider, billed against its own API key. The one that matters most for cost is the narrator, whose entire job is a one-line status update between steps: “Reading the battle schema,” “Writing three new scenes.” When I added it in v0.7.7, the commit message says it defaults to claude-haiku-4-5, described as “~free per run.” Its system prompt is 46 words long:
“You are an agent’s narrator. In one sentence (<= 12 words), tell the user what the agent is doing right now. Active voice, present tense, no preamble… Never repeat the previous update verbatim.”
Narrator output is capped at 60 tokens, and its failures are swallowed entirely — the code just logs “narrator skipped” and moves on, because a UI status line is never worth risking the run for. Put a number on “~free”: the narrator prompt stays deliberately small — task snippet, prior update, truncated tool summaries, not the model’s full context — call it 300 input tokens plus up to 60 output. At Haiku’s fabri.pricing rate ($1/$5 per million in/out) that’s roughly $0.0006 a call; the same call on Opus ($5/$25 per million) is roughly $0.003 — about 5x more for one status line. Both are pricing-table estimates, not measured invoice lines — fabri doesn’t telemeter narrator tokens separately — but the shape holds: there’s no reason to spend Opus-grade tokens on step-three status updates.
Single-threaded by default
The bigger decision is upstream of any per-role tuning: fabri does not spawn sub-agents unless it has to. The doctrine is written directly into the system prompt appended to every run:
“Do the work inline by default. Spawn a sub-agent ONLY for a subtask that is independent, parallelizable, AND too large for your own context — never for sequential/dependent steps or just because the tool exists. A spawn re-runs the whole loop, so an unnecessary one multiplies cost.”
The README backs that with a citation, not a vibe: “a multi-agent run costs ~15× a single agent; coordination is a top failure source,” pointing at Anthropic’s own multi-agent research post and Cognition’s “Don’t Build Multi-Agents.” Worth being straight about that number: it’s theirs, not mine. I haven’t taken one ludexel-gba build, run it once single-threaded and once fanned out, and diffed the two total_cost_usd numbers — fabri has the rollup to make that exact comparison, I’ve just never pointed it at this question. Until that A/B exists, “single-threaded by default” is a policy borrowed from someone else’s data, not proof fabri’s own overhead is 15x. spawn_subagent is just another tool call, gated by the same frugality logic — it only pays off if you resist reaching for it. fabri watches itself for the failure mode too: if a spawned sub-agent finishes in one step or less but still cost money, the parent logs a delegation_regret event: “spawn ran <=1 step but cost >0; likely inlinable.” The orchestrator narcing on its own bad call to fan out.
The number that matters is the subtree, not the step
Once you spawn, the child’s spend has to roll up. cost_usd is a run’s own tokens; total_cost_usd is “own + sub-agent subtree, the number a host persists as run COGS,” per the comment in core/agent.py. Before v0.5.0 that rollup didn’t exist — a spawned sub-agent ran as a separate subprocess with its own trace, its tokens invisible to the parent. The changelog says it plainly: “a fan-out run was massively undercounted.” You cannot make a cost decision about orchestration if the accounting itself goes blind.
v0.7.8: the release where fabri caught itself lying
This is the release I’d point to as proof the cost model was actually enforced, not just documented. Four call sites were spending real provider tokens without ever rolling into total_cost_usd: the planner’s own LLM pass, the decompose tool, memory-compression synthesis (which runs after the visible run returns), and crashed sub-agent spawns. Commit ea08081 states the size without softening it: “causing recorded COGS to under-report real spend by ~10x on planner-engaged runs” — not a rounding error, an order of magnitude.
The fix added two new event types, cost_unaccounted and post_run_usage, so a host can tell “we know the exact cost” from “this number is a floor.” The case that stuck with me: if spawn_subagent fails before printing its final JSON — say qdrant is down mid-flight — the tokens already sent to the provider are real and billed, but never reported back. A crashed sub-agent still burns money. fabri’s answer isn’t to estimate the cost; it emits the child’s stderr tail and return code and says, explicitly, that the recorded number is a lower bound, not a fact.
Fan-out has a memory bill too
Cost leaks aren’t only about dollars. Before v0.8.1, a parallel_group spawned max_workers=len(group) — as many fresh subprocesses as members in the wave, no ceiling. The commit explains why: an unbounded fan-out “let a wide wave spike memory enough to OOM-kill the host in a memory-capped container.” max_parallel_spawns, default 4, queues the overflow instead — same eventual result, bounded peak memory. On the token side, TOON — a vendored codec that collapses tool results from JSON into one header row plus data rows before they enter the model’s context — cuts “~30-40% fewer chars on tabular results” against the same data serialized as plain JSON. It shipped the same calendar day as fabri’s first commit.
What this actually costs in production
The ludexel-gba config is where all of this lands as real numbers instead of doctrine. max_cost_usd: 8.0 is a hard halt — the run breaks with outcome=budget_exceeded before an LLM call would push it over. The comment sizes it against reality: “Sized above a normal cold build (~$2-4 observed) with headroom for repair strikes.” That $2-4 is total_cost_usd for the whole run — orchestrator plus every child’s subtree, not one sub-agent’s bill — and the arithmetic only works if most children land well under their own $3 cap: 6-7 children anywhere near their ceiling would already total $18-21, blowing past the $8 halt before the orchestrator spent a cent of its own. Sub-agent children get that independent budget, max_steps: 14 / max_cost_usd: 3.0, because before it existed every domain child inherited the orchestrator’s full 48-step allowance — “an inherited 48 let a looping child burn the whole budget,” tagged in the yaml as “(Resolved TODO fabri#2.)”
None of these numbers are tuned to some theoretical optimum. $8 is “more than $2-4, with headroom.” 14/$3 is “less than 48, because that’s what killed us once.” I don’t have a principled derivation for any of it — I have incidents, and constants sized to survive the incident that already happened. The yaml comment for the sub-agent budget even says to “tune from /metrics cost.avg_per_build” — an admission this is still a live dial, not a solved equation. That’s the honest state of it: the accounting is real now, the leaks I know about are closed, and the thresholds are still guesses backed by receipts instead of guesses backed by nothing.