The Agent Loop
Tools & ExecutionBash is All You Need
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
- User prompt becomes the first message:
messages.append({"role": "user", "content": query})
- Send messages + tool definitions to the LLM:
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000,
)
- 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
- 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
| Component | Before | After |
|---|---|---|
| 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_resultmessage 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.