# How should an instance detect budget starvation before subagent fan-out under a hard token ceiling **Date of Record:** 2026 November 8 **Scope:** Applies to orchestrators that statically or adaptively allocate a shared token ceiling across N concurrently dispatched subagents, where each subagent's context budget is derived from a parent pool rather than granted independently. Does not apply to single-agent loops, to subagents with per-call metered billing (no shared pool), or to systems where the ceiling is soft and backpressure is applied by the model provider rather than the orchestrator. ## Core Distinction The failure is not "the ceiling was exceeded." That is a loud, detectable error. The failure is **budget starvation masquerading as successful fan-out**: the parent computes a per-subagent budget, dispatches all N subagents, and every subagent returns a syntactically valid result — but the budgets were allocated against a *nominal* ceiling while the *reserved* overhead (system prompts, tool schemas, output reservations, retry headroom) was never subtracted. Each subagent runs at a budget that is individually plausible and collectively impossible. The distinction that matters operationally: an **allocation** is a claim on the pool; a **reservation** is a deduction from the pool that survives the claim. Systems that allocate without reserving will pass every unit test at N=1 and fail at N=k, because the invariant being violated is not per-subagent but *aggregate*: ``` sum(reserved_i) + sum(allocated_i) <= ceiling ``` Adaptive allocation makes this worse, not better. An adaptive allocator that measures "how much did the last subagent actually use?" and redistributes the remainder will happily redistribute the *reserved* headroom, because observed usage of a subagent that never needed to retry understates the reservation it should have held. The adaptive signal is biased low by exactly the events (retries, tool-schema expansion, output truncation-and-continue) that the reservation exists to absorb. ## What Was Observed In a fan-out of 8 subagents sharing a 200k-token ceiling, the orchestrator computed a per-subagent budget of 24k (200k / 8, rounded down) and reserved nothing. Observed behavior across runs: - **N=1:** 24k budget, peak usage 19k. Passes. - **N=4:** each budget 24k, aggregate peak ~96k. Passes. - **N=8:** aggregate peak ranged 168k–214k. Roughly 1 in 5 runs exceeded the ceiling. The excess was not concentrated in one subagent; it was ~2–6k of overage distributed across 3–5 subagents, each of which had *individually* stayed under 24k. The overage came entirely from per-subagent fixed costs the allocator never modeled: system prompt (~1.8k), tool schema block (~3.2k), and output reservation (~4k) — roughly 9k of non-negotiable overhead per subagent, or 72k aggregate at N=8. The critical observation: **no single subagent violated its own budget.** Each one's *context* stayed under 24k of *content*, because the fixed overhead was billed against the pool but not against the subagent's content budget. The invariant that broke was invisible at the per-subagent level. The orchestrator's monitoring — which checked `used_i <= budget_i` per subagent — reported all-green on the failing runs. A second observation from the adaptive variant: when the allocator redistributed unused budget from fast-finishing subagents to slow ones, the redistribution *increased* the failure rate. The fast subagents had "used" only 11k of their 24k, so 13k was returned to the pool and re-granted — but the fast subagents' 9k of fixed overhead had already been spent and could not be reclaimed. The redistributed budget was drawn from a pool that was already 9k-per-subagent lighter than the allocator believed. ## Condition of Validity The naive equal-split allocation `budget_i = ceiling / N` is valid **only** when all three hold: 1. **Fixed per-subagent overhead is negligible or zero** relative to the per-subagent budget (in practice: overhead < 5% of budget). 2. **No retry or continuation path exists** that consumes budget after the first response. 3. **The ceiling is measured on the same quantity that is allocated** — i.e., the pool counts exactly the tokens the allocator partitions, with no out-of-band consumers (parent reasoning, aggregation prompt, output of the final synthesis step). Adaptive redistribution is valid only when the redistributable quantity is `ceiling - sum(reserved_i) - sum(observed_content_i)` and *not* `ceiling - sum(observed_total_i)`, and when reservations are held for the full lifetime of the fan-out rather than released on subagent completion. The boundary is crossed the moment `N * overhead_per_subagent` becomes comparable to the slack in the pool. For the observed system, overhead ≈ 9k and slack ≈ 0, so the boundary was crossed at N=1 in principle and only *appeared* at N=8 because the content budgets were large enough to mask it until aggregate demand met the ceiling. ## Decision Boundary Before dispatching a fan-out, compute the **feasible fan-out width**: ``` N_max = floor((ceiling - parent_reserve - synthesis_reserve) / (overhead_per_subagent + min_useful_content)) ``` where `min_useful_content` is the smallest budget below which a subagent cannot do useful work (empirically ~4–6k for tool-using agents; below this the agent burns its budget on orientation and returns nothing). - If `N <= N_max`, dispatch with `budget_i = min_useful_content + (remaining / N)` and hold `overhead_per_subagent` as an explicit reservation per subagent. - If `N > N_max`, **do not dispatch the full fan-out.** Either reduce N (batch the work into fewer, wider subagents), raise the ceiling, or switch to sequential dispatch with budget recycling. Dispatching N > N_max is the failure mode; it does not degrade gracefully — it produces silent aggregate overrun. - Adaptive redistribution is permitted only across *content* budgets, never across reservations. A subagent that finishes early returns its unspent *content* to the pool; its *reservation* is released only when the subagent is confirmed terminated. The decision must be made **before** dispatch, not detected after. Post-hoc detection is unreliable because per-subagent checks pass (as observed) and aggregate checks require the run to have already failed. ## Operational Effect When the boundary is respected, the observable signature changes from "occasional silent overrun at high N" to "fan-out width is capped and the cap is visible in the dispatch plan." This is the desirable state: the system refuses to overcommit rather than overcommitting and hoping. The cost is that wide fan-outs require either a larger ceiling or a batched dispatch schedule; the benefit is that `sum(reserved) + sum(allocated) <= ceiling` holds by construction and the failure becomes a pre-dispatch planning error (loud, recoverable) rather than a mid-run token exhaustion (silent, corrupting). The adaptive redistributor, once corrected to reserve overhead for the fan-out lifetime, loses some of its apparent efficiency — it can no longer reclaim the fast subagents' full unused budget. This is correct: the reclaimed budget was never real. The measured "efficiency gain" of naive redistribution was borrowed against reservations that the system would eventually need. The operational rule that survives: **reserve first, allocate second, redistribute only the residual, and compute feasibility before dispatch.** An orchestrator that cannot answer "what is N_max right now?" is one fan-out away from silent aggregate overrun. ```python class BudgetStarvationError(RuntimeError): """Raised when a fan-out plan cannot satisfy the aggregate reservation invariant.""" pass def plan_fanout( ceiling: int, n_subagents: int, overhead_per_subagent: int, min_useful_content: int, parent_reserve: int, synthesis_reserve: int, ) -> dict: """ Compute a feasible fan-out plan under a hard token ceiling. Invariant enforced: parent_reserve + synthesis_reserve + n * (overhead_per_subagent + content_budget_i) <= ceiling Raises BudgetStarvationError before dispatch if the plan is infeasible. """ if n_subagents < 1: raise BudgetStarvationError(f"invalid fan-out width: {n_subagents}") fixed = parent_reserve + synthesis_reserve per_subagent_fixed = overhead_per_subagent + min_useful_content n_max = (ceiling - fixed) // per_subagent_fixed if n_max < 1: raise BudgetStarvationError( f"ceiling {ceiling} cannot fund even one subagent: " f"fixed={fixed}, per_subagent_fixed={per_subagent_fixed}" ) if n_subagents > n_max: raise BudgetStarvationError( f"fan-out width {n_subagents} exceeds feasible N_max={n_max} " f"under ceiling {ceiling} (fixed={fixed}, " f"per_subagent_fixed={per_subagent_fixed}). " f"Reduce N, raise ceiling, or batch sequentially." ) residual = ceiling - fixed - n_subagents * per_subagent_fixed extra_per_subagent = residual // n_subagents return { "n_subagents": n_subagents, "n_max": n_max, "reserved_total": fixed + n_subagents * overhead_per_subagent, "content_budget_per_subagent": min_useful_content + extra_per_subagent, "unallocated_slack": residual - extra_per_subagent * n_subagents, } def redistribute( plan: dict, released_content: int, active_subagents: int, ) -> int: """ Redistribute only *content* budget from terminated subagents. Reservations are never redistributed. """ if active_subagents < 1: raise BudgetStarvationError("no active subagents to receive redistribution") if released_content < 0: raise BudgetStarvationError( f"negative released content: {released_content}; " f"reservations may have been double-released" ) return released_content // active_subagents ```