Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0cca58d
feat(pkg/config/hcl/hcl.go): updating up hcl by adding budgets config…
dwin-gharibi Jul 17, 2026
c70fc55
feat(pkg/config/latest/types.go): adding up the Budgets config type f…
dwin-gharibi Jul 17, 2026
e130642
feat(pkg/config/latest/validate.go): feating the budget config valida…
dwin-gharibi Jul 17, 2026
ed4ba97
test(pkg/config/latest/budget_test.go): adding tests for budget confi…
dwin-gharibi Jul 17, 2026
31396ed
feat(pkg/config/doc_yaml_test.go): adding the budget and budgets top …
dwin-gharibi Jul 17, 2026
59b00dc
feat(pkg/config/schema_test.go): adding the budgets into schema test
dwin-gharibi Jul 17, 2026
1b8959f
feat(pkg/runtime/budget.go): adding the agent and total budget managm…
dwin-gharibi Jul 17, 2026
977722c
test(pkg/runtime/budget_test.go): adding tests for the main budget co…
dwin-gharibi Jul 17, 2026
773826f
feat(pkg/runtime/runtime.go): wiring up the budget core into the dock…
dwin-gharibi Jul 17, 2026
53d5952
feat(pkg/teamloader/teamloader.go): adding up the teamloaders into do…
dwin-gharibi Jul 17, 2026
3456e50
feat(pkg/tui/page/chat/runtime_events.go): adding the budget related …
dwin-gharibi Jul 17, 2026
ad31900
feat(pkg/tui/components/sidebar/sidebar.go): adding the agent budgets…
dwin-gharibi Jul 17, 2026
2dbfa64
test(pkg/tui/components/sidebar/budget_line_test.go): adding some ama…
dwin-gharibi Jul 17, 2026
d0dfd39
feat(cmd/root/run.go): adding the with budget and with total budgets …
dwin-gharibi Jul 17, 2026
87ea0fd
feat(agent-schema.json): adding BudgetConfig into agent-schema.json a…
dwin-gharibi Jul 17, 2026
e13c179
feat(examples/budget.yaml): adding up an amazing budget manifest exam…
dwin-gharibi Jul 17, 2026
dc10ffe
docs(docs/configuration/budget): introducing the docker agent budget …
dwin-gharibi Jul 17, 2026
020079e
fix(pkg/runtime/budget.go): fixing lint problems in budget.go file
dwin-gharibi Jul 17, 2026
5cf4d02
fix(pkg/runtime/budget_test.go): updating up some parts for price for…
dwin-gharibi Jul 19, 2026
2c269fe
fix(pkg/tui/components/sidebar/sidebar.go): applying some fixes on si…
dwin-gharibi Jul 19, 2026
f02c703
test(pkg/tui/components/sidebar/budget_line_test.go): adding up some …
dwin-gharibi Jul 19, 2026
3589dcd
docs(docs/configuration/budget/index.md): updating up the budget docs
dwin-gharibi Jul 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions agent-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@
"runtime": {
"$ref": "#/definitions/RuntimeDefaults",
"description": "Execution-time defaults the agent author wants applied. Values act as defaults only — explicit CLI flags or user-config settings always win."
},
"budget": {
"$ref": "#/definitions/BudgetConfig",
"description": "Ceilings on what a single run may consume before the agent is stopped, regardless of which agent spends. Scoped to a run and shared by every sub-session spawned inside it, so delegated work spends against the same budget as its parent."
},
"budgets": {
"type": "object",
"description": "Reusable, named budget definitions. An agent opts into one by listing its name in its 'budgets' field. A named budget is a single shared pot: when several agents reference the same name they draw from the SAME ceiling rather than each getting a copy, so a fan-out to N sub-agents cannot spend N times the limit. Give agents distinct budget names when you want independent pots.",
"additionalProperties": {
"$ref": "#/definitions/BudgetConfig"
}
}
},
"additionalProperties": false,
Expand Down Expand Up @@ -637,6 +648,22 @@
"description": "Maximum number of iterations",
"minimum": 0
},
"budgets": {
"type": "array",
"description": "Names of top-level 'budgets' entries this agent spends against. Every listed budget must be defined under the top-level 'budgets' key. An agent may list several, in which case all apply and the first to be exhausted stops the run. Agents sharing a budget name share one pot, so a fan-out cannot multiply the ceiling.",
"items": {
"type": "string"
},
"examples": [
[
"tight"
],
[
"tight",
"daily"
]
]
},
"max_consecutive_tool_calls": {
"type": "integer",
"description": "Maximum consecutive identical tool calls before the agent is terminated. Prevents degenerate loops. 0 uses the default of 5.",
Expand Down Expand Up @@ -1784,6 +1811,42 @@
},
"additionalProperties": false
},
"BudgetConfig": {
"type": "object",
"description": "Ceilings on what a single run may consume. Every limit is optional; an unset or zero limit is unlimited. The budget is scoped to a run and shared by every sub-session spawned inside it, so a fan-out to sub-agents spends against one budget rather than one per child. Exceeding any limit stops the run with an assistant message and a budget_exceeded event; raising a limit means editing the config, not answering a prompt.",
"properties": {
"max_cost": {
"type": "number",
"description": "Maximum spend for the run, in USD. Only responses the runtime can price count towards it: a model with no pricing data (unknown ID, or a custom endpoint without a model-level 'cost' block) contributes nothing, and such a run emits a warning rather than behaving as if the spend were free.",
"minimum": 0,
"examples": [
0.5,
5,
25
]
},
"max_tokens": {
"type": "integer",
"description": "Maximum cumulative input+output tokens for the run, counted over its whole lifetime. This is not the session's context length: compaction resets that, while this keeps counting.",
"minimum": 0,
"examples": [
100000,
1000000
]
},
"max_time": {
"type": "string",
"description": "Maximum time the agents spend working, in Go duration format (e.g., '10m', '30s', '1h30m'). This is the sum of turn durations, NOT wall-clock since the session opened, so idle time while the user reads and types does not count. Checked at turn boundaries, so it will not interrupt an in-flight model call or tool.",
"pattern": "^([0-9]+(ns|us|µs|ms|s|m|h))+$",
"examples": [
"30s",
"10m",
"1h30m"
]
}
},
"additionalProperties": false
},
"PermissionsConfig": {
"type": "object",
"description": "Tool permission configuration. Controls tool call approval behavior with optional argument matching.",
Expand Down
2 changes: 2 additions & 0 deletions cmd/root/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,8 @@ func (f *runExecFlags) runtimeOpts(loadResult *teamloader.LoadResult, runConfig
runtime.WithWorkingDir(runConfig.WorkingDir),
runtime.WithTracer(otel.Tracer(AppName)),
runtime.WithModelSwitcherConfig(modelSwitcherCfg),
runtime.WithBudget(loadResult.Budget),
runtime.WithNamedBudgets(loadResult.Budgets, loadResult.AgentBudgets),
}
return opts
}
Expand Down
194 changes: 194 additions & 0 deletions docs/configuration/budget/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
---
title: "Budget"
description: "Cap what a single run may spend in money, tokens, or wall-clock time."
keywords: docker agent, ai agents, configuration, yaml, budget, cost, limits
weight: 75
canonical: https://docs.docker.com/ai/docker-agent/configuration/budget/
---

_Cap what a single run may spend in money, tokens, or wall-clock time._

## Overview

A budget sets ceilings on a run. When a ceiling is crossed the run stops with a message naming the exact limit that tripped, and the TUI tracks consumption live while the run is still going.

Every limit is optional and an unset limit is unlimited, so you can cap money, tokens, time, or any combination. Declare no budget at all and runs are unbudgeted, which is the default.

There are two ways to declare one, and they compose:

| Block | Scope |
| --- | --- |
| `budget` | One run-wide ceiling, charged for every agent. |
| `budgets` | Named budgets an agent opts into by name. |

### A run-wide budget

```yaml
agents:
root:
model: openai/gpt-4o-mini
description: An agent on a short leash.
instruction: You are a helpful assistant.

budget:
max_cost: 0.50
max_tokens: 100000
max_time: 10m
```

| Field | Type | Description |
| --- | --- | --- |
| `max_cost` | number | Maximum spend, in USD. |
| `max_tokens` | integer | Maximum cumulative input+output tokens. |
| `max_time` | string | Maximum time the agents spend working, in Go duration format (`10m`, `30s`, `1h30m`). |

### Named budgets

Define budgets by name under the top-level `budgets` key, then have each agent opt in by listing names in its own `budgets` field. The fields are the same three.

```yaml
budgets:
shell-work:
max_cost: 0.03
max_tokens: 8000
research:
max_time: 1m

agents:
root:
model: openai/gpt-4o-mini
description: Does shell work.
instruction: You are a helpful assistant.
budgets: [shell-work]
researcher:
model: openai/gpt-4o-mini
description: Answers questions.
instruction: You answer questions concisely.
budgets: [shell-work, research]
```

An agent may list several budgets; all of them apply, and the first to be exhausted stops the run. A run-wide `budget` applies on top of any named budget, so the ceiling that binds is whichever runs out first.

Referencing a budget name that isn't defined is a config error, caught at parse time rather than silently leaving the agent uncapped.

## A name is one shared pot

When several agents reference the same budget name they draw from the **same** ceiling — not a copy each. Above, `root` and `researcher` share `shell-work`: together they cannot spend more than `$0.03`.

This is deliberate, and it is the whole reason budgets are worth having. If each agent received its own allowance, a run could spend `max_cost` × N simply by fanning out to N sub-agents, and the ceiling would mean nothing for exactly the workloads that most need one.

Give agents **distinct budget names** when you want independent pots.

The same applies to the run-wide `budget`: every sub-session inside a run (transferred tasks, sub-agents, skills) spends from that one wallet.

## Scope: a budget spans the session

Spend accumulates for the life of the **session**, across every message you send — it does not reset each time you hit enter. A `max_cost: 0.50` you could re-spend on every message would not be a ceiling at all.

Starting a new session starts a fresh budget. It is a per-session ceiling, not a lifetime quota across sessions.

> [!NOTE]
> `max_time` measures the time the agents actually spend **working** — the sum of their turn durations — not wall-clock since the session opened. Because a budget spans a session, and a session sits idle while you read and type, wall-clock would let a budget expire during a coffee break: leave the TUI open for ten minutes and your next message would instantly trip a `max_time` of `2m` without the agent having done anything.

## What stops a run

Crossing any limit — run-wide or named — stops the run and produces:

- an assistant message in the transcript naming the limit and the amounts,
- a `budget_exceeded` event carrying `budget`, `limit`, `used`, `max` and `config_path`,
- a `notification` hook at `warning` level,
- a stream end reason of `budget_exceeded`, so a stopped run is distinguishable from a completed one in telemetry.

The message names the exact YAML path to raise, so there is no ambiguity about which of several budgets tripped:

```text
Execution stopped after reaching the configured budgets.shell-work.max_cost
limit (used $0.0312 of $0.0300).
```

Unlike [`max_iterations`](../agents/index.md), a budget stop is **terminal** — there is no prompt offering to continue. A budget is a ceiling you set deliberately, so raising it means editing the config rather than answering a dialog.

## Tracking spend in the TUI

The sidebar's Token Usage panel lists every active budget by name, with consumption against each ceiling it declares:

```text
run $0.12/$0.50 · 12.3K/100.0K · 2m14s/10m
shell-work $0.09/$0.10 · 4.3K/20.0K
```

Only the ceilings you configured appear. Each reading is colored by the sidebar's shared gauge bands — the same ones a context gauge uses as it nears compaction — so a budget turns amber well before its ceiling and red just short of it, and a run about to be stopped is visible before it stops.

For a per-agent view of who spent what, set **Sidebar info mode** to `Detailed` in `/settings` → Appearance: the Agents section then reports each agent's cost alongside its effort and context. The budget line deliberately does not repeat that breakdown — the same numbers twice would crowd the sidebar's narrowest column. The per-agent split is still carried on the `budget_usage` event for programmatic consumers, and `/cost` has a **By Agent** section.

## Limits and caveats

### Unpriced models do not count towards `max_cost`

Only responses the runtime can price count towards `max_cost`. A model with no pricing data — an unknown model ID, or a custom endpoint such as a local or private deployment — contributes nothing, because there is no honest number to add.

Such a run emits a warning and the TUI marks the reading `(unpriced spend)`, rather than silently reading low because the spend is invisible. To make a custom endpoint count, price it explicitly with a model-level [`cost`](../models/index.md#custom-token-pricing) block:

```yaml
models:
local:
provider: openai
model: my-model
base_url: http://localhost:8000/v1
cost:
input: 0.15
output: 0.60

agents:
root:
model: local
description: A locally-served agent with real cost accounting.
instruction: You are a helpful assistant.

budget:
max_cost: 0.50
```

### Limits are checked at turn boundaries

A run is checked between turns, so it can overshoot by at most the turn already in flight. `max_time` in particular will not interrupt a model call or tool that has already started; the run stops at the first boundary after the limit is reached.

This is the same granularity [`max_iterations`](../agents/index.md) has, and it keeps the ceiling out of the streaming hot path. Set limits with a little headroom rather than at the exact number you cannot exceed.

### `max_tokens` here is not the model's `max_tokens`

The `max_tokens` in a budget is a **cumulative** count of input+output tokens across the whole run. It is unrelated to the provider- or model-level [`max_tokens`](../models/index.md), which caps the output of a single response.

It is also not the session's context length: compaction resets that, while the budget keeps counting.

## Examples

Cap money only, and let the run take as long as it needs:

```yaml
agents:
root:
model: openai/gpt-4o
description: A cost-capped agent.
instruction: You are a helpful assistant.

budget:
max_cost: 5.00
```

Cap wall-clock time for an unattended job:

```yaml
agents:
root:
model: openai/gpt-4o-mini
description: A time-boxed agent.
instruction: You are a helpful assistant.
toolsets:
- type: shell

budget:
max_time: 15m
```

See [`examples/budget.yaml`](https://github.com/docker/docker-agent/blob/main/examples/budget.yaml) for a runnable configuration.
97 changes: 97 additions & 0 deletions examples/budget.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# yaml-language-server: $schema=../agent-schema.json
#
# Run budgets — stop an agent before it spends more than you meant
# ==========================================================================
#
# A budget caps what a session may consume in money, tokens, or working
# time. Every limit is optional and an unset limit is unlimited.
#
# Spend accumulates for the life of the session, across every message —
# it does not reset each time you hit enter. And max_time counts the time
# the agents actually spend working, so reading and typing is free.
#
# There are two ways to declare one:
#
# budget: a single run-wide ceiling, charged for every agent
# budgets: named budgets an agent opts into by name
#
# This example runs against OpenRouter (an OpenAI-compatible endpoint) so
# you can try it with an OpenRouter key instead of a first-party provider:
#
# export OPENROUTER_API_KEY=<your-key>
# docker agent run examples/budget.yaml \
# "count from 1 to 40, calling the shell once per number"
#
# The TUI's Token Usage panel lists every active budget by name and tracks
# spend against it live, breaking each down per agent once more than one
# agent draws on it. When a ceiling is crossed the run stops with a message
# naming the exact YAML path to raise. The token cap below is deliberately
# low so the counting demo trips it partway through.

providers:
# OpenRouter exposes an OpenAI-compatible API, so it is configured as a
# custom provider with an explicit base_url. Its key is read from
# OPENROUTER_API_KEY.
openrouter:
base_url: https://openrouter.ai/api/v1
token_key: OPENROUTER_API_KEY

models:
budget-model:
provider: openrouter
model: openai/gpt-4o-mini
# A custom base_url endpoint is not in the models.dev catalogue, so the
# runtime cannot price it automatically — and an unpriced model does not
# count against a max_cost limit. Declaring cost here (USD per 1M
# tokens) makes max_cost enforceable. These match gpt-4o-mini's list
# price; edit to match whatever model you point at.
cost:
input: 0.15
output: 0.60

# Named budgets. An agent opts in by listing a name under its `budgets`.
#
# A name is ONE shared pot: `shell-work` below is referenced by both agents,
# so together they cannot spend more than it allows. That is what keeps a
# budget meaningful when an agent fans out — ten sub-agents on one budget
# still cannot spend ten times the limit. Use distinct names when you want
# independent pots.
budgets:
shell-work:
max_cost: 0.03
max_tokens: 8000
research:
max_time: 1m

# The run-wide budget applies to every agent on top of any named budget it
# references. The first ceiling to be exhausted — run-wide or named — stops
# the run.
budget:
max_cost: 0.05
max_tokens: 15000
# Time the agents spend working — the sum of their turn durations, not
# wall-clock since the session opened.
max_time: 2m

agents:
root:
model: budget-model
description: An agent on a short leash.
instruction: |
You are a helpful assistant with shell access. Be concise.
Delegate research questions to the researcher sub-agent.
toolsets:
- type: shell
sub_agents:
- researcher
# root draws on the run-wide budget plus the shared shell-work pot.
budgets: [shell-work]

researcher:
model: budget-model
description: Answers questions without touching the shell.
instruction: |
You answer questions concisely. You have no tools.
# researcher shares shell-work with root — the same pot, not a copy —
# and additionally has its own working-time ceiling.
budgets: [shell-work, research]
2 changes: 2 additions & 0 deletions pkg/config/doc_yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var topLevelConfigKeys = map[string]bool{
"rag": true,
"metadata": true,
"permissions": true,
"budget": true,
"budgets": true,
}

// TestDocYAMLSnippetsAreValid extracts every ```yaml code block from docs/
Expand Down
Loading