Lesson 12 / 27
A Complete Tool Loop With the Anthropic SDK
Define a tool, detect tool_use, return tool_result.
tool_use blocks and tool_result blocks
With Anthropic, tools are listed with name, description and input_schema. A reply with stop_reason == "tool_use" contains one or more tool_use content blocks (each with an id, the tool name and its input dict). You append the whole assistant reply to messages, then add a user message whose content is a list of tool_result blocks, each referring to the matching tool_use_id. Return errors as text in the result (optionally flagged as an error) rather than raising, so the model can recover. Always cap the number of loop steps.
Anthropic tool loop, run
I ran this in a Python virtual environment with the anthropic 1.11.0 and openai 3.22.1 SDKs against a small local stand-in server (shown in the testing topic, saved as mock.py). The server returns canned replies, so no key, network or real model is involved: it proves how the SDK builds requests and handles replies, not what a real model would say. The stand-in server asks for get_order_status on the first call and returns text on the second. The loop prints the requested tool and arguments, runs the local function, and ends with the final answer after 2 API calls.
import json
import mock, anthropic, openai
srv = mock.start(); base = f"http://127.0.0.1:{srv.server_address[1]}" # local stand-in server, not a real API
client = anthropic.Anthropic(api_key="k", base_url=base)
tools = [{"name": "get_order_status", "description": "Look up an order by 6-digit id.",
"input_schema": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}}]
def get_order_status(order_id): return {"order_id": order_id, "status": "shipped"}
messages = [{"role": "user", "content": "Where is order 481516?"}]
for step in range(5): # always cap the loop
reply = client.messages.create(model="demo-model", max_tokens=200, tools=tools, messages=messages)
if reply.stop_reason != "tool_use":
print("final:", reply.content[0].text); break
messages.append({"role": "assistant", "content": reply.content})
results = []
for block in reply.content:
if block.type == "tool_use":
print("model asked for:", block.name, block.input)
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": json.dumps(get_order_status(**block.input))})
messages.append({"role": "user", "content": results})
print("calls to the API:", len(mock.STATE["log"]))
Output:
model asked for: get_order_status {'order_id': '481516'}
final: Your order 481516 has shipped.
calls to the API: 2Return errors as results
If a tool fails, send the error text as the tool result so the model can explain or retry instead of the loop crashing.
Quick check: How does a tool_result block link to the request?
- Through the API key
- Through tool_use_id matching the tool_use block id
- Through the model name
- It does not link
Answer
Through tool_use_id matching the tool_use block id — Each result must reference the id of the call it answers.