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:
- Receive prompt — Claude receives the user prompt, system prompt, tool definitions, and full conversation history
- Evaluate and respond — Claude produces text, tool call requests, or both
- Execute tools — The SDK runs each requested tool and collects results
- Repeat — Steps 2-3 repeat (each cycle = one "turn") until Claude responds with no tool calls
- 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
- Required Reading: Agent SDK Documentation — Focus on the "Agent Loop" section
- Required Reading: Building Effective Agents (Anthropic Engineering Blog) — The foundational multi-agent design post
- Reference: Tool Use Overview — For understanding the tool_use content block structure
🏋️ Hands-On Exercise (15-30 min)
Build a minimal agentic loop:
- Create a Python script with the Anthropic SDK (
pip install anthropic) - Define ONE tool (a calculator, weather lookup mock, or file reader)
- Implement the
while response.stop_reason == "tool_use"loop - Print each turn: what tool was called, what input it received, what result was returned
- 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?
- Parse Claude's response text for phrases like "task complete" or "I'm finished"
- Set a fixed iteration count of 10 and always run exactly 10 turns
- Check if
response.stop_reason == "tool_use"and continue only while true - 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?
- Replace the stop_reason check with
max_turns=5as the primary loop control - Use
max_turnsandmax_budget_usdas safety guards alongside thestop_reason-based loop - Implement a timer that kills the process after 60 seconds
- 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?
- Execute only the first tool_use block and ignore the rest
- Execute all tool_use blocks and return all results in a single user message, each with matching
tool_use_id - Execute them sequentially in separate API calls, one per tool
- Pick the most relevant tool_use block based on the user's original question
Answers:
- Q1: C —
stop_reasonis 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.