Source code for ralph.recovery.agent_budget_registry
"""Registry mapping (phase, agent_name) to budget state."""
from __future__ import annotations
from typing import TYPE_CHECKING
from .budget_state import BudgetState
if TYPE_CHECKING:
from collections.abc import Iterable
from .classifier import ClassifiedFailure
[docs]
class AgentBudgetRegistry:
"""Registry mapping (phase, agent_name) -> BudgetState.
Immutable-value-returning: ``debit`` returns a new registry instance.
The previous ``reset`` method was removed in wt-024 memory-perf
AC-01: it had zero callers (repo-wide grep) and violated the
AGENTS.md "Absolutely Zero Dead code" rule.
"""
def __init__(self, budgets: dict[tuple[str, str], BudgetState] | None = None) -> None:
self._budgets: dict[tuple[str, str], BudgetState] = budgets or {}
def get(self, phase: str, agent: str) -> BudgetState | None:
return self._budgets.get((phase, agent))
[docs]
def set_budget(self, phase: str, agent: str, max_retries: int) -> AgentBudgetRegistry:
"""Return a new registry with this budget initialized."""
new = dict(self._budgets)
new[(phase, agent)] = BudgetState(max_retries=max_retries)
return AgentBudgetRegistry(new)
[docs]
def debit(self, phase: str, agent: str, failure: ClassifiedFailure) -> AgentBudgetRegistry:
"""Return a new registry with the failure debited for (phase, agent).
The previous ``failures=(*current.failures, failure)`` accumulator
was removed in wt-024 memory-perf AC-01: the failures tuple was
appended on every debit and never read for any decision, while
retaining heavyweight ``ClassifiedFailure`` objects
(original_exception + traceback frames) for the lifetime of the
registry. Only ``consumed`` is needed to drive the
exhausted / remaining decisions.
"""
if not failure.counts_against_budget:
return self
current = self._budgets.get((phase, agent), BudgetState(max_retries=3))
new_state = BudgetState(
max_retries=current.max_retries,
consumed=current.consumed + 1,
)
new = dict(self._budgets)
new[(phase, agent)] = new_state
return AgentBudgetRegistry(new)
[docs]
def is_exhausted(self, phase: str, agent: str) -> bool:
"""Check if the budget for (phase, agent) is exhausted."""
state = self._budgets.get((phase, agent))
if state is None:
return False
return state.exhausted
[docs]
def items(self) -> Iterable[tuple[tuple[str, str], BudgetState]]:
"""Iterate over ((phase, agent), state) pairs without exposing the internal dict."""
return self._budgets.items()