Skip to main content
Claude Code Tutorial: From First Command to Custom Workflows

The Agent Loop: How Claude Code Executes One Turn

Inside Claude Code's decision loop -- tool_use messages, tool_result structures, context accumulation across turns, when the agent decides to stop, and how the loop handles errors and constraints.

Intermediate18 minBy ToolDix Editorial

Learning objectives

  • Trace the exact message flow in one turn of the agent loop
  • Understand tool_use and tool_result message structures
  • Recognize how context window constraints affect loop behavior
  • Identify the stopping conditions that end a turn
  • Understand how the agent decides what to do next based on tool results

ToolDix original visual

Claude Code Tutorial practice loop
1

Frame

Name the outcome and constraints.

2

Build

Try one bounded workflow.

3

Review

Keep evidence, revise, and share.

The loop structure: one turn at a time

Claude Code executes as a series of turns. Each turn is a complete decision cycle: the model reads its context, decides what tool to call (or whether to produce a final answer), the tool executes, the result is captured, and the model observes it. A task might require 1 turn (simple question) or 20-50 turns (complex refactor with multiple edits and test runs).

ToolDix original diagram
The core workflow loop
1
Plan
Read codebase, understand goal, decide changes needed
2
Edit
Propose and execute file changes (with approval)
3
Test
Run tests, linters, or validation
4
Observe
Read results and decide next step
Loop continues until goal is met

One turn proceeds in this exact order:

  1. Assemble context. Load system instructions, project memory (CLAUDE.md, auto-memory), conversation history, and the current prompt
  2. Call the model. Send all context to Claude, asking it to decide what to do
  3. Receive response. The model outputs either a final answer or a tool_use request
  4. Check for stop condition. If the model produced a final answer, the turn stops and returns to the user
  5. Execute tool (if needed). Call the tool the model requested with the exact arguments
  6. Capture result. Save the tool's output, error message, or timeout
  7. Append to context. Add both the tool_use request and its result to the conversation history
  8. Loop. Go back to step 1, now with the new tool result in context

The message structures: tool_use and tool_result

Claude Code exchanges messages in Anthropic's standard message format. The conversation history is a list of messages, each of which is either a user message, an assistant message, or a tool result. This structure is the same across all Claude models and the Agent SDK.

Here is a minimal traced example:

// Turn 1, step 2: Context sent to model
{
  "messages": [
    {
      "role": "user",
      "content": "Fix the failing test in test/auth.test.js"
    }
  ],
  "system": "You are Claude Code, an agentic coding assistant...",
  "model": "claude-3-5-sonnet-20241022"
}

The model observes: it has no information about the test file, so it must read it first.

// Turn 1, step 3: Model response (tool_use)
{
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "I'll start by reading the test file to see what's failing."
    },
    {
      "type": "tool_use",
      "id": "toolu_01HZ...",
      "name": "Read",
      "input": {
        "path": "/path/to/test/auth.test.js"
      }
    }
  ]
}

This tool_use message tells Claude Code: invoke the Read tool with the given path. The id field (toolu_01HZ...) is a unique identifier for this specific tool call, used to match the result back.

// Turn 1, step 6: Tool result is captured
{
  "role": "user",
  "content": [
    {
      "type": "tool_result",
      "tool_use_id": "toolu_01HZ...",
      "content": "1    describe('Authentication', () => {\n2      it('should reject invalid tokens', async () => {\n3        const token = 'invalid';\n4        const result = await validateToken(token);\n5        expect(result.valid).toBe(true);\n6      });\n7    });"
    }
  ]
}

The model now has the file contents. It sees the test (line 5) expects result.valid to be true for an invalid token, which is contradictory. It must be a test bug or the implementation changed.

// Turn 2, step 2: Model now has context of the file
// It needs to understand the implementation to decide if it's a test bug or impl bug
{
  "messages": [
    { "role": "user", "content": "Fix the failing test..." },
    { "role": "assistant", "content": [...tool_use to Read...] },
    { "role": "user", "content": [...tool_result with file...] },
    {
      "type": "tool_use",
      "id": "toolu_02AB...",
      "name": "Read",
      "input": { "path": "/path/to/auth.js" }
    }
  ]
}

And the loop continues. Each turn appends a new tool_use and its corresponding tool_result to the messages array.

Context window: what loads and when

Claude Code's context window holds the conversation history, file contents, tool results, system instructions, and project memory. The window is finite (200,000 tokens by default), and as a session grows, space pressure increases.

ToolDix original diagram
Context window: filling and compaction
Session starts
System prompt, CLAUDE.md, conversation history loads
Turns 1-5
File reads, tool results accumulate in context
Context fills (70-80%)
Approaching limit
Auto-compaction triggered
Old tool outputs cleared, conversation summarized
Continue
More capacity freed, session proceeds with fresh space
What survives compaction:
  • Your requests and key code snippets
  • CLAUDE.md and auto-memory (reloaded from disk)
  • Recent tool results (last ~10 turns)

At session start, the context loads in this order:

  1. System prompt (~4,200 tokens) — Core instructions for behavior, tool use, and response formatting
  2. Auto memory (first 200 lines or 25KB, whichever is smaller) — MEMORY.md, Claude's notes to itself from previous sessions
  3. Environment info (~280 tokens) — Working directory, platform, git branch, recent commits
  4. MCP tool names (variable, deferred by default) — Names of available tools; full schemas load on demand
  5. Project CLAUDE.md (entire file) — Your project's standing instructions
  6. Conversation history (grows with each turn)

As the session continues and you run more commands, file contents and tool results accumulate. Eventually the window fills.

When context fills (typically after 15-30 turns), Claude Code triggers automatic compaction:

  1. First, clear old tool outputs. Results from turns 1-5 are removed, keeping recent results (usually last 10 turns)
  2. If still full, summarize the conversation. Early messages are replaced with a summary paragraph
  3. Preserve critical information: CLAUDE.md survives compaction; your requests are preserved; key code snippets are kept

After compaction, the session continues with more capacity. If a single tool output is so large that context refills immediately after compaction, Claude Code stops auto-compacting and shows an error. This prevents infinite loops. You can trigger manual compaction with /compact or reduce context pressure by using subagents for isolated tasks.

Stopping conditions: when does a turn end

A turn ends when one of these conditions is met:

ConditionWhat happensOutcome
Model produces final answerModel outputs text with language like "The fix is complete" or "All tests pass"Turn stops, result returned to you
Tool returns errorFileNotFoundError, CommandFailed, or other error. Error appended to context.Loop continues; model tries different approach
Tool times outLong operation exceeds timeout limit (e.g., test suite runs 5+ minutes)Loop continues; model decides whether to retry or report
Maximum turn limit reachedInteractive: ~100 turns default. Non-interactive (-p flag): 10-20 turns default.Turn stops, partial result returned; you can resume with /continue
Model repeats same actionModel makes identical tool calls 3+ times (stuck in unproductive loop)Turn stops; Claude Code reports being stuck and asks for human direction
Manual stop (user presses Esc)In terminal/interactive mode onlyCurrent tool is canceled, loop waits for your next command

Decision flow: how the model chooses the next action

After observing a tool result, the model faces a branching decision:

Tool result received.
├─ Does the result show the goal is met?
│  └─ Yes → Output final answer, stop turn
│  └─ No → Is the error recoverable?
│     ├─ Yes → Call a different tool or the same tool with different args
│     └─ No → Output error explanation, stop turn

Example: test failure

The model runs a test and sees it fail:

AssertionError: expected admin role but got null

The model must decide:

  • Is this the final answer? No, the test should pass.
  • Is the error recoverable? Yes, the test shows a specific setup issue.
  • What is the next action? Read the test setup code to understand why admin role is null.

The loop continues.

Example: wrong tool call

The model tries to read a file that doesn't exist:

FileNotFoundError: path/to/wrong_file.js

The model must decide:

  • Goal met? No.
  • Recoverable? Yes. Use the file-search tool to find the correct file, then read it.
  • Next action? Search for files matching "auth" in the codebase.

The loop continues.

Example: goal actually met

The model runs the test suite and sees:

All tests passed (142 passed, 0 failed) in 2.3s

The model decides:

  • Goal met? Yes, the failing test now passes.
  • This is a final answer: "The failing test in test/auth.test.js is now fixed. The issue was that the test expected the wrong behavior; I corrected the assertion to match the actual implementation."

The turn stops. You review the result.

A realistic multi-turn trace: fixing a failing test

Scenario: You ask Claude Code to fix a failing test.

Turn 1:
  Context: User prompt "fix the failing test in src/payment.test.js"
  Model: "I need to read the test first."
  Tool call: Read(path="src/payment.test.js")
  Result: [test file content, shows it's testing payment processing]

Turn 2:
  Context: Previous + test file
  Model: "The test expects validatePayment() to return { valid: true }. I need to read the impl."
  Tool call: Read(path="src/payment.js")
  Result: [payment module, shows validatePayment doesn't check merchant ID]

Turn 3:
  Context: Previous + implementation
  Model: "The test requires a merchant ID, but the impl doesn't validate it. The test is correct; the impl is incomplete."
  Tool call: Bash(command="npm test -- src/payment.test.js")
  Result: [full test output, confirming exact failure]

Turn 4:
  Context: Previous + test output
  Model: "Now I'll fix the implementation to validate merchant ID."
  Tool call: Edit(path="src/payment.js", old_string="...", new_string="...")
  Result: [confirms edit applied]

Turn 5:
  Context: Previous + edit confirmation
  Model: "Let me run the test again to verify the fix."
  Tool call: Bash(command="npm test -- src/payment.test.js")
  Result: [test output: "1 passed"]

Turn 6:
  Context: Previous + passing test result
  Model: "The test now passes. Task complete."
  Final answer: "The failing test is now fixed. The issue was that validatePayment() wasn't checking the merchant ID. I added that check, and the test passes."
  Turn stops.

Each turn's tool result becomes input to the next turn's context. The model observes the actual state of the code and tests, not guessing from training data.

Common mistake

Interrupting Claude Code when the first action returns an error. If Claude Code reads a file and gets an error, or runs a test and it fails, that's not a failure of the agent -- that's normal loop operation. The agent will observe the error and adapt. Stopping the agent after the first failed tool call loses the whole advantage of multi-step iteration. Give the agent 3-5 turns before deciding the approach is wrong; often it will self-correct.

Sources and license context

These references informed the lesson. ToolDix adds its own explanation, workflow, and practice rather than reproducing source material. Every link below leaves ToolDix and opens the publisher's own site in a new tab.

Keep going

Read these next on ToolDix.

Original lessons that build on what you just read.