JJoeven

Curriculum/Agent Architectures

Stop Conditions and Budgets

Success, max steps, max dollars, max wall time, or handoff. Without stop, you bought a furnace.

beginner20 min4 / 24

Stop is a contract, not a vibe. An agent that can call tools and cannot stop is a furnace: it burns tokens, retries, and maybe money until someone kills the process.

The prompting track wrote stop rules in text (“after at most 8 steps, call finish or handoff”). Text is a suggestion. The model will ignore it when it is stuck. Code will not. Put the same cap in the worker that runs the loop, not only in the prompt.

There are three honest ways to stop:

  1. Successgoal_satisfied(result) is true
  2. Budget — max steps, max dollars, max wall time
  3. Handoff — unknown, unsafe, or irreversible without a human

If none of those fire, you do not have a loop. You have a while True.

Stop is a contract in code
Keep workingCheck stopSuccessBudgetHandoff

After each step, code asks three questions. A guessed answer is not a fourth exit.

Stop is a contract in code

Success is a predicate

Success is not “the model sounds done.” It is a function of the observation (and maybe typed state). Examples: the observation is a dict with final; the ticket is closed in the store; the plan list has every step done.

Write goal_satisfied so a test can pass a fake observation and get true or false. If you cannot write that function, you cannot stop on success. You can only stop on budget — which is still better than never.

Do not let the model mark success by emitting a happy sentence. If you use a finish tool, the executor runs it and stop reads the result. finish is data. Stop is code.

Budget is three meters

Steps are the easiest meter. Each model call, or each tool call, increments. Pick one and stick to it. Count parse retries and tool retries toward the budget (tools track). A “free” retry is how 8 steps become 40.

Dollars are tokens plus tool fees. You do not need a billing system in this lesson. You need a counter you increment with a rough cost per step, and a cap. When it trips, stop.

Wall time is how long the run has been alive. Cloud functions time out. Humans wait. A loop that is “still thinking” after N minutes should handoff or pause as a job (later lesson). Wall time is not step count. A single hung tool can burn the clock with one step.

When any meter trips, return cannot: step budget (or money, or time). Do not invent a final answer because the loop ended. That is how you get a confident lie after three useless searches. Hitting a cap is a stop. Inventing an answer after the cap is a lie.

Handoff is a stop, not a crash

Unknown intent, unsafe tools, missing evidence, or “I would need to click Pay” are handoffs. The loop ends with a packet: why, what you tried, any frozen args. A later lesson is the packet. Here: handoff is an allowed exit. Treat it like success in the control-flow sense: the while loop breaks. Treat it unlike success in the product sense: the user is not done.

If the model emits a handoff tool, stop should honor it — after the parser blesses the name. If stop only honors finish, the model cannot escalate.

Prompt stop vs code stop

Put the cap in three places if you can: the assembled prompt (“you have 3 of 8 steps left”), the parser (reject extra tools after cap), and the worker (should_stop). The worker is the one that matters. The prompt is courtesy. Surface step 3/8 in the UI so operators see the furnace cooling down.

Never raise the cap automatically because the model asked for “one more search.” That is the furnace talking. A human can raise a cap. Code should not.

What to return when you stop

WhyReturnDo not
SuccessThe blessed final payloadExtra tools after done
Step/money/time capcannot: ... budgetA guessed answer
HandoffPacket with why + triedA shrug or a crash stack
Parse fail after repaircannot: bad_json or handoffeval()

The user-facing string can be short. The operator trace should show the reason enum. You will count those reasons later. This track only needs the enum to exist.

Stuck loops

The classic stuck loop is: search, search, search, never finish. Budget is the backstop. Tool thrash (same args, same name) is a sharper backstop in a later lesson. Reflection is not a substitute for stop. A critic that says “try again” without a cap is another furnace.

If the agent hits the budget with no final, refuse. Operators would rather see cannot: step budget than a fluent wrong refund.

Common mistakes

  • Stop only in the prompt.
  • Inventing an answer at the cap.
  • Not counting retries as steps.
  • No wall-time cap on the worker.
  • Treating handoff as an exception instead of a reason.
  • Letting the model raise its own budget.
Live PythonOpen full playgroundpython
Output
Run to execute this in your browser. Nothing is sent to a server.

The happy path finishes on step 1 with a final. The stuck agent searches until the cap, then refuses. It does not guess. Read the return values: one dict with final, one string that starts with cannot. That difference is the product.

Flip always_search and the same should_stop serves both exits. Add a handoff decision in your head: the function already returns "handoff" and run would return the last obs. A real worker would return a packet instead.

How agents use this

Put the same cap in the worker that runs the loop, not only in the prompt. Count tool retries toward the budget (tools track). Surface step 3/8 in the UI so operators see the furnace cooling down.

Jobs and HITL later pause instead of burning wall time. Pause is a stop of a different shape: the process ends, the run record waits. Budget still applies when the worker is awake. An overnight job with no step cap is still a furnace; it just bills slowly.

Check your understanding

The agent used its last step and still has no final answer. What should it return?