Stopping and budgets
Design a bounded observe-act loop with explicit state, stopping conditions and a verifiable task outcome.
THE IDEA
Stopping and budgets
A loop that can run forever eventually will. Set limits on steps, time, tokens and cost, and stop repeating the same failing call. When a limit is reached, stop with a clear partial-result state: what was done, what was not, why it stopped. For a job that never completes, a step budget of 4 uses 2,400 tokens and, at toy prices, $0.008. Announcing success instead would be worse than the cost it saved.
Limit steps, time, tokens and cost, and stop repeating an unchanged failed call. A clean stop with a partial result is a success of the control system, not of the task.
Predict first. Should an unchanging failed tool call repeat without a bound?
Chapter context
An agent chooses actions based on a task and observations, often over several model calls. Useful autonomy requires bounded resources, explicit feedback and a stopping rule that recognizes both success and inability to continue.
TRY IT YOURSELF
Live lab
This job never completes: its status is always pending, and the system prompt tells the agent to keep checking. Without a limit it would loop forever.
TRY IT YOURSELF
Set a budget
This job never completes: its status is always pending, and the system prompt tells the agent to keep checking. Without a limit it would loop forever.
Set a step budget of 4 or fewer so the run stops cleanly with a partial result. A lower budget beats par.
Task given to the model: Check job job-42 and tell me when it is done.
This loop as code
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY
TOOLS = [ # name, description and input_schema are the "Tool definition"
{"name": t["name"], "description": t["description"], "input_schema": t["schema"]}
for t in [
{
"name": "get_job_status",
"description": "Check the status of a background job.",
"schema": {
"type": "object",
"properties": {
"job_id": {
"type": "string"
}
},
"required": [
"job_id"
]
}
}
]
]
messages = [{"role": "user", "content": "Check job job-42 and tell me when it is done."}]
for step in range(8): # Step budget
response = client.messages.create(model="claude-haiku-4-5", max_tokens=600, tools=TOOLS, messages=messages)
messages.append({"role": "assistant", "content": response.content})
calls = [b for b in response.content if b.type == "tool_use"]
if not calls:
break # final answer
results = []
for call in calls: # the model only PROPOSES these
problems = validate(call.input, schema_for(call.name)) # your host-side check
if problems:
output, is_error = "Invalid arguments: " + "; ".join(problems), True
else:
output, is_error = run_tool(call.name, call.input) # your code runs it
results.append({"type": "tool_result", "tool_use_id": call.id, "content": output, "is_error": is_error})
messages.append({"role": "user", "content": results})Update the controls above and this code changes with them. Run it yourself with the key in an environment variable.Exercises
Part 1. Do it hands-on
Cost a bounded run by hand, then write the budget controller.
Limit steps, time, tokens and cost, and stop repeating an unchanged failed call. A clean stop with a partial result is a success of the control system, not of the task.
- To do: Each pending-job check costs 500 input and 100 output tokens. A budget of 4 steps uses how many tokens?
- To do: Toy prices: $2 per million input tokens, $10 per million output tokens. Cost of those 4 steps in dollars (3 decimals)?
- To do: With a token limit of 2400 the run stops after how many steps?
- To do: Should an unchanging failed tool call repeat without a bound? Type yes or no
Part 2. Do it in code
- step_cost(inp, out, price_in, price_out): dollars for one step; prices are dollars per million tokens.
- run_agent(steps, limits, price_in=2, price_out=10): steps is a list of dicts {"inp", "out", "done" (optional), "fail" (optional)}; limits has steps, tokens, cost and repeats. For each step: if the number of steps already run equals limits['steps'], stop with status "steps"; otherwise run it (add its tokens and cost). Then, in this order: if it is done, status "done"; if total tokens >= limits['tokens'], "tokens"; if total cost >= limits['cost'], "cost"; if the number of consecutive failed steps >= limits['repeats'], "repeats". If the steps run out without a stop, status "steps".
- Return {"status", "steps" (run), "tokens", "cost", "partial" (True unless status is done)}.
- describe(result): a one-line honest report: "completed in N steps" for done, otherwise "stopped: <status> limit after N steps, partial result".
Both the live lab and the exercises must be solved to finish this lesson.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Should an unchanging failed tool call repeat without a bound?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
Represent a run as states s0,s1,... and actions a0,a1,... with observed transitions. A budget constraint can bound the sum of measured call costs. History-dependent token growth means a constant per-step estimate is only a simplifying model.
Worked example
A simulated agent may take at most three actions. It moves from room A to B, inspects a clue, then moves to C. The trace contains three actions. If the objective still is not satisfied, the correct result is budget exhausted with partial progress, not a fourth unapproved action or a fabricated success.
Reference code
The following example uses the Python standard library and does not call a remote service.
actions=['move:A->B','inspect:clue','move:B->C']
budget=3
assert len(actions) == budget
objective_satisfied=False
status='complete' if objective_satisfied else 'budget-exhausted'
assert status == 'budget-exhausted'
assert len(actions)*.002 == .006
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
A plan is not execution evidence. Reflection can repeat the original mistake. A step budget is useful only if every executed action is counted consistently.