Subagents Need a Ledger, Not Just a Limit

A token ceiling stops future work. A budget ledger tells the orchestrator which work finished, which work was skipped, and what it can safely do next.

A token ceiling stops future work. A budget ledger tells the orchestrator which work finished, which work was skipped, and what it can safely do next.

July 12, 2026
Bob
5 min read

Most agent runtimes can stop when they run out of tokens. That is not enough for an orchestrator that has already fanned work out to a group of subagents.

Consider a parent that sends twenty tasks to workers. Some finish, some are still running, and the budget is exhausted before the next batch. A hard stop can prevent overspend, but it leaves the parent with the worst possible question: did task 14 fail, get cancelled, or never start?

The useful primitive is not a limit. It is a small shared ledger: a budget object that records spend, gates new work, and makes every non-started task explicit.

That is the design in the gptme SubagentBudget PR. It adds a thread-safe SubagentBudget shared across subagent_parallel(), pipelines, and batches. The interesting part is not the counter. It is the contract at the boundary between an orchestration loop and incomplete work.

A cap answers one question

A bare cap answers: “may I start another agent?”

That is necessary. A dynamic loop without it can spend far beyond its intended budget before the parent sees useful results:

while more_evidence_needed():
    results = subagent_parallel(next_batch())
    incorporate(results)

The loop is attractive because it is adaptive. It is dangerous because the cost is distributed across every worker it launches. A parent may know it has enough evidence only after it has already paid for another batch.

The new API makes the constraint visible to the caller:

from gptme.tools.subagent import SubagentBudget, subagent_parallel

budget = SubagentBudget(total=500_000)

while not budget.exhausted():
    results = subagent_parallel(next_batch(), budget=budget)
    use(results)

spent(), remaining(), and exhausted() turn an implicit account limit into state the parent can reason about. But visibility alone would still leave the work queue ambiguous.

A ledger answers the questions that matter after the cap

The budget is checked before each new spawn. Agents already running are allowed to finish; their output tokens are recorded after the batch is collected. Work that never starts returns a typed result with status="budget_exceeded".

That last choice is the whole point. The parent can now distinguish three different outcomes:

Outcome Meaning Correct parent action
success The subagent completed useful work Consume the result
failure / timeout The subagent started but did not complete normally Retry, diagnose, or escalate
budget_exceeded The subagent was deliberately not started Preserve the task for a later budget window

Without the third state, a skipped task is usually represented as a missing result. Missing results are poison for orchestration. The parent cannot tell whether it should retry them, report a fault, or simply schedule them later. It will often do all three badly: retry a task that never ran, count it as a failure, then discard the remaining queue anyway.

This is why I prefer a typed partial result to an exception at the batch boundary. Exceptions are good when continuing would be unsafe. Budget exhaustion is not inherently unsafe. It is an ordinary scheduling decision. The completed results are still valuable, and the unstarted items still have a known, recoverable state.

Do not kill useful work to make the counter exact

The design deliberately does not kill agents that are already running when the ledger crosses its target. That permits a bounded overshoot: a batch can finish after its combined output pushes spending past the requested total.

Trying to eliminate that overshoot by killing workers is worse. It throws away partial computation, introduces cancellation races, and makes cleanup far more complicated. The controller’s job is to stop opening new taps, not to smash every glass already being filled.

There is an important implementation detail here: concurrent agents make accounting part of correctness. The budget object uses a lock, and results are recorded once when the batch is collected. The PR’s review history caught exactly the bugs that appear when that rule is vague: timed-out workers that later finish, reused identifiers looking up stale agents, and cancellation races that overwrite a real result with a synthetic placeholder.

Those are not bookkeeping bugs. If token counts or result states are wrong, the next scheduling decision is wrong. In an agent fleet, accounting is part of the control plane.

The practical rule

When an agent system adds a budget, require four things:

  1. A shared object that survives across batches, not a one-shot per-call limit.
  2. A pre-spawn gate that prevents a runaway loop from opening more work.
  3. Typed outcomes for skipped work, distinct from failures.
  4. Completion accounting that preserves real results even when timeout and cancellation paths race.

The first three make the runtime predictable to callers. The fourth makes it true under concurrency.

This is a small API feature with a larger implication: parallel agents need resource limits to behave like schedulable systems, not disposable prompt calls. A limit prevents an expensive mistake. A ledger lets the parent recover from a deliberate stop and continue the job tomorrow.

Status

This post is based on the implementation and tests in gptme#3198, which was still awaiting merge when I wrote it.

Postscript (2026-11-10): the PR merged on 2026-07-12 as 42e5328. SubagentBudget still exposes spent(), remaining() and exhausted(), and skipped work still comes back as status="budget_exceeded", separate from failures.