Token / cost budget¶
Accumulate token and cost usage, and stop an autonomous run at a cap. See the budget-aware stopping recipe.
Usage is surfaced on TurnEnd.usage by backends that report it.
agentflow.prebuilt.budget
¶
Token / cost budget for an autonomous run.
An agentic loop that proposes with an LLM runs many rounds, each costing
tokens. Time has a guard (with_timeout), load has one (max_concurrency),
loop length has one (step_limit) — this is the guard for spend.
Two pieces, matching "how much did I spend?" and "stop at a cap":
-
:class:
TokenMeteris a :class:~agentflow.observability.Hookslistener that sums usage across a run from the backend events a node emits. Pure observation — it never affects the run (a hook cannot, by contract), so it answers "how much?" but cannot stop anything. -
To stop, budget is state. Mix :class:
BudgetStateinto your schema, have the node that drives the LLM add its turn's usage with :func:record_usage, and have your router call :func:over_budgetand route to a stop/escalate node when the cap is exceeded. Because the budget rides the checkpoint, it survives a resume (a resumed run does not forget what it already spent), and stopping is a routing decision — the same discipline as every other stop in the SDK: the work writes an artifact, a router decides what happens next.class SearchState(BudgetState): ...
async def propose(state, ctx): turn = await call_llm(...) # yields a TurnEnd with .usage return {result, record_usage(turn.usage)}
def route(state): if over_budget(state, max_tokens=100_000): return "stop" return "continue"
The stop takes effect at the next super-step boundary, not mid-turn, so a run may overshoot the cap by at most the turn in flight. That is the deliberate trade of the state-based design: durable and engine-free, at the cost of turn-granular (not token-exact) enforcement.
BudgetState
¶
Bases: State
Mix into a schema to add the budget channel the helpers accumulate.
The channel is a dict whose numeric fields each sum via add, so many
nodes (or many rounds) fold their spend together. Fields mirror
:class:~agentflow.events.Usage: input_tokens, output_tokens,
total_tokens, cost_usd.
Source code in agentflow/prebuilt/budget.py
TokenMeter
¶
Bases: Hooks
A Hooks listener that accumulates token/cost usage across a run.
Attach at compile(hooks=...). It sums the usage on every TurnEnd a
node surfaces via ctx.emit. Pure observation for logging or a metrics
readout — it does not and cannot stop a run (hooks must never change
behavior). To stop on a cap, use :func:over_budget in a router.
Source code in agentflow/prebuilt/budget.py
record_usage
¶
record_usage(usage: Usage | None) -> dict[str, Any]
Return a state update that adds usage into the budget channel.
A None usage (a backend that reports no counts) contributes nothing —
the returned update is empty, so a node can unconditionally
return {**result, **record_usage(turn.usage)}. Only the fields the
backend actually reported are added; a missing field is treated as zero and
omitted, so it does not fabricate a total.
Source code in agentflow/prebuilt/budget.py
over_budget
¶
over_budget(state: Any, *, max_tokens: int | None = None, max_cost_usd: float | None = None) -> bool
True when accumulated spend has reached or passed a given cap.
Checks whichever caps are provided: max_tokens against the summed
total_tokens, max_cost_usd against the summed cost_usd. With no
caps given it is always False (nothing to enforce). Spend not yet
recorded (a backend that reports nothing) reads as zero, so a token cap can
only fire when some backend actually surfaced counts.