Lesson 01 · Basics

No loop, no agent

The entire secret of Claude Code fits on one line: while stop_reason == "tool_use"

⏱ ~10 min · 📝 3 interactive widgets · 🧑‍💻 Based on shareAI-lab · s01_agent_loop.py

What is an agent actually doing?

When you run Claude Code in the terminal and ask it to "collect all TODO comments into a list", what you see is: it decides to first grep, then cat a few files, then output Markdown. The model never executes code directly — it only requests execution. What makes it look like it has hands and feet is the ~30 lines of glue code wrapped around it.

This lesson tears that glue apart. The pattern is called the agent loop, and its structure is:

while response.stop_reason == "tool_use":
    response = LLM(messages, tools)      # 1. ask the model
    execute_tools(response.tool_calls)   # 2. run what it asked for
    messages.append(tool_results)        # 3. feed results back

That's it. Production Claude Code layers on permissions, hooks, sub-agents, worktree isolation, and memory compaction on top of this — but the core is still these four lines.

Key intuition: Every model response either says "I want to call a tool" or "I'm done." As long as it's in the first state, the loop continues. Once it enters the second, the loop exits. The signal is the stop_reason field in the response.

Watch messages[] grow step by step

The loop below simulates the task: "What files are in the current directory? Then read package.json." Click Step to advance one action at a time. The left panel shows a human-readable conversation view; the right shows the actual messages[] array fed to the model — watch it grow.

Tool results must be fed back into message history

The most common beginner mistake is treating "execute the tool" as a side effect — run it and move on. But the model's next inference can only see messages[]. If something isn't in there, the model has no idea it happened. Miss the append step and the whole loop breaks.

The failure modes are not all the same. Two common ones:

  • Forget to append: the model doesn't see the result and asks for the same tool again — you get an infinite loop.
  • Append but drop the tool_use_id: the Anthropic API throws tool_result must have tool_use_id and the loop crashes at the API call.

Same task: "Count how many Python files are in this project." Run both versions side by side and see how differently they end.

Reading stop_reason

Every model response carries a stop_reason. Whether to keep looping depends entirely on this field:

  • tool_use — model wants to call a tool: keep looping.
  • end_turn — model is done: exit the loop.
  • max_tokens — generation was cut off at the token limit: exit the loop (treat as an error; tell the user the output is incomplete).
  • stop_sequence — a custom stop string was hit: exit the loop.

Common bugs: writing the condition as "continue unless tool_use", or silently treating max_tokens as a normal end turn.

Run it yourself

Clone shareAI-lab/learn-claude-code locally:

git clone https://github.com/shareAI-lab/learn-claude-code
cd learn-claude-code
pip install -r requirements.txt
cp .env.example .env  # fill in ANTHROPIC_API_KEY
python agents/s01_agent_loop.py

Then give it a real task: Count how many .py files are in this repo. You'll see it issue ls -R, read the output, then issue find . -name "*.py" | wc -l, and give you the answer. The whole process is this loop running.

Interactive

Widget 1 · Loop Stepper · messages[] growth

Watch how tool results are fed back into messages in a special format (tool_result blocks with a tool_use_id). This is the only way the model knows on the next turn that "the tool ran and the result was X".

Conversation ViewNot started
Click Step to begin →
messages[] JSONlength: 0
[]
stop_reason:
Interactive

Widget 2 · Break the Loop · Correct vs Forgetting to append

Watch how missing just one line — messages.append(tool_results) — turns the agent into a broken record that forgets the previous turn on every cycle.

while True:
    resp = LLM(msgs, tools)
    msgs.append({"role":"assistant",
                 "content": resp.content})
    if resp.stop_reason != "tool_use":
        return
    results = execute(resp.tool_uses)
    msgs.append({"role":"user",
                 "content": results})
Simulation
while True:
    resp = LLM(msgs, tools)
    msgs.append({"role":"assistant",
                 "content": resp.content})
    if resp.stop_reason != "tool_use":
        return
    results = execute(resp.tool_uses)
    # BUG: forgot to append results
    # msgs.append(...)
Simulation
Interactive

Widget 3 · Spot the stop_reason · Classify 4 real responses

Each snippet is a plausible model response. Click "Continue" or "Exit" and get instant feedback.

Correct: 0 / 4