Skip to main content
Module 1: Core Agent 1 / 6
Beginner Session 1 Architecture Agent Loop Fundamentals

The Agent Loop

Understand the core async message loop that powers Claude Code — how it processes messages, routes tool calls, and decides when to stop.

March 20, 2026 18 min read

What You’ll Learn

In this first session, you’ll understand the heart of Claude Code: the agent loop. Every interaction — from a simple “fix this bug” to a complex multi-file refactor — flows through this single loop.

By the end, you’ll know:

  • How the message loop processes each turn
  • What stop_reason routing means and why it matters
  • How tool results feed back into the loop
  • Why this architecture enables complex multi-step tasks

The Problem

When you type a prompt into Claude Code, something remarkable happens: the AI doesn’t just respond once. It can read files, run commands, edit code, and keep going — all in a single interaction. How?

The naive approach would be a simple request-response:

User → AI → Response (done)

But Claude Code does something fundamentally different. It runs a loop that keeps going until the AI decides it’s done:

User → AI → Tool Call → Result → AI → Tool Call → Result → AI → Response (done)

This is the agent loop.

How It Works

Here’s the core architecture, simplified:

┌─────────────────────────────────────────┐
│              Agent Loop                  │
│                                          │
│  ┌──────────┐                            │
│  │  Prompt   │◄──── User message         │
│  └────┬─────┘                            │
│       │                                  │
│       ▼                                  │
│  ┌──────────┐                            │
│  │  Claude   │◄──── System prompt +      │
│  │   API     │      message history      │
│  └────┬─────┘                            │
│       │                                  │
│       ▼                                  │
│  ┌──────────────┐                        │
│  │ stop_reason?  │                       │
│  └──┬───────┬───┘                        │
│     │       │                            │
│  "end_turn" "tool_use"                   │
│     │       │                            │
│     ▼       ▼                            │
│   Done    ┌──────────┐                   │
│           │ Execute   │                  │
│           │  Tools    │                  │
│           └────┬─────┘                   │
│                │                         │
│                │ Append results           │
│                │ to messages              │
│                │                         │
│                └──────► Loop back ◄──────│
│                                          │
└──────────────────────────────────────────┘

The key routing decision is based on stop_reason:

stop_reasonActionWhat it means
end_turnStop the loopAI is done, return response to user
tool_useExecute tools, loop backAI wants to use tools before responding

The Message Array

The loop maintains a growing array of messages:

const messages = [
  { role: "user", content: "Fix the bug in auth.ts" },
  { role: "assistant", content: [
    { type: "text", text: "Let me look at the file..." },
    { type: "tool_use", id: "1", name: "Read", input: { file_path: "auth.ts" } }
  ]},
  { role: "user", content: [
    { type: "tool_result", tool_use_id: "1", content: "// file contents..." }
  ]},
  { role: "assistant", content: [
    { type: "text", text: "I see the issue..." },
    { type: "tool_use", id: "2", name: "Edit", input: { ... } }
  ]},
  // ... continues until stop_reason === "end_turn"
];

Notice something crucial: tool results are injected as user messages. This is how the AI “sees” the output of its actions. The API has no concept of tools executing — it only sees the message array grow.

Parallel Tool Calls

In a single turn, Claude can request multiple tool calls simultaneously:

{
  "role": "assistant",
  "content": [
    { "type": "tool_use", "id": "1", "name": "Read", "input": { "file_path": "src/a.ts" } },
    { "type": "tool_use", "id": "2", "name": "Read", "input": { "file_path": "src/b.ts" } },
    { "type": "tool_use", "id": "3", "name": "Read", "input": { "file_path": "src/c.ts" } }
  ]
}

The agent loop executes all three in parallel (where safe), then sends all results back:

{
  "role": "user",
  "content": [
    { "type": "tool_result", "tool_use_id": "1", "content": "..." },
    { "type": "tool_result", "tool_use_id": "2", "content": "..." },
    { "type": "tool_result", "tool_use_id": "3", "content": "..." }
  ]
}

This is why Claude Code can read multiple files at once — it’s not magic, it’s parallel tool execution within the loop.

Key Insight

The agent loop is not a chat bot. Chat bots process one message and respond. The agent loop is more like an event loop in Node.js — it keeps processing until there’s nothing left to do.

This distinction matters because:

  1. The AI controls the flow. It decides when to use tools and when to stop. The loop doesn’t impose a fixed number of steps.
  2. Context accumulates. Each tool result adds to the message array, giving the AI more information for its next decision.
  3. Errors are recoverable. If a tool fails, the error becomes a tool result. The AI can read the error and try a different approach.

Many simplified implementations use a fixed while True loop with a maximum step count. The real architecture is more nuanced — it uses stop_reason routing, which means the AI itself decides the termination condition.

Hands-On Example

Here’s a minimal agent loop you can experiment with:

import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "read_file",
        "description": "Read a file's contents",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "File path to read"}
            },
            "required": ["path"]
        }
    }
]

def execute_tool(name, input):
    if name == "read_file":
        try:
            with open(input["path"]) as f:
                return f.read()
        except Exception as e:
            return f"Error: {e}"

def agent_loop(user_message):
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=4096,
            tools=tools,
            messages=messages,
        )

        # Route based on stop_reason
        if response.stop_reason == "end_turn":
            # Extract text response
            return [b.text for b in response.content if b.type == "text"]

        # stop_reason == "tool_use" → execute tools
        messages.append({"role": "assistant", "content": response.content})

        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result),
                })

        messages.append({"role": "user", "content": tool_results})

# Usage
result = agent_loop("What's in the README.md file?")
print(result)

Try modifying this to:

  1. Add a write_file tool
  2. Add a maximum iteration limit as a safety net
  3. Log each loop iteration to see the flow

What Changed

Before (Chat Bot)After (Agent Loop)
One request → one responseOne request → many tool calls → one response
Fixed interaction patternAI controls the flow
No tool executionTools executed within the loop
Context is staticContext grows with each tool result
Errors are terminalErrors are recoverable (AI reads error, tries again)

Next Session

Now that you understand the loop, Session 2 dives into the Tool System & Permissions — how Claude Code decides which tools to expose, how permission callbacks work, and why some tools require approval while others don’t.