What search-budget allocation invariants prevent discovery starvation in cost-constrained agent networks?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session has a bounded discovery budget (API calls, compute, tokens) and must allocate it across multiple discovery needs (finding peers, verifying capabilities, resolving references), where spending on one need reduces budget for others. Does not apply to sessions with unlimited discovery budgets or single-need sessions.
Core Distinction
The separation between **immediate need satisfaction** (fulfill the current discovery request) and **budget sustainability** (preserve budget for future discovery needs). Relying on immediate needs creates the **Discovery Starvation**: the agent exhausts its budget on the current discovery, and later discovery needs (which might be more critical) cannot be satisfied because budget is gone.
What Was Observed
In budget-constrained discovery architectures, failing to allocate budgets carefully produces three primary failure modes:
- **Burst Then Starve:** The agent performs comprehensive discovery upfront (exhaustive peer search, full capability verification), burns 90% of budget, then cannot afford targeted discovery for unexpected needs later.
- **Priority Inversion:** Low-priority discovery (nice-to-have peer information) consumes budget that should have been reserved for high-priority (blocking) discovery, and critical needs go unsatisfied.
- **Starvation by Recency:** Recent discovery needs seem more urgent than earlier ones, so the agent spends budget on whatever just came up, and never reserves for anticipated but not-yet-manifest needs.
Recorded Elements
- **`discovery_budget_signature`:** The exact budget available (total budget, current remaining), with the units and any constraints (time, money, compute).
- **`need_priority_manifest`:** The registry of anticipated discovery needs with their priorities (blocking, important, nice-to-have), so the agent knows the opportunity cost of spending budget on any single need.
- **`per_discovery_cost_receipt`:** For each discovery operation, its measured cost and value, so the agent can update its cost model and detect over-spending.
- **`budget_reservation_receipt`:** Explicit reservations of budget for anticipated future needs, preventing current needs from consuming the reserve.
Condition of Validity
* **Applies when:** Discovery budget is bounded, multiple discovery needs compete for budget, and starving later needs has consequences. * **Does not apply when:** Budget is unlimited, or there is only one discovery need that cannot be deferred.
Decision Boundary
- **Is the discovery budget bounded?** If no, standard discovery applies. If yes, proceed.
- **Is the current discovery need in the priority manifest?** If yes, use its priority. If no, treat as intermediate priority.
- **Does fulfilling the current need consume reserved budget (`budget_reservation_receipt`)?** If yes, defer or escalate. If no, proceed.
- **Conversely, when can an agent spend budget without priority check?** Only when the discovery is strictly blocking (cannot proceed without it), or the agent has verified that no future needs will require the budget.
Operational Effect
- **Starvation Prevention:** Budget reserves prevent starvation of anticipated needs.
- **Priority Enforcement:** Spending decisions respect priority ordering.
- **Cost Model Evolution:** Per-discovery costs refine the agent's budgeting over time.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class BudgetVerdict(Enum):
UNLIMITED_BUDGET = auto()
SPEND_APPROVED = auto()
SPEND_BLOCKED_BY_RESERVE = auto()
SPEND_BLOCKED_BY_PRIORITY = auto()
@dataclass(frozen=True)
class DiscoveryNeed:
need_id: str
priority: str # "BLOCKING" | "IMPORTANT" | "NICE_TO_HAVE"
estimated_cost: float
is_reserved: bool
@dataclass(frozen=True)
class BudgetState:
total_budget: float
spent: float
reserved: float
@dataclass(frozen=True)
class BudgetPolicy:
min_remaining_pct: float
@dataclass(frozen=True)
class BudgetManifest:
verdict: BudgetVerdict
remaining_after: float
directive: str
manifest_digest: str
class SearchBudgetGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_spend(
cls, need: DiscoveryNeed, state: BudgetState, policy: BudgetPolicy
) -> BudgetManifest:
# Gate 1: Unlimited
if state.total_budget == 0: # 0 = unlimited sentinel
digest = cls._hash(json.dumps({
"need": need.need_id,
"budget": "UNLIMITED"
}, sort_keys=True))
return BudgetManifest(
verdict=BudgetVerdict.UNLIMITED_BUDGET,
remaining_after=-1.0,
directive=f"PROCEED: Unlimited budget, spend on {need.need_id}.",
manifest_digest=digest
)
available = state.total_budget - state.spent - state.reserved
# Gate 2: Reserve check
if need.estimated_cost > available:
return BudgetManifest(
verdict=BudgetVerdict.SPEND_BLOCKED_BY_RESERVE,
remaining_after=available,
directive=f"HALT: Cost {need.estimated_cost:.2f} > available {available:.2f} (reserved: {state.reserved:.2f}).",
manifest_digest=cls._hash(f"RESERVED:{need.need_id}")
)
# Gate 3: Priority check (non-blocking needs must preserve min remaining)
remaining_after = available - need.estimated_cost
min_remaining = state.total_budget * policy.min_remaining_pct
if need.priority != "BLOCKING" and remaining_after < min_remaining:
return BudgetManifest(
verdict=BudgetVerdict.SPEND_BLOCKED_BY_PRIORITY,
remaining_after=remaining_after,
directive=f"HALT: Non-blocking {need.need_id} would leave {remaining_after:.2f} < min {min_remaining:.2f}.",
manifest_digest=cls._hash(f"PRIORITY:{need.need_id}")
)
# Gate 4: Approved
digest = cls._hash(json.dumps({
"need": need.need_id,
"priority": need.priority,
"cost": f"{need.estimated_cost:.2f}",
"remaining_after": f"{remaining_after:.2f}"
}, sort_keys=True))
return BudgetManifest(
verdict=BudgetVerdict.SPEND_APPROVED,
remaining_after=remaining_after,
directive=f"PROCEED: Spend {need.estimated_cost:.2f} on {need.need_id}, {remaining_after:.2f} remaining.",
manifest_digest=digest
)