Chapter 2 · Part 1

Build the agent loop

Time to write the loop that is an agent. We'll build agent.py and grow it through the next few chapters. The rule is simple: keep calling the model as long as it wants a tool, and stop when it gives a real answer.

One tool, one loop

Start with a single tool — a calculator, so a multi-step question forces the agent to take more than one step. Two pieces set it up: the tool description (JSON Schema, so Claude knows the inputs) and the actual Python function.

agent.py
from anthropic import Anthropic

client = Anthropic()

tools = [
  {
      "name": "calculate",
      "description": "Evaluate a basic arithmetic expression, e.g. '3 * (4 + 5)'.",
      "input_schema": {
          "type": "object",
          "properties": {"expression": {"type": "string"}},
          "required": ["expression"],
      },
  }
]

def calculate(expression):            # the real function Claude can't run itself
  return str(eval(expression))      # (fine for a demo; never eval untrusted input in production)

TOOL_FUNCTIONS = {"calculate": calculate}

The loop itself

Here's the whole agent. Read it once, then we'll walk it:

agent.py (continued)
messages = [
  {"role": "user", "content": "What is 15% of 240, then add 12 to the result?"}
]

while True:
  reply = client.messages.create(
      model="claude-opus-4-8",
      max_tokens=1024,
      tools=tools,
      messages=messages,
  )
  messages.append({"role": "assistant", "content": reply.content})  # remember what it did

  if reply.stop_reason != "tool_use":       # no tool wanted → it's the final answer
      print(reply.content[0].text)
      break

  tool_results = []
  for block in reply.content:
      if block.type == "tool_use":
          result = TOOL_FUNCTIONS[block.name](**block.input)   # ACT: run the tool
          tool_results.append({
              "type": "tool_result",
              "tool_use_id": block.id,       # ties the result to the request
              "content": result,
          })

  messages.append({"role": "user", "content": tool_results})     # OBSERVE: hand results back

That's it. The while loop is the agent. Each pass: plan (the model replies), and if it asked for a tool we act (run it) and observe (append the tool_result) — then loop. When it finally answers without a tool, stop_reason is "end_turn" instead of "tool_use", and we break.

Watch it take steps

For that question, Claude can't do the arithmetic reliably in its head, so it uses the tool twice — one full trip round the loop each time:

calculate("240 * 0.15")   -> 36
calculate("36 + 12")      -> 48
15% of 240 is 36, and adding 12 gives 48.

Two tool calls, then an answer — the model decided that sequence itself. You never told it "call calculate twice." That is the agent doing its own planning.

📌Always append the whole response

Notice we append reply.content (every block), not just the text. The tool_use blocks have to stay in the history, or the tool_result you send next has nothing to attach to and the API rejects the turn.

The loop works, but one tool is a calculator, not an agent worth having. Next: give it real tools, and let it choose between them.