Spawn fuel: a generous, uniform ceiling on delegation depth
Unbounded spawn depth is a resource-exhaustion hazard with no structural
floor: a model stuck delegating “redo this” to a fresh child has no reason to
ever stop, and each hop pays a full session boot, a detached OS thread, and a
place in the fleet registry. This decision gives every agent a fuel: u32
that a fork spends one unit of on its child; once an agent’s fuel reaches zero,
uniform-agent-nodes’s Gate mechanism
removes the spawn tools from its view. This refines, rather than reverses,
that decision’s central claim — no agent is privileged by special-case code,
only by tree position — because fuel is a pure function of position
(SPAWN_FUEL - depth), computed identically for every node.
Context
uniform-agent-nodes deliberately
deleted a depth-1 spawn cap: “every agent may spawn, so the spawn tree is
unbounded in depth rather than capped at one level.” Nothing since has bounded
the other axis — a chain of amnemon/mnemon calls, each one a genuine
sub-agent forking a genuine sub-agent, can recurse for as long as a model keeps
choosing to delegate. The only guidance against this is prose in the tool
description (“NEVER delegate a single grep/view/read/edit you can run
inline”), which shapes judgment but enforces nothing. The only numeric
ceilings nearby — MAX_STEPS (round-trips within one turn) and
AGENT_CEILING (an abandoned worker’s one-hour wall-clock reaper) — bound
different axes and do not compose into a depth limit.
Decision
Agentcarriesfuel: u32. The trunk is born withSPAWN_FUEL = 3(agent.rs);Agent::forkcomputes the child’s fuel asself.fuel.saturating_sub(1)and never lets it wrap below zero.- A new
Gate::Spawnsaxis (tools.rs) admitsamnemon/mnemononly whilefuel > 0, alongside the existingReturnsandSchedulesaxes.tools_forgains a third parameter,can_spawn, threaded from each fork’s freshly computed fuel. - Depletion is silent tool absence, not a refusal. This is the same shape
replyand the self-wakeup family already use: “a tool absent from an agent’s slice is neither advertised nor dispatchable” (tools.rs), so a depleted agent’s view simply lacks the tool — dispatch needs no new branch, and a well-behaved model never sees a call it must recover from. agents/message/agent_cancelstayGate::Always. They observe and manage already-live agents rather than create new ones, so a fuel-exhausted agent keeps the ability to coordinate with whatever peers it was told about, even though it cannot mint new ones./branchbypasses tool gating — it is a host command that also calls the fork primitive directly rather than going through a model tool, so it is not subject toGate::Spawns; a branch spends one unit of fuel like any fork.- Tool descriptions say depth is finite.
amnemon/mnemonnow state that each spawn spends one unit of the caller’s own budget on the child, so a chain cannot recurse forever.
Why this shape
- Bounds the reported hazard, not a different one. A runaway spawn chain (A delegates to B delegates to C, indefinitely) is a depth problem; a single agent fanning out many independent children in one turn is a breadth problem, already gated by the tool description’s cost warning and by each spawn being a distinct, described call rather than a loop. This decision targets depth only.
- Uniform, not privileged. Every agent computes its own gate from its own
fuel, andfuelis nothing butSPAWN_FUELminus this agent’s distance from the trunk — a structural fact identical in kind toparent, not anis_root-style branch reintroduced through the back door. - Matches the existing idiom.
MAX_STEPSis already “a generous ceiling no genuine turn reaches, guarding a different runaway axis.”SPAWN_FUELis the same shape for generations across the tree instead of round-trips within one turn. - No new permission predicate. Gating through
tools_forreuses the exact mechanismReturns/Schedulesalready prove out, rather than adding a second, dispatch-time check that the comment onTool::gateexplicitly says the design avoids.
Alternatives considered
- A fleet-wide live-agent cap, bounding total concurrent agents regardless of tree shape. Rejected for now: it catches breadth explosions a chain-only guard misses, but couples unrelated branches’ budgets — one branch’s legitimate fan-out could starve a sibling’s spawn — and breadth is not the failure mode this decision responds to. An orthogonal cap remains available later if fan-out itself proves hazardous.
- A runtime refusal inside
dispatch_spawn(aninvalid_input-style error returned to a model that callsamnemonanyway). Rejected: it reintroduces exactly the “separate permission predicate to keep in sync” that tool-membership gating exists to avoid, and it teaches a well-behaved model a call it must fail and recover from instead of one it never sees. - A configurable
--max-agent-depthflag. Deferred: no user-facing need has surfaced, and a bare constant matchesMAX_STEPS’s own unconfigured shape. A flag can be layered on later without touching the mechanism.
See also
async-agent-tool (the earlier, fully-superseded depth-1 cap), agents, agent, tools.