Skip to content
Chapter 2 of 8

Domain 1: the agent loop and stop_reason

The agentic loop is the core pattern of the heaviest domain, and understanding it precisely is not optional. Every API call returns a stop_reason field, and the correct response to each value is different. When stop_reason is tool_use, the model wants to call a tool: you execute it, append the result to the message history, and call the API again. When stop_reason is end_turn, the model is finished: you show the result to the user. The loop runs until end_turn, and that is the only reliable completion signal. The exam plants tempting distractors here. Parsing the assistant's text content to detect completion is always wrong, because a model can say "I am done thinking" mid-task. Using an arbitrary iteration limit as the primary stop condition is wrong. Checking whether the assistant produced any text is wrong. In this lab you build the loop as a small state machine, drive a scripted transcript with two tool calls and a final end_turn, and watch the correct loop run both tools and stop cleanly, while the text-parsing and iteration-cap anti-patterns both halt too early on the very same trace.

The lab: read it, then run it

labs/cca-f/cf2-agent-loop.py
#!/usr/bin/env python3
"""
LAB CF2: The agent loop and stop_reason (Domain 1, 27%).

Every API call returns a stop_reason. The loop runs until stop_reason is
"end_turn", executing a tool on every "tool_use". That is the only reliable
completion signal. This lab implements the loop as a small state machine and
proves two things the exam tests directly: the correct loop terminates exactly
when the model ends its turn, and the tempting anti-patterns (parsing the
assistant's text for a completion word, or using an iteration cap as the primary
stop) fail on the same trace.

Deterministic, offline, standard library only.
Run: python3 modules/academy-content/labs/cca-f/cf2-agent-loop.py
"""
import sys

# A scripted transcript: what the "model" returns on each call. Each turn has a
# stop_reason and some text. Two tool_use turns, then it ends the turn.
TRANSCRIPT = [
    {"stop_reason": "tool_use", "text": "I am done thinking, let me look that up"},
    {"stop_reason": "tool_use", "text": "done with the first tool, one more"},
    {"stop_reason": "end_turn", "text": "Here is your final answer"},
]

# The correct action for each stop_reason (from the study guide table).
ACTION = {
    "tool_use": "execute tool, append result, call again",
    "end_turn": "finished, show the user",
    "max_tokens": "truncated, raise limit or split",
    "stop_sequence": "handle per application logic",
}


def correct_loop(transcript):
    """Drive the loop the right way: terminate only on end_turn, run a tool on
    every tool_use. Returns (tools_run, final_text)."""
    tools_run = 0
    for turn in transcript:
        if turn["stop_reason"] == "end_turn":
            return tools_run, turn["text"]
        if turn["stop_reason"] == "tool_use":
            tools_run += 1  # execute the tool, then the loop would call again
    raise RuntimeError("transcript ended without end_turn")


def text_parsing_antipattern(transcript):
    """WRONG: stop as soon as the assistant text contains a completion word.
    This fires on turn 1 ('I am done thinking'), ending the loop early and
    skipping both real tool calls."""
    for i, turn in enumerate(transcript):
        if "done" in turn["text"].lower():
            return i + 1  # number of turns before it (wrongly) stopped
    return len(transcript)


def iteration_cap_antipattern(transcript, cap=1):
    """WRONG: use an arbitrary iteration cap as the PRIMARY stop condition. With
    cap=1 it halts after one turn, before the model has ended its turn."""
    for i, turn in enumerate(transcript):
        if i + 1 >= cap:
            return i + 1
    return len(transcript)


tools_run, final = correct_loop(TRANSCRIPT)
print("STEP 1: the correct loop (terminate only on end_turn)")
for i, turn in enumerate(TRANSCRIPT):
    print(f"  call {i+1}: stop_reason={turn['stop_reason']:<12} -> {ACTION[turn['stop_reason']]}")
print(f"  tools executed: {tools_run}   final text: {final!r}")

early_stop = text_parsing_antipattern(TRANSCRIPT)
cap_stop = iteration_cap_antipattern(TRANSCRIPT, cap=1)
print("")
print("STEP 2: the anti-patterns the exam plants as distractors")
print(f"  parse-text-for-'done'  stopped after turn {early_stop} (should be 3)")
print(f"  iteration-cap(=1)      stopped after turn {cap_stop} (should be 3)")

# Invariants.
loop_ok = (tools_run == 2 and final == "Here is your final answer")
text_bad = early_stop < len(TRANSCRIPT)     # the text anti-pattern stops too early
cap_bad = cap_stop < len(TRANSCRIPT)        # the cap anti-pattern stops too early
end_turn_is_signal = TRANSCRIPT[-1]["stop_reason"] == "end_turn"

ok = loop_ok and text_bad and cap_bad and end_turn_is_signal
print("")
print(f"  correct loop ran both tools and ended cleanly : {loop_ok}")
print(f"  text-parsing stop fired too early (is wrong)  : {text_bad}")
print(f"  iteration-cap stop fired too early (is wrong) : {cap_bad}")
print("")
print(f"AGENT LOOP TERMINATES ONLY ON end_turn: {'YES' if ok else 'NO'}")
if not ok:
    sys.exit(1)
print("stop_reason == end_turn is the only reliable completion signal. Next: orchestration and hooks.")
Runnable lab
cf2-agent-loop.py

Implement the stop_reason state machine and prove the correct loop ends only on end_turn while the anti-patterns halt early.

Proves: AGENT LOOP TERMINATES ONLY ON end_turn: YES

Open the notebook

Runs in your browser via Pyodide. First run loads the runtime once; no install, no server.

The correct loop ran both tools and returned the final text exactly when the model ended its turn. The two anti-patterns stopped after the first turn, before any real work was done, which is precisely how they fail on the exam. Commit the stop_reason table to memory: tool_use means execute and continue, end_turn means done, max_tokens means the response was truncated so you raise the limit or split the task, and stop_sequence means handle it per your application logic. When any answer choice involves inspecting the text of an assistant response to decide whether the task is finished, eliminate it. The next chapter stays in Domain 1 and moves up a level to coordinating many agents.
Check your understanding
  1. 1. What is the only reliable signal that an agent has finished its task?

  2. 2. When stop_reason is tool_use, what is the correct action?

  3. 3. Why is using an iteration limit as the primary stop condition wrong?

  4. 4. What does stop_reason equal to max_tokens mean?