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.
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:
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 backThat'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.
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.