No loop, no agent
The entire secret of Claude Code fits on one line: while stop_reason == "tool_use"
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 throwstool_result must have tool_use_idand 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.