Part 3 of 5 · 5 min read
Good runs, timeouts and endings
Guards that stop good runs, cancellation, and finding out why it looped.
Guards that stop good runs
A guard that fires on healthy behaviour is its own bug. The polling task checks the same job status four times while the job runs, then finishes:
| Guard | Polling task |
|---|---|
| repeat of 3, comparing call and result | finishes (the progress text changes: "running 20%", "running 55%") |
| repeat of 3, comparing the call only | stopped at step 9 |
| no progress for 4 steps | stopped at step 10 |
| no progress for 5 steps | finishes |
The lessons:
- Compare the result as well as the call. The same call with a changed result is progress; the same call with the same result is not.
- Set the no-progress window longer than any legitimate wait. Better, give waiting tools their own rule (a poll interval and a maximum number of polls) and exclude them from the generic one.
- Test every guard on your good runs, not only on stuck ones. A guard's false-stop rate belongs in the same table as its catch rate.
Timeouts and cancellation
A time limit checked only between steps cannot stop a step that is already running. A tool call that takes 120 seconds, with the user cancelling at 10 seconds or a time limit of 30:
| How the check is made | Cancel at 10 s: run stops at | Time limit 30 s: run stops at |
|---|---|---|
| between steps only | 128 s | 128 s |
| a deadline inside the call | 10 s | 30 s |
So give every tool call a deadline that is the smaller of its own timeout and the run's remaining time or cancel flag. Concretely:
- A cancel token (a flag) is checked before each step and passed into tools, which check it in long loops and while waiting.
- Network and subprocess calls use timeouts, and a hard kill after a grace period.
- Clean up after a cancel: a call cut off halfway may have changed something. Use idempotency keys and preview steps (module 2) so a retry or a half-done effect is safe, and record in the trace that the call was cut off.
- Tell the user what happened: "stopped, here is what was done."
Ending well
When a guard fires, the run should end with a report, not an exception:
status: stall
steps: 8 words: 310 seconds: 24
facts: f0, f1, f2
answer: Partial answer: found 3 of 6 facts (f0, f1, f2).
Stopped because: several steps in a row found nothing new.
trace: every call and result, in order
What this buys: in the example, a run stopped by a guard still hands back 50% of the needed facts (it had found 3 of 6 before going wrong); an abrupt crash hands back 0%. The user gets something to use, and an honest note about what is missing. For anything with side effects, list what was already done so a person can finish or undo it. Common choices on stopping: return the partial answer; ask the user whether to continue with a larger budget; hand over to a person; or retry once with a different approach (module 3's replan), never forever.
Finding out why it looped
"Your agent loops forever. How do you find out why?" Read the trace. The patterns in the example are distinct enough to write down as rules, and the code does:
| What the last steps show | Diagnosis | Usual fix |
|---|---|---|
| same call, same result, again and again | loop | tell the model it already has this result; stop on repeat; check it is not ignoring an error |
| two or three calls taking turns, no change | thrash | detect cycles; give the plan more structure (module 3) |
| new queries, results "no results" | wandering | the information may not exist: allow "I could not find it" as an answer; stop on no progress |
| the same failing tool, same arguments | retry storm | bounded retries with backoff; return the error to the model; circuit-breaker |
| the same failing tool, changed arguments | jittered retries | stop on no progress; fix the tool or tell the model it is down |
| results of 1,000+ words | huge observations | cut or summarise tool results (module 2); budget words |
| one call that never returns | slow tool | per-call deadline |
The example's diagnose() function applies exactly these rules to a report's trace and names the right cause for each of the seven stuck runs (a person reading the same trace would, too). In a real system, the trace also shows why the model repeated: the prompt it saw at the loop's start, and whether the last error message told it what to do differently (module 2).
Interview angle
"Your agent loops forever. How do you find out why, and how do you stop it?" Stop it with guards that live in the loop code and do not rely on the model: a step limit, a token or cost budget, a time limit with a deadline on each tool call, a cancel flag, and detectors for repeats and for no progress. Find out why from the trace: classify the last steps (same call repeating, two calls alternating, searches finding nothing, a failing tool being retried, enormous results, a slow call). Then fix the cause, not only the limit: a better error message, a tool that returns less, an allowed "not found" answer, a plan with structure. Mention the trade-offs: limits set from the 95th percentile of good runs, guards tested for false stops (polling), and ending with a partial answer and a trace, not an exception.
Check yourself
An agent calls the same search with the same arguments three times, but the third result is different. Should the repeat guard fire? (No. A guard that compares only the call would stop a run that is making progress. Compare the call and the result, the "naive repeat" bug in section 4.)
For the curious Limits that stop everything
repeat_n=1 would stop every run after its first step, because one call is already a "repeat" of itself, and negative limits are meaningless. The limits object now refuses both with a clear message. It is a small case of a general rule: validate guard settings when they are created, not when they first trigger in production.
What you will use daily at work
| Practice | What it looks like |
|---|---|
| Four limits on every run | Steps, tokens or cost, wall-clock time, and a repeat check, with the reason logged. |
| A partial answer on stop | The user gets what was found and why the run ended, not an error. |
| Cancellation | A cancel button that stops the loop and any running tool. |
Draft: confirm how your framework exposes step, token and time limits.