Budgets Are Stop Conditions
max_steps, max_usd, max_wall_clock, max tokens in, plus a per-tenant monthly cap. When a cap hits, stop with a structured error — never a silent model downgrade.
A budget that cannot stop the loop is a dashboard. Per job you need max_steps, max_usd, max_wall_clock, max_tokens_in per call (the assembler must obey), and a per-tenant monthly cap. When a cap hits, stop with a structured error and offer a human if the product needs it. Do not silently switch to a worse model without recording it — that is how quality falls and nobody knows why.
Swarms should compute n * child_cost and refuse if over cap. That check belongs here as a hard gate, not a comment in a prompt. Cancel in the UI must reach the worker. Otherwise users hammer cancel and you pay twice.
Caps are the difference between “we watch spend” and “we operate spend.” The previous lesson estimated dollars. This lesson kills the job.
How the box actually works
Check caps at the start of each slice, after adding the next estimated cost, not only after the invoice arrives.
| Cap | Stops | Typical code |
|---|---|---|
max_steps | Infinite ReAct-shaped loops | MAX_STEPS |
max_usd | Transcript growth, retries, fat observations | MAX_USD |
max_wall_s | Stuck tools, HITL forever if you forgot a wait cap | MAX_WALL |
max_tokens_in | Assembler packing | Refuse the complete() |
| Tenant monthly | One customer eating the org bill | Pause tenant, TENANT_CAP |
Never silently downgrade the model. Stamp a fallback or halt.
A cap that can stop the loopOn trip: set job status to failed (or waiting_for_human), write a span, emit the metric that can page, do not call the next complete or write tool. Offer a human path if the ticket still matters.
Silent downgrade: some systems catch MAX_USD and retry with a smaller model without stamping versions. Billing goldens fail, HITL rises, dashboards still say “success.” Record route changes as version flags. If you allow an explicit fallback, it is a new stamp, a metric, and probably a canary — not an if-statement in the dark.
Owners: runtime enforces per-job caps. Finance + product set the numbers. Ops owns tenant pause. The assembler owner owns max_tokens_in. Nobody owns a comment in the prompt that says “be brief.”
Cancel is a cap with a human finger. It must be as hard as MAX_USD.
Check order is part of the spec. Typical: cancel flag, then steps, then dollars (including next-step estimate), then wall, then tokens-in. Document it so two workers do not disagree. Persist counters on the job row. A worker-local steps = 0 on every retry is how caps never trip and finance still calls.
Swarms: the parent refuses to start child N+1 when n * child_cost would exceed the remaining budget. “Each child is small” is how you buy a thousand small fires.
A silent-downgrade ticket
Jobs started hitting a dollar cap during a launch. A helper caught the error and “helpfully” switched fake-large to fake-small so the loop could finish. Success rate on the dashboard stayed green. Refund answers drifted. HITL reject rate climbed. Nobody connected the dots until a versions chart showed model changing mid-job with no flag.
The fix: tick returns MAX_USD and stops. A separate, reviewed fallback route exists as a named flag with its own eval gate. Launch traffic that exceeded cap went to humans for a day. That was cheaper than wrong refunds at scale.
Run to execute this in your browser. Nothing is sent to a server.
The first tick is ok (0.01 is under 0.05). The second adds 0.05, running total 0.06, and returns ok: False with code: MAX_USD. The job object still has steps, usd, and wall_s for the trace. Nothing in tick changes the model id. The job stops. That is the whole control.
What goes wrong
Caps only in the prompt. Caps checked after the full vendor invoice. Caps that skip write tools “so we can finish.” Tenant without a monthly cap. Swarms that spawn 200 children, each “inside” the per-job cap. Retry storms that reset steps on each queue attempt — retries must count.
Wall clock that includes HITL wait and fails jobs while a human sleeps. Separate max_wall_s for machine time vs hitl_sla_s (you already page on the latter).
How to test it
- Unit: under cap ok; each cap trips with the right code; order of checks is documented.
- No silent route: after
MAX_USD, model id on the job is unchanged. - Queue retries increment a persistent
steps/usdon the job row, not a worker-local counter. - Tenant cap: the Nth job in a month is refused with
TENANT_CAP. - Cancel: a flagged job’s next
tickdoes not add spend.
Game day: run a looping fixture in staging and time until MAX_STEPS. If it never trips, the cap is not wired.
How agents use this
Review cost per successful job weekly by tenant. A test tenant that loops can look like product-market fit on the invoice. Caps per tenant exist for this. Circuit-break a vendor instead of 3× retrying a huge context.
Surface MAX_USD to the user as a structured message (“stopped to protect spend; ask a human”) plus the support code. Do not return a truncated hallucination that looks like an answer.
When you add multi-agent children, the parent’s cap must include n * child. If that math is annoying, you are not ready for swarms in prod.
Tenant monthly caps are how a load test in a forgotten project does not look like product-market fit. Reset them on a calendar you document. Alert when a tenant hits 80% so pause is a decision, not a surprise at 100%.
Watch out:A cap you have never tripped in staging is decorative. Force a trip on game day.
Check your understanding