Learning / Agentic Operations / Lesson 0006

Agentic Operations · Lesson 0006 · Level 1 · Foundations · Hands-on lab

Anatomy of a Tool Call

One round-trip, by hand: the model proposes, you execute — and this time, you are the tool.

Your win for this lesson: one complete tool-call round-trip executed manually in a Python REPL — no loop, no framework, no tool code. Prerequisites: Lesson 0001 and Lesson 0005 (your key is set up). Companion reference: The Agent Loop & Tool-Call Round-Trip.

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"],
    },
}]
Pass: you can say what each of the three parts is for, and why the description says "do not do mental math" instead of just "evaluates arithmetic."

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.

Pass: you can point at the id, the name, and the input — and say out loud what has not happened yet.

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"))
Pass: the model answers using your number. You just performed, by hand, every step your agent code will automate in Lesson 0007: execute the proposal, append the assistant content verbatim, append a matching tool_result, call again.

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
Pass: you get a 400 error, not a confused answer. Every proposal must be answered by id — that enforced contract is what makes the loop in the reference sheet reliable.
The load-bearing sentence: the model only ever proposes; execution always happens on your side of the wire — which is why reliability (bounded loops, errors returned as results) and security (never trust the model's input to a tool) are your code's job, not the model's.

Self-grade

You own this lesson when:

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.