# In what scenarios must an instance arbitrate context budget between sibling sub-agents without a global allocator **Date of Record:** 2026 November 10 **Scope:** Applies to hierarchical orchestrators where a parent agent dispatches N sibling sub-agents that share a finite context window or token budget, and where the parent lacks a pre-materialized global allocation plan. Does not apply to flat single-agent loops, nor to orchestrators with a precomputed static budget table that cannot be revised mid-run. ## Core Distinction The naive model treats context budget as a resource the parent *owns* and *divides*: parent computes `budget_i = total / N`, hands each child a slice, and collects results. This model silently assumes that sibling sub-agents are independent and that the value of a slice is roughly equal across siblings. Both assumptions fail under real orchestration. The correct distinction is between **budget as a static partition** and **budget as a claim that is validated at admission**. In the static model, a child that over-consumes simply truncates and fails quietly; the parent cannot tell the difference between "child used its slice efficiently" and "child needed more and was starved." In the arbitration model, the parent treats each child's context demand as a *claim* against a shared pool, and the invariant is that **sum of admitted claims plus reserved headroom must never exceed the pool, and every starvation must be observable as a first-class event rather than a silent truncation**. The non-obvious boundary: the failure is not when the pool runs out. The failure is when the pool runs out *and the parent's synthesis step still needs the full results of every child*. At that moment, the parent has admitted N children that each consumed tokens, but the parent's own remaining context cannot hold N complete results. The orchestrator has budgeted the children but not the *reassembly*. ## What Was Observed In a hierarchical run dispatching six sibling research sub-agents against a 200k-token shared pool, each child was admitted with a nominal 28k slice (168k total, 32k reserved for parent synthesis). Five children returned results within slice. The sixth child — a synthesis-heavy task — returned a 41k result after internally compressing its own reasoning, exceeding its slice by 13k. The parent accepted the result because the child's return was well-formed. The parent then attempted to assemble the six results into a final answer. The combined result payload was 5×27k + 41k ≈ 176k tokens, leaving 24k for the parent's own reasoning and output — but the parent's synthesis instructions and the original task framing occupied 19k, leaving 5k of working room. The parent produced a truncated, partially-correct synthesis and reported success. No error was raised at any layer. The observable signature was: **child-level budget compliance was enforced per-child, but parent-level reassembly was never checked as a budget claim.** Each child passed its own admission check; the parent's synthesis step had no admission check at all. ## Condition of Validity Arbitration is required precisely when all of the following hold: 1. Siblings are dispatched concurrently or in an order the parent does not control, so the parent cannot observe aggregate consumption before committing later siblings. 2. Child result size is not known a priori and is not bounded by the child's slice — a child may return more than it was allotted if its internal compression is lossy or if it is permitted to return raw context. 3. The parent performs a join over *all* child results (not a reduce/select that can discard), so parent working context scales with the sum of result sizes. 4. No external ledger exists that could reject the parent's synthesis step for over-budget. When any of these fails — e.g., children are strictly sequential and the parent can re-plan between them, or the join is a top-k selection — static partitioning is adequate and arbitration is over-engineering. ## Decision Boundary The boundary is not "pool exhausted." It is **whether the parent's reassembly cost is modeled as a claim against the same pool**. Concretely: - If `sum(admitted_child_slices) + parent_reassembly_reserve <= pool`, static partitioning is safe. - If a child is permitted to return more than its slice (over-return), the parent must either (a) enforce hard truncation at return with an explicit `OverReturnError`, or (b) treat the over-return as a *post-hoc claim* that reduces every subsequent sibling's admissible slice and the parent's reserve. - If neither is done, the parent will silently synthesize on a starved context and produce a plausible-but-wrong answer — the worst failure mode, because it is unobservable. The decision to arbitrate is triggered by the presence of over-return combined with a full join. Over-return without a full join is harmless. A full join without over-return is harmless. Both together require arbitration. ## Operational Effect An orchestrator that implements claim-based arbitration with an explicit reassembly reserve converts a silent truncation failure into a loud, attributable error. The parent, on detecting that admitted claims plus reserve exceed the pool, must either reject the largest child's over-return, re-dispatch the child with a compressed-output instruction, or drop a sibling and record the drop as a degraded-synthesis event. Each of these is observable and recoverable. The static-partition orchestrator, by contrast, produces a final answer whose failure is invisible until downstream evaluation. The operational rule: **reserve the parent's reassembly budget before admitting any child, and treat every child return larger than its slice as a claim that must be re-validated against the remaining reserve — never accept it silently.** ```python class ReassemblyBudgetExceededError(RuntimeError): """Raised when admitted child claims plus parent reassembly reserve exceed the shared pool.""" pass class ContextPool: def __init__(self, total: int, reassembly_reserve: int): if reassembly_reserve >= total: raise ReassemblyBudgetExceededError( f"reserve {reassembly_reserve} leaves no room for children in pool {total}" ) self.total = total self.reserve = reassembly_reserve self.admitted = 0 self.returns = [] def admit(self, child_id: str, slice_tokens: int) -> None: if self.admitted + slice_tokens + self.reserve > self.total: raise ReassemblyBudgetExceededError( f"admitting child {child_id} at {slice_tokens} would exceed pool: " f"admitted={self.admitted} reserve={self.reserve} total={self.total}" ) self.admitted += slice_tokens def accept_return(self, child_id: str, slice_tokens: int, returned_tokens: int) -> None: # Over-return is a post-hoc claim against the reserve, not a free event. over = returned_tokens - slice_tokens if over > 0: if over > self.reserve: raise ReassemblyBudgetExceededError( f"child {child_id} over-returned by {over} tokens, " f"exceeds reassembly reserve {self.reserve}; reject or re-dispatch" ) self.reserve -= over self.returns.append(returned_tokens) def assert_synthesis_fits(self, framing_tokens: int, working_tokens: int) -> None: payload = sum(self.returns) if payload + framing_tokens + working_tokens > self.reserve + (self.total - self.admitted): raise ReassemblyBudgetExceededError( f"synthesis infeasible: payload={payload} framing={framing_tokens} " f"working={working_tokens} available={self.reserve + (self.total - self.admitted)}" ) def run_orchestration(pool: ContextPool, children, framing_tokens: int, working_tokens: int): for cid, slice_tokens in children: pool.admit(cid, slice_tokens) for cid, slice_tokens, returned in children: pool.accept_return(cid, slice_tokens, returned) pool.assert_synthesis_fits(framing_tokens, working_tokens) return "synthesis admitted" ```