Lesson 5 of 20Track 1

The orchestration loop

Loop control: exit conditions and convergence

Max iterations, goal satisfaction, and convergence detection.

Video lesson Interactive exercise ~10 min

Video coming soon

When does the loop stop?

A loop without an exit condition is a bug. Agents are loops, so the question of "when do we stop iterating" is one of the most important things you'll design. Get it wrong and you either:

  • Cut the agent off mid-thought and ship a half-finished answer, or
  • Let it spin forever, burning tokens and wallclock time

There are three exit conditions every production agent needs. We'll build each one in code.

Exit 1: goal satisfaction

The most natural stopping point is when the model decides it's done. It signals this by returning a message with no tool calls.

if not message.tool_calls:
    return message.content  # the model is done

This is the happy path. The model has gathered enough information, written its answer, and wants to hand back control. Most well-designed agents exit this way the majority of the time.

The risk is that the model can be wrong about being done. It might hallucinate a confident answer with incomplete information. Goal satisfaction alone is not sufficient.

Exit 2: max iterations

Every agent loop needs a hard cap on how many turns it can take. This is the seatbelt that prevents runaway costs.

for step in range(max_iterations):
    response = ollama.chat(...)
    if not response.message.tool_calls:
        return response.message.content
    # ... execute tools ...
 
return {"error": "max_iterations_exceeded"}

Pick max_iterations based on the task. Simple Q&A: 5 to 10. Multi-step research: 20 to 30. Long-running coding agents: 50 to 100. If you're tempted to go higher, that's usually a sign the loop needs to be split into a hierarchy (we cover that in Track 2).

Max iterations is not a fix

A high iteration limit hides bugs. If your agent regularly hits max_iterations, something else is wrong: the goal is too vague, the tools are too granular, or the model is stuck repeating itself. Treat max_iterations as a circuit breaker, not a feature.

Exit 3: convergence detection

This is the one most beginners miss. Even with goal satisfaction and max iterations, an agent can fail in a third way: it gets stuck in a loop, doing the same useless thing over and over.

Step 5: search("authentication code")
Step 6: search("auth code")
Step 7: search("login implementation")
Step 8: search("authentication code")  # repeating itself
Step 9: search("auth code")            # still going

The model thinks it's making progress. It isn't. Each call returns the same result, and the model keeps trying minor variations. Without a check, the loop will run until max_iterations and then fail.

A simple convergence detector watches for repeated tool calls:

from collections import Counter
 
def is_stuck(messages, window=5, threshold=3):
    """Return True if the same tool call appears repeatedly."""
    recent_calls = []
    for m in messages[-window * 2:]:
        if m.get("role") == "assistant" and m.get("tool_calls"):
            for c in m["tool_calls"]:
                recent_calls.append((c.function.name, str(c.function.arguments)))
 
    if not recent_calls:
        return False
 
    most_common = Counter(recent_calls).most_common(1)[0]
    return most_common[1] >= threshold

Plug it into the loop:

for step in range(max_iterations):
    response = ollama.chat(...)
    messages.append(response.message)
 
    if not response.message.tool_calls:
        return response.message.content
 
    if is_stuck(messages):
        # nudge the model out of the rut
        messages.append({
            "role": "user",
            "content": "You seem to be repeating yourself. Try a different approach or finalize your answer with what you have."
        })
        continue
 
    # ... execute tools ...

You can either break out and return what you have, or inject a nudge and let the model recover. The nudge approach often works because the model can see its own repetition once you point it out.

A complete control loop

Putting all three together:

def run_agent(goal, tools, tool_registry, max_iterations=15):
    messages = [{"role": "system", "content": f"Goal: {goal}"}]
 
    for step in range(max_iterations):
        response = ollama.chat(model="llama3", messages=messages, tools=tools)
        message = response.message
        messages.append(message)
 
        # Exit 1: goal satisfied
        if not message.tool_calls:
            return {"answer": message.content, "steps": step + 1, "exit": "satisfied"}
 
        # Exit 3: stuck
        if is_stuck(messages):
            return {"answer": None, "steps": step + 1, "exit": "stuck"}
 
        for call in message.tool_calls:
            fn = tool_registry[call.function.name]
            result = fn(**call.function.arguments)
            messages.append({"role": "tool", "content": str(result), "tool_call_id": call.id})
 
    # Exit 2: max iterations
    return {"answer": None, "steps": max_iterations, "exit": "max_iterations"}

Three exit paths, three different exit strings, so logging and metrics can tell you which kind of failure you have.

Why this matters for evaluation

When you start measuring agent quality (Track 4), you'll group runs by exit reason:

Exit reasonWhat it meansWhat to do
satisfiedModel returned a final answerCheck answer correctness with eval rubric
stuckDetector caught a repetitionImprove tool descriptions or add more tools
max_iterationsHit the capGoal is too big, or model is confused

Without distinct exit reasons, you can't tell whether your agent is failing because it gives bad answers or because it never finishes. They're different bugs with different fixes.

Log every exit

Always log the exit reason, the iteration count, and the final messages. When something goes wrong in production, this is the first thing you'll look at. We'll formalize this in Track 4 with structured logging, but start the habit now.

Key takeaway

A loop without explicit exit conditions is unsafe. A real agent has at least three: goal satisfaction (the happy path), max iterations (the seatbelt), and convergence detection (the stuck check). Pick reasonable defaults, log which exit you took, and tune from there.

>_loop-control.py
Loading editor...
Output will appear here.

Done with this lesson?