Learning / Agentic Operations / Lesson 0006
Anatomy of a Tool Call
One round-trip, by hand: the model proposes, you execute — and this time, you are the tool.
Before you build an agent (next lesson), slow the machinery down to one frame at a time. A
"tool call" sounds like the model reaches out and runs your code. It doesn't. The model emits
structured text saying what it would like run; your code decides whether and how to run it; you
mail the result back. In this lesson you play the part of the code — by hand — so there is
nowhere for that fact to hide. Open a REPL at the repo root with uv run python and
keep it open for all four stages.
Stage 1 · Define the tool
A tool definition is three things: a name, a description, and a JSON Schema for the input. The description is a prompt — it should say when to call the tool, not just what it does:
import anthropic
client = anthropic.Anthropic()
TOOLS = [{
"name": "calculator",
"description": "Evaluate one arithmetic expression. Call this for every "
"arithmetic step; do not do mental math.",
"input_schema": {
"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"],
},
}]
Stage 2 · Trigger the proposal
messages = [{"role": "user", "content": "What is 137 * 41?"}]
response = client.messages.create(
model="claude-opus-4-8", max_tokens=2000,
tools=TOOLS, messages=messages,
)
print(response.stop_reason) # tool_use
block = next(b for b in response.content if b.type == "tool_use")
print(block.id, block.name, block.input)
Stop and look. stop_reason == "tool_use" means the model has paused mid-turn,
waiting on you. The block carries an id (a claim ticket), a name
(which tool), and an input (the arguments). Nothing has been computed. No code has
run anywhere. This is a proposal, written in JSON.
Stage 3 · Be the tool
Normally your code would execute the expression. Today you are the code: work out 137 × 41
yourself, then hand the answer back as a tool_result whose
tool_use_id matches the proposal's id:
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": "5617",
}]})
final = client.messages.create(
model="claude-opus-4-8", max_tokens=2000,
tools=TOOLS, messages=messages,
)
print(final.stop_reason) # end_turn
print(next(b.text for b in final.content if b.type == "text"))
Stage 4 · Break the contract
The id-matching isn't a convention — the API enforces it. Rebuild the same follow-up with a wrong id and watch the request get refused outright:
bad = messages[:-1] + [{"role": "user", "content": [{
"type": "tool_result",
"tool_use_id": "toolu_bogus",
"content": "5617",
}]}]
client.messages.create(model="claude-opus-4-8", max_tokens=2000,
tools=TOOLS, messages=bad)
# anthropic.BadRequestError (HTTP 400): the tool_use id has no matching result
Self-grade
- You can narrate the round-trip — send, propose, execute, append, resend — without notes
- You can say what a tool_use block is — structured text with a claim-ticket id, not an action
- You can predict the failure — what happens when the id doesn't match, and why the API enforces it
Next
Lesson 0007 — Build the Single-Tool Agent: do it in code, with a loop — ship the agent that automates the round-trip you just performed by hand, and log the world it sees on every turn.