You're reading the 0.10.0 archive.

Budgets

An unattended agent run can spend money faster than you can watch it. Budgets are how a Hankweave workflow carries spending limits in the program itself: the author records which steps are expensive, which are critical, and what partial work is still worth keeping, and the operator sets the dollar and time envelope for a particular run. This page walks through that contract in the order you will meet it – who sets which limits, the four resources a budget can cap, how dollars reach individual codons, what happens when a cap trips, how loops and resumed runs interact with budgets, and how to catch a bad configuration before any tokens are spent.

Why is cost a program construct and not a dashboard?#

A dashboard can tell us what a run spent, but it cannot carry spending decisions into the next version or the next handoff. When we author a workflow, we already know which steps are expensive, which are critical, and what partial output would still be useful. Putting those decisions in the workflow definition keeps them versioned and reviewable alongside the work they govern, and leaves the operator free to choose the spending and time limits for a particular run.

The contract divides responsibility between two parties. A hank is the workflow definition; a codon is one step in that definition. The author describes the shape of the work – its expensive and critical steps – and sets policies for partial output. The operator owns the wallet, clock, and context envelope around a run. The runtime combines those responsibilities into a resolved plan.

The contract is concrete: the author can set shares and onExceeded: "fail" when partial output is worse than none; the operator can set an envelope such as --max-cost 5.00 and --max-time 600. The runtime resolves the applicable ceiling for each axis, derives per-codon limits, and shows that plan before tokens are spent. The author's shape cannot raise the operator's envelope, and a CLI ceiling does not bypass the author's onExceeded: "fail" policy.

The diagram below shows how the two inputs flow into enforcement. Author and operator settings resolve into ceilings, which become per-codon budgets; a metered-message check and a time watchdog can each request a SIGTERM, and onExceeded decides the outcome.

FIG. 1 budgets: the mental model diagram
Read the diagram as text
Output
author: { shares, onExceeded } ─┐
operator: { maxDollars, maxTime } ─┴→ resolved ceilings
                                      ↓
                              per-codon budgets
                         ↙                     ↘
             metered messages                 time watchdog
                         ↓                     ↓
                 limit check ─────────────→ SIGTERM request
                                      ↓
                              onExceeded outcome

Reading the figure top to bottom gives the three verbs this page uses throughout: a budget allocates shares, a cap trips, and onExceeded decides whether the codon is marked completed or failed. We can let savings flow forward under proportional allocation, design a loop to add value one iteration at a time, and inspect the resolved plan with --validate before spending. A cheap first run is the practical starting point while you learn what the work needs:

⌁ Terminal
# A 50-cent ceiling for a first safety run
bunx hankweave@0.10.0 hank.json data/ --max-cost 0.50 -m haiku

That command caps the whole run at fifty cents on a small model, so a misconfigured workflow fails cheaply.

Budgets also change what a spending limit can buy. Without loops, cost-to-benefit can be a step function: $9.99 buys nothing useful while $10.03 buys 10 units. A cost-driven loop lets each iteration add marginal value, so the operator can choose a point on that curve rather than betting on a single threshold.

One complete 0.10.0 execution of the anchor workload used five Haiku codons and two pi/baseten/deepseek-ai/DeepSeek-V4-Pro judgment codons. Its configured per-codon caps sum to $9.00; the accepted capture reports $0.8830 in tracked codon cost for that complete execution. The configured sum is a ceiling, not a measured total or price promise, and the tracked figure covers codons in that capture rather than provider health checks or sentinel calls.

The enforcement boundary matters as much as the numbers. Dollar caps are checked incrementally as metered assistant messages arrive. A cost delta is accumulated, the cap trips when the accumulated value reaches the limit, and the running agent receives a SIGTERM request. That can interrupt a codon after an already-billable message; a cap is not a prepaid spending guarantee. Time uses a separate watchdog described below. A provider estimate that lands exactly on a dollar cap is still an edge case: the >= comparison operates on floating-point accumulation, so provider-side rounding precision determines the boundary result.

There are two further limits to this contract. Some models have no provider-registry pricing; for those models cost tracking remains $0, so a dollar cap never trips and preflight warns. The runtime loads model and pricing data as part of provider resolution; the pricing is embedded in that model data rather than exposed as a separate reader-facing pricing surface. Provider rate limits, database-read limits, and customer-facing API-call counts are not budget axes in this release. The four implemented currencies are dollars, time, output tokens, and context tokens.

You can still use a dashboard. By keeping cost decisions in the workflow definition, we also carry them through versioning and handoffs, rather than depending on monitoring configuration left in another environment.

Who sets what – the two-party contract#

With the rationale in place, the next question is which party controls which field. The author sets the shape: child codon and loop IDs can receive proportional shares, and a step that must not produce partial output can set onExceeded: "fail". The operator sets the envelope with --max-cost <dollars> and --max-time <seconds>, or with budget.maxDollars and budget.maxTimeSeconds in hankweave.json.

For ordinary runtime settings, the precedence order is CLI, then HANKWEAVE_RUNTIME_* environment variables, then hank overrides, then hankweave.json, then defaults. Budget ceilings are special: hankweave.json and hank-override maxDollars/maxTimeSeconds combine with min() to form the effective global ceiling, while an explicit --max-cost or --max-time replaces that result and can loosen it. HANKWEAVE_RUNTIME_* parsing does not provide budget settings. This is distinct from model selection and from the persisted execution plan; see Hanks for the broader configuration discussion.

The following tables list the fields each party can set, starting with the author-controlled fields on a hank override:

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
maxDollarsnumberno> 0Total cost budget in USD.
maxTimeSecondsnumberno> 0Wall-clock time limit in seconds.
allocationenumsharednoshared | proportional | proportional-strictHow the dollar budget is distributed among children. 'shared': first-past-the-post (default). 'proportional': pre-allocate shares, unspent flows back. 'proportional-strict': pre-allocate shares, unspent evaporates.
sharesobject<number>noMap of child codon/loop ID to fraction (0-1) of the budget.
onExceededenumnocomplete | failWhat happens when budget is exceeded. 'complete' = graceful completion (default). 'fail' = codon failure, triggers onFailure policy.

Codons accept a narrower set of fields – hard caps rather than allocation policy:

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
maxDollarsnumberno> 0Max cost in USD. Execution stopped when exceeded.
maxTimeSecondsnumberno> 0Max wall-clock time in seconds.
maxOutputTokensintegerno> 0Max output tokens.
maxContextTokensintegerno> 0Max context window tokens (high-water mark of input+output per turn). Useful for capping context growth independent of cost.
onExceededenumnocomplete | failOverride hank-level onExceeded for this codon.

A loop uses the same container shape as a hank override for its dollar and time settings:

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
maxDollarsnumberno> 0Total cost budget in USD.
maxTimeSecondsnumberno> 0Wall-clock time limit in seconds.
allocationenumsharednoshared | proportional | proportional-strictHow the dollar budget is distributed among children. 'shared': first-past-the-post (default). 'proportional': pre-allocate shares, unspent flows back. 'proportional-strict': pre-allocate shares, unspent evaporates.
sharesobject<number>noMap of child codon/loop ID to fraction (0-1) of the budget.
onExceededenumnocomplete | failWhat happens when budget is exceeded. 'complete' = graceful completion (default). 'fail' = codon failure, triggers onFailure policy.

On the operator side, the runtime configuration exposes the two global budget fields:

Scroll to explore the table →
fieldtypedefaultrequiredconstraintsdescription
maxDollarsnumberno> 0Global cost budget in USD. Effective global = min(runtime, hank).
maxTimeSecondsnumberno> 0Global wall-clock time limit in seconds. Effective = min(runtime, hank).

In 0.10.0, the parser accepts --max-cost and --max-time, although both flags are omitted from --help. The resolved plan at --validate is the useful confirmation that the flags are active. The JSON equivalents are budget.maxDollars and budget.maxTimeSeconds.

The budgets scenario shows the author side of this contract in practice: a $0.20 proportional pool split into equal shares, with the hank defaulting to onExceeded: "complete" and write-second overriding that policy to "fail".

JSON
  "overrides": {
    "budget": {
      "maxDollars": 0.2,
      "maxTimeSeconds": 600,
      "allocation": "proportional",
      "shares": {
        "write-line": 0.5,
        "write-second": 0.5
      },
      "onExceeded": "complete"

Since 0.6.1, the resolved plan has shown the global ceiling, allocation mode, per-codon dollar caps with their source, time limits, and onExceeded policies before any tokens are spent. It appears in --validate and at run startup, and the validation section below shows what it looks like.

Which resource are you capping?#

So far we have talked about "limits" generically. A budget actually has four currencies. Dollars and time can be set at hank, loop, and codon levels. Output tokens and context tokens are codon-level only.

Scroll to explore the table →
CurrencyFieldAccepted levelsWhat it caps
DollarsmaxDollarshank, loop, codonUSD cost, fungible across codons
TimemaxTimeSecondshank, loop, codonWall-clock seconds
Output tokensmaxOutputTokenscodonModel-generated output tokens
Context tokensmaxContextTokenscodonHigh-water mark of context fill

Each currency behaves differently once set. Under shared and proportional allocation, maxDollars is fungible: money one codon does not spend can remain available to later codons. Under proportional-strict, unspent shares evaporate. Cost tracking requires the model to have provider-registry pricing.

maxTimeSeconds is a wall-clock watchdog. The runtime checks elapsed time during cost updates and also checks it with a one-second interval independent of cost events. When the limit is exceeded, the codon runner requests SIGTERM. This is an active watchdog, not merely an end-of-session check.

That mechanism does not establish prompt teardown in every execution harness (the process wrapper). An older recorded attempt ran for roughly 24,923 seconds against a 2,400-second cap before its budget error surfaced with another failure; the source/runtime cause of that observation is unresolved, and it is not a new 0.10.0 timer experiment. For unattended work, add an external process deadline as a separate safety boundary; it is not a checkpoint-recovery mechanism.

maxOutputTokens caps model-generated output for one codon. Context handling is separate: continuationMode: "fresh" starts a new session, while continuationMode: "continue-previous" continues the previous codon's session and conversation history.

maxContextTokens tracks the high-water mark of inputTokens + outputTokens per turn, not a cumulative total. A turn here is one assistant-message round trip. This limit is useful when context-window fill affects quality independently of dollar cost.

Provider quota, database-read limits, and customer-facing API-call counts are not additional currencies in this release.

How dollars are allocated among codons#

Of the four currencies, only dollars are divided among children. A container budget can allocate its dollar pool in three modes: shared, proportional, and proportional-strict. The default is shared.

Scroll to explore the table →
ModeHow children receive dollarsWhat happens to savings
sharedChildren draw from one remaining pool in execution order; the next codon receives what remains.A cheaper earlier codon leaves more for later codons.
proportionalNamed children receive shares before work begins.Unspent dollars flow back to the pool for later codons.
proportional-strictNamed children receive the same kind of share.Unspent dollars evaporate; the partition stays strict.

For example, when a child has a $2 share and spends $1, proportional returns the unused dollar to the pool, while proportional-strict lets it evaporate.

shares maps child codon or loop IDs to fractions from 0 through 1. It is meaningful only with proportional or proportional-strict. Setting it without one of those allocation modes is a preflight error. Proportional allocation also requires maxDollars on the container, because there must be a total to divide.

Given those inputs, the resolution chain for a codon is:

  1. Find the effective ceiling.
  2. Subtract prior spending to find the remaining pool.
  3. Apply the allocation mode.
  4. Apply the codon's hard cap: min(allocated, codon.maxDollars).
  5. If there is no codon-specific result, that codon-level result is uncapped; the enclosing effective ceiling still bounds the shared pool.

The codon's own dollar cap is applied last, so it remains a hard ceiling even when its allocation is more generous. onExceeded at the codon level overrides the hank or loop container; when neither level sets it, the policy defaults to complete.

The anchor workload's three normalize codons each carry a $0.50 hard cap. Those configured caps are separate from the workload's $9.00 sum of per-codon caps and from its measured tracked cost.

Time is not allocated proportionally. Codons execute sequentially, so dividing a time budget by shares has no useful meaning. Time remains a shared watchdog at the container level and a hard cap at the codon level.

What happens when a cap trips#

A metered cost or watchdog check can request an interrupt while the codon is running. After that interrupt, onExceeded decides the codon's outcome; the policy is separate from the interrupt mechanism.

Scroll to explore the table →
PolicyResultWhen the result fits
complete (default)The codon is marked completed, partial output survives, and downstream codons still run.Best-effort work where partial output is useful.
failThe codon is marked failed non-retriably with reason budget exceeded; its onFailure policy determines what follows.All-or-nothing work where partial output is worse than none.

The default is a soft-cap policy: "completed" describes the runtime outcome, not proof that the agent finished the task. A completed codon can carry budgetExceeded metadata rather than a failure reason. That object identifies the currency that tripped, its limit, and the amount used: { currency, limit, used }.

If the cap trips before a required handoff is written, complete can leave the next codon with no file, truncated JSON, or an old artifact. A later model may improvise a plausible replacement instead of exposing the missing work. Put a deterministic boundary check in the consumer's rig, before its agent starts, and check required fields and input coverage rather than just file existence. See Start with a guarded handoff.

Keeping useful partial work is separate from allowing the pipeline to continue. For a required producer, choose onExceeded: "fail" with an intentional onFailure policy such as "abort"; failure status does not itself erase work already written. Checkpoints govern what recovery can restore. If best-effort completion is intentional, define the partial-result contract and make the boundary check reject anything the consumer cannot safely use. Prompt instructions alone cannot enforce that contract after interruption.

See Events for the complete event shapes and surrounding contract. The two events most relevant here are codon.completed, which carries the budgetExceeded? flag, and budget.summary, which reports the ceiling, allocation, and per-codon rows:

Scroll to explore the table →
idcategoryjournaledsentinelRoutedpayloadFieldsreceipts
codon.completedserver-statetruetruecodonId, success, cost, duration, exitStatus, failureReason?, failureIgnored?, budgetExceeded?schemas/event-schemas.ts:560, schemas/event-schemas.ts:961, schemas/event-schemas.ts:1235, hankweave-runtime.ts:1886, hankweave-runtime.ts:2064, hankweave-runtime.ts:2253, hankweave-runtime.ts:3365
Scroll to explore the table →
idcategoryjournaledsentinelRoutedpayloadFieldsreceipts
budget.summaryserver-statetruetrueceiling, allocation, rows, totalsschemas/event-schemas.ts:724, schemas/event-schemas.ts:979, schemas/event-schemas.ts:1267, hankweave-runtime.ts:6478

budget.summary is a runtime state-based summary built from the budget and the run's codons. It is not a provider invoice and is not a guaranteed retry-inclusive equivalent of every billing record.

When the outcome is failure instead, onFailure is the follow-up policy for a failed codon: abort, retry, or ignore. abort keeps a retriable failure active but shuts down for a non-retriable failure; retry retries only retriable failures until maxAttempts and shuts down for a non-retriable failure or exhausted attempts; ignore continues. A retry configuration's delayMs has a maximum of 60,000 milliseconds.

Two configurations deserve an early warning. onExceeded: "fail" with onFailure: "retry" still triggers a preflight warning, but a budget-exceeded failure is non-retriable: the run shuts down rather than automatically retrying the same allocation. A fail-policy codon late in a shared pool may find that earlier codons have exhausted the pool before it runs.

Budgets inside loops#

Loops change what a budget means, because the same codons run repeatedly against one pool. A loop-level dollar budget is one pool shared across all iterations. When that pool is exhausted, the planner terminates the loop early and removes the remaining codons of the current iteration. The loop can therefore end before all planned work in that iteration runs.

A codon-level maxDollars inside a loop means "per iteration." A $1.00 codon cap provides $1 for each iteration, not $1 for the whole loop; the loop-level budget controls the aggregate.

shared is usually the useful allocation mode inside a loop. The number of iterations is often unknown, so assigning a proportional share to each iteration is rarely meaningful. An iteration limit remains a separate safety valve for how many iterations may run.

A loop-level maxTimeSeconds is a watchdog across all iterations. When elapsed loop time exceeds the limit, the current codon is stopped and the loop terminates. If a codon's time cap is longer than its parent loop's time cap, preflight warns because the loop will terminate first.

One reporting caveat: budget exhaustion is not a terminationReason value on loop.iteration.completed. The allowed values are iteration_limit, context_exceeded, sentinel_skip, and failure; do not report "budget exceeded" as one of them.

Scroll to explore the table →
idcategoryjournaledsentinelRoutedpayloadFieldsreceipts
loop.iteration.completedserver-statetruetrueloopId, iteration, durationMs, costUsd, tokensUsed, isFinal, terminationReason?schemas/event-schemas.ts:612, schemas/event-schemas.ts:976, schemas/event-schemas.ts:1245, hankweave-runtime.ts:6035

What survives when a run resumes?#

A resumed run does not forget earlier spending. Dollar use is hydrated from prior run costs in the state file: if $6 was spent against a $10 budget, $4 remains for the resumed run.

Elapsed wall-clock time is hydrated from prior run timestamps too. If 45 minutes have elapsed against a 60-minute budget, 15 minutes remain; the runtime offsets the hank start time by the prior elapsed duration.

Run-level budget settings are read again when a continuation run starts, but per-codon budgets in the persisted execution plan remain frozen. Raising the applicable run-level maxDollars can increase the remaining pool; editing a saved codon's cap in hank.json does not replace that cap on resume. Use the change/resume table before choosing between another attempt and --start-new.

Retry attempts during one codon execution add to that codon's running total. When the codon completes, the retry-only accumulator is cleared; that does not erase the completed codon's cost from the spending map. Costs from already completed codons remain immutable there and hydrate the next run.

Budget state is persisted in .hankweave/state.json; see State file for its storage contract. The state manager writes the file atomically. A resumed run combines prior dollar and time usage, current run-level settings, and saved per-codon definitions.

Catching budget misconfigurations before tokens are spent#

Everything so far can be inspected before it costs anything. --validate runs the budget checks before any tokens are spent and prints the resolved budget table. Errors block execution; warnings inform without blocking. The table gives the global ceiling, allocation mode, per-codon limits with source attribution, time limits, and onExceeded policies.

The captured plan below starts with a $0.20 hank ceiling and proportional allocation. Each codon has a $0.05 hard cap against a $0.10 share, and the table notes both the cap and the share it came from.

Output
Budget
─────────────────────────────────────────────────────────────────
  Global ceiling:  $0.20 (hank)
  Time limit:      600s (hank)
  Allocation:      proportional (unspent flows to later codons)

  Codon             Model       Max Dollars                       Max Time          On exceeded
  ─────             ─────       ───────────                       ────────          ───────────
  write-line        Claude Ha…  $0.05 (codon cap, share was $0.10)120s (cap)        completes
  write-second      Claude Ha…  $0.05 (codon cap, share was $0.10)120s (cap)        ⚠ fails run

A tighter operator ceiling changes the global limit to $0.10 and shows each share as 50% of $0.10; the codon caps and policies stay as the author set them. The operator-tightened plan is captured separately:

Output
Budget
─────────────────────────────────────────────────────────────────
  Global ceiling:  $0.10 (--max-cost, hank wanted $0.20)
  Time limit:      600s (hank)
  Allocation:      proportional (unspent flows to later codons)

  Codon             Model       Max Dollars                       Max Time          On exceeded
  ─────             ─────       ───────────                       ────────          ───────────
  write-line        Claude Ha…  $0.05 (50% of $0.10)              120s (cap)        completes
  write-second      Claude Ha…  $0.05 (50% of $0.10)              120s (cap)        ⚠ fails run

Errors block execution#

Scroll to explore the table →
ErrorWhat it means
Shares sum to more than 1.0More than the whole budget is allocated.
Shares reference unknown IDsA share key names no codon or loop.
Shares without proportional allocationshares is set without proportional or proportional-strict.
Proportional allocation without maxDollarsThere is no total to divide.

Warnings inform without blocking#

The validator warns about cases such as:

  • A codon or loop cap is greater than its proportional allocation.
  • Budget remains unallocated and no child is available to receive it.
  • exhaustWithPrompt is combined with a time budget.
  • A dollar budget names a model without provider-registry pricing; tracking reports $0 and the dollar budget never trips.
  • Shares total 100% but this codon has no share, so its effective budget is $0 and it immediately trips.
  • onExceeded: "fail" is combined with onFailure: "retry".
  • A fail-policy codon comes late in a shared pool that earlier codons may exhaust.
  • A codon time cap is greater than its loop time cap.

These checks distinguish a configuration that blocks the run from one that is valid but likely to surprise you. The resolved plan is the pre-spend inspection point; the runtime's later summary is a state-based report, not an invoice.