The tool-use loop
Build the core agent loop: call Claude, run any tools it requests, feed results back, repeat until it answers.
An agent is a loop. You send the conversation to Claude with the available tools. If it wants a tool, it says so; you run the tool and send the result back; it may want another; eventually it produces a final answer. This is the manual tool-use loop, and it's about 30 lines of Python.
import anthropic
from tools import TOOLS, run_tool
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
MODEL = "claude-opus-4-8"
def answer(messages):
while True:
response = client.messages.create(
model=MODEL,
max_tokens=4096,
thinking={"type": "adaptive"},
system=SYSTEM_PROMPT,
tools=TOOLS,
messages=messages,
)
# Keep the full assistant turn (text + tool_use blocks) in history.
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return "".join(b.text for b in response.content if b.type == "text")
# Run every tool the model asked for; return all results together.
results = []
for block in response.content:
if block.type == "tool_use":
output = run_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id, # must match the tool_use block
"content": output,
})
messages.append({"role": "user", "content": results})The shipped file wraps this same loop in a class: AnalyticsAgent.answer(user_message, history) returns a dict with the reply text, the updated messages history, and the tool_calls it made — and it caps the loop at max_turns=8 instead of while True, so a confused model can't spin forever. That class is what the eval suite (Module 7) and the web app (Module 8) import.
The three rules that make it work
- Check `stop_reason`.
"tool_use"means run tools and continue;"end_turn"means the model is done — return its text. - Append the full `response.content`. It contains the tool_use blocks (and thinking); the next request needs them for context.
- Match `tool_use_id`. Each tool_result must carry the id of the tool_use it answers, so the model knows which result is which.
thinking: {type: "adaptive"} lets the model reason before deciding which metrics to pull — useful for multi-step questions ("compare CAC and AOV by channel and tell me which channels are efficient"). The loop preserves those thinking blocks automatically because it appends the whole content.
A typical question triggers one or two tool calls: maybe list_metrics to check names, then query_metrics for the numbers. The loop handles any number of calls without you writing branch logic — that's the elegance of the pattern.