How must an instance bound cost cascades when agents recursively invoke peers?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session invokes peer agents, and those peers may themselves invoke other peers, forming recursive invocation chains, and each hop consumes cost (tokens, compute, money) that compounds across the recursion. Does not apply to strictly flat single-hop invocations or invocations with fixed cost budgets.
Core Distinction
The separation between **per-invocation cost** (each individual hop's cost) and **cumulative cascade cost** (the total cost of the recursive tree of invocations). Relying on per-invocation creates the **Recursive Cost Explosion**: each hop costs $0.10, but agent A invokes 5 peers, each of which invokes 5, which invoke 5, for a total of 156 invocations and $15.60—150x the cost of the single perceived call, and no one at the top has visibility.
What Was Observed
In recursive agent architectures, failing to bound cascade costs produces three primary failure modes:
- **Silent Budget Depletion:** The root agent budgets $1 for the task. The recursion consumes $50. The budget is depleted before the root agent knows.
- **Compounding Latency:** Each hop adds latency. Recursive invocations compound it. A 10-second task becomes 3 minutes because each of 5 sub-agents adds its own latency.
- **No Attributed Cost:** When the cascade finishes, no one can attribute the total cost to specific invocation paths, preventing optimization.
Recorded Elements
- **`cascade_budget_envelope_receipt`:** The total budget available for the recursive cascade, distributed across the tree by the root agent.
- **`per_hop_budget_allocation_signature`:** Each hop's allocated share of the budget, enforced by the delegating agent before invocation.
- **`cascade_cost_consumption_receipt`:** A real-time record of actual cost consumption per invocation path, so the root agent can see the cascade's progress.
- **`cascade_abort_predicate_signature`:** The exact condition under which the cascade halts (budget exhaustion, depth limit, or time limit), preventing runaway recursion.
Condition of Validity
* **Applies when:** Agents can recursively invoke peers, invocation cost is non-trivial, and cascade cost could exceed budget. * **Does not apply when:** Invocations are flat (no recursion) or cost is negligible relative to budget.
Decision Boundary
- **Does the invocation support recursion?** If no, flat budget applies. If yes, proceed.
- **Is the `cascade_budget_envelope_receipt` allocated before invocation?** If yes, each hop has a bounded share. If no, the cascade could exceed budget.
- **Has the `cascade_cost_consumption_receipt` shown any path exceeding its allocation?** If yes, halt that path. If no, proceed.
- **Conversely, when can a cascade proceed without budget allocation?** Only when total cost is verified to be below any conceivable budget (e.g., all peers are free), or the root agent explicitly accepts cost risk.
Operational Effect
- **Silent Depletion Prevention:** Budget envelopes prevent any single path from consuming unbounded cost.
- **Latency Bounds:** Depth limits or time limits are enforced alongside cost.
- **Attribution:** Per-path cost records allow post-hoc optimization.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class CascadeVerdict(Enum):
FLAT_INVOCATION = auto()
CASCADE_BUDGET_OK = auto()
CASCADE_BUDGET_EXCEEDED = auto()
CASCADE_DEPTH_EXCEEDED = auto()
@dataclass(frozen=True)
class InvocationNode:
node_id: str
depth: int
allocated_budget: float
consumed_budget: float
children: tuple[str, ...]
@dataclass(frozen=True)
class CascadeState:
cascade_id: str
nodes: tuple[InvocationNode, ...]
total_budget: float
max_depth: int
@dataclass(frozen=True)
class CascadePolicy:
require_budget_allocation: bool
@dataclass(frozen=True)
class CascadeManifest:
verdict: CascadeVerdict
total_consumed: float
max_depth_reached: int
directive: str
manifest_digest: str
class RecursiveCostGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_cascade(
cls, state: CascadeState, policy: CascadePolicy
) -> CascadeManifest:
# Gate 1: Flat invocation
max_depth_reached = max((n.depth for n in state.nodes), default=0)
if max_depth_reached <= 1:
digest = cls._hash(json.dumps({
"cascade": state.cascade_id,
"type": "FLAT"
}, sort_keys=True))
return CascadeManifest(
verdict=CascadeVerdict.FLAT_INVOCATION,
total_consumed=sum(n.consumed_budget for n in state.nodes),
max_depth_reached=max_depth_reached,
directive=f"PROCEED: Flat invocation, no cascade.",
manifest_digest=digest
)
# Gate 2: Depth
if max_depth_reached > state.max_depth:
return CascadeManifest(
verdict=CascadeVerdict.CASCADE_DEPTH_EXCEEDED,
total_consumed=sum(n.consumed_budget for n in state.nodes),
max_depth_reached=max_depth_reached,
directive=f"HALT: Depth {max_depth_reached} > {state.max_depth}.",
manifest_digest=cls._hash(f"DEPTH:{max_depth_reached}")
)
# Gate 3: Budget
total_consumed = sum(n.consumed_budget for n in state.nodes)
if total_consumed > state.total_budget:
return CascadeManifest(
verdict=CascadeVerdict.CASCADE_BUDGET_EXCEEDED,
total_consumed=total_consumed,
max_depth_reached=max_depth_reached,
directive=f"HALT: Consumed {total_consumed:.2f} > budget {state.total_budget:.2f}.",
manifest_digest=cls._hash(f"OVER_BUDGET:{total_consumed:.2f}")
)
# Gate 4: Per-node check
if policy.require_budget_allocation:
over_allocated = [
n.node_id for n in state.nodes
if n.consumed_budget > n.allocated_budget
]
if over_allocated:
return CascadeManifest(
verdict=CascadeVerdict.CASCADE_BUDGET_EXCEEDED,
total_consumed=total_consumed,
max_depth_reached=max_depth_reached,
directive=f"HALT: {len(over_allocated)} nodes exceeded allocation.",
manifest_digest=cls._hash(f"NODE_OVER:{len(over_allocated)}")
)
# Gate 5: OK
digest = cls._hash(json.dumps({
"cascade": state.cascade_id,
"nodes": str(len(state.nodes)),
"consumed": f"{total_consumed:.2f}",
"depth": str(max_depth_reached)
}, sort_keys=True))
return CascadeManifest(
verdict=CascadeVerdict.CASCADE_BUDGET_OK,
total_consumed=total_consumed,
max_depth_reached=max_depth_reached,
directive=f"PROCEED: Cascade within budget ({total_consumed:.2f}/{state.total_budget:.2f}), depth {max_depth_reached}.",
manifest_digest=digest
)