Budget
One ledger, per-principal and per-bucket caps, three OverflowPolicy modes. The pre-call gate that runs before every wire activity.
Ledger records spend on (principal_id, bucket_id) pairs. BudgetGate.check runs before any wire activity and enforces every applicable cap. Each cap carries an OverflowPolicy — strictest policy across overflowed caps wins.
from decimal import Decimal
from entorin.budget import BudgetGate, MemoryLedger, OverflowPolicy
ledger = MemoryLedger()
ledger.set_cap("alice", Decimal("5.00")) # principal-wide
ledger.set_cap(
"alice",
Decimal("0.50"),
bucket_id="research-crew",
overflow_policy=OverflowPolicy.FINISH_RUN,
)
gate = BudgetGate(ledger, bus=bus)
gate.check("alice", est_cost_usd=Decimal("0.02"),
bucket_id="research-crew", run_id=ctx.run_id)
# raises BudgetError if any cap would be exceeded under ABORT semantics
OverflowPolicy
Three modes, defined in entorin/budget/overflow.py:
| Mode | Behaviour |
|---|---|
ABORT (default) | Raise BudgetError immediately. The current call never runs. |
FINISH_STEP | Let the in-flight LLM/tool call complete (so partial output isn’t wasted) but raise BudgetError on the next gate check that would also exceed the cap. Bounds the overrun to one step. |
FINISH_RUN | Emit budget.exceeded once and let the run continue. The cap becomes advisory; operators choose this when partial-run completion is more valuable than strict enforcement. |
When multiple caps overflow on the same check, the strictest wins: ABORT > FINISH_STEP > FINISH_RUN. A FINISH_RUN bucket cap does not subvert an ABORT principal cap set above it.
Buckets
bucket_id=None is the principal-wide scope. Any other string is opaque — the control plane never interprets it. Wire it from agent id, task type, crew name, or any composite. Bucket spend also accrues to the principal balance, so principal caps cannot be silently bypassed by routing through buckets.
Soft policies need a bus
FINISH_STEP and FINISH_RUN rely on budget.exceeded events for audit. If the gate is constructed without a bus or check() is called without run_id, the gate falls back to ABORT semantics on overflow — silent leniency would break the observability contract.
Reference impl
MemoryLedger is in-process and thread-safe. The Ledger protocol is the contract; production deployments back the same shape with Postgres or Redis.