Learn Claude Code
s01

The Agent Loop

Tools & Execution

Bash is All You Need

98 LOC1 toolsSingle-tool agent loop
The minimal agent kernel is a while loop + one tool

[ s01 ] s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12

"One loop & Bash is all you need" -- one tool + one loop = an agent.

Harness layer: The loop -- the model's first connection to the real world.

Problem

A language model can reason about code, but it can't touch the real world — can't read files, run tests, or check errors. Without a loop, every tool call requires you to manually copy-paste results back. You become the loop.

Solution

+--------+      +-------+      +---------+
|  User  | ---> |  LLM  | ---> |  Tool   |
| prompt |      |       |      | execute |
+--------+      +---+---+      +----+----+
                    ^                |
                    |   tool_result  |
                    +----------------+
                    (loop until stop_reason != "tool_use")

One exit condition controls the entire flow. The loop runs until the model stops calling tools.

How It Works

  1. User prompt becomes the first message:
messages.append({"role": "user", "content": query})
  1. Send messages + tool definitions to the LLM:
response = client.messages.create(
    model=MODEL, system=SYSTEM, messages=messages,
    tools=TOOLS, max_tokens=8000,
)
  1. Append assistant response. Check stop_reason — if no tool call, done:
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
    return
  1. Execute tool calls, collect results, append as user message. Loop to step 2:
results = []
for block in response.content:
    if block.type == "tool_use":
        output = run_bash(block.input["command"])
        results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results})

Key Code

def agent_loop(query):
    messages = [{"role": "user", "content": query}]
    while True:
        response = client.messages.create(
            model=MODEL, system=SYSTEM, messages=messages,
            tools=TOOLS, max_tokens=8000,
        )
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            return  # Model decided it's done
        results = []
        for block in response.content:
            if block.type == "tool_use":
                output = run_bash(block.input["command"])
                results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
        messages.append({"role": "user", "content": results})

Under 30 lines. This is the entire agent. All 20 subsequent sessions add mechanisms on top — without changing the loop itself.

What Changed

ComponentBeforeAfter
Agent loop(none)while True + stop_reason
Tools(none)bash (one tool)
Messages(none)Accumulating list
Control flow(none)stop_reason != "tool_use"

Deep Dive: Design Decisions

Q1: Why stop_reason instead of a fixed iteration count?

stop_reason lets the model decide when to stop, not the programmer. This is the fundamental difference between an agent and a script:

# ❌ Script: programmer decides loop count
for i in range(5): run_step(i)

# ✅ Agent: model decides when it's done
while True:
    response = call_model(messages)
    if response.stop_reason != "tool_use": break

Risk: the model might never stop (infinite loop). Production implementations add max-turns protection. Claude Code defaults to 200 turns max.

Q2: Why does tool_result go in a "user" role message?

The Claude API protocol requires strict alternation: user → assistant → user → assistant. Tool results, though produced by code execution (not human input), must be placed in user role messages to maintain this alternation. This keeps the message format consistent — the API only handles two roles.

Q3: Can the messages array grow infinitely? What happens?

Yes, it grows infinitely. Each loop iteration adds 2 messages (assistant + user/tool_result). Consequences: linear cost growth, context window overflow errors, increased latency. This is exactly what s06 (Context Compact) solves — but s01 intentionally ignores it to focus on the core loop.

Q4: How similar is this 30-line loop to Claude Code's core?

Extremely similar. Claude Code's core loop (agent_loop.ts) has the same structure with production enhancements: 10+ tools (vs 1), permission tiers (s18), three-layer compression (s06), sub-agent isolation (s04), Think Tool (s19). The core loop is identical: call model → check stop_reason → execute tools → append results → repeat.

Q5: Is one bash tool enough? Why not more tools from the start?

It works but isn't ideal. Bash can do almost anything but it's unsafe (rm -rf /), imprecise (model must compose shell commands), and uncontrollable (no fine-grained permissions). s02 splits bash into specialized tools (read_file, write_file, grep). The single bash tool in s01 is an intentional simplification — to focus purely on the loop mechanism.

Try It

cd learn-claude-code
python agents/s01_agent_loop.py

Recommended prompts:

  • "Create a file called hello.py that prints 'Hello, World!'" — simplest file creation
  • "List all Python files in this directory" — watch model choose tools autonomously
  • "What is the current git branch?" — observe git info extraction
  • "Create a directory called test_output and write 3 files in it" — observe multi-turn loop

References

  • Building Effective Agents — Anthropic, Dec 2025. Defines the "Augmented LLM" concept: LLM + tools + retrieval = minimum agent unit. s01 is its simplest implementation.
  • Claude API: Tool Use — Anthropic Docs. Official tool use API documentation, including stop_reason, tool_use/tool_result message format.
  • Claude Code: Agent Loop — Anthropic Docs. Claude Code's overall architecture, whose core is an extended version of s01.
  • Agentic Coding Trends 2026 — Anthropic, Mar 2026. Paradigm shift from "human in the loop" to "agent autonomous loop" — s01's loop is the technical foundation.