Set an agent's budget
This guide caps the model calls, the spend and the time of each agent turn, and handles a turn that runs out.
A budget caps each turn of an agent: how many model calls it makes, what they cost, and how long it takes. A turn that runs out fails, and the run says which limit it hit.
Before you start
Section titled “Before you start”- An agent, in a pack on a runtime such as
kindgi dev, with Kindgi 0.1's CLI. This guide usesacme.order-desk, from Write an agent. - A model registered with prices, for the cost budget. A model with a price of zero never uses it.
- For the app's half: Node 22.12 or later, Python 3.11 or later, or Java
17 or later, with Kindgi 0.1's client, and
KINDGI_API_URLandKINDGI_API_TOKENset.
Set the budget
Section titled “Set the budget”Ask your coding agent: "Limit what each turn of my agent can spend, and how
long it can take." Kindgi's skills in your pack tell it how (How it knows
Kindgi). For reference, it adds budget to
the agent's definition:
// in agents/order-desk/index.ts budget: { maxSteps: 6, maxCostUsd: 0.05, maxWallMs: 60_000 },# in agents/order_desk.py budget={"maxSteps": 6, "maxCostUsd": 0.05, "maxWallMs": 60_000},// in src/main/java/acme/agents/OrderDesk.java .set("budget", Map.of("maxSteps", 6, "maxCostUsd", 0.05, "maxWallMs", 60_000))// in src/main/scala/acme/agents/OrderDesk.scala .set("budget", Map("maxSteps" -> 6, "maxCostUsd" -> 0.05, "maxWallMs" -> 60000))| Field | Caps | Default |
|---|---|---|
maxSteps |
Model calls in the turn. | 8 |
maxCostUsd |
The turn's model spend, in US dollars. | none |
maxWallMs |
The turn's time, in milliseconds. | 120000 |
Each turn of the agent now stops at the first limit it reaches.
A step is one model call. The tool calls the model asks for run between steps, so a turn that looks an order up and then answers takes two. Repairing a typed answer takes a step too.
Cost is what the models' registered prices make of the tokens each call used, as Models describes. A model registered with a price of zero never uses the cost budget. Cost is an estimate from the vendor's published prices: your provider's invoice is what you pay.
When a turn runs out
Section titled “When a turn runs out”Each limit fails the turn with its own error. kindgi runs start prints it
and exits 1. The run, on stdout, has "status": "failed" and the error in
its failureMessage. These are from acme.order-desk with one limit set low
at a time, on a local llama3.1 registered with a price.
Steps. After a model call that asks for more tool calls, with no steps left:
Error [budget-exceeded]: Agent turn steps budget exceeded (limit 1, observed 1)Cost. Checked after each model call, against the turn's total so far:
Error [budget-exceeded]: Agent turn cost budget exceeded (limit 0.0001, observed 0.0001084)Time. A timer starts with the turn. When it fires, the model call in flight is cancelled:
Error [agent-turn-aborted]: Agent turn aborted: Request was aborted.kindgi runs journal <run-id> shows how far the run got, where <run-id> is
the failed run's id.
Leave room for real models: one call to a hosted model takes a second or more, and a model on your own machine can take ten. A turn with a tool call makes two calls.
Handle it in your app
Section titled “Handle it in your app”Your app can tell a turn that ran out from other failures. The failed turn comes back as an error, with the limit in its fields. Ask your coding agent: "I want my app to tell when a turn ran out of budget, and which limit it hit." For reference, the code looks like this:
import { KindgiApiError, createClient } from '@kindgi/sdk/client';
const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
try { await kindgi.runs.start({ agent: 'acme.order-desk', input: { userMessage: 'Where is my order A-1001?' }, });} catch (err) { if (!(err instanceof KindgiApiError)) throw err; const e = err.error; if (e.code === 'server' && e.serverCode === 'budget-exceeded') { console.log(e.fields?.kind, e.fields?.limit, e.fields?.observed, e.fields?.runId); } else { throw err; }}steps 1 1 2cc56724-92e7-42e8-aefd-14ad5c294d1ffrom kindgi.client import Kindgi, KindgiApiError
kindgi = Kindgi() # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi devtry: kindgi.runs.start(agent="acme.order-desk", input={"userMessage": "Where is my order A-1001?"})except KindgiApiError as err: if err.server_code == "budget-exceeded": print(err.details["kind"], err.details["limit"], err.details["observed"], err.details["runId"]) else: raisesteps 1 1 3deffc12-053e-43e3-b735-9a415d95645fpackage com.acme.app;
import com.kindgi.client.Kindgi;import com.kindgi.client.KindgiApiException;import com.kindgi.client.models.StartRunBody;import java.util.Map;
public final class Budget { public static void main(String[] args) { Kindgi kindgi = Kindgi.create(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
try { kindgi.runs().start(StartRunBody.WithAgent.builder() .agent("acme.order-desk") .input(Map.of("userMessage", "Where is my order A-1001?")) .build()); } catch (KindgiApiException e) { if (!"budget-exceeded".equals(e.serverCode())) throw e; Map<String, Object> d = e.details(); System.out.println(d.get("kind") + " " + d.get("limit") + " " + d.get("observed") + " " + d.get("runId")); } }}steps 1 1 e95037bd-6df7-43c9-a706-743270a947b1package acme.app
import com.kindgi.client.{Kindgi, KindgiApiException}import com.kindgi.client.models.StartRunBodyimport scala.jdk.CollectionConverters._
object Budget { def main(args: Array[String]): Unit = { val kindgi = Kindgi.create() // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
try { kindgi.runs().start(StartRunBody.WithAgent.builder() .agent("acme.order-desk") .input(Map("userMessage" -> "Where is my order A-1001?").asJava) .build()) } catch { case e: KindgiApiException if e.serverCode == "budget-exceeded" => val d = e.details.asScala println(s"${d("kind")} ${d("limit")} ${d("observed")} ${d("runId")}") } }}steps 1 1 4eb76089-0d4a-4256-8a2c-4df1c12c7f46It prints which limit the turn hit, the limit, what the turn used, and the
run's id. A turn that runs out of time has serverCode agent-turn-aborted
instead, with reason timeout.
In a flow
Section titled “In a flow”An agent step whose turn runs out fails, and the flow run fails with it. The run carries the reason:
pnpm exec kindgi runs start --flow=acme.notify-customer --input='{"orderId":"A-1001"}'uv run kindgi runs start --flow=acme.notify-customer --input='{"orderId":"A-1001"}'./kindgiw runs start --flow=acme.notify-customer --input='{"orderId":"A-1001"}'./kindgiw runs start --flow=acme.notify-customer --input='{"orderId":"A-1001"}'--flow is the flow to run, and --input its input. It prints the failed
run:
{ … "flowId": "acme.notify-customer", … "status": "failed", … "failureMessage": "budget-exceeded: Agent turn cost budget exceeded (limit 0.00001, observed 0.00031)", "output": {}, …}Here the step's agent, acme.order-update from
Give an agent its input, had
budget: { maxCostUsd: 0.00001 }.