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:

agent.py — the tool-runner version
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:

agent.py (continued)
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.

📌Why build the loop first, then use this?

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.

⚠️It's a beta helper

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.