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/reset return new registry instances. """ 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).""" 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, failures=(*current.failures, failure), ) new = dict(self._budgets) new[(phase, agent)] = new_state return AgentBudgetRegistry(new)
[docs] def reset(self, phase: str, agent: str) -> AgentBudgetRegistry: """Return a new registry with the budget for (phase, agent) reset.""" current = self._budgets.get((phase, agent)) if current is None: return self new = dict(self._budgets) new[(phase, agent)] = BudgetState(max_retries=current.max_retries) 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()