Reading tools and contents
Harness Engineering & Sandboxes

Chapter 7 of 10

Budgets, termination, idempotency, and recovery

Control

About 4 minutes · includes examples, an exercise, and references

Chapter at a glance

  • Reserve resources before execution and charge them through a durable ledger.
  • Propagate deadlines while preserving time for evaluation and cleanup.
  • Retry only according to error class; reconcile ambiguous effects before retrying.

An agent loop without enforceable budgets is an unbounded distributed job. Natural-language requests such as “be efficient” are preferences, not controls. The harness must track and enforce limits outside the model for elapsed time, planner calls, input and output tokens, tool invocations, CPU, memory, processes, storage, network bytes, external API spend, and consequential actions. Different resources fail differently, so one aggregate budget is not enough.

Represent a budget as a ledger. Each reservation identifies the run, resource, amount, purpose, and expiration. Reserve before starting an operation that could exceed the remaining balance; charge the actual amount afterward and release unused capacity. For uncertain external spend, reserve a conservative upper bound. If reservation fails, the operation does not begin. This prevents concurrent tool calls from each seeing the same remaining allowance.

Deadlines propagate. The run has an absolute deadline. Each model or tool call receives a shorter child deadline that leaves time for observation capture, evaluation, and cleanup. A library timeout that merely stops waiting is insufficient if the remote request or subprocess continues. Cancellation must reach the execution boundary, and the harness must reconcile whether an effect completed. Record timeout, cancellation, and budget exhaustion as distinct result classes.

Termination predicates should be explicit and ordered. Verified success requires evaluator evidence. Hard policy denial terminates or pauses according to policy. Budget exhaustion stops new work and enters evaluation or cleanup. User cancellation stops new effects. No-progress detection identifies repeated state, repeated equivalent calls, or lack of measurable improvement over a bounded window. A maximum-step count remains necessary even with no-progress logic because novelty can still be useless.

No-progress detection should operate on normalized state, not raw prose. Hash the relevant task state, artifact manifest, open obligations, and last observations. Track whether evaluations or acceptance-test deltas improve. A planner that changes wording while repeating the same failed action has made no progress. Conversely, a long compilation step may have no intermediate artifact change but remains useful; task-specific phase models reduce false trips.

Retries need a taxonomy. Validation and policy denials are deterministic until input or policy changes; do not retry unchanged. Rate limits and transient dependency failures may be retryable with bounded exponential backoff and jitter. Timeouts and transport failures around side effects are ambiguous; reconcile first. Internal harness bugs should trip a circuit breaker rather than consume the entire task budget. The retry budget itself is explicit.

Idempotency keys should represent intended logical effects, not attempts. The same logical write retried after transport loss uses the same key. A changed payload or target uses a new key. Store the key and committed result in a durable scope controlled by the adapter. If the downstream service supports idempotency, propagate it; otherwise maintain a local outbox or reconcile through a read interface. Never derive a key from volatile sequence alone if a recovered run could assign a different number.

Recovery begins from checkpoints that include control state: fixture identity, policy and adapter versions, remaining budget, planner-visible observations, idempotency records, pending approvals, leases, and artifact manifest. Checkpoint before and after consequential effects. A checkpoint cannot prove an external effect did not occur if the worker died between effect and commit, which is why idempotency and reconciliation remain necessary.

Budget tuning is empirical. Too-small limits cause systematic task failure; too-large limits expand cost and blast radius. Analyze distributions by task class, outcome, and failure mode. Set defaults from successful percentiles plus justified headroom, then define exception policies. High-consequence action counts should remain low even when token or time budgets are generous.

Expose remaining budgets to the planner as observations so it can adapt, but never delegate enforcement. The planner may choose a cheaper approach; it cannot grant itself more resources. Approval can amend a budget through a durable policy event with identity and reason. Silent automatic expansion turns a limit into a suggestion and makes incident costs unpredictable.

Key points

  • Reserve resources before execution and charge them through a durable ledger.
  • Propagate deadlines while preserving time for evaluation and cleanup.
  • Retry only according to error class; reconcile ambiguous effects before retrying.
  • Checkpoint control state and keep enforcement outside the model.

Enforce a multi-resource budget

Read the expected behavior in the surrounding walkthrough, then copy and run this reference implementation.

Enforce a multi-resource budgetpython
from dataclasses import dataclass

@dataclass
class Budget:
    steps: int
    tool_calls: int
    network_bytes: int

    def reserve(self, *, steps=0, tool_calls=0, network_bytes=0) -> None:
        requested = (steps, tool_calls, network_bytes)
        remaining = (self.steps, self.tool_calls, self.network_bytes)
        if any(need < 0 for need in requested):
            raise ValueError("budget reservations must be non-negative")
        if any(need > have for need, have in zip(requested, remaining)):
            raise RuntimeError("budget exhausted")
        self.steps -= steps
        self.tool_calls -= tool_calls
        self.network_bytes -= network_bytes

budget = Budget(steps=8, tool_calls=4, network_bytes=20_000)
budget.reserve(steps=1, tool_calls=1, network_bytes=4096)
print(budget)

Exercise

Repair an unsafe retry loop

A payment adapter times out after sending a request, and the planner proposes calling it again.

  1. Classify the failure and identify the logical idempotency key.
  2. Design a reconciliation call and timeout budget.
  3. Specify the durable states for committed, not committed, and ambiguous outcomes.

Success criteria

  • The payment is never repeated solely because the response was lost.
  • Retry and reconciliation consume explicit budgets.
  • Ambiguous state terminates or pauses with an owner.

Reflect: Which resource in your current agent loop is monitored but not actually enforced?

References and further reading