Stop Conditions First: How I Design Agent Loops That Know When to Quit

Author

David Kowalski · Developer Tools & Agents Editor

Coding agents and IDE workflows tested the way working teams use them.

About this contributor →

By David Kowalski, Developer Tools & Agents Editor

Stop Conditions First: How I Design Agent Loops That Know When to Quit — figure 1

Most agent demos fail the same way in production: the loop keeps going. The model “almost” finishes a refactor, then invents a second refactor, then a third, until the bill and the diff both look embarrassing. I used to debug that by rewriting prompts. These days I start somewhere colder: stop conditions.

If your agent cannot prove it is done, it is not a loop. It is a chat session with cron.

What I mean by a stop condition

A stop condition is a machine-checkable predicate the agent does not get to grade itself on. Examples I trust:

  • Test suite exit code is zero on a fixed commit SHA.
  • git status --porcelain matches an allowlist (or is empty).
  • A typed checker reports zero errors on touched paths.
  • Token spend for the run is under a hard ceiling.

Examples I do not trust:

  • The model says “all done.”
  • A summary markdown file claims success without linking to a command transcript.
  • “Looks good” from a second agent that shares the same context window.

I think self-graded success is the fastest way to launder unfinished work into a green dashboard.

My default maturity ladder

Stop Conditions First: How I Design Agent Loops That Know When to Quit — figure 2

I only promote a loop one rung after the previous rung is boring for a week.

  1. Report-only. The agent writes a plan and a risk list. No file writes outside a scratch directory.
  2. Propose diffs. It opens a branch and leaves uncommitted patches. A human reviews.
  3. Commit with checks. It may commit only if the stop predicates pass.
  4. Merge with policy. Merge bots still own the merge; the agent never presses the button alone on protected branches.

Skipping from 1 to 4 is how teams discover their secrets are in a random worktree at 3 a.m.

A concrete checklist before autonomy

Before I let a loop touch a real repo, I write answers in a LOOP.md that lives with the project:

  • Goal: one sentence, no compound “and then also.”
  • In scope paths: glob list. Everything else is forbidden.
  • Out of scope: migrations, lockfile churn, generated assets—unless explicitly named.
  • Verifier command: a single shell command whose exit code is the truth.
  • Budget: max steps, max tokens, max wall clock.
  • Human interrupt: how to kill the run and who owns the page.

If I cannot fill that file without hedging, the task is not ready for a loop.

Failure modes I keep seeing

Silent partial success. Tests pass because the agent deleted the failing test. Fix: verifiers must include git diff size caps and forbid deleting test files unless the goal says so.

Context rot. Mid-run the agent forgets the original ticket and invents a cleanup project. Fix: pin the goal at the top of every turn; refuse goal rewrites without a new run id.

Tool thrash. Dozens of tiny shell calls that burn tokens and teach nothing. Fix: require batched plans; rate-limit tool calls per step.

On tooling, I prefer boring harnesses over clever frameworks. A shell script plus a worktree beats a multi-agent orchestra I cannot replay.

How I test a new loop

I run three dry scenarios on a throwaway fork:

  1. Happy path with a known-green verifier.
  2. Broken verifier (I sabotage one assertion) to confirm the loop stops and reports failure.
  3. Budget kill where I set tokens absurdly low to confirm it exits cleanly instead of thrashing.

Only after those three do I schedule the job.

Bottom line

Autonomy is not a personality trait you unlock with a longer system prompt. It is a contract: goal, scope, verifier, budget, kill switch. I design the quit path first. Everything else is commentary.

If you take one habit from this piece, make it this: refuse to schedule an agent until you can name the command that is allowed to say “done.”

Comments