Budgets & cost
apomesh meters every session’s spend and lets you cap it. A budget bounds a single session’s agent tree; a quota bounds a whole tenant across every session. The substrate accounts cost as work runs — folding each priced action onto the event log — and enforces the caps from that same accounting. This page covers the three places you set a budget, how the cap carves down the agent tree, what happens when a session exhausts one, how a quota differs, and where spend surfaces in the console.
The three budget dimensions
Section titled “The three budget dimensions”A Budget
has three independent dimensions — tokens, time, and cost — and you
declare any subset, including none. A dimension you leave unset pins no cap on
that dimension; an all-unset budget runs the agent unconstrained (bounded only
by the rings above it — the substrate global cap and the tenant quota).
tokens— a whole-number token ceiling across the agent’s LLM calls.time— a wall-clock ceiling, expressed in whole seconds.cost— a monetary ceiling. Cost is carried as an integer count of micros (millionths of a currency unit) plus a currency tag, not a float — integer math avoids drift when thousands of per-call costs sum into one running total.2_000_000micros is $2.00. USD and EUR are the currencies the substrate carries today.
Dimensions compose with AND semantics: crossing any declared dimension exhausts the budget. The three are metered independently, so a token-heavy run can trip the token cap long before the cost cap, and either ends the session the same way.
How the budget carves down the agent tree
Section titled “How the budget carves down the agent tree”A session is a tree of agents — a planner spawns children, children spawn grandchildren. The budget you set on the root bounds the whole tree, not just the root agent, because every spawn carves the child’s allowance out of the parent’s remaining pool.
The substrate tracks this per agent as three slots: allocated (set once at
spawn, never changes), consumed_self (what this agent has spent), and
committed_to_children (what it has handed down). An agent’s available
budget is allocated − consumed_self − committed_to_children, saturating at
zero. Two paths carve it:
- At an explicit spawn, the
BudgetAllocatorreserves the child’s declared budget against the parent’s available pool. It validates every dimension first and mutates nothing on failure, so a child asking for more than the parent has left fails the spawn cleanly rather than overcommitting the tree. - At a coordinator fan-out, a wave of N sibling members each takes an
equal share of the parent’s available budget —
min(requested, available / N)per dimension, rounded down so the wave can never over-commit the pool. This is the per-wave carve every coordinator loop performs; see Loops for the wave structure it rides.
An unbounded root carves nothing: with no ceiling to divide, every child’s share is unbounded too, so the whole tree runs uncapped. Set a real budget on the root and it propagates down every spawn.
Set a budget — the three homes
Section titled “Set a budget — the three homes”A budget reaches a session from one of three surfaces, in ascending order of precedence.
From the manifest (the default). A published agent’s manifest carries a default budget. Its per-role configuration can additionally carve a per-role budget across all three dimensions; a per-role budget left unset inherits the session budget. These are the shipped defaults every launch starts from.
From the console launch form. When you start a session from a published agent on the Configure & Launch surface (Catalog → Published), the launch form exposes a per-launch budget seeded from the manifest’s default. Editing it overrides the default for that one run without republishing the agent. The per-role three-dimension editor lives in the agent’s configuration, not the launch form. See The operator console.
From the SDK. Attach a Budget to the agent you dispatch — the most direct
control, and the shape the console forms project onto. The shipped sample caps
tokens and cost:
from apomesh.types import Budget, Currency, MonetaryAmount
budget = Budget( tokens=200_000, cost=MonetaryAmount(micros=2_000_000, currency=Currency.USD), # $2.00 — example)Time is whole seconds (time=); cost is a MonetaryAmount in micros. Omit a
field to leave that dimension uncapped. See the full driver in
deploy/local/sample-project/dispatch.py.
What happens at exhaustion
Section titled “What happens at exhaustion”When a declared dimension crosses its cap, the substrate raises
FailureClass::Budget on the offending agent and hands it to the supervisor
cascade. What the cascade does next depends on the session’s on-stall
policy, set at session start.
- Terminate (the default). The session ends with the budget error
(
Lifecycle::Failed(Budget)). Non-interactive callers — batch jobs, CI, scripts — get this behavior; a session started without an explicit policy terminates on exhaustion. - Pause for a human. When the session opts into human-in-the-loop on stall,
the coordinator pauses at the budget boundary and emits a
HitlPausedevent (reasonBudgetExhausted) instead of failing. An operator then approves a grant carryingadditional_budget, which raises the exhausted allowance in place and resumes the session from the paused iteration with planner state intact. A grant of zero (an approve with no added budget) or a rejection terminates the session with the same budget error a Terminate policy would have raised.
A budget stall can also surface at a spawn, not only mid-dispatch: when a
carve genuinely can’t fit a child into the parent’s remaining pool, the
rejection folds into a Failed(Budget) terminal. During a pause the budget is
not consumed — the resolver blocks the session’s dispatches, so cost accounting
halts and the grant raises the ceiling rather than refunding spend. A paused
session surfaces in the console’s HITL facet (Execution mode), where you
resolve it. The orchestrator owns these enforcement points; see
The orchestrator.
Budgets vs. quotas
Section titled “Budgets vs. quotas”A per-session budget is distinct from a per-tenant quota. A budget
bounds one session’s agent tree and is enforced from that session’s cost
accounting. A quota is a daemon-side, rolling-window ceiling evaluated across
every session for a tenant, held in one
QuotaTracker
for the daemon’s lifetime.
Operators configure quotas as (dimension, period) → ceiling mappings on the
tenant’s config. The dimensions cover both cumulative and rate-shaped ceilings:
| Dimension | Shape | Fed by |
|---|---|---|
DollarsPerPeriod, TokensPerPeriod | cumulative spend/tokens per window | each CostEvent |
RatePerSecond, RatePerMinute | dispatch rate | dispatch starts |
Concurrent | in-flight agents right now | dispatch start/end |
Each ceiling is one [[tenants.<id>.quota]] entry:
[[tenants.acme.quota]]dimension = "dollars_per_period"period = "month"dollars = "250.00" # quoted exact decimal — TOML has no decimal type, # and a float would round on the way into a ceiling
[[tenants.acme.quota]]dimension = "tokens_per_period"period = "day"count = 5_000_000
[[tenants.acme.quota]]dimension = "concurrent"count = 8 # `period` is taxonomy here; the window is impliedOne entry per (dimension, period) — a duplicate is a boot error, because
which of two would bind depends on authoring order. A ceiling must be positive:
omitting the entry is how you say “no ceiling on that dimension”, so zero would
be a third decision wearing a ceiling’s representation. period is required
for the two cumulative dimensions, where it decides when the running total
resets.
Periods reset at Hour, Day, Week, or Month boundaries (UTC). The tracker
checks the ceiling before each dispatch: a crossed ceiling raises
FailureClass::QuotaExceeded { dimension, period } and blocks the dispatch,
independent of any single session’s budget. Quotas are soft ceilings by
design — the check-then-record path is non-atomic, so a burst can slip a couple
of dispatches past a tight Concurrent ceiling before the counter catches up;
the next dispatch observes the inflated count and is refused. They exist to bound
runaway behavior and give billing-period control, not to enforce strict
concurrency invariants.
A cumulative ceiling needs a durable ledger
Section titled “A cumulative ceiling needs a durable ledger”A quota is a ceiling plus a running total, and only the ceiling is durable
for free. The running total for the two cumulative dimensions
(DollarsPerPeriod, TokensPerPeriod) lives in a per-tenant usage ledger keyed
(dimension, period, window start) — Postgres-backed on a Postgres
[statestore] backend, in-process otherwise. On the in-process arm a Month
ceiling bounds only the time since the last restart, because each restart opens
a fresh window; the daemon logs which arm it resolved at boot, and warns on the
in-process one whenever a tenant quota is configured.
The lifecycle-derived dimensions deliberately do not persist, and that is not
the same gap: after a restart nothing is running, so a Concurrent count of
zero is the correct reading rather than a lost one, and the one- and
sixty-second rate windows have genuinely elapsed.
Replacing a tenant’s ceilings — editing the TOML and restarting, or
PATCH /api/v1/tenants/{id}/quotas — re-reads the running total rather than
zeroing it. Raising a monthly ceiling mid-month changes the ceiling; it does not
un-spend the month.
The substrate global cap
Section titled “The substrate global cap”Between the per-agent budget and the tenant quota sits one more ring: a deployment-wide per-agent ceiling the operator sets, which bounds every agent whatever budget it declared — including the common case of declaring none.
[substrate.global_cap]tokens = 10_000_000# time_seconds = 3600# [substrate.global_cap.cost]# micros = 5_000_000 # $5.00# currency = "Usd"Each dimension is independently optional and an omitted one imposes no ceiling there; the shipped default caps nothing, because the substrate does not impose ceilings you did not ask for. A dimension set to zero is a boot error for the same reason a zero quota ceiling is — leaving it out is how you say “no cap”, so zero would have to mean something else, and the only thing it could mean is refusing every agent before its first call.
The cap is per agent, not per tree: a session that spawns N agents may spend
N times the cap, because each of those agents is separately bounded. A ceiling
over a whole tenant is the quota’s job, which is aggregate by construction. When
it fires it raises the same FailureClass::Budget as per-agent exhaustion, and
stopping a workload already in flight is reported as a cancellation — never
a failure class, which would invite the supervisor cascade to restart exactly
what the ceiling stopped.
Read the spend
Section titled “Read the spend”Every priced action emits a CostEvent onto the session’s event log — the
single source of truth the rollups
fold over. A CostEvent carries the agent it attributes to, the prompt /
completion / cached token counts, a media_units counter (per-asset media
metering, e.g. video-seconds), and a cost breakdown split into prompt,
completion, cached, and total USD. Two kinds emit today: LLM calls
(CostSource::LLMCall), and paid tool executions
(CostSource::ToolExecution) — a tool whose own dispatch makes an internal LLM
call, such as the wrapped web-search tool, reports its cost across the sandbox
boundary. A per-dispatch overhead source is reserved for a later increment.
A spend carries only the dimensions it actually has. provider, profile
and model are optional, and a tool execution rides its own tool_id instead
of borrowing the provider’s slot. That is what keeps a tool from appearing as a
provider in the ledger, and keeps “this cost had no model” distinct from “this
cost’s model is the empty string” — they were the same value once, and the
blank key held real dollars.
The orchestrator folds every CostEvent into per-session, per-agent, and
per-tenant totals and serves them over three RPCs — QuerySessionCost,
CostForAgent (self plus subtree), and CostForTenant. Each rollup breaks down
across four dimensions, and a fold takes a dollar only where the event carries
that dimension:
| Dimension | Keyed on | Why it is its own dimension |
|---|---|---|
by_provider | provider kind | the vendor the spend went to |
by_profile | resolved profile id | two instances of one kind (openai-prod, openai-eu) are distinguishable |
by_model | model id | carries no blank key |
by_tool | tool id | a paid tool is not a provider |
Tenant isolation is intrinsic: the only tenant you can query is your own.
The per-tenant rollup is cumulative for the daemon’s lifetime, so it deliberately
carries no per-session or per-agent maps — those would grow one entry per
session and per agent ever, reclaimed by nothing. A per-session figure is the
completed-session index’s to give; a per-agent figure is CostForAgent’s.
In the console, open the Cost facet in Execution mode (see
The operator console). With the session’s root selected it
shows the session-wide rollup; select a child agent and it splits into that
agent’s self-cost and its subtree, with by-model bars and prompt / completion /
cached token rows. Because every CostEvent rides the event bus, a tail
consumer can forward spend to a monitoring stack — the
Prometheus tail-consumer sample exposes per-tenant,
per-provider cost counters for Prometheus to scrape.
Configure model pricing
Section titled “Configure model pricing”Cost figures are only as accurate as the per-model rate table behind them. Each
model carries a
ModelRate:
per-million-token USD rates for input, cached, and output, plus two media
axes — per_second_usd (a video model billing per clip-second) and
per_unit_usd (a per-artifact model billing per generated image or TTS request).
A token call costs (rate × tokens) / 1_000_000 per component; a media
generation bills its per-second or per-unit rate against the metered units. Every
rate is validated non-negative at config decode, so a mis-typed negative rate is
rejected rather than minting a negative (credit) cost.
Rates live in the model catalog, provenance-layered so a shipped default and an operator override coexist without the override being clobbered on re-import. You edit them in the console’s Infrastructure → Models surface. If a model has no rate entry, the substrate emits a zero cost breakdown and a warning: the token counts stay authoritative, but the dollar figure under-counts until you add a rate for that model.
Batch work is priced at half
Section titled “Batch work is priced at half”Work submitted through a provider’s batch API bills at 50% of the synchronous rate. That is not an apomesh convention — Anthropic Message Batches, the OpenAI Batch API, Gemini batch mode, and Bedrock batch inference all publish the same discount, so one constant covers every provider. The discount applies where each provider projects its batch entries into priced results, so a batch job’s rollup and its budget consumption both reflect the rate you were actually charged.
Where this shows up
Section titled “Where this shows up”- The operator console — the Cost and HITL facets, and the Configure & Launch budget field.
- Monitoring — forwarding
CostEventto Prometheus and Grafana. - The orchestrator — the budget, quota, and cost-aggregation enforcement points.
- Loops — the coordinator waves the per-wave budget carve rides.
- The event log — the bus the cost rollup folds over.