AI

CCA-F Study Day 1/20: The Agentic Loop & Stop Reason Mechanics

Domain 1: Agentic Architecture & Orchestration (~25-27% of exam)


📌 Today's Focus

We're starting with the foundational pattern of the entire exam: the agentic loop. This is how Claude operates when given tools — it's a continuous cycle of reasoning, tool execution, and evaluation. Domain 1 is the heaviest-weighted domain (25-27%), so we're spending 5 full days here. Today establishes the mental model you'll apply to every scenario question on the exam.

If you understand nothing else, understand this: stop_reason is the ONLY reliable termination signal. The exam will test this concept from multiple angles.

📚 Core Concepts

1. The Agentic Loop Pattern

The Claude Agent SDK operates in a continuous cycle with 5 phases:

  1. Receive prompt — Claude receives the user prompt, system prompt, tool definitions, and full conversation history
  2. Evaluate and respond — Claude produces text, tool call requests, or both
  3. Execute tools — The SDK runs each requested tool and collects results
  4. Repeat — Steps 2-3 repeat (each cycle = one "turn") until Claude responds with no tool calls
  5. Return result — Final text response delivered with usage/cost metadata

The critical mechanism: The loop terminates when stop_reason changes from "tool_use" to "end_turn". This is purely programmatic — you check a field value, not parse language.

2. stop_reason Values

Value Meaning Action
"tool_use" Claude wants to call a tool Execute the tool, send result back, continue loop
"end_turn" Claude is finished Exit the loop, return final response
"max_tokens" Hit token limit mid-response Handle truncation (may need to continue)

3. Message Types in the Agent SDK

Type Description When You See It
SystemMessage Session lifecycle events Has subtypes: "init", "compact_boundary"
AssistantMessage Claude's response per turn Contains text + tool_use blocks
UserMessage Tool results or user inputs Contains tool_result content blocks
StreamEvent Raw API streaming events When partial messages are enabled
ResultMessage End of loop — final output Contains final text, usage, cost, session ID

4. Budget Controls

Option Purpose Default On Exceeded
max_turns / maxTurns Maximum tool-use round trips No limit Returns ResultMessage with subtype error_max_turns
max_budget_usd / maxBudgetUsd Maximum cost before stopping No limit Returns ResultMessage with subtype error_max_budget_usd

Key exam point: These are safety guards, NOT the primary termination mechanism. The loop still terminates naturally via stop_reason == "end_turn".

🚨 Anti-Patterns & Exam Traps

The exam heavily tests anti-patterns. Here's what you'll see as wrong answers and why they're wrong:

❌ Anti-Pattern (WRONG ANSWER) Why It's Wrong ✅ Correct Approach
Parsing natural language for loop termination(e.g., checking if Claude said "I'm done") Natural language is ambiguous. Claude might say "I'm done with this step" but still have more work. Brittle regex/string matching breaks on phrasing variations. Check stop_reason == "tool_use" programmatically
Arbitrary iteration caps as primary stopping mechanism(e.g., for i in range(5): call_claude()) Might terminate mid-task (too few iterations) or waste tokens looping after completion (too many). Doesn't respond to actual task completion. Use stop_reason for termination. Use max_turns only as a safety budget guard.
Checking response content for tool calls(e.g., looking for "I need to call..." in text) Tool calls are structured content blocks with type "tool_use", not text. The model separates text from tool invocations. Inspect response.content blocks for type == "tool_use"

💻 Code Examples

The Canonical Agentic Loop (Memorize This Pattern)

import json
from anthropic import Anthropic

client = Anthropic()

# Define a simple tool
tools = [{
    "name": "calculate",
    "description": "Perform arithmetic calculations. Use when the user asks for math.",
    "input_schema": {
        "type": "object",
        "properties": {
            "expression": {"type": "string", "description": "Math expression to evaluate (e.g., '2 + 2')"}
        },
        "required": ["expression"]
    }
}]

def run_tool(name: str, input_data: dict) -> str:
    """Execute a tool and return the result."""
    if name == "calculate":
        try:
            result = eval(input_data["expression"])  # Simplified for demo
            return {"result": result, "expression": input_data["expression"]}
        except Exception as e:
            return {"is_error": True, "errorCategory": "validation", "isRetryable": True, "context": str(e)}
    return {"is_error": True, "errorCategory": "not_found", "isRetryable": False}

# Initial request
messages = [{"role": "user", "content": "What is 42 * 17 + 3?"}]
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=messages
)

# THE AGENTIC LOOP — this is the exam pattern
while response.stop_reason == "tool_use":
    # 1. Find the tool_use block
    tool_use = next(block for block in response.content if block.type == "tool_use")
    
    # 2. Execute the tool
    result = run_tool(tool_use.name, tool_use.input)
    
    # 3. Append assistant response (preserves full content including text + tool_use)
    messages.append({"role": "assistant", "content": response.content})
    
    # 4. Append tool result (MUST match tool_use_id)
    messages.append({
        "role": "user",
        "content": [{
            "type": "tool_result",
            "tool_use_id": tool_use.id,
            "content": json.dumps(result)
        }]
    })
    
    # 5. Get next response
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        tools=tools,
        messages=messages
    )

# Loop exited because stop_reason == "end_turn"
final_text = next(block.text for block in response.content if block.type == "text")
print(f"Final answer: {final_text}")

Handling Multiple Tool Calls in One Response

# Claude can request multiple tools in a single response
# You must execute ALL of them and return ALL results

while response.stop_reason == "tool_use":
    # Collect ALL tool_use blocks (not just the first)
    tool_uses = [block for block in response.content if block.type == "tool_use"]
    
    messages.append({"role": "assistant", "content": response.content})
    
    # Execute each tool and collect results
    tool_results = []
    for tool_use in tool_uses:
        result = run_tool(tool_use.name, tool_use.input)
        tool_results.append({
            "type": "tool_result",
            "tool_use_id": tool_use.id,
            "content": json.dumps(result)
        })
    
    # Send ALL results back in one user message
    messages.append({"role": "user", "content": tool_results})
    
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        tools=tools,
        messages=messages
    )

Budget Controls (Safety Guards)

# These are safety guards — NOT primary termination
from claude_agent_sdk import Agent

agent = Agent(
    model="claude-sonnet-4-20250514",
    tools=tools,
    max_turns=10,          # Safety: abort if 10+ tool calls (abnormal)
    max_budget_usd=0.50    # Safety: abort if cost exceeds $0.50
)

result = agent.run("Analyze this codebase and suggest improvements")

# Check if we hit a budget limit vs. natural completion
if result.subtype == "error_max_turns":
    print("⚠️ Hit turn limit — task may be incomplete")
elif result.subtype == "error_max_budget_usd":
    print("⚠️ Hit budget limit — task may be incomplete")
else:
    print(f"✅ Completed naturally: {result.text}")

📖 Supplementary Material

🏋️ Hands-On Exercise (15-30 min)

Build a minimal agentic loop:

  1. Create a Python script with the Anthropic SDK (pip install anthropic)
  2. Define ONE tool (a calculator, weather lookup mock, or file reader)
  3. Implement the while response.stop_reason == "tool_use" loop
  4. Print each turn: what tool was called, what input it received, what result was returned
  5. Verify: Does the loop terminate naturally? Does it handle the case where Claude decides not to use a tool?

Bonus: Add max_turns=3 and observe what happens when you ask Claude something that needs 5+ tool calls.

📝 Quick Quiz

Question 1: An architect is implementing an agentic loop with Claude. The agent should continue executing tools until the task is complete. Which approach correctly determines when to exit the loop?

  1. Parse Claude's response text for phrases like "task complete" or "I'm finished"
  2. Set a fixed iteration count of 10 and always run exactly 10 turns
  3. Check if response.stop_reason == "tool_use" and continue only while true
  4. Count the number of tool_result messages and stop after reaching a threshold

Question 2: A development team wants to prevent their Claude agent from running indefinitely on expensive tasks. Which combination correctly implements budget controls while maintaining proper loop termination?

  1. Replace the stop_reason check with max_turns=5 as the primary loop control
  2. Use max_turns and max_budget_usd as safety guards alongside the stop_reason-based loop
  3. Implement a timer that kills the process after 60 seconds
  4. Add a system prompt instruction: "You must complete within 5 tool calls"

Question 3: When Claude's response contains multiple tool_use content blocks, what is the correct handling pattern?

  1. Execute only the first tool_use block and ignore the rest
  2. Execute all tool_use blocks and return all results in a single user message, each with matching tool_use_id
  3. Execute them sequentially in separate API calls, one per tool
  4. Pick the most relevant tool_use block based on the user's original question

Answers:

  • Q1: C — stop_reason is the only reliable termination signal. A is parsing natural language (anti-pattern). B is arbitrary iteration cap (anti-pattern). D invents a non-standard mechanism.
  • Q2: B — Budget controls are safety guards that work alongside the natural loop. A replaces proper termination (anti-pattern). C is brittle and platform-dependent. D relies on prompt-based enforcement (anti-pattern for critical controls).
  • Q3: B — All tool_use blocks must be executed and all results returned in one message with matching IDs. A loses information. C breaks the message protocol. D arbitrarily discards valid requests.

👀 Tomorrow's Preview

Day 2: We'll cover Effort Levels, Permissions & Session Management — how effort levels map to extended thinking depth, the 6 permission modes that gate tool execution, and how sessions maintain state across turns.