Reliabilityreliabilitybudgetsstep limitloop detectiontermination

Budgets, Limits and Termination

An agent loop must be bounded by hard limits on steps, tokens, cost and wall-clock time, with loop detection and a graceful degradation path, all enforced in code outside the model.

Interview question
Progress

Why the model cannot bound itself

A model deciding “am I done?” is a probabilistic judgement, and a model that is stuck is precisely the one whose judgement has failed. The agent loop from The Agent Loop has a decision node at the bottom; production code needs a second, deterministic exit that fires regardless of what the model thinks. That exit is a set of budgets checked before every step.

Budgets serve three purposes: they cap cost (a runaway request cannot spend more than N dollars), they cap latency (the user gets an answer or a clear failure within T seconds), and they turn silent failures into loud ones (a run that hits its step limit is a bug report with a trace attached).

The five limits

Set every one of these per request, derived from the task type. Values for a support bot differ from a research agent, but the shape is the same.

  • Max steps: iterations of the loop. Typical: 5–15 for task agents, 30–50 for coding agents. Set it at roughly 2× the p95 of successful runs.
  • Max tokens: cumulative input plus output across all calls. Catches context growth that the step limit misses.
  • Max cost: computed from per-model prices; the limit users and finance actually care about.
  • Wall-clock: total elapsed time including tool latency. Enforced with a deadline passed to every tool and model call.
  • Loop detection: a hash of (tool_name, canonical_args) per step; if the same hash appears k times (k = 2 or 3), stop. Also detect A-B-A-B oscillation with a small window.

Graceful degradation

Hitting a limit should not throw a stack trace at the user. The run ends in one of three ways, chosen by the controller: return the best partial result with an honest note, escalate to a human (In-the-Loop vs On-the-Loop and Escalation), or fail with a specific error code that the caller can handle. Whichever it is, the trace records which limit fired and the state at that moment, so the incident is reproducible.

A useful pattern is a final-answer call: when a limit is reached, make one last model call with tools disabled and the instruction “summarise what you found and what remains”. It costs one call and converts a dead run into a partially useful one.

A bounded loop: every limit is checked in code before the next step; loop detection hashes (tool, args).
1import hashlib, json, time
2from collections import Counter
3
4class Budget:
5 def __init__(self, max_steps=12, max_tokens=60_000, max_cost=0.50, max_seconds=90, repeat_limit=2):
6 self.max_steps, self.max_tokens, self.max_cost = max_steps, max_tokens, max_cost
7 self.deadline = time.monotonic() + max_seconds
8 self.repeat_limit = repeat_limit
9 self.steps = self.tokens = 0; self.cost = 0.0
10 self.seen: Counter[str] = Counter()
11
12 def check(self) -> str | None:
13 if self.steps >= self.max_steps: return "max_steps"
14 if self.tokens >= self.max_tokens: return "max_tokens"
15 if self.cost >= self.max_cost: return "max_cost"
16 if time.monotonic() >= self.deadline: return "deadline"
17 return None
18
19 def note_call(self, tool: str, args: dict) -> str | None:
20 key = hashlib.sha256((tool + json.dumps(args, sort_keys=True)).encode()).hexdigest()
21 self.seen[key] += 1
22 return "repeated_call" if self.seen[key] > self.repeat_limit else None
23
24def run_agent(messages, tools, budget: Budget):
25 while True:
26 if reason := budget.check():
27 return finalize(messages, reason) # one tools-off call: summarise partial result
28 resp = call_model(messages, tools)
29 budget.steps += 1; budget.tokens += resp.usage.total; budget.cost += resp.usage.cost
30 if not resp.tool_calls:
31 return resp.text
32 for tc in resp.tool_calls:
33 if reason := budget.note_call(tc.name, tc.args):
34 return finalize(messages, reason)
35 result = tools[tc.name](**tc.args, deadline=budget.deadline)
36 messages.append(tool_result(tc.id, result))

Choosing values and tuning them

Start from traces of successful runs: take the p95 of steps, tokens and cost and multiply by two. Then watch the limit-hit rate per limit. A limit that fires on 5% of runs is either too tight or is revealing a real loop; read the traces to decide. A limit that never fires may be too loose to protect you. Alert on limit-hit rate the same way you alert on error rate (Logging, Metrics and Alerts).

Budgets compose across agents. In a Supervisor Pattern system, the supervisor’s budget must include its workers’ spend, or each worker will individually stay under budget while the total explodes.

Key points

  • The model cannot be trusted to stop itself; a deterministic exit checks budgets before every step.
  • Five limits: max steps, max tokens, max cost, wall-clock deadline, and loop detection via (tool, args) hashes.
  • Hitting a limit ends gracefully: partial result with a note, escalation, or a specific error, always with a trace.
  • Set limits at ~2× the p95 of successful runs and alert on limit-hit rate.
  • Budgets must be aggregated across sub-agents or the total is unbounded.

When to use — and when not to

Use it when
  • Every agent loop in production, without exception.
  • Long-running research and coding agents where one step is expensive.
  • Multi-agent systems where sub-agents can spawn their own loops.
Avoid it when
  • A fixed-step workflow cannot loop; it still needs a token and cost cap per call.
  • Do not set the step limit so low that the p95 successful run is cut off.
  • Do not raise a limit to hide a loop; fix the tool result that causes it.

Failure modes

  • Step limit exists but tokens per step grow, so cost is still unbounded.
  • Loop detection hashes raw argument strings and misses the same call with reordered JSON keys.
  • Limit fires and the user sees a raw exception instead of a partial answer.
  • Each worker agent has a budget; the supervisor does not, and total spend is 10× the intended cap.
  • Deadline is checked in the loop but not passed to tools, so one slow HTTP call blows through it.

Tradeoffs

Complexity
low → high
Latency
low → high
Cost
low → high
Reliability
poor → strong
Debuggability
hard → easy

Fifty lines of code; the highest reliability return of anything in this module.