🧭 Bad agent behavior usually starts at the end
Most teams debug agents by looking first at prompts, model choice, or tool quality. That makes sense because those parts are visible and easy to change. But many bad production experiences are not really answer quality problems. They are termination problems. The agent keeps going after the task is already solved, or it stops after producing something that sounds complete even though nothing important was verified.
This matters because stopping is the point where product quality meets systems engineering. A user does not care whether your agent had elegant internal reasoning. They care whether it finished at the right time, with the right level of certainty, using a reasonable amount of time and money. If an order status agent calls lookup tools three extra times after it already found the package, the system feels sloppy. If it says an issue is resolved without ever confirming the order ID, it feels reckless.
The practical lesson is simple. Completion is not a vibe. In modern tool calling agents, the runtime should stop because it has evidence that the task is complete, blocked, unsafe, cancelled, or no longer worth continuing. Anthropic frames the basic agent loop as running until the task is completed or some stopping condition such as human review is reached. That framing is useful because it moves the question away from model phrasing and toward runtime control. You are not asking whether the model sounds done. You are asking whether the system has enough proof to end the run. See Anthropic’s architecture patterns for that perspective.
⚙️ Stopping is a runtime policy, not a prompt instruction
A common engineering mistake is to bury stop behavior inside the prompt with instructions like “finish when you have enough information” or “do not overuse tools.” That advice can help, but it is not a real control mechanism. It is guidance to the model, not a contract enforced by the system. If the model misreads the situation, or if a tool result is ambiguous, the prompt will not save you.
A stronger design is to encode stopping in code as a small policy stack. The model can propose actions. The runtime owns termination. In practice, that usually means checking ordered conditions such as success criteria met, external cancel or unsafe condition, waiting for user input or human approval, step budget exhausted, otherwise continue. This is not overengineering. It is the difference between a demo and a production workflow.
def stop_policy(state):
if state.success:
return "SUCCESS"
if state.cancelled or state.unsafe:
return "FAILURE"
if state.needs_user_input or state.needs_human_review:
return "SUSPENDED"
if state.step_count >= 8:
return "BUDGET_EXHAUSTED"
return "CONTINUE"
The code is simple on purpose, but each line defines an operational promise.
state.success
means the workflow has met a verified completion condition, not merely produced a polished answer. The next check for
cancelled
or
unsafe
matters because some runs should stop immediately even if more progress is possible. Then the runtime checks whether the agent is blocked on a human, which is different from failure. Finally it checks budget. If none of those conditions apply, the system continues.
Why this matters operationally is easy to miss until costs show up. Every extra turn adds latency, token usage, and often tool charges. Every premature stop creates fake completion, which is worse than a visible failure because it looks correct until a user notices the missing evidence. OpenAI’s agent guidance increasingly emphasizes tracing and evaluation because systems like these are not reliable when behavior is left implicit. The same principle applies here. If you cannot point to the code path that ended the run, you do not really control the agent. For a broader view of agent workflows, agent orchestration, and traceable control, see the OpenAI Agents SDK guide.
✅ Success should mean verified completion, not polished text
Premature stopping usually means success was never defined properly. Teams often say an agent should “solve the task,” but they do not break that into observable checks. So the model produces a plausible final message, the runtime sees no obvious remaining work, and the run ends. From the outside it looks fine. Under the hood, the system never proved anything.
A better pattern is to define task-specific evidence for completion. OpenAI’s evaluation guidance uses examples like order agents that must call the lookup tool, extract the correct order ID, and return required fields. That same logic should drive termination. An order status agent should not stop because it generated a clean explanation. It should stop because the required lookup succeeded and the required fields are present in state. The final prose is almost the least important part.
This shift matters because it changes what you optimize. Once success is defined as evidence, you can evaluate whether the agent stopped correctly across traces. You can ask concrete questions. Did it call the required tool? Did it ask for missing input instead of guessing? Did it stop immediately after the needed fields were retrieved? OpenAI’s evaluation best practices are useful here because they push developers toward task-specific grading rather than broad impressions.
In code, that often looks like a completion checklist attached to state. For example, an order support workflow might require four things before success can be set to true: the customer intent has been identified, the order ID has been validated, the lookup has completed successfully, and the response fields required by the UI are populated. Only when all four are true should the runtime allow the run to terminate as success.
The deeper reason this matters is long-term maintainability. If success is defined as a checklist, new developers can understand the contract quickly. Product teams can review it. QA can test it. If success is hidden inside model phrasing, every prompt edit risks changing the meaning of “done” without anyone noticing. That creates a fragile system where correctness drifts over time. In practice, this is a core part of LLM agent architecture and agent state management.
🧰 Tools should return control signals, not just raw text
One of the most useful design choices in tool-calling agents is to make tool outputs machine-readable enough for the runtime to route on them directly. If your tool returns a blob of text and asks the model to interpret whether the task is done, you are making termination depend on another round of language inference. That adds ambiguity exactly where you want precision.
Instead, let tools return typed signals. A lookup tool might return success, missing input, retryable error, fatal error, or escalation required. LangChain’s tools documentation notes that tools can update state, and that structured tool interaction can be paired with messages that confirm outcomes to the model. That is the foundation for stop logic that stays stable under pressure. See the LangChain tools docs for the relevant runtime patterns.
{
"status": "missing_order_id",
"terminal": false,
"ask_user": "Please share your order ID."
}This small shape does a lot of work. The
status
field tells the runtime what happened in a normalized way.
terminal
tells the orchestrator whether more automated work makes sense.
ask_user
gives the product layer a safe next message without requiring the model to invent one from scratch. The result is that the system can move directly into a suspended state instead of wasting another model turn to decide what the tool already knows.
Why this matters is subtle but important. The runtime does not need to ask, “Does this sound complete?” It can route from a contract. If
status
is
missing_order_id
, the next state is suspended pending user input. If
status
is
delivered
and all required fields exist, the next state is terminal success. This is both cheaper and safer because you avoid unnecessary reasoning turns after the tool already told you what happened.
It also makes failures easier to communicate. A tool result like
human_review_required
tells the product exactly what to say to the user. There is no vague “done” state hiding very different outcomes. That clarity becomes critical when several tools are involved and not all of them complete cleanly. Typed outputs are not just cleaner engineering. They reduce the number of places where language ambiguity can distort control flow.
🔁 Retry logic and stop logic are different systems
Endless loops often happen because teams mix up two questions that should be separate. Retry logic asks whether a failed action should be attempted again. Stop logic asks whether the task as a whole is still worth continuing. If you collapse those into one vague “keep trying” instinct, the agent drifts into retry storms, repeated searches, or circular handoffs.
Consider an AI coding agents workflow running tests after a patch. A failed test run does not automatically mean the whole task should stop. It may mean one more repair attempt is reasonable. But after several attempts, or after a fatal compiler error, the question changes. You are no longer choosing whether to rerun one step. You are deciding whether the entire workflow should terminate, suspend for human help, or report budget exhaustion.
This distinction matters because the costs are different. A retry policy is local. It protects against transient errors such as timeouts or flaky APIs. A stop policy is global. It protects your users and your budget from stubborn but unproductive persistence. LangGraph’s recursion limit exists for exactly this reason. Even a graph that looks sensible can cycle between nodes until the framework raises a limit error. The LangGraph recursion limit documentation is a good concrete reminder that loops are not theoretical.
A practical pattern is to have tools classify failures into categories like retryable error, fatal error, user input required, and human review required. Retry counters live with the action. Stop decisions live at the workflow level. That keeps your system from confusing “one more attempt might work” with “we should continue indefinitely.” In real systems, this separation is what prevents a flaky dependency from hijacking the entire agent loop.
🕸️ In graph runtimes, completion should be an explicit edge
If you use a graph runtime, stopping becomes easier to reason about because the workflow already has nodes, edges, and state transitions. But the same mistake still appears. Developers assume the model will naturally end the run when it has no more work. That may happen in simple cases, but production systems deserve something more explicit.
LangGraph’s basic tool-using pattern often relies on continuing while the model emits tool calls and returning the assistant response when no tool is selected. That is useful as a minimal loop, and the LangGraph graph API makes this style visible in code. The important lesson is not the helper itself. The lesson is that completion should map to a specific route, often a conditional edge to
END
. Once you see stop behavior as graph routing, you stop treating it like a side effect of text generation.
For nontrivial agents, that minimal pattern is not enough. Before routing to the end, you often need guards. Did required tools run? Did any tool return a blocked state? Is there unresolved ambiguity? Has the retry budget been consumed? These checks belong before the completion edge, not after a suspiciously polished final answer.
This matters for debugging as much as for execution. When the stop path is an explicit edge, traces tell a clearer story. You can inspect which node triggered termination, which state fields were true, and whether the route was success, suspended, or failed. That turns an agent run from a fuzzy conversation into a workflow you can reason about. For teams building complex loops, that is the difference between guessing and engineering. Explicit edges are also easier to test, because you can simulate state and verify the graph routes where you expect. This becomes even more important in multi-agent systems, including supervisor worker agents and agent handoffs, where one agent’s stop condition can become another agent’s start condition.
⏸️ Not every ending is success, and not every pause is failure
Many teams collapse all exits into one vague idea of “done.” That sounds harmless until you try to operate the system. A user waiting for approval, a cancelled background job, a fatal tool error, and a verified success should not all appear as the same terminal status. They have different product implications, different trace semantics, and different next steps.
A more operational taxonomy helps. Useful end states include completed successfully, blocked pending user input, blocked pending human approval, exhausted budget or step count, hard tool failure, unsafe to continue, and cancelled externally. This is not bureaucracy. It is how you keep traces interpretable and user messaging honest. If the agent is waiting for an order ID, that is not failure. If it hit a safety boundary, that is not “completed.”
This distinction becomes even more important in long-running agents. OpenAI’s background mode for the Responses API exposes asynchronous states like queued, in progress, completed, failed, and cancelled. In these systems, “finished” is not only a model decision. The runtime may end the job because a user cancelled it, because a timeout expired, or because work completed outside the interactive loop. See the background mode guide for the status model.
Why this matters is trust. If your agent says “done” when it really means “paused waiting for approval,” users lose confidence quickly because the language and the system state no longer match. Clear terminal and suspended states let you resume correctly later, inform the user honestly, and decide what metrics to track. A system that distinguishes pause from failure is easier to operate because each state implies a clear next action.
🧠 Memory affects stopping more than most teams expect
Stopping decisions depend on what the system remembers about the run. That makes AI agent memory and agent state management part of the termination problem, not a separate concern. If the agent forgets that a required tool already succeeded, it may call the tool again. If it loses track of missing inputs, it may stop too early with a guessed answer.
This is where short-term vs long-term memory in agents matters. Short-term memory tracks the live working state of the current task, such as step count, last tool result, validation flags, and pending approvals. Long-term memory stores reusable knowledge across runs, such as user preferences, prior resolutions, or procedural patterns. If you blur those together, stop logic becomes unreliable because the runtime cannot tell whether a condition belongs to the current workflow or to historical context.
For example, a support agent may know from long-term memory that a customer usually asks about shipping, but it still should not terminate an order-status task until the current order lookup succeeds. A coding agent may retain repair strategies over time, which starts to look like procedural memory for agents, but the stop condition for a single run still depends on the current tests, current patch, and current retry budget.
This is one reason frameworks emphasize explicit state. If your workflow stores completion evidence in structured memory rather than in a vague message history, the runtime can make better decisions. Developers using systems like LangGraph memory or Responses API agents benefit from this because durable state makes stop conditions auditable instead of implicit.
🧪 Stop quality should be evaluated on trajectories, not just outputs
If you only evaluate final answers, you will miss a large class of stopping failures. An agent can end with a correct-looking response but still take an unnecessarily expensive path. It can also stop with a superficially acceptable answer while skipping required tool use. In both cases, output-only evaluation tells you too little.
Trajectory evaluation is a better fit because stop behavior is about the path taken through the loop. LangSmith’s evaluation approaches and trajectory evals are especially relevant here. You can inspect whether the agent called required tools, whether it avoided irrelevant tools, and whether it stopped promptly after evidence was sufficient. Those questions map directly to stop correctness. See evaluation approaches and trajectory evaluations.
This matters because over-calling tools is often just a stopping failure wearing another costume. The task was solved, but the agent did not recognize it. Under-calling tools can be the mirror image. The final answer looks plausible, but the workflow stopped before the evidence-gathering steps occurred. Anthropic’s discussion of evals for agents is useful here because it stresses that without proactive evaluation, teams discover these failures reactively in production, which is expensive and demoralizing. Their article on demystifying evals for AI agents connects directly to this operational mindset.
A practical evaluation set might ask three separate questions. First, did the run satisfy the task contract before ending. Second, did it use a reasonable number of steps and tools to get there. Third, if it did not finish, did it stop for the correct reason. That last question is easy to overlook, but it matters because a clean suspension can be good behavior while a fake success is not. Good stop evaluation measures the path and the exit label together. In other words, agent evaluation and tracing should measure the route, not just the response.
📊 The traces you keep determine what you can fix
When a developer asks, “Why did it stop there?” the worst possible answer is “I am not sure.” Yet that is common because stop behavior is often under-instrumented. Teams may log the final answer and some tool outputs, but they do not record the specific reason the runtime ended the loop. Without that, regressions are nearly impossible to localize.
At minimum, traces should include the stop reason, step count, triggering node or decision point, last tool result, and whether success criteria were satisfied. That turns a fuzzy event into a debuggable one. If a support agent exits as success after only one turn but the success checklist is false, you know the bug is in routing or state mutation. If many runs end with budget exhausted after repeated retries on one tool, your retry policy is likely too permissive or your tool reliability is poor.
This matters because agent observability changes the kind of engineering conversation you can have. Instead of arguing about whether the model “felt uncertain,” you can look at traces and say that 18 percent of runs reached the step cap after receiving the same retryable error three times, or that the agent continued two extra tool calls after the required fields were already in state. OpenAI’s broader agent framing has pushed tracing to the center for exactly this reason. Systems like these are easier to improve when the loop is visible.
Once these fields exist, you can also trend them. Which stop reasons are increasing over time? Which tools most often precede failure? How often do runs suspend for user input versus fail outright? Those metrics are not observability vanity. They tell you whether your workflow is careful, wasteful, or brittle. Good traces turn stopping from a mystery into an engineering surface you can improve deliberately.
🧯 Production edge cases make stopping more subtle than it looks
Stopping sounds simple until a real system is interrupted in the middle of side effects. Suppose an agent starts a refund operation, then the run is paused, cancelled, or crashes before the tool result is fully recorded. The agent may appear ready to end or resume, but the world has already changed. In these cases, stop logic needs to account for in-flight work, idempotency keys, compensating actions, or resumable markers. Otherwise you end up with duplicate actions or logically inconsistent state.
Another edge case is protocol correctness. Sometimes what looks like a reasoning failure is really a malformed tool interaction. LangChain documents errors like invalid tool result alignment when tool calls and returned messages do not match properly. In practice, that can make the runtime reject continuation even though the model seems ready to proceed or finish. The lesson is important. Termination depends on protocol integrity, not only on model judgment.
Why this matters is that many “mysterious” loops or abrupt endings are really systems bugs at the boundary between tools and orchestration. If tool results are incomplete, the model may keep trying to recover. If the runtime cannot validate state, the run may fail before it reaches a legitimate stop condition. These are exactly the issues that disappear in toy demos and reappear in production when load, interrupts, and asynchronous work are involved.
The practical response is to validate tool call and result pairs before asking the model for the next step, and to treat interrupted side effects as first-class workflow states. A stopped run is only truly safe if the system knows what was already attempted and what remains unresolved. This is one of those areas where stopping logic overlaps with distributed systems thinking. The more your agent can change the outside world, the less you can treat “stop” as just the end of a conversation. For teams operating durable execution for agents, this is what separates resumable workflows from brittle ones.
🧠 A concrete pattern that holds up in production
If you want one practical rule to carry forward, it is this: the model should suggest, but the runtime should decide. That means explicit exit states, typed tool outcomes, separate retry policy, a hard step budget, and traces that record exactly why a run ended. None of these pieces are glamorous, but together they create the feeling users describe as reliability.
Take a simple order status agent. It receives a question about a package. If the order ID is missing, the lookup tool returns a structured result that asks the user for it, and the run suspends. If the order exists and required fields are present, the runtime marks success and stops. If the tool times out once, the retry policy allows another attempt. If it fails repeatedly, the global stop policy exits with budget exhausted or hard failure. At no point does the system rely only on whether the model sounds finished.
This same pattern scales to coding agents and research agents. A coding agent can stop when tests pass or when max repair attempts are reached. A research agent can stop when enough cited sources have been gathered or when new search iterations stop improving coverage. The structure stays the same even as the task changes. What changes is the evidence contract for completion.
That is why stopping conditions deserve more attention than they usually get. They look like a small implementation detail, but they define whether the agent feels disciplined or careless. In real systems, that feeling is not cosmetic. It is the visible result of good control flow. If the runtime makes the final decision from explicit state, your agent becomes easier to test, cheaper to run, and much less likely to fake confidence. That is the core of agent reliability in production OpenAI Agents SDK and similar runtimes.
🔚 The operational takeaway
Prompt-only stopping is a bad engineering habit. It asks a probabilistic model to own a deterministic systems concern. Sometimes it works, which is exactly why it is dangerous. It passes in demos, then quietly leaks cost, trust, and time when the workflow gets messy.
The better framing is straightforward. Stopping is a runtime contract. The contract says what success looks like, what blocked means, what failure means, when to suspend, and when to stop trying. Once that contract is explicit, you can trace it, evaluate it, and improve it. Until then, the end of the loop will keep feeling mysterious.
For AI agents used by developers, this is one of the most practical design choices you can make. Not because it sounds architectural, but because it changes user experience immediately. A well-stopped agent feels careful. A badly stopped one feels either lazy or out of control. That difference is rarely about the final sentence. It is about the policy that decided whether there should have been another step at all.
🔢 #5 of 12 | The Agent Loop








