Harsh Sinha
9 min read
views

Building AI Agents That Know When to Stop

A practical design for bounded agent loops: finish when the work is good enough, compact before context degrades, and stop safely when progress stalls.

Most agent demos celebrate motion: another tool call, another plan, another attempt. In production, motion is not the same as progress.

An agent that cannot stop will eventually spend too much, repeat itself, run out of useful context, or take an action it should have escalated. The reliable agent is not the one that works forever. It is the one that can tell the difference between unfinished, good enough, and unlikely to improve.

That makes stopping a product behavior—not an exception at the bottom of an orchestration file.

A reliable run earns the right to continue at every iteration.

“Stop” is more than one condition

There are at least four legitimate endings to an agent run:

  1. Success: the acceptance criteria are satisfied.
  2. Budget stop: the run has exhausted its time, token, cost, or iteration allowance.
  3. No-progress stop: repeated attempts are not materially improving the result.
  4. Safety or authority stop: the next action needs permission, carries too much risk, or belongs with a person.

These endings should not look identical to the user. Success should produce a finished answer. A budget stop should preserve partial work and explain what remains. A safety stop should request the exact approval or missing input. A stalled run should report the attempts made and avoid pretending that repetition is progress.

The OpenAI Agents SDK follows the same broad shape: a runner continues through model calls, tools, and handoffs until it receives a final output, while a maximum-turn limit acts as a separate safety bound. LangGraph’s recursion limit provides an equivalent guard against graphs that never reach a stop condition.

GateObservable questionTerminal outcome
SuccessDo the required checks pass?Complete
ProgressDid the latest loop materially improve the result?Continue or stop stalled
ContextCan the active working set support another reliable step?Continue, compact, or hand off
BudgetIs time, cost, token, or iteration capacity left?Continue or return partial
SafetyIs the next action safe and authorized?Continue or escalate
The evaluator should rely on observable evidence, not the model merely saying it feels confident.

Context can stop an agent before the task does

Yes—agents can stop or fail because of context.

The obvious failure is the hard one: the prompt, conversation, retrieved documents, and tool outputs grow beyond the model’s context window. But there is a softer failure before that. A long context may still fit while becoming harder to use well. The Lost in the Middle research found that model performance can depend on where relevant information appears in a long input; a larger window does not guarantee that every included fact will be used reliably (Liu et al., 2024).

This is why I treat context as a managed resource rather than a transcript that only grows.

A context gate can choose among four actions:

  • continue with the current working set;
  • discard bulky or obsolete tool output;
  • compact completed work into a durable summary;
  • hand off or restart from a clean state with the summary and required artifacts.

OpenAI describes compaction as a way to preserve key details while removing material that no longer needs to occupy the active window. Its description of the Codex agent loop likewise treats compaction as part of context management when a run approaches its token threshold. Anthropic’s guidance on context engineering makes the broader point: an agent loop continuously creates more data, so the active context must be selected and refined.

Context pressure is a routing decision, not automatically a failed run.

The practical mistake is a “context grab”: loading the entire content set because some of it might help. Retrieval should be just in time. Large tool results should be summarized or stored outside the prompt. Decisions, constraints, and artifact paths should survive compaction; raw exploratory chatter usually should not.

Loop engineering: a hard ceiling with an early exit

This is the pattern I used while working on MultiBagg AI: make learning iterative, but bounded.

Set a hard ceiling—say seven loops for a focused task or ten for a wider search. Inside that boundary, let the agent finish early as soon as the result is good enough. The ceiling is not the target number of loops. It is the most the system is allowed to spend before it must stop, return partial work, or escalate.

That distinction matters:

  • max_iterations = 7 prevents an unbounded run;
  • quality_score >= threshold permits an early successful finish;
  • no_progress_rounds >= 2 stops repetitive refinement;
  • a guardrail veto can force escalation at any point;
  • a context threshold can trigger compaction without consuming the entire reserve needed for the final answer.

Evaluator–optimizer loops work best when the evaluation criteria are clear and repeated refinement can demonstrably improve the output, according to Anthropic’s guide to building effective agents. Without those conditions, another loop may only rephrase the same answer.

The hard cap controls cost; the success gate controls when the work is complete.
ControlExamplePurpose
Hard iteration cap7 focused / 10 exploratoryPrevents an unbounded run
Early success gateChecks pass and quality >= 0.85Finishes before the cap
Stagnation gate2 no-progress roundsStops repetitive refinement
Context gateCompact before reserve is consumedProtects working quality and final synthesis
An illustrative bounded-loop policy. Thresholds should be calibrated for the task, not copied blindly.

A control loop you can implement

The controller—not the prose prompt—should own the hard limits. The evaluator returns evidence and a decision proposal; deterministic code enforces budgets and guardrails.

MAX_ITERATIONS = 7
MAX_NO_PROGRESS = 2
QUALITY_THRESHOLD = 0.85

def run_agent(goal, initial_context):
    state = create_state(goal, initial_context)

    for iteration in range(1, MAX_ITERATIONS + 1):
        state.iteration = iteration
        observation = agent_step(state.working_context)
        state.record(observation)

        evaluation = evaluate(
            goal=goal,
            result=state.current_result,
            acceptance_checks=state.acceptance_checks,
            previous_result=state.previous_result,
        )

        if evaluation.guardrail_violation:
            return escalate(state, reason="safety_or_authority")

        if evaluation.requirements_pass and evaluation.quality >= QUALITY_THRESHOLD:
            return deliver(state, status="complete")

        state.no_progress_rounds = (
            state.no_progress_rounds + 1
            if not evaluation.material_progress
            else 0
        )

        if state.no_progress_rounds >= MAX_NO_PROGRESS:
            return handoff(state, reason="no_material_progress")

        if state.context_pressure_high:
            state.working_context = compact_and_retrieve(state)

    return handoff(state, reason="iteration_budget_exhausted")

The order is deliberate. A safety violation cannot be overruled by a high quality score. Success is checked before the iteration ceiling turns the best available answer into a partial handoff. Context maintenance happens while there is still enough room to summarize accurately. The final iteration never silently becomes an eighth.

What belongs in the evaluator

The evaluator should answer narrow questions that map to the product’s promise:

  • Did every required deliverable get produced?
  • Which claims or changes were independently verified?
  • Did the latest loop materially improve the result?
  • What known defects remain?
  • Is the next action reversible and authorized?
  • Can the active context support another reliable step?

For code, “good enough” might mean tests pass, lint passes, the requested behavior exists, and no critical review findings remain. For research, it might mean every important claim has an authoritative source and contradictory evidence is represented. For a writing agent, it might mean the brief is satisfied, factual claims are sourced, and a final editing pass finds no blocking issue.

FieldWhy keep it
terminal_statusSeparates success, budget, safety, and stalled endings
iterations_usedShows whether early exit actually works
quality_by_iterationReveals diminishing returns
context_compactionsMeasures context pressure and recovery
no_progress_roundsExposes loops that are active but not improving
If these fields are logged per run, loop behavior can be tuned from evidence rather than intuition.

Do not collapse all of this into one opaque confidence number. A result can read confidently while failing a required check. Keep the score, checklist, progress delta, budget state, and guardrail decision visible as separate signals.

Common failure modes

The loop always uses its full budget

If most successful runs take exactly seven iterations, the success gate is probably vague or unreachable. Inspect the evaluator and the acceptance criteria before increasing the cap.

Compaction happens only after an error

By then, the model may not have enough clean context left to produce a faithful summary. Reserve room for context maintenance and the final response.

The evaluator rewards activity

“Called another tool” is not progress. Measure a changed artifact, a newly satisfied requirement, reduced uncertainty, or a passed check.

The final answer hides the stop reason

A user should be able to tell whether the agent completed the goal, hit a budget, stalled, or needs authority. Operationally, these are different states and should be logged separately.

A practical stopping contract

Before shipping an agent, define this contract:

  1. Write the acceptance checks that make a run complete.
  2. Choose the hard loop, token, cost, and time ceilings.
  3. Define “material progress” and how many stagnant rounds are tolerated.
  4. Decide what must survive compaction: constraints, decisions, open work, and artifact references.
  5. Reserve context for the final synthesis or handoff.
  6. Specify actions that always require human approval.
  7. Return an explicit terminal status: complete, partial, blocked, budget_exhausted, or needs_approval.

The best stopping system is not the one that ends runs as quickly as possible. It is the one that spends another iteration only when that iteration has a reasonable chance of improving the outcome.

That is the heart of loop engineering: finish early when the evidence says the work is good enough, preserve state before context becomes unreliable, and never let persistence turn into an infinite loop.

Further reading