Trail content
9 minEnglish narration, Portuguese subtitlesPodcast (RSS)
Chapters
  1. 0:00Opening
  2. 0:59The idea
  3. 2:02How it works
  4. 2:38A real case
  5. 4:00A challenge
  6. 4:42Choosing the limits
  7. 5:24Common mistakes
  8. 6:18In production
  9. 7:34The code
  10. 8:17Wrap-up
Transcript

Opening

Larissa put a research agent into production a few weeks ago. It takes a market question, searches the web, reads sources, and writes a report. The first weeks went fine. Then three kinds of complaints started.

Some tasks never ended. The agent kept looking for one more source, until the budget ran out.

Others ended too soon. After two searches, the agent wrote a confident report with shallow information.

And some ended with: I couldn't finish. Without saying what it had found, or why it stopped.

All three share one root. Done was defined in one sentence of the prompt. When you have enough information, write the report. The model decided alone what enough meant, and there was no other way to end. In this lesson, you'll learn to design the end of an agent, so every run finishes, and finishes in the right state.

The idea

Treat the end as part of the design. With four explicit exits, each one checked by code whenever possible.

Success. The goal was met, and there's a verifiable criterion for it. The test passes. The report has the required number of valid sources.

Limit. The step, time or cost budget ran out. The agent delivers what it has, and says it stopped at the limit.

Giving up. The agent concluded it can't do the task, for lack of access or information. And it explains why.

And asking for help. The agent needs a decision, or a fact, that only a person has.

The model can signal success, giving up, or asking for help, through a tool of its own, or through structured output. Your code confirms success when it can. And the limit always belongs to your code. A referee ends the match at the whistle, not when one team gets tired.

How it works

Each turn of the loop ends with a decision from the model.

But code checks the budget before anything else. With no budget left, the run stops with its current state, and the reason.

The model signals one of four intents, in a structured way.

And done isn't enough. Code checks the success criterion, when it's verifiable.

Confirmed success ends the task.

Giving up is a valid exit, with an explanation of what was tried.

And asking for help pauses the run, until a person answers.

A real case

Back to Larissa's agent. The success criterion became a rule in code. The report must cite at least three independent sources, and every link must open. The budget is twenty-five steps.

At step six, the model says: report ready. The check finds two valid sources out of three. Rejected. And the model gets the reason back. One independent source is missing.

So it keeps going. At step fourteen, it says done again. Four valid sources, all links open. Accepted. Stop reason: success.

That first done was exactly the shallow report from the complaints. The check returned the reason, and the model kept working. Without it, the task would have stopped at step six, one source short.

Now a question with no public answer, like a private company's profit margin. There are two good exits.

At step nine, the agent gives up. No public source gives the margin. I only found third party estimates, listed below.

Or, if it keeps trying, code stops it at step twenty-five, and delivers a summary of what it found.

The bad exit, the one this pattern prevents, is a confident report with a made up number.

A challenge

Here's one for you. Your coding agent calls finish, with done, and the note: fixed the bug. The tests are still red. What should the loop do?

Reject the success, tell the model which tests fail, and keep looping. The model's declaration is a request to end, not proof. When there's a verifiable criterion, code decides.

And it's not a give-up either. The model didn't give up. It thought it was done. Ending here would waste a task that might be two steps from passing. And the budget still applies, so the loop can't get stuck on this forever.

Choosing the limits

Don't guess the ceiling. Look at the traces of runs that ended in success, and see how many steps, seconds and dollars they used. Then put the ceiling above the vast majority of them, with room to spare.

Say ninety-five percent of successful runs use up to fourteen steps, and the longest uses nineteen. A ceiling of twenty-five leaves room for the hard case, and cuts a stuck loop early. A ceiling of two hundred, just in case, lets a stuck run spend ten times more before it stops.

And revisit the limits when the task type changes. A ceiling that's right for fix one test is too short for migrate a module.

Common mistakes

Now the common mistakes. A limit that throws an exception. The task vanishes, with no state, no record and no answer. The limit is an exit like the others. It delivers what was done, and says it stopped there.

Success means the model said it was done. If there's a test, a schema or a count, use it. The model's opinion only decides when there's nothing to check.

A prompt that pushes toward giving up. Phrases like when in doubt, give up, make the give-up rate spike. Say exactly when to give up. Missing access, data that doesn't exist, permission denied.

And asking for help with nobody on the other end. If the person takes hours, the run hangs. Set a waiting deadline, and decide what happens when it expires.

In production

In production, record a stop reason on every trace, and look at the distribution per task type. Success, limit, give-up and help.

And watch the limit stops closely. Say in the browser agent, limit stops go from three percent to twenty-five percent of runs, in one week. Nobody deployed anything. Doubling the budget only lets stuck runs spend more. Switching models is the most expensive and least likely guess. Open the traces. A jump like that, with no code change, usually comes from the outside world. A portal changed, or a tool started failing silently. That's exactly why the stop reason goes on the trace.

And build at least one eval case per exit. An easy task expects success. A task that's impossible on purpose, like a portal that's offline, expects a give-up. A task that depends on a business decision expects a help request. And a tool that answers okay with no progress expects a limit stop. If the agent answers the impossible task with success, you found the most dangerous case of all. A false success.

The code

On the page, every version gives the model a finish tool, with three kinds, done, give up and need help, and runs the loop under a step and time budget.

When the signal is done, code runs the tests. If they pass, success. If not, the model gets told, and the loop goes on.

And when the budget runs out, it returns a limit stop, with a summary.

In Python and TypeScript, the signal is read inside the loop. In LangGraph, the same signal is read on an edge. Limit, success, give-up and human become destinations in the graph. Not sentences in a prompt.

Wrap-up

So here's the idea to take with you. An agent needs a designed ending, not a hopeful one. Success that code can confirm, a limit that code owns, a give-up that explains, and a way to ask a person. Then every run ends in a known state, and the user knows what they got, and why.

In the next lesson, we switch from how an agent ends to what it holds in its hands: tool sprawl. See you there.

PatternAgent behavior

Stop Conditions

Condições de Parada

Stop Conditions is an AI design pattern for LLM apps and agents, in the Agent behavior group. Define clearly when the agent is done, has given up, or needs help.

Element
51/70
Level
Intermediate
Read
8 min
Updated
On this page 17
  1. 01Intent
  2. 02Problem
  3. 03Solution
  4. 04Structure
  5. 05Pseudocode
  6. 06Real-world scenarios
  7. 07Applicability
  8. 08When not to use
  9. 09How to implement
  10. 10Variations
  11. 11Cost, latency, and failure modes
  12. 12How to evaluate
  13. 13In production
  14. 14Pros and cons
  15. 15Code
  16. 16Framework support
  17. 17Check your understanding
  18. ··Quick answers

Intent

Define, outside the model, when an agent is done, when it has given up, and when it needs help. It is for every agent that runs in a loop and must always end in a known state.

In one sentence
A referee: the match ends at the whistle, not when one team gets tired.

Problem

Larissa shipped a research agent. It takes a market question, searches the web, reads sources, and writes a report. The first weeks went fine. Then three kinds of complaints started.

Some tasks never ended: the agent kept looking for “one more source” until the budget ran out. Others ended too soon: after two searches, the agent wrote a confident report with shallow information. And some ended with “I could not finish”, without saying what it had found or why it stopped.

All three share one root. “Done” was defined in one prompt sentence: “when you have enough information, write the report”. The model decided alone what enough meant, and there was no other way to end.

The question this pattern answers: how do you make sure every agent loop ends, and ends in the right state?

Solution

Treat the end as part of the design, with four explicit exits, each checked by code whenever possible:

  • Success: the goal was met, and there is a verifiable criterion for it (the test passes, the report has N valid sources).
  • Limit: the step, time, or cost budget ran out. The agent delivers what it has and says it stopped at the limit.
  • Giving up: the agent concluded it cannot do the task, for lack of access or information. It explains why.
  • Asking for help: the agent needs a decision or a fact only a person has.

The model can signal success, giving up, or asking for help through its own tool or through structured output. Code confirms success when it can, and the limit always belongs to code.

This is not an exotic idea. In “Building effective agents” (December 2024), Anthropic recommends that agents include stopping conditions, such as a maximum number of iterations, to keep control. This pattern takes the advice further: beyond the ceiling, each way of ending gets a name and a handler.

Worked example

Go back to Larissa’s agent. The success criterion became a rule in code: the report must cite at least three independent sources, and every link must open. The budget is 25 steps. A trace with all four exits working:

step 6   finish(kind="done", note="Report ready")
         verify: 2 valid sources of 3 required          -> rejected
         tool_result: "Criterion not met: 1 independent source missing."
step 7   web_search("instant payments market share 2026")
...
step 14  finish(kind="done", note="Report ready, 4 sources")
         verify: 4 valid sources, all links open        -> accepted
stop.reason = success

The model’s first “done” was the same shallow report from the complaints. The check returned the reason, and the model kept going. Without it, the task would have stopped at step 6, one source short.

Now a question with no public answer, such as a private company’s profit margin. Two exits are good: at step 9 the agent calls finish(kind="give_up", note="No public source gives the margin; I only found third-party estimates, listed below"). Or, if it keeps trying, code stops it at step 25 and delivers a summary of what it found, with stop.reason = limit. The bad exit, which the pattern prevents, is a confident report with a made-up number.

Structure

flowchart TD
  L(["1. Loop turn"]):::llm --> B{"2. Budget ok?"}
  B -->|no| LIM(["3. Stop at limit"]):::io
  B -->|yes| S{"4. Model signal"}
  S -->|continue| L
  S -->|done| V["5. Verify criterion"]:::tool
  V -->|passed| OK(["6. Success"]):::io
  V -->|failed| L
  S -->|cannot do it| G(["7. Explained give-up"]):::io
  S -->|need help| H((("8. Human"))):::human
  1. Each loop turn ends with a decision from the model.
  2. Code checks the budget before anything else.
  3. With no budget left, the run stops with the current state and the reason.
  4. The model signals one of four intents, in a structured way.
  5. “Done” is not enough: code checks the success criterion when it is verifiable.
  6. Confirmed success ends the task.
  7. Giving up is a valid exit, with an explanation of what was tried.
  8. Asking for help pauses the run until a person answers.
Check +20 XP on first tryYour coding agent calls finish with kind=done and the note ‘fixed the bug’. The tests are still red. What should the loop do?

Pseudocode

schema Signal { kind: "continue" | "done" | "give_up" | "need_help", note: string }

function run(task):
    while true:
        if not budget.check(steps=25, seconds=600, dollars=2.00):      # 2
            return finish("limit", summarize(state))                    # 3

        reply = llm_with_tools(SYSTEM, TOOLS + [signal_tool], state)    # 1 and 4
        signal = reply.signal or Signal("continue")
        trace.span("signal", signal)

        if signal.kind == "done":
            if verify(task.success_criteria, state):                    # 5
                return finish("success", reply.text)                    # 6
            state.add("The criterion is not met yet: " + failed_checks())
        elif signal.kind == "give_up":
            return finish("gave_up", signal.note)                       # 7
        elif signal.kind == "need_help":
            answer = human.ask(signal.note)                              # 8
            state.add(answer)
        else:
            state.add(execute(reply.tool_calls))

Real-world scenarios

  • Coding agent. Success has a verifiable criterion: tests pass and the linter is quiet. The agent cannot declare “done” with a red test.
  • Browser agent for repetitive tasks. The portal may be down or show a captcha. Giving up and asking for help prevent hours of useless clicks.
  • Deep research with citations. “Enough” becomes a rule: at least N independent sources per claim, or the report goes out marked incomplete.

Applicability

  • Any agent that runs in a loop without a person watching every step.
  • Tasks with a success criterion code can verify: tests, schema, counts, queries.
  • Tasks where access, data, or permission may be missing, and giving up early beats trying forever.
  • Flows where a business decision can come up midway.

When not to use

  • A single model call with no loop. Use max_tokens and handle the API’s stop reason.
  • A fixed chain, where code already defines every step and the end. There, the end is the last step.
  • A chat where the user controls every turn. The user is the stop condition.

How to implement

  1. Write the success criterion for each task type as one verifiable sentence.
  2. Turn whatever you can into code: a test command, schema validation, a source count.
  3. Give the model a finish tool (or structured output) with the four kinds of signal.
  4. Implement the budget as in Add Loop Budget.
  5. Write the wrap-up for each exit: what the user receives on limit, give-up, or wait.
  6. Record the stop type on every run’s trace.
  7. Build eval cases for each exit, including tasks that are impossible on purpose.

How to choose the limits

Do not guess the ceiling. Look at the traces of runs that ended in success and see how many steps, seconds, and dollars they used. Put the ceiling above the vast majority of them, with room to spare, so the limit catches stuck runs and not hard tasks.

Suppose 95% of successful runs use up to 14 steps and the longest uses 19. A ceiling of 25 leaves room for the hard case and cuts a stuck loop early. A ceiling of 200 “just in case” lets a stuck run spend ten times more before it stops.

Revisit the limits when the task type changes. A ceiling that is right for “fix one test” is too short for “migrate a module”.

Common mistakes

  • A limit that throws an exception. The task vanishes with no state, no record, and no answer. The limit is an exit like the others: it delivers what was done and says it stopped there.
  • Success means “the model said it was done”. If there is a test, a schema, or a count, use it. The model’s opinion decides only when there is nothing to check.
  • A prompt that pushes toward giving up. Phrases like “when in doubt, give up” make the gave_up rate spike. Say when to give up: missing access, nonexistent data, denied permission.
  • Asking for help with nobody on the other end. If the person takes hours, the run hangs. Set a wait deadline and what happens when it expires.

Variations

  • Criterion by external check. Instead of the model saying “done”, code checks the state every turn (the file exists, the test passed) and ends on its own.
  • Stagnation detection. With no measurable progress in N turns (same files, same tool, same result), code ends the run as a give-up.
  • Stop by evaluator. A second model judges whether the result meets the criterion. Useful when the criterion is qualitative; it is the loop in Evaluator-Optimizer.

Cost, latency, and failure modes

Aspect What happens How it shows up in the trace
Calls None extra on the normal path; one for the wrap-up at the limit wrap_up span only when there is a limit
Latency Success checks add their own time verify span after each done
Failure: false success The model declares “done” and the check is weak task with stop=success failing the eval
Failure: too many give-ups Prompt or tools make giving up too easy gave_up rate rising
Failure: too many help requests The agent asks what it could find out alone need_help rate above target
Check +20 XP on first tryIn the browser agent, limit stops go from 3% to 25% of runs in one week. Nobody deployed anything. What is the next step?

How to evaluate

  • Stop distribution: share of success, limit, give-up, and help per task type.
  • True success: of the tasks marked as success, how many pass the eval’s independent check.
  • Correct give-up: on tasks that are impossible on purpose, the agent gives up instead of making things up.
{ "task": "Download the September invoice from portal X", "fixture": "portal_offline", "expected_stop": "gave_up" }

Build at least one case per exit. An easy task expects success. A task that is impossible on purpose (portal down, a file that does not exist) expects gave_up. A task that depends on a business decision expects need_help. And a fixture that makes the tool answer “ok” with no progress expects limit. If the agent answers the impossible task with success, you found the most dangerous case: false success.

In production

  • A stop.reason attribute on every trace, with a dashboard per task type.
  • An alert when the share of limit stops rises. It is usually a broken tool or a change in the outside world.
  • A review queue for give-ups, which become eval cases or tool fixes.
  • Time waiting for human help as its own metric.

Pros and cons

Pros Cons
Every run ends in a known state Needs a written success criterion per task type
Success stops being the model’s opinion Checks take time
Give-ups become useful data More exit paths to test
The user knows what they got and why Asking for help needs someone to answer

Code

The tabs use a finish tool with the signals and a criterion verified by code. Python and TypeScript read the signal inside the while. LangGraph reads the same signal on an edge: limit, success, give-up, or human are graph destinations, not sentences in the prompt.

Python. Use this when you want the four exits explicit in a hand-written loop.

import subprocess
import time

import anthropic

client = anthropic.Anthropic()

FINISH = {
    "name": "finish",
    "description": "Call when you are done, giving up, or need help. Never answer without calling this tool.",
    "strict": True,
    "input_schema": {
        "type": "object",
        "properties": {
            "kind": {"type": "string", "enum": ["done", "give_up", "need_help"]},
            "note": {"type": "string", "description": "Result, reason for giving up, or question for the person"},
        },
        "required": ["kind", "note"],
        "additionalProperties": False,
    },
}


def tests_pass() -> bool:  # 5. verifiable criterion
    return subprocess.run(["pytest", "-q"], capture_output=True).returncode == 0


def run(task: str, tools: list, execute, ask_human, max_steps=25, max_seconds=600) -> dict:
    messages, start = [{"role": "user", "content": task}], time.monotonic()
    for _ in range(max_steps):
        if time.monotonic() - start > max_seconds:
            break
        reply = client.messages.create(
            model="claude-opus-5-5", max_tokens=8000, tools=tools + [FINISH], messages=messages,
        )
        messages.append({"role": "assistant", "content": reply.content})
        results = []
        for block in (b for b in reply.content if b.type == "tool_use"):
            if block.name != "finish":
                results.append({"type": "tool_result", "tool_use_id": block.id, "content": execute(block)})
                continue
            kind, note = block.input["kind"], block.input["note"]
            if kind == "done" and tests_pass():
                return {"stop": "success", "result": note}               # 6
            if kind == "give_up":
                return {"stop": "gave_up", "result": note}               # 7
            content = ask_human(note) if kind == "need_help" else "Tests still fail. Keep going."
            results.append({"type": "tool_result", "tool_use_id": block.id, "content": content})
        messages.append({"role": "user", "content": results or "Use a tool or call finish."})
    return {"stop": "limit", "result": "Stopped at the limit. Last state is in the trace."}  # 3

What you gained: every exit is a testable code path. What you lost: more code than a plain while, and the limit wrap-up is still a fixed message.

TypeScript. Use this when the agent runs on Node and you want the finish tool typed.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const FINISH: Anthropic.Tool = {
  name: "finish",
  description: "Call when you are done, giving up, or need help. Never answer without calling this tool.",
  strict: true,
  input_schema: {
    type: "object",
    properties: {
      kind: { type: "string", enum: ["done", "give_up", "need_help"] },
      note: { type: "string" },
    },
    required: ["kind", "note"],
    additionalProperties: false,
  },
};

type Stop = { stop: "success" | "gave_up" | "limit"; result: string };

export async function run(
  task: string,
  tools: Anthropic.Tool[],
  execute: (b: Anthropic.ToolUseBlock) => Promise<string>,
  verify: () => Promise<boolean>,
  askHuman: (q: string) => Promise<string>,
  maxSteps = 25,
): Promise<Stop> {
  const messages: Anthropic.MessageParam[] = [{ role: "user", content: task }];
  for (let step = 0; step < maxSteps; step++) {
    const reply = await client.messages.create({
      model: "claude-opus-5-5", max_tokens: 8000, tools: [...tools, FINISH], messages,
    });
    messages.push({ role: "assistant", content: reply.content });
    const results: Anthropic.ToolResultBlockParam[] = [];
    for (const b of reply.content) {
      if (b.type !== "tool_use") continue;
      if (b.name !== "finish") {
        results.push({ type: "tool_result", tool_use_id: b.id, content: await execute(b) });
        continue;
      }
      const { kind, note } = b.input as { kind: string; note: string };
      if (kind === "done" && (await verify())) return { stop: "success", result: note };
      if (kind === "give_up") return { stop: "gave_up", result: note };
      const content = kind === "need_help" ? await askHuman(note) : "The criterion still fails. Keep going.";
      results.push({ type: "tool_result", tool_use_id: b.id, content });
    }
    messages.push({ role: "user", content: results.length ? results : "Use a tool or call finish." });
  }
  return { stop: "limit", result: "Stopped at the limit. Last state is in the trace." };
}

What you gained: the Stop type forces callers to handle every exit. What you lost: same as Python, plus verification injected as a parameter.

Python + LangGraph. Use this when every loop exit is a graph destination.

from typing import Literal

from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict

class State(TypedDict):
    steps: int
    max_steps: int
    signal: str  # continue | done | blocked | help
    verified: bool
    output: str

def turn(state: State) -> dict:
    # 1. one turn; the model returns the signal as a schema
    signal = ask_model(state)  # your call, structured output
    return {"signal": signal, "steps": state["steps"] + 1}

def route(state: State) -> Literal["turn", "limit", "success", "give_up", "human"]:
    if state["steps"] >= state["max_steps"]:
        return "limit"  # 2 and 3
    if state["signal"] == "continue":
        return "turn"
    if state["signal"] == "done":
        return "success" if state["verified"] else "turn"  # 5
    if state["signal"] == "blocked":
        return "give_up"  # 7
    return "human"  # 8

graph = StateGraph(State)
graph.add_node("turn", turn)
graph.add_node("limit", lambda s: {"output": "stopped at the cap"})
graph.add_node("success", lambda s: {"output": "ok"})
graph.add_node("give_up", lambda s: {"output": "could not finish"})
graph.add_node("human", lambda s: {"output": "waiting for a person"})
graph.add_edge(START, "turn")
graph.add_conditional_edges("turn", route, ["turn", "limit", "success", "give_up", "human"])
for name in ("limit", "success", "give_up", "human"):
    graph.add_edge(name, END)
app = graph.compile()

LangGraph’s recursion_limit cuts the whole graph. It does not replace these edges. The loop on the other stacks is on ReAct Loop.

What you gained: each stop is a named node on the trace. What you lost: the criterion check (verified) stays a function of yours, called before you accept done.

Framework support

Stack Where the stop lives
Python, TypeScript The if after finish
Python + LangGraph The edge with five destinations
Python + LangChain, OpenAI Agents, Pydantic AI, AI SDK Only the step cap. You write the other stops

Check your understanding

Check +20 XP on first tryA colleague proposes giving the model an out_of_budget tool, so it can report on its own when the budget runs out. What do you answer?

Check +20 XP on first tryBefore going to production, how do you find out whether the agent makes up a result when the task is impossible?

Check +20 XP on first tryThe agent asks for help in 40% of tasks, with questions like ‘what is the date format?’, which is written on the very page it reads. What do you adjust?

Check +20 XP on first tryYour product is a chat where the user sends one message at a time. On some turns, the model calls tools in a loop before answering. Where do stop conditions go?

Quick answers

What is Stop Conditions?

Stop Conditions is an AI design pattern for LLM apps and agents, in the Agent behavior group. Define clearly when the agent is done, has given up, or needs help.

Which smells does the Stop Conditions pattern help cure?

Unbounded Loop, Subagent Recursion, and No Cost Budget.

Where does Stop Conditions show up in practice?

In the real-world scenarios Coding agent, Browser agent for repetitive tasks, and Deep research with citations.

Go further

Relations

Refactorings that lead here 1
Part of the tracks 1
Search