Skip to content
intermediate Phase 12 · Agent Loops and Planning

Stopping Conditions and Budgets

Define iteration caps, goal checks, and token budgets so agents terminate reliably.

45m
0 problems
Topic Progress 0%

Four Guards, One Termination Argument

Ask what stops an agent run. If the answer is "the model will stop calling tools eventually," the run is not a program — it is a wager. The protocol offers exactly one natural stop: the model returns no tool calls. A confused model never returns it. Every other guarantee has to come from your code.

Four guards cover the exits you actually need:

Guard Fires when Guarantees Failure it prevents
Max steps iterations reach the cap hard termination a runaway loop draining tokens overnight
Done tool the model calls done(answer) explicitly early exit on real success accepting fluent prose that never met the goal
Token budget cumulative spend reaches the per-task limit bounded cost one task consuming the whole month's budget
No-progress detector N identical observations in a row stall detection echo steps repeating one call forever

They are not redundant. The done tool lets a three-step task end at step 3 instead of always burning the full cap; the cap saves the run whose done signal never arrives; the budget bounds runs the cap would otherwise allow to be ruinous; the no-progress detector catches the specific failure where steps keep being taken and nothing changes — new iterations, same observation.

Check order matters as much as presence. Goal first, because verified success is the best ending. Then no-progress, then budget, then steps. Each stop reason carries different information: max_steps says the goal check never passed, budget_exceeded says cost was the binding constraint, no_progress says the loop was alive but not moving. A single boolean "stopped" erases exactly the signal you need to tune the run — this is the guard rail layer of The Core Agent Loop.

Implementing the Four Guards

One function decides the reason; the loop only obeys it:

import hashlib, json

MAX_STEPS = 12
MAX_TOKENS = 60_000          # per task, cumulative over every model call
STALL_LIMIT = 3

def digest(obs):             # normalized so key order and whitespace do not mask a stall
    return hashlib.sha1(json.dumps(obs, sort_keys=True).encode()).hexdigest()

def stop_reason(state):
    if state["goal_met"]:                     return "done"
    if state["repeats"] >= STALL_LIMIT:       return "no_progress"
    if state["tokens"] >= MAX_TOKENS:         return "budget_exceeded"
    if state["steps"] >= MAX_STEPS:           return "max_steps"
    return None

def run(goal, ask, act, verify_goal):
    state = {"steps": 0, "tokens": 0, "prev": None, "repeats": 0,
             "goal_met": False, "answer": None}
    while stop_reason(state) is None:
        thought, used = ask(goal, state)        # returns model text, tool call, token usage
        state["tokens"] += used                 # budget counts every call, not just the last
        state["steps"] += 1

        if thought["tool"] == "done":           # explicit done signal, not inferred prose
            state["answer"] = thought["args"]["answer"]
            state["goal_met"] = verify_goal(state["answer"])   # verifier accepts or rejects
            continue

        obs = act(thought["tool"], thought["args"])
        state["repeats"] = state["repeats"] + 1 if digest(obs) == state["prev"] else 0
        state["prev"] = digest(obs)

    return {"status": stop_reason(state), "steps": state["steps"],
            "tokens": state["tokens"], "answer": state["answer"]}

Two real terminations from the same task type:

step 7   tokens=41,820   observation identical to steps 5 and 6
stop: no_progress (3 identical observations)
status=no_progress steps=7 answer=null

step 11  tokens=61,004 >= 60,000
stop: budget_exceeded
status=budget_exceeded steps=11 answer=null

The subtle lines: tokens += used on every call, because re-sending a growing transcript means early steps are the cheap ones and the budget must be charged continuously; state["goal_met"] = verify_goal(...) accepts the done tool only after an independent check — the model declaring done is a proposal, not a fact; and the digest comparison, which counts an observation as new only when its normalized content changes, is what distinguishes "still working" from "spinning in place."

Exiting Early, Escalating Cleanly

A stop reason is only useful if the caller can act on it. Split the four outcomes into two classes:

  • Success: done, with an answer the goal verifier checked. Nothing else qualifies.
  • Bounded failure: max_steps, budget_exceeded, no_progress. The run is safe — cost was capped, no damage was done — but the task is unfinished, and unfinished tasks need an owner: retry with a narrower goal, hand to a worker queue, or escalate to a person.

The goal verifier is where teams cut corners. It must be a deterministic function over the answer — a JSON schema check, "the record now has status refunded," "the file exists in the bucket" — never the model's self-assessment. A verifier that trusts done(answer) turns the done tool into a rubber stamp, and the loop degenerates into a race to declare completion.

Budget accounting deserves the same rigor. Charge every call: the initial prompt, each revision, each tool result that gets re-sent in the transcript. Plans that grow the transcript — like the replanning strategy in Planning Strategies for Agents — spend faster than step count suggests, so metering at the call site rather than per turn is what keeps budget_exceeded truthful; the mechanics live in Metering Token Usage.

Watch for guards misfiring on legitimate work:

  • Cap too tight. A cap of 4 on a task that genuinely needs 7 turns every run into a failure that looks like model incompetence. Tune it from p95 step counts on real traces, not from taste.
  • Detector tripping on polling. Checking a build status five times returns five identical observations by design. Let a tool declare itself pollable and exclude those results from the digest comparison.
  • Invisible budget. A budget nobody reads is a guard that only exists in code review.
  • All failures treated alike. no_progress points at a bad tool or prompt; budget_exceeded points at transcript growth. Routing both to the same retry hides which one you have.

Once a run stops for the right reason with the right classification, the remaining risk is what happens when the tools themselves fail — the subject of Error Recovery in Agent Runs.

Quiz

1. Why is a max-step guard mandatory even when you have a goal check?

Question 1 options

2. What must verify_goal check before a done signal is accepted?

Question 2 options

3. A run ends with status no_progress. What does that tell you that max_steps would not?

Question 3 options

4. Why does the token budget increase on every model call rather than at the end of the run?

Question 4 options

Flashcards

Question

What four guards bound an agent run?

Answer

Max steps (termination), explicit done tool (verified early exit), per-task token budget (cost bound), no-progress detector (stall detection).

Question

In what order should stop conditions be checked, and why?

Answer

Goal, then no-progress, then budget, then steps. Verified success is the best ending, and each remaining reason tells you a different thing about why the task failed.

Question

Why is the done tool a proposal rather than a decision?

Answer

The model can claim completion while criteria are unmet. A deterministic verifier over the answer must confirm it before goal_met is set.

Question

How does the no-progress detector distinguish a stall from real work?

Answer

Hash each normalized observation and count consecutive repeats; three identical digests means no_progress. Polling tools should be excluded so legitimate repeated checks do not trip it.

Question

How should a caller treat the four stop reasons?

Answer

done → success with a verified answer. max_steps, budget_exceeded, no_progress → bounded failure: safe and cheap to stop, but the task still needs a retry, a narrower goal, or a human.

Revision Notes

Key Takeaways

  • 1. Termination must come from your code — the protocol only stops when the model returns no tool calls
  • 2. The cap guarantees exit; the done tool, budget, and stall detector decide how a run ends before the cap
  • 3. Accept done only after a deterministic goal verifier, and charge tokens on every call
  • 4. Classify the stop reason: no_progress, budget_exceeded, and max_steps point at different fixes

Interview Tips

  • • List the four guards and say which one guarantees termination — interviewers probe for that distinction
  • • Walk the check order and justify goal before budget before steps
  • • Explain why self-reported completion needs an independent verifier, with an example of a false done
  • • Describe how you would set the cap: p95 step counts from traces, not a round number

Cheat Sheet

Stopping Conditions and Budgets — Cheat Sheet

Four guards

Guard Fires on Failure prevented
max steps iterations ≥ cap runaway loop, tokens drained overnight
done tool explicit done(answer) + verifier unverified fluent prose accepted as success
token budget cumulative spend ≥ limit one task eating the whole budget
no-progress 3 identical observation digests echo steps repeating one call forever

Check order: goal → no_progress → budget → max_steps

Skeleton

while stop_reason(state) is None:
    thought, used = ask(goal, state)
    state["tokens"] += used                 # charge every call, transcript grows each round
    if thought["tool"] == "done":
        state["goal_met"] = verify_goal(thought["args"]["answer"])  # deterministic check
        continue
    obs = act(thought["tool"], thought["args"])
    state["repeats"] = state["repeats"] + 1 if digest(obs) == state["prev"] else 0
    state["prev"] = digest(obs)

Stop reasons

  • done → success (only verdict that is)
  • max_steps / budget_exceeded / no_progress → bounded failure; classify before retrying

Tuning: size the cap from p95 step counts on real traces; exempt pollable tools from the digest check; meter tokens at the call site.

Continue Learning

Before You Start

  • No prerequisites listed.

Related Reading