Pedro Sousa
‹ ← Articles

Article · Blog

The agent didn't freeze because it lacked capability. It froze because you never told it when to stop.

Most problems with agents in production don't come from the model being dumb. They come from the agent being smart enough to keep running when it should have stopped. Stop conditions are architecture, not an implementation detail.

The agent didn't freeze because it lacked capability. It froze because you never told it when to stop.

At a financial app I worked on, we built an agent to automate part of an internal reconciliation flow. The idea was simple: the agent received input data, queried APIs, validated inconsistencies, and generated a consolidated report.

It worked fine in tests. The first few days in production, too.

On the twelfth day, the agent went into a loop. It kept calling the same API seventeen times in a row because the response had a field with a slightly different format than expected. On each attempt, it reinterpreted the response, decided it still didn't have what it needed, and tried again. No timeout configured correctly. No clear exit condition. No signal whatsoever that this was a problem and not progress.

We burned credits, delayed the report, and spent two hours debugging an unstructured log.

The model wasn't wrong. It did exactly what it was trained to do: persist until it had the complete information.

The problem was that nobody had defined what "complete" means.

The illusion that the agent will know when to finish

There's a silent assumption in most agent implementations: that the model will figure out when the task is done.

Sometimes it does. When the context is simple and the goal is obvious, the stop condition emerges naturally from the task description. But in production, the simple cases aren't the problem. The problem is the cases that fall off the happy path.

An API returns 200 with an invalid payload. A tool returns an empty string. Data that should be there simply isn't. The model doesn't know whether that's an error, an intermediate state, or the final result. And when in doubt, it tries again.

I call this the Implicit Persistence Trap. You train the right behavior for the happy path, but you don't define the expected behavior for the uncertain path. The agent applies the same persistence logic from normal cases to anomalous ones. And it persists.

Stop conditions are not validation. They are architecture.

When I started working with agent orchestration more seriously, my instinct was to treat the stop condition as output validation. Check whether the final result was correct before wrapping up.

I was looking in the wrong place.

Stop conditions have three distinct dimensions, and each one needs to be handled separately:

Stop by success: the agent reached the goal. Seems obvious, but the goal needs to be defined in a way the model can verify on its own. "Generate the report" is ambiguous. "The report is complete when all schema fields are filled in and the status field is validated" is verifiable.

Stop by explicit failure: something went wrong in a recognizable way. API returned 500. Data not found after N attempts. Tool returned a typed error. These cases need explicit handling in the context, not just in the orchestration code. The model needs to know that this is a failure state, not an uncertainty state that justifies more attempts.

Stop by irresolvable uncertainty: the agent couldn't finish because the necessary information isn't available and won't be. This is the hardest case. And the most common one in production.

Most implementations handle the first dimension well. They completely ignore the third.

What changed when I started treating stop conditions as a contract

After the reconciliation incident, I changed how I structure agent context.

I started explicitly including what I call an exit contract: a section in the system prompt that defines, in plain language, the conditions under which the agent should terminate regardless of the task's state.

The structure ended up looking like this:

## Termination conditions

Terminate with SUCCESS status when:
- [verifiable condition 1]
- [verifiable condition 2]

Terminate with FAILED status when:
- Tool X returns an error on 2 consecutive attempts
- Field Y is absent after querying sources A and B
- The number of steps exceeds 15

Terminate with BLOCKED status when:
- The required information is not available in authorized sources
- There is irresolvable ambiguity that requires a human decision

It looks bureaucratic. In practice, it dramatically reduced loops and silent timeouts.

The model doesn't need to infer when to stop. It's in the contract. And when it reaches a state of uncertainty, instead of persisting indefinitely, it classifies the state and terminates with enough context for the next step, whether human or automated, to know what to do.

Step count as a signal, not just a limit

Another mistake I made for a while was using the step limit only as a safety mechanism. A ceiling to prevent infinite loops.

I started treating it as an architectural signal.

If an agent consistently gets close to the step limit, that's not a limit problem. It's a signal that the goal is poorly defined, the context is incomplete, or the available tools aren't sufficient for the task.

An agent that finishes in 3 steps when 8 are expected is also a signal. It may have skipped steps. It may have assumed data it never verified.

Monitoring the distribution of steps per execution is one of the most useful metrics I have today. More useful than success rate on its own.

The trade-off nobody mentions

Defining explicit stop conditions has a real cost: you increase the size of the system prompt, which burns tokens and can compress the space available for operational context.

On models with a smaller context window, that matters. You're trading flexibility for predictability.

I believe the trade-off is worth it in most production cases. Flexibility without predictability in production isn't a feature. It's a risk.

But there are cases where the goal is genuinely open-ended, where you want the agent to explore without rigid boundaries. In those cases, the exit contract needs to be lighter, focused only on explicit failures, and monitoring needs to be more rigorous.

There's no universal configuration. There's the right configuration for the behavior you want in production.

What I would do differently today

I wouldn't have treated the stop condition as an orchestration concern. I would have treated it as part of the task design, from the very beginning.

Before writing a single line of orchestration code, the first question should be: under what conditions should this agent stop, at any point in the execution?

If you can't answer that in a verifiable way, the agent isn't ready for production. It doesn't matter how well it performs on the happy path.


The model persists because you told it to be persistent. The problem is you never said until when.

And "until it's done" is not a stop condition. It's an intention. In production, intention isn't enough.