Chapter 4 · Part 2
Let the SDK drive the loop
You built the loop by hand so you'd know exactly what an agent is. Now that the gears are no mystery, you don't have to turn them yourself. The Anthropic SDK ships a tool runner that runs the entire plan–act–observe cycle for you — you just hand it your tool functions.
Decorate your functions
Instead of writing a JSON Schema and a Python function for each tool, you write one function
and add the @beta_tool decorator. It builds the schema from your function's signature and
docstring — so the docstring becomes the tool description the model reads:
from anthropic import Anthropic, beta_tool
client = Anthropic()
@beta_tool
def calculate(expression: str) -> str:
"""Evaluate a basic arithmetic expression, e.g. '3 * (4 + 5)'."""
return str(eval(expression))
@beta_tool
def get_weather(city: str) -> str:
"""Get the current weather for a city.
Use this whenever the user asks about temperature, rain, or what to wear.
"""
return f"18°C and sunny in {city}."Hand them over and let it run
Now pass the decorated functions to client.beta.messages.tool_runner. It calls the model, runs
whatever tools it asks for, feeds the results back, and loops — until Claude is done. Iterating
the runner gives you each message as it happens:
runner = client.beta.messages.tool_runner(
model="claude-opus-4-8",
max_tokens=1024,
tools=[calculate, get_weather],
messages=[{"role": "user", "content": "What is 15% of 240, and what's the weather in Paris?"}],
)
for message in runner:
if message.content and message.content[-1].type == "text":
print(message.content[-1].text)That's the same agent as the last three chapters — the loop, the tool calls, the results handed
back — in a few lines. Everything you wrote by hand is still happening; the runner just does it
for you. If you only want the finished answer and not the play-by-play, call
runner.until_done() instead of looping.
The runner is what you'd reach for in a real app. But it would have been a black box if we'd
started here — you'd know that agents work, not how. Now you know precisely what
tool_runner automates, which means you'll know what to do when it does something surprising.
The tool runner lives under client.beta.messages because it's still a beta feature — the name
and shape can change. The plain client.messages.create loop you wrote by hand is the stable
foundation underneath it, and it isn't going anywhere.
Your agent is real and compact. But letting a model run tools on its own raises an obvious question: what stops it from doing something you didn't want? That's next.