Chapter 3 · Part 1
Give it real tools
An agent is only as capable as its tools. A calculator is a nice demo; a useful agent can reach into the real world — read files, look things up, call other services. The loop from the last chapter doesn't change at all. You just add more tools, and the model picks.
Add a couple more
Give the agent the ability to read a file and to look up a (pretend) weather service, alongside the calculator:
tools = [
{
"name": "calculate",
"description": "Evaluate a basic arithmetic expression.",
"input_schema": {
"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"],
},
},
{
"name": "read_file",
"description": "Read a text file from the current folder and return its contents.",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
},
{
"name": "get_weather",
"description": "Get the current weather for a city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
]
def read_file(path):
with open(path) as f:
return f.read()
def get_weather(city):
return f"18°C and sunny in {city}." # a real one would call a weather API
TOOL_FUNCTIONS = {
"calculate": calculate,
"read_file": read_file,
"get_weather": get_weather,
}Because the loop looks tools up by name in TOOL_FUNCTIONS, nothing else changes — it
already runs whichever tool the model asks for. Ask "read trip.txt and tell me the weather
where I'm going," and the agent reads the file, sees the city, then calls the weather tool.
Tool descriptions are the instructions
The model chooses tools almost entirely from their descriptions — that's the interface. Say what a tool does and when to reach for it, in plain language.
- Weak:
"weather"— the model has to guess what it does and when to use it. - Better:
"Get the current weather for a city. Use this whenever the user asks about temperature, rain, or what to wear."
The trigger — when to call it — matters as much as what it does. Vague descriptions are the #1 reason an agent ignores a tool it should have used.
Several tools at once
Ask a question that needs two independent lookups — "What's the weather in Paris and in Tokyo?"
— and Claude may return two tool_use blocks in a single turn. That's not a special case:
your loop already handles it, because it iterates over every block in the reply:
tool_results = []
for block in reply.content: # could be one tool_use, or several
if block.type == "tool_use":
result = TOOL_FUNCTIONS[block.name](**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results}) # ALL results, one messageWhen the model makes several tool calls in one turn, return all of their tool_result
blocks in a single user message — exactly as the loop does. Splitting them across separate
messages quietly teaches the model to stop calling tools in parallel, making it slower.
Your agent now has a toolbox and picks from it on its own. In the next chapter, we hand the whole loop to the SDK so you write far less of this by hand.