The Tool Use Loop

How a Tool Call Flows

1. you  → model:  messages + tools
2. model → you:   stop_reason "tool_use", content: [text?, tool_use {id, name, input}]
3. you:           run the function with input
4. you  → model:  append assistant content, then a user message with
                  tool_result {tool_use_id, content}
5. model → you:   final text answer (stop_reason "end_turn") ... or another tool_use → go to 3

The Loop with a Scripted Model

Example

A scripted stand-in model returns the same block shapes as the real API, so the loop code is the real pattern:

import json

ORDERS = {"SE-10423": {"status": "out for delivery", "eta": "today by 8 pm"}}
def get_order_status(order_id):
    return ORDERS.get(order_id, {"error": "order not found"})
TOOLS = {"get_order_status": get_order_status}

def scripted_model(messages):
    """Stand-in for the model. Turn 1: ask for a tool. Turn 2: answer from the tool result."""
    last = messages[-1]["content"]
    if isinstance(last, str):
        return {"stop_reason": "tool_use", "content": [
            {"type": "text", "text": "Let me check that order."},
            {"type": "tool_use", "id": "toolu_01", "name": "get_order_status", "input": {"order_id": "SE-10423"}}]}
    result = json.loads(last[0]["content"])
    return {"stop_reason": "end_turn", "content": [
        {"type": "text", "text": f"Your order is {result['status']} and should arrive {result['eta']}."}]}

messages = [{"role": "user", "content": "Where is my order SE-10423?"}]
while True:
    response = scripted_model(messages)
    messages.append({"role": "assistant", "content": response["content"]})
    if response["stop_reason"] != "tool_use":
        break
    results = []
    for block in response["content"]:
        if block["type"] == "tool_use":
            output = TOOLS[block["name"]](**block["input"])
            print(f"tool call: {block['name']}({block['input']}) -> {output}")
            results.append({"type": "tool_result", "tool_use_id": block["id"], "content": json.dumps(output)})
    messages.append({"role": "user", "content": results})

print("final answer:", response["content"][0]["text"])
print("messages in conversation:", len(messages))

Output

tool call: get_order_status({'order_id': 'SE-10423'}) -> {'status': 'out for delivery', 'eta': 'today by 8 pm'}
final answer: Your order is out for delivery and should arrive today by 8 pm.
messages in conversation: 4

The Same Loop with the Claude API

import json, anthropic

client = anthropic.Anthropic()
tools = [{
    "name": "get_order_status",
    "description": "Get the live status of a ShopEase order. Call this whenever the customer asks about an order.",
    "input_schema": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]},
}]

messages = [{"role": "user", "content": "Where is my order SE-10423?"}]
while True:
    response = client.messages.create(model="claude-sonnet-5-5", max_tokens=1000,
                                      system="You are ShopEase support.", tools=tools, messages=messages)
    messages.append({"role": "assistant", "content": response.content})
    if response.stop_reason != "tool_use":
        break
    results = []
    for block in response.content:
        if block.type == "tool_use":
            output = TOOLS[block.name](**block.input)
            results.append({"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(output)})
    messages.append({"role": "user", "content": results})

print(next(b.text for b in response.content if b.type == "text"))

Run it

python order_tool.py

Example output

Good news! Your order SE-10423 is out for delivery and should reach you today by 8 pm.

Tip: The Python SDK also has a tool runner (client.beta.messages.tool_runner(...) with @beta_tool functions) that runs this loop for you. Learn the manual loop first so you understand what it does.