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.
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.
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.
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.
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.
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.
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.
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, 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.
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.
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.
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.
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.
A referee: the match ends at the whistle, not when one team gets tired.
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?
Treat the end as part of the design, with four explicit exits, each checked by code whenever possible:
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.
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.
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
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))
max_tokens and handle the API’s stop reason.finish tool (or structured output) with the four kinds of signal.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”.
gave_up rate spike. Say when to give up: missing access, nonexistent data, denied permission.| 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 |
{ "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.
stop.reason attribute on every trace, with a dashboard per task type.| 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 |
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."} # 3What 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.
| 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 |
Answer the 6 questions to complete this lesson. Watching alone does not complete it: the check is what counts toward the trail and the certificate.
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.
In the real-world scenarios Coding agent, Browser agent for repetitive tasks, and Deep research with citations.