Skip to content

Budget

Budget enforcement applies resource limits to every run. When any limit is hit, a budget.exhausted event is written to the ledger and the run stops.

Budget

kando.responders.budget.Budget(max_events=10000, max_llm_cost_usd=10.0, max_wall_seconds=3600.0, max_recursion_depth=50) dataclass

BudgetEnforcer

kando.responders.budget.BudgetEnforcer(budget, run_id)

Tracks cumulative usage and emits budget.exhausted when limits are hit.

Source code in kando/responders/budget.py
def __init__(self, budget: Budget, run_id: str) -> None:
    self._budget = budget
    self._run_id = run_id
    self._event_count = 0
    self._llm_cost = 0.0
    self._start_time = time.monotonic()
    self._depths: dict[str, int] = {}
    self._last_depth = 0

record(event, world)

Unconditional accounting -- call this for every event, always.

Source code in kando/responders/budget.py
def record(self, event: KandoEvent, world: World) -> None:
    """Unconditional accounting -- call this for every event, always."""
    self._event_count += 1
    if event.type == "llm.response":
        self._llm_cost += event.data.get("cost_usd", 0.0)
    if event.cause:
        depth = max(self._depths.get(c, 0) for c in event.cause) + 1
    else:
        depth = 0
    self._depths[event.id] = depth
    # Prune entries that can never be referenced again as a cause
    if len(self._depths) > self._budget.max_recursion_depth * 4:
        cutoff = depth - self._budget.max_recursion_depth
        self._depths = {k: v for k, v in self._depths.items() if v >= cutoff}
    self._last_depth = depth

violations(event)

Return budget-exhausted events if any limit is breached. Call after record().

Source code in kando/responders/budget.py
def violations(self, event: KandoEvent) -> list[KandoEvent]:
    """Return budget-exhausted events if any limit is breached. Call after record()."""
    elapsed = time.monotonic() - self._start_time
    depth = self._last_depth
    reasons = []
    if self._event_count >= self._budget.max_events:
        reasons.append(f"max_events={self._budget.max_events}")
    if self._llm_cost >= self._budget.max_llm_cost_usd:
        reasons.append(f"max_llm_cost_usd={self._budget.max_llm_cost_usd}")
    if elapsed >= self._budget.max_wall_seconds:
        reasons.append(f"max_wall_seconds={self._budget.max_wall_seconds} (elapsed={elapsed:.1f}s)")
    if depth >= self._budget.max_recursion_depth:
        reasons.append(f"max_recursion_depth={self._budget.max_recursion_depth} (depth={depth})")
    if not reasons:
        return []
    return [KandoEvent(
        id=f"budget-{self._event_count}",
        type=BUDGET_EXHAUSTED,
        source=f"run:{self._run_id}",
        actor="budget-enforcer",
        cause=[event.id],
        timestamp=datetime.now(timezone.utc),
        data={"reasons": reasons, "event_count": self._event_count,
              "llm_cost_usd": self._llm_cost, "elapsed_seconds": elapsed, "depth": depth},
    )]

Limits

Limit Default Description
max_events 10,000 Maximum total events in the run
max_llm_cost_usd $10.00 Maximum LLM spend (accumulated from llm.response events with cost_usd)
max_wall_seconds 3,600 Maximum wall-clock time (1 hour)
max_recursion_depth 50 Maximum causal depth: how many responder hops from the seed

Usage

from kando.responders.budget import Budget
from kando.runtime import Runtime

# Tight limits for a fast smoke test
runtime = Runtime(
    ledger=store,
    responders=create_kit(),
    budget=Budget(
        max_events=100,
        max_llm_cost_usd=0.50,
        max_wall_seconds=30.0,
        max_recursion_depth=10,
    ),
)
world = runtime.run(seed)

# Check if budget was exhausted
from kando.schema.events import BUDGET_EXHAUSTED
all_events = list(store.read_all())
exhausted = [e for e in all_events if e.type == BUDGET_EXHAUSTED]
if exhausted:
    reasons = exhausted[0].data["reasons"]
    print("Budget exhausted:", reasons)

The budget.exhausted event

When a limit is hit, the enforcer emits:

KandoEvent(
    type="budget.exhausted",
    data={
        "reasons": ["max_events=10000"],  # list of triggered limits
        "event_count": 10001,
        "llm_cost_usd": 2.50,
        "elapsed_seconds": 42.1,
        "depth": 7,
    }
)

Multiple limits can be reported in a single reasons list.

Recursion depth

Depth is tracked via the causal chain: depth[event] = max(depth[cause] for cause in event.cause) + 1. Root events (no cause) have depth 0. This makes infinite responder chains impossible.