Budget Management
Per-agent spend limits with automatic enforcement on every acquire() call.
Quick Setup
import loco
loco.configure(capacity=3, budget_mode="reject")
loco.set_budget("analyst", max_cost=50.0)
loco.set_budget("batch-processor", max_cost=20.0)
# coordinator has no limit -- uncapped by default
Budget Units
Budget units are weight units -- the same as Task.weight. Not dollars, not tokens.
| Model tier | Weight | Meaning |
|---|---|---|
| haiku / gpt-4o-mini | 1.0 | 1 budget unit per call |
| sonnet / gpt-4o | 2.0 | 2 budget units per call |
| opus / o1 | 5.0 | 5 budget units per call |
A budget of 50.0 means: the agent can make 25 sonnet-class calls (weight=2.0 each), or 10 opus-class calls (weight=5.0 each), or any mix that sums to 50.
Enforcement Modes
| Mode | What happens when budget is exceeded |
|---|---|
"reject" |
Raises BudgetExceededError. Resource is released and given to the next waiting agent. |
"alert" |
Task proceeds. An alert is recorded. Budget overspend is tracked. |
"downgrade" |
Task proceeds. Flagged for model tier downgrade. |
from loco.budget import BudgetExceededError
try:
result = await loco.wrap(call_llm, agent_id="analyst", weight=2.0)
except BudgetExceededError as e:
print(f"Agent {e.agent_id} over budget: spent {e.current}, limit {e.limit}")
How Enforcement Works
Budget is checked after the resource is acquired but before the work executes. This means:
- Agent waits in the scheduling queue like normal
- Agent wins a resource slot via L(i) scoring
- Budget check runs -- if over budget:
- Resource is immediately released
- Next waiting agent gets the slot
BudgetExceededErroris raised (reject mode) or alert recorded (alert mode)
- If within budget: work proceeds, spend is recorded on release
This design ensures rejected tasks don't inflate cost metrics or emit phantom grant events.
Checking Budget State
from loco.budget import BudgetManager
budget = scheduler.budget
budget.spent("analyst") # Cumulative spend
budget.remaining("analyst") # Budget remaining (None if uncapped)
budget.get_limit("analyst") # The configured limit
budget.summary() # Full state for all agents
budget.alerts # List of all exceeded events
Session Cost Tracking
Tag tasks with a session_id to track costs per session (request, workflow, conversation):
Query session costs:
scheduler.metrics.cost_by_session()
# {"req-abc123": 17.0, "req-def456": 8.0}
scheduler.metrics.session_cost("req-abc123")
# 17.0
scheduler.metrics.cost_by_session_and_agent("req-abc123")
# {"analyst": 12.0, "reviewer": 5.0}
scheduler.metrics.sessions()
# ["req-abc123", "req-def456"]
Tasks without session_id are tracked in agent totals but not in session rollups.
Resetting Budgets
Use this for daily/monthly budget cycles.
Full API (BudgetManager)
For direct use without the convenience API:
from loco import AsyncLOCOScheduler, SharedResource
from loco.budget import BudgetManager
budget = BudgetManager(default_limit=100.0, on_exceeded="reject")
budget.set_limit("expensive-agent", max_cost=50.0)
scheduler = AsyncLOCOScheduler(
agents, resource,
budget=budget, # wired into acquire() automatically
)
Pretty Output
With LOCO_LOG=pretty, budget events show in the terminal: