The problem
Budget enforcement fails in two directions. Enforced only at admission, a budget stops new tasks and does nothing about the one that is currently looping. Enforced only inside the loop, a workspace that has already blown its monthly ceiling keeps accepting work as long as each individual run is small. The second problem is arithmetic. The loop tracks spend to decide whether to keep iterating; the per-call gate tracks spend to decide whether to allow the next call. If those two read different numbers, the one that is easier to satisfy is the real limit, and the other is decoration.The dimensions and their ceilings
They are written as
cap rules. target: spend with params.period of month
or run sets the spend caps: a monthly rule bound to the workspace caps the
workspace, the same rule bound to an agent caps that agent’s own spend, and an
agent cap above the workspace cap is refused; target: service sets the service cap;
target: tokens sets both token values. A cap rule missing its amount is
skipped and logged rather than treated as zero.
The default baseline for a new workspace is 500.00 USD per month, 50.00 USD per
run, 20,000,000 total tokens and 100,000 tokens per call.
Currency
Inference amounts — task costs,monthly_spend_cap_usd and run_budget_usd —
are in the deployment’s billing currency, despite the _usd suffix. The suffix
is kept for API compatibility. The platform converts each LLM call once, when
the call returns, through the customer_pricing extension, and every total and
budget check after that reads the converted amount. Without the extension the
billing currency is USD and the amount is the provider cost, so an OSS
deployment behaves as the field names say. GET /v1/pricing/currency returns
the code. Service spend is outside this: it stays in USD, because the payments
it bounds are made in USD.
Where each is enforced
Monthly caps: the spend ledger
Month-to-date spend is summed from the spend ledger,cost_entries: one row per
paid line — an LLM call, a compaction, a platform model a tool ran, a payment a
tool made from the agent’s wallet — with the task, the agent, what was bought and
the evidence it was priced from. Rows are written from the run’s persisted events
as they are stored, so a running task’s spend counts as it happens. Wallet
payments are recorded in USD and count towards the caps only where USD is the
billing currency.
When a task is created or continued, TaskService._enforce_budget_cap compares
the workspace’s month-to-date spend with monthly_spend_cap_usd and the agent’s
own with agent_monthly_spend_cap_usd, and raises BudgetCapExceededError when
either has been reached. No cap configured means no check. The rejection reaches
a client as HTTP 402 in problem+json, with code: budget_cap_exceeded,
current_mtd_usd, cap_usd, workspace_id and, for an agent cap, agent_id.
A run reads both again when it starts and whenever it resumes. The agent cap is
also checked after every paid call: once the agent’s month-to-date spend plus
what the run has spent since reaches the cap, further tool calls are denied and
the run stops before its next model turn. The cap can be overshot by the call
that crosses it, and by what other runs of the same agent spend between two of
this run’s checks.
Run budget: two enforcement points, one number
The per-task USD budget has two enforcement points, and they are reconciled deliberately.resolve_effective_budget takes the minimum of the budget on the task request
and the policy’s run_budget_usd. That single number configures the workflow’s
BudgetTracker, which decides whether the loop continues, and the same policy
value reaches CostBudgetGuard through the flattened execution_state. Neither
source can loosen the other.
CostBudgetGuard runs at both pre_llm_call (priority 100) and pre_tool_call
(also 100). It compares consumed cost against the budget: WARN at 80 percent,
DENY at 100 percent. A denial at the activity boundary raises
GovernanceDenied, which fails the activity.
If nothing is configured at all, BudgetTracker falls back to a default of
10.00 USD per run, and its warning threshold is 80 percent.
Service budget
Service spend — payments an agent makes through a paid tool — is tracked separately from inference cost.ServiceBudgetGuard runs at pre_tool_call
(priority 105) with the same thresholds: warn at 80 percent, deny when exhausted.
BudgetTracker maintains the matching counters in the workflow, where a limit of
zero or less means unlimited.
Tokens
TokenBudgetGuard runs at pre_llm_call (priority 110), warning at 85 percent of
max_tokens and denying at 100 percent.
How the numbers reach the gates
The gates do not read policy objects. The effective policy is flattened into a plain dictionary —budget_usd, service_budget_usd, max_tokens,
tools_config, escalation_rules, content_safety — and the runtime counters
the activity request carries (cost_used, service_cost_used, tokens_used) are
merged into it before each phase runs.
Money crosses that boundary as a float, on purpose. The gates do ratio arithmetic
against running counters, and mixing Decimal with float there would raise
inside the gate — where the pipeline swallows the exception and the guard would
silently stop guarding. Authoritative money accounting stays in Money (Decimal)
in BudgetTracker and upstream of it.
Why not a central quota service
The obvious alternative is one counter service that every component increments and consults, so there is exactly one number. AgentArea does not have one, for a structural reason: the loop check and the call check are asked at different times and need different guarantees. The loop decision has to be deterministic. It runs inside a Temporal workflow, which replays its history on recovery, so it cannot make a network call to a counter service — the answer would differ between the original run and the replay and break determinism. The per-call decision runs inside an activity, where IO is allowed but where the value must already be in the request for the interceptor to see it. The design that survives that constraint is: resolve one ceiling, carry it in the snapshot, and have both points enforce the same number. What it costs is that counters are per-task and local. There is no cross-task, real-time spend counter, which is why the monthly cap is checked at admission against the database rather than continuously.Limits
max_tokens_per_callis configured and never enforced. It is a field on the token policy, it is merged monotonically across scopes, it is in the default workspace baseline at 100,000, and it appears in the API. It is not written intoexecution_stateand no gate reads it. A single call may exceed it freely.- The monthly cap is admission-only. A task that starts under the cap runs to completion regardless of how far past the cap the workspace goes, and concurrent task creation can cross it — the check is a read-then-decide with no lock.
- A budget denial fails the activity.
CostBudgetGuarddenying atpre_tool_callraisesGovernanceDeniedrather than returning a message the model can respond to. The graceful path — the loop noticing exhaustion and completing — comes fromBudgetTracker, not from the gate. - A guard that crashes stops guarding. The pipeline catches exceptions from an interceptor, logs the traceback and moves on. Failure of a budget gate is fail-open for that dimension.
- Unconfigured means unlimited. Each guard allows unconditionally when its ceiling is absent or not greater than zero. A workspace whose default policies were removed has no token ceiling and no service ceiling.
- Cost figures depend on the provider. Per-call cost is read from the LLM response where the provider supplies it, and estimated at a flat rate per token when it does not. A budget is therefore as accurate as the upstream cost reporting.
- There are no rate or concurrency quotas. Core bounds spend and tokens. It does not bound requests per second, concurrent tasks per workspace, or tool calls per minute. Anonymous or public execution has no quota of its own.
- Nothing writes budget events to the audit log. Budget warnings and denials
appear in the task event stream and in logs, not in
audit_events. See audit.
Related
The policy engine
How ceilings merge and where the snapshot comes from
Tool authorization
The other gates sharing the pre-tool phase
Audit
What is and is not recorded