Skip to content
HomeConsoleGet started

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:

  1. reserves an amount before it starts. If the budget can’t cover it, the function is not called;
  2. runs the function;
  3. 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.

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');
  • limit declares the budget. The first call creates it if it doesn’t exist, which needs a key with the resource_config permission. Later calls check that it matches and raise ConflictError if not. Leave limit out to use a budget created in the console.
  • currency defaults to USD. scale is 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 to scale, so float noise like 0.1 + 0.2 becomes 0.30. A string with more decimal places than scale is a ConfigError.

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;
}

Reservations 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.

  • After the function returns, cost(result) is charged and the rest of the reservation is released.
  • cost may return a number or a decimal string. Returning 0 releases 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.

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);
}

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 },
});

The budget must already exist. Declare it with budget(..., limit) elsewhere, or create it in the console.

  • 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 needs resource_config. See Configuration & auth.