Spend budgets
Give the swarm one spend ceiling. Each call reserves its worst case before it starts, is charged its real cost afterwards, and releases the rest.
A hundred agents shouldn’t get a hundred independent chances to overspend. A runstate budget is one spend ceiling shared by every agent that uses it. Wrap the expensive function with budget, and each call:
- reserves an amount before it starts. If the budget can’t cover it, the function is not called;
- runs the function;
- charges the real cost, read from the result, and releases the rest of the reservation.
Amounts are converted to exact integers in minor units (cents for USD). There is no floating-point arithmetic in the budget itself.
Wrap a function
Section titled “Wrap a function”import runstate from 'runstate-sdk';
const ask = runstate.budget('model-usd', callModel, { reserve: 0.4, // held before each call cost: (result) => result.usd, // the real cost, read from the result limit: 200, // declare a 200 USD budget});
const answer = await ask('Summarize the Acme filing');import runstate
@runstate.budget( "model-usd", reserve=0.40, # held before each call cost=lambda result: result.usd, # the real cost, read from the result limit=200, # declare a 200 USD budget)def call_model(prompt: str): ...
answer = call_model("Summarize the Acme filing")limitdeclares the budget. The first call creates it if it doesn’t exist, which needs a key with theresource_configpermission. Later calls check that it matches and raiseConflictErrorif not. Leavelimitout to use a budget created in the console.currencydefaults toUSD.scaleis the number of decimal places, 2 by default.- Amounts are numbers or decimal strings in major units:
0.40,'0.40'and'12.5'all work. Numbers are rounded half up toscale, so float noise like0.1 + 0.2becomes0.30. A string with more decimal places thanscaleis aConfigError.
When the budget runs out
Section titled “When the budget runs out”If the budget can’t cover reserve, the function is not called and the wrapper raises BudgetExceededError:
import { BudgetExceededError } from 'runstate-sdk';
try { return await ask(prompt);} catch (err) { if (err instanceof BudgetExceededError) return null; // out of budget: skip this call throw err;}try: return call_model(prompt)except runstate.BudgetExceededError: return None # out of budget: skip this callReservations are checked on the server against what is left (limit minus reserved minus charged) in one step, so agents calling at the same time can’t jointly reserve more than the limit.
How the cost is charged
Section titled “How the cost is charged”- After the function returns,
cost(result)is charged and the rest of the reservation is released. costmay return a number or a decimal string. Returning0releases everything.- If the function throws, the whole reservation is released and nothing is charged.
- If the real cost is higher than the reservation, the extra is charged from what is left in the budget. If the budget can’t cover it, the SDK charges what it can, still returns the result, and reports the overrun in the run’s events as
budget.overrun.
Pick reserve as your worst case for one call. Then overruns don’t happen, and the budget refuses work before it would be crossed, not after.
Watch spending
Section titled “Watch spending”Budget changes show up in the run’s events, and the console’s Resources page shows each budget’s limit, reserved and charged amounts.
for await (const event of runstate.events({ types: ['budget.overrun'] })) { console.warn('budget overrun', event.data);}for event in runstate.events(types=["budget.overrun"]): print("budget overrun", event.data)Budgets on queues
Section titled “Budgets on queues”A queue can reserve from a budget for each job. The reservation is taken together with the job, so a job only starts when the budget can cover it:
export const summarize = runstate.queue('summarize', summarizeDoc, { budget: { name: 'model-usd', reserve: 0.4 },});@runstate.queue("summarize", budget={"name": "model-usd", "reserve": 0.40})def summarize(doc: dict) -> dict: ...The budget must already exist. Declare it with budget(..., limit) elsewhere, or create it in the console.
Things to know
Section titled “Things to know”- The ceiling covers calls that go through runstate. An agent that calls the provider without the wrapper isn’t stopped by the budget.
- A crash holds money for a while. If a process dies between the reservation and the charge, runstate releases that reservation after an hour, or an hour after the run ends. Until then it counts against the budget. The console’s Resources page shows open reservations.
- Permissions. Reserving and charging need
coordination_write; creating a budget needsresource_config. See Configuration & auth.
Related
Section titled “Related”- Limits and slots: the same idea for rate limits and concurrency.
- Events and monitoring: budget events.
- TypeScript SDK and Python SDK reference.
- Other languages: HTTP API reference.