1. The Agent Loop

"One loop & Bash is all you need"

15 min read
πŸ’‘New to this?

What's an API?

An API (Application Programming Interface) is a way for programs to talk to each other. When we call the Claude API, we send text and get a response back β€” like texting a very smart friend.

What does 'while True' mean?

It's a loop that runs forever until something tells it to stop. In our agent, it keeps running until the model decides it's done (stop_reason is not 'tool_use').

What's a tool call?

When the AI model wants to do something in the real world (run a command, read a file), it sends back a special 'tool_use' message instead of regular text. Our code then executes that action and sends the result back.

The Problem

How does a language model go from generating text to actually doing things in the real world?

The model can reason, plan, and generate code β€” but it has no hands. It can’t run a command, read a file, or check the result. It’s a brain in a jar.

The Solution

One loop. One tool. That’s the entire architecture.

while True:
  response = LLM(messages, tools)
  if stop_reason != "tool_use": return
  execute tools
  append results
  loop back

The model decides when to call tools and when to stop. The code just executes what the model asks for.

The Core Loop

def agent_loop(messages):
    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

        results = []
        for block in response.content:
            if block.type == "tool_use":
                output = TOOL_HANDLERS[block.name](**block.input)
                results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": output,
                })
        messages.append({"role": "user", "content": results})

Four steps, repeated:

  1. Append the user prompt to messages
  2. Send messages + tool definitions to the LLM
  3. Check stop_reason β€” if it’s not tool_use, the model is done
  4. Execute each tool call, append the results, loop back

The Bash Tool

TOOLS = [{
    "name": "bash",
    "description": "Run a shell command.",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"],
    },
}]

def run_bash(command: str) -> str:
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    r = subprocess.run(command, shell=True, cwd=os.getcwd(),
                       capture_output=True, text=True, timeout=120)
    out = (r.stdout + r.stderr).strip()
    return out[:50000] if out else "(no output)"

One tool definition. One handler. The model now has hands β€” it can run any shell command and read the output.

The Full Implementation

#!/usr/bin/env python3
"""s01_agent_loop.py - The Agent Loop"""

import os, subprocess
from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv(override=True)
client = Anthropic()
MODEL = os.environ["MODEL_ID"]
SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks."

TOOLS = [{
    "name": "bash",
    "description": "Run a shell command.",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"],
    },
}]

def run_bash(command: str) -> str:
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    try:
        r = subprocess.run(command, shell=True, cwd=os.getcwd(),
                           capture_output=True, text=True, timeout=120)
        out = (r.stdout + r.stderr).strip()
        return out[:50000] if out else "(no output)"
    except subprocess.TimeoutExpired:
        return "Error: Timeout (120s)"

def agent_loop(messages: list):
    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
        results = []
        for block in response.content:
            if block.type == "tool_use":
                print(f"\033[33m$ {block.input['command']}\033[0m")
                output = run_bash(block.input["command"])
                print(output[:200])
                results.append({"type": "tool_result",
                                "tool_use_id": block.id,
                                "content": output})
        messages.append({"role": "user", "content": results})

if __name__ == "__main__":
    history = []
    while True:
        try:
            query = input("\033[36ms01 >> \033[0m")
        except (EOFError, KeyboardInterrupt):
            break
        if query.strip().lower() in ("q", "exit", ""):
            break
        history.append({"role": "user", "content": query})
        agent_loop(history)

Key Takeaway

The entire secret of an AI coding agent is this loop. The model is the intelligence β€” it decides what to do. The code is just the harness β€” it gives the model a tool and feeds back results. In the next session (Tool Use), we’ll add more tools without changing the loop at all.

Interactive Code Walkthrough

The Core Agent Loop
1def agent_loop(messages):
2 while True:
3 response = client.messages.create(
4 model=MODEL, system=SYSTEM,
5 messages=messages, tools=TOOLS,
6 max_tokens=8000,
7 )
8 messages.append({"role": "assistant",
9 "content": response.content})
10 
11 if response.stop_reason != "tool_use":
12 return
13 
14 results = []
15 for block in response.content:
16 if block.type == "tool_use":
17 output = TOOL_HANDLERS[block.name](**block.input)
18 results.append({
19 "type": "tool_result",
20 "tool_use_id": block.id,
21 "content": output,
22 })
23 messages.append({"role": "user", "content": results})
24 
The infinite loop. It keeps running until the model decides to stop. This is the heartbeat of every agent.
Step 1 of 6
πŸ§ͺ Try it yourself
πŸ”₯ Warm-up ~5 min

Before running the code, predict: what happens if you remove the if response.stop_reason != 'tool_use': return check? Will the agent run forever?

Hint

Think about what stop_reason the API returns when the model has no tool calls to make.

πŸ”¨ Build ~20 min

Clone the repo, run python agents/s01_agent_loop.py, and ask it to create a file. Watch the tool calls in the terminal.

Hint

Set MODEL_ID=claude-sonnet-4-20250514 in your .env file

πŸš€ Stretch ~45 min

Add a turn counter to the agent loop that limits execution to 20 turns maximum. Print a warning when the agent hits the limit. Then test it by giving the agent an impossible task.

Hint

Add a turns variable before the while loop and increment it each iteration.

Found a mistake? Report it β†’