Lesson 2 - Handling tool_use and tool_results

Welcome to Handling tool_use and tool_results

In Lesson 1 you learned to read Claude’s response: when stop_reason is "tool_use", the content holds a tool_use block — a structured request to run a tool, paused and waiting for you. That’s only half the exchange. A request that never gets answered leaves Claude stuck mid-thought. This lesson closes the loop on a single tool call: you’ll take the tool_use block, run the matching real function, package the return value as a tool_result, and hand it back so Claude can finish its answer. The same four steps repeat in a loop for multi-step agents — that’s the next lesson — but everything starts with getting one round trip exactly right.

By the end of this lesson, you will be able to:

  • Find the tool_use block in response.content and run the function it names with block.input
  • Build a tool_result block whose tool_use_id matches the request’s id
  • Shape the next request: append the assistant turn, then a user turn carrying the result
  • Call the model again so it can answer using the tool’s output

You already know how to read a tool_use block. Now you’ll learn how to answer it.


Running the Requested Tool

The tools you give Claude are descriptions, but somewhere you have the real Python functions behind them. Here are Atlas’s two tools — ordinary functions that take arguments and return strings:

def get_weather(city):
    table = {"Kyoto": "16°C, clear and crisp", "Lisbon": "19°C, sunny"}
    return table.get(city, "no data for that city")

def convert_currency(amount, from_currency, to_currency):
    rates = {("JPY", "USD"): 0.0067, ("USD", "EUR"): 0.92}
    rate = rates[(from_currency, to_currency)]
    return f"{amount} {from_currency} = {round(amount * rate, 2)} {to_currency}"

TOOL_FUNCTIONS = {"get_weather": get_weather, "convert_currency": convert_currency}

The TOOL_FUNCTIONS dictionary is the bridge between the tool’s name (a string Claude sends back, like "get_weather") and the function you actually call. When a tool_use block arrives, block.name is the key, block.input is a dict of arguments, and TOOL_FUNCTIONS[block.name](**block.input) runs the right function with those arguments unpacked. Because block.input matches the tool’s input_schema, the keys line up with the function’s parameters — that’s why the schemas in Lesson 1 mattered.


Building the tool_result Block

Once you’ve run the function, you wrap its return value in a tool_result block — a plain dictionary with three keys:

{
    "type": "tool_result",
    "tool_use_id": block.id,        # MUST match the tool_use block's id
    "content": str(result),
}

Two things make this block work. First, tool_use_id must equal the id of the tool_use block you’re answering (block.id from Lesson 1, e.g. "toolu_01..."). This id is the wire that connects request to result: Claude requested a tool and tagged it with an id, and your result echoes that exact id so Claude knows which request this answers. A mismatched or missing id breaks the link and the API rejects the request.

Second, content is a string (or a list of content blocks for richer results — but a string is the common case). Function return values are often other types — numbers, dicts, custom objects — so wrap them with str(...) to be safe. Here the helpers already return strings, but str(result) keeps the code correct no matter what a tool returns.


Shaping the Messages and Calling Again

This is where most first attempts go wrong — not in the tool_result block itself, but in where it goes in the conversation. The order is fixed: append the assistant turn (Claude’s full response, which contains the tool_use block), then a user turn whose content is your list of tool_result blocks. Here’s the complete single-step handler:

# 1. Claude responded with stop_reason == "tool_use".
#    Append the assistant turn (it contains the tool_use block):
messages.append({"role": "assistant", "content": response.content})

# 2. Run each tool_use block and collect tool_result blocks:
tool_results = []
for block in response.content:
    if block.type == "tool_use":
        result = TOOL_FUNCTIONS[block.name](**block.input)     # run the real function
        tool_results.append({
            "type": "tool_result",
            "tool_use_id": block.id,        # MUST match the tool_use block's id
            "content": str(result),
        })

# 3. Send the results back as a NEW user message:
messages.append({"role": "user", "content": tool_results})

# 4. Call the model again — now it can answer using the result.
response = client.messages.create(
    model="claude-haiku-4-5", max_tokens=1024,
    system=system, tools=tools, messages=messages,
)

Walk through what each step does. Step 1 records what Claude said — passing response.content (the whole list of blocks, including the tool_use block) keeps the conversation faithful; Claude needs to see its own request in the history. Step 2 loops over the content because a turn can hold more than one tool_use block (you’ll lean on that in Lesson 4); for now there’s just one, and you collect a tool_result for each. Step 3 wraps the results in a single user message — tool results always come back from the "user" role, since you are answering Claude. Step 4 re-sends the whole conversation, now four-deep, so the model can read its result and respond.

After step 4, Claude typically returns stop_reason == "end_turn" with a plain text answer that uses the tool’s output (for a weather lookup it might say something like “Kyoto is 16°C, clear and crisp.” — the exact wording varies, since that text is the model’s to write). The transcript now holds four messages: user (the question) → assistant (the tool_use request) → user (your tool_result) → assistant (the final answer). That four-message shape is the anatomy of one completed tool call.

The two message-shape rules people get wrong

Two ordering rules trip up nearly everyone the first time, and both cause API errors:

  1. Append the assistant turn before the tool_result. The tool_result has to follow the assistant message that contains the matching tool_use block. Skip the assistant turn and jump straight to a user message of tool_results, and the API has no tool_use to attach them to.
  2. tool_use_id must match the tool_use block’s id. Every tool_result must echo the exact id of the request it answers. A mismatched id (or a tool_use with no corresponding tool_result) is rejected.

In short: assistant turn first (with the tool_use block), then the user turn carrying tool_results, with ids that line up.


Practice Exercises

A tool_use block came back with block.id == "toolu_01Xy" and block.name == "get_weather". You run the function and get "16°C, clear and crisp". Write the tool_result block, and say which field must carry "toolu_01Xy".

Hint

{
    "type": "tool_result",
    "tool_use_id": "toolu_01Xy",   # must match block.id
    "content": "16°C, clear and crisp",
}

The tool_use_id is the link. It must equal the id of the tool_use block you’re answering — that’s how Claude knows which request this result belongs to. The content is a string (here the function already returned one; otherwise wrap it with str(...)).

Exercise 2: Which role carries the tool_result?

You’ve built your tool_results list. Should it go in a message with "role": "assistant" or "role": "user", and why?

Hint

It goes in a "user" message: messages.append({"role": "user", "content": tool_results}). The assistant turn is Claude’s request (the tool_use block); you are supplying the answer, so the result comes back from the user role. The conversation alternates: assistant asks (tool_use), user responds (tool_result).

Exercise 3: What if you forget the assistant turn?

A learner runs the tool and appends only the user message with the tool_results, skipping step 1. The next messages.create call fails. What went wrong?

Hint

A tool_result must follow the assistant message that contains the matching tool_use block. Without appending {"role": "assistant", "content": response.content} first, there’s no tool_use block in the history for the result’s tool_use_id to point at, so the API rejects the request. Always append the assistant turn (with its tool_use block) before the user turn carrying the tool_results.


Summary

Closing the loop on a single tool call is four steps. When stop_reason is "tool_use", you (1) append the assistant turnresponse.content, which carries the tool_use block — so Claude’s request stays in the history; (2) run the requested function, looking it up by block.name and calling it with block.input; (3) build a tool_result block{"type": "tool_result", "tool_use_id": block.id, "content": str(result)} — where tool_use_id matches the request’s id and content is a string; and (4) append that result as a user message and call the model again. Claude reads the result and finishes with a text answer (stop_reason == "end_turn"). The completed exchange is four messages: user → assistant(tool_use) → user(tool_result) → assistant(final).

Key Concepts

  • tool_result block{"type": "tool_result", "tool_use_id": ..., "content": <string>}, the answer to one tool_use request.
  • tool_use_id linkage — the result’s tool_use_id must equal the tool_use block’s id.
  • Message shapes — append the assistant turn (with the tool_use block) first, then a user turn whose content is the list of tool_result blocks.
  • content is a string — wrap function output with str(...) (or use a list of content blocks for richer results).

Why This Matters

A tool_use request that never gets a result is a dead end — Claude paused to act and can’t continue. The handling pattern here is what turns that pause into a finished answer, and it’s the exact body of the agent loop you’ll write next. Get the message shapes right once — assistant turn, then user turn with matching ids — and the same code scales from one tool call to a model that calls tools repeatedly until the task is done.


Next Steps

Continue to Lesson 3 - Building the Agent Loop

Wrap the run-and-return pattern in a loop so Claude can call tools repeatedly until the task is done.

Back to Module Overview

Return to The Agent Loop with Claude module overview


Continue Building Your Skills

You can now complete one full tool-use round trip: detect the request, run the function, return a matching tool_result, and let Claude answer. Next you’ll generalize this into the agent loop — repeating the run-and-return cycle until stop_reason is "end_turn", so Claude can chain several tool calls together to finish a multi-step task.