Chapter 21

Tool use

Tool use, how production agents call functions, query databases, browse the web, and control applications. From ReAct's prompting-era pattern (Yao 2022) to modern function-calling APIs (OpenAI, Anthropic, Google), tool schemas (JSON Schema), constrained decoding for structured tool calls, the multi-step agent loop, multi-tool routing strategies, observation and error handling, Anthropic's Model Context Protocol (MCP), and computer use as the frontier. The chapter that turns reasoning into agency.

Chapter 20 ended with reasoning models that can think for minutes before answering, but they can only describe what they would do, not do it. A reasoning model that cannot act on the world is limited. Tool use removes that limit. Modern LLMs are trained to emit structured calls to external functions; the surrounding system executes those calls; the results flow back into the model’s context. The model decides when to invoke a tool and which tool to invoke. This is the bridge from chat to agency.

The conceptual pattern is ReAct (Yao 2022), which Chapter 20 §4 introduced: think → act → observe → repeat. This chapter is about turning that pattern into production engineering. Function-calling APIs from OpenAI, Anthropic, and Google standardized the structured-call interface. JSON Schema specifies what arguments each tool takes. Constrained decoding from Chapter 19 guarantees that the model’s tool calls are well-formed. The agent loop iterates think-act-observe until the model produces a final answer. Multi-tool routing, observation handling, and error recovery turn the basic loop into a robust system. Anthropic’s Model Context Protocol (MCP) standardizes how tools and clients communicate. Computer use generalizes tool calls to any application’s UI.

Every production LLM agent runs on this stack, customer support bots, autonomous coding agents, retrieval pipelines, Claude with tool use, GPT with function calling, AI assistants embedded in IDEs and operating systems. By the end of this chapter you should know how to design and deploy a tool-using system, where the production failure modes are, and which protocols are likely to matter in 2025 and beyond. Tool use is reasoning made effectual, the capability that turns a model into an agent.

From ReAct to tools

The bridge from Chapter 20

Chapter 20 §4 introduced the ReAct pattern: the model emits a Thought, then an Action, receives an Observation, and continues with the next Thought. The loop runs until the model emits a final answer. That pattern is the oldest idea in this chapter, yet it has held up unusually well: every modern agent system still runs some form of Thought → Action → Observation underneath, regardless of which model or framework is in use, even as the API surface around it has churned repeatedly.

What has changed is everything under the pattern. In 2022 (the ReAct era), actions were emitted as natural-language strings, Action: search("population of France"), and the surrounding system parsed them with regex or heuristics. Brittle on a good day, broken on a bad one. In modern production (2024 – 2025), actions are emitted as structured JSON matching a declared schema; the system parses with a JSON parser; constrained decoding (Chapter 19 §8) guarantees that the call is well-formed token by token. The structural reliability is no longer a tuning concern; it is a property of the decoder.

What changed

The conceptual loop, think → act → observe → repeat, is identical. The grounding effect is identical: observations are real, not fabricated, so the model’s reasoning is anchored in actual tool outputs rather than its own priors. The model’s autonomy in choosing when and which tool to invoke is identical. What is new is production-grade infrastructure: schemas, validators, dispatchers, retry logic, monitoring, idempotency guards, audit logs, and the interoperability protocols this chapter closes with.

The empirical scale tells the same story. Toolformer (Schick 2023) demonstrated self-supervised tool-use training across a small fixed set of five tools, a calculator, a question-answering system, a search engine, a translation system, and a calendar, improving downstream task accuracy without degrading the model’s core language ability. GPT-4 with function calling (June 2023) reached 85\sim 8595%95\% on standard tool benchmarks, with user-supplied tools rather than a fixed catalog. The modern frontier in 2025 routinely runs agentic loops across hundreds of tools and dozens of steps, hitting 70\sim 7085%85\% on complex multi-step tasks (TaskBench, ToolBench), lower than single-call benchmarks because the loop has more chances to fail. The benchmark built specifically for this multi-turn regime is τ-bench (Yao et al. 2024, arxiv.org/abs/2406.12045), which drops an agent into a simulated customer-service environment with a scripted user simulator and a policy the agent must follow, then scores whether the end state is policy-compliant rather than whether any individual call was well-formed. Single-call tool selection has its own standard, the Berkeley Function-Calling Leaderboard (gorilla.cs.berkeley.edu/leaderboard.html). Chapter 30 returns to tool-use evaluation directly.

Training tool use

Tool-calling accuracy is not a property of clever prompting; it is trained in with the same SFT machinery Chapter 13 covered. A base model handed a tool schema for the first time in its life produces malformed calls, ignores tools it should use, and invents tools it should not, no matter how well the schema is written. Reliable tool use requires the model to have already seen many examples of exactly this shape: a conversation, a set of schemas, a well-formed call, an observation, and a continuation that uses the observation correctly.

The training data for this is built, not collected. Synthetic generation produces the raw supply of traces: a model, often a stronger one, or the base model itself in a self-supervised loop, is sampled against tool schemas to produce candidate call-observation-continuation sequences. Toolformer’s (Schick 2023) mechanism is a clean example: insert a candidate API call into existing text, run it, and keep the insertion only if it lowers the perplexity of the text that follows. Filtering is where the real work happens: a candidate trace is kept only if the call is well-formed and the tool actually helped, the calculator call lowered the perplexity of the correct continuation, the retrieved document contained the fact the answer needed. Traces where the tool was unnecessary, wrong, or actively hurt the final answer get discarded, not “corrected,” bad traces teach the wrong lesson no matter how they are patched. SFT then trains on the filtered set exactly as Chapter 13 describes: response-only loss masking over (context, response) pairs, except here the “response” alternates between a structured tool call and a text continuation conditioned on the tool’s observation. The model is not reasoning its way into tool use fresh at every inference call; it has seen thousands of instances of “when the context looks like this, the right move is a call, not text.”

Function calling APIs

The interface convention

The convention was established by OpenAI’s June 2023 function-calling release and the rest of the industry converged on it within a year. The model is given a list of available tools, each with a name, a description, and a JSON-Schema parameter spec, along with the user’s request and the conversation history. The model emits either a regular text response (no tool needed) or a structured tool call that names one of the available tools and includes a JSON-encoded argument object. The system parses the call, executes the corresponding function, and returns the result as a tool message in the conversation. The model continues with the result in context, possibly emitting more tool calls, until it produces a final text answer.

model(context,schemas)    either text response or {name,args (JSON conforming to schema)}\text{model}(\text{context}, \text{schemas}) \;\to\; \text{either text response or } \{\text{name}, \text{args (JSON conforming to schema)}\}

(21.tool-call)

Provider variations

The differences across providers are small and shrinking. OpenAI uses a tools array on the request, a tool_choice parameter to bias selection, and a tool_calls field on the response. Anthropic uses a tools array as well, but the response contains tool_use content blocks rather than a separate field, and parallel tool calls, multiple tool invocations in a single response, are supported natively. Google Gemini uses the term “function declarations” but the shape is the same. All three converged on JSON Schema as the parameter specification language, which means a tool definition written for one provider is mechanically convertible to the others. The cross-provider portability story was different even a year ago; today it is real.

Why structured beats natural-language

The structural shift from ReAct-era string parsing to modern JSON tool calls is not cosmetic. Reliability is the headline: a JSON parser either succeeds or fails cleanly, while regex parsing of natural language is a long tail of edge cases that breaks in production six months after launch. Programmatic dispatch collapses from many steps (parse → extract intent → match function → marshal arguments → call) into one: parse the JSON, look up the function by name, splat the arguments. Type safety at the parameter level means bad calls fail at the API boundary with a clear error, not somewhere deep inside the tool. Composability is the most underrated benefit, when calls are structured uniformly, the agent loop can iterate over them without special-casing each tool’s parsing quirks.

The Chapter 19 bridge is what makes the structural guarantee bite. FSM masking (Ch 19 §8) constrains the decoder at each step to emit only tokens that continue a valid JSON object matching the declared schema. The model cannot emit a "location" field where an integer is required, cannot emit unbalanced braces, cannot emit a string that does not match a declared enum. Structural validity is guaranteed at the token level; the model never gets the chance to produce malformed output. Semantic correctness is still on the model: it can still pass location="Atlantis" to a weather tool, but the bytes are always well-formed.

The OpenAI shape

The example above is Anthropic’s wire format. OpenAI encodes the same round trip differently in two places. The call rides inside the assistant message’s tool_calls array, and the arguments are a JSON-encoded string, not a parsed object, the caller has to json.loads it before use. The result is not a content block nested inside a user message; it is its own message with role: "tool", addressed back to the call it answers via tool_call_id.

The field names and nesting differ; the principle underneath does not, a structured call goes out, a structured result comes back, and the model conditions on the result. Code written against one shape ports to the other by relabeling fields, not by rethinking the pattern.

Tool schemas

Anatomy of a schema

A tool schema has three parts. Name is a stable identifier, get_weather, search_database, send_email, used by the dispatcher to match the model’s call to the underlying function. Description is natural-language guidance: a short paragraph that tells the model what the tool does, when to use it, and what it returns. The description is the model’s primary signal for tool selection and the place where schema quality lives or dies. Parameters is a JSON Schema object describing the arguments, types, enums, required vs optional, ranges, and per-parameter descriptions.

A well-formed schema for a knowledge-base search tool:

name: search_database
description: |
  Search the company knowledge base for documents matching a query.
  Returns up to 5 most relevant documents with titles and excerpts.
parameters:
  type: object
  properties:
    query:
      type: string
      description: "The search query. Be specific."
    max_results:
      type: integer
      default: 5
      minimum: 1
      maximum: 10
  required: ["query"]

Schema best practices

The model picks tools by reading their descriptions; vague descriptions produce bad tool selection. Be concrete in the first sentence, what the tool does and what it returns. Use enums wherever a string parameter has a known set of valid values; unit: ["celsius", "fahrenheit"] is strictly better than unit: string because the constraint propagates into the schema and the model gets a stronger signal. Mark only truly-required parameters as required; the agent loop has to fill in every required field, so over-marking creates failure modes. Include short examples in the description when the input format is non-obvious, “Examples: ‘pricing changes’, ‘API authentication’” outperforms a parameter description that says only “the query.” Avoid overlapping tools: two tools whose descriptions are interchangeable will produce inconsistent selection, and the model will pick whichever one is mentioned first in context.

The production sweet spot for tool descriptions is 50 to 200 characters, plus one or two example inputs when helpful. Below 50 characters the description is usually too thin to disambiguate from neighboring tools; above 200 characters the description bloats the context window without proportionate selection accuracy. Each tool’s description is paid for at every model call in the conversation, so the per-tool budget compounds when the catalog grows.

Tool schema validator

Interactive
Tool schema validator
See what passes structural validation, and what doesn't
Pick a schema:
Schema definition
{
  "name": "get_weather",
  "description": "Get current weather for a location.",
  "input_schema": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "City name, e.g., \"San Francisco\" or \"Tokyo\"."
      },
      "unit": {
        "type": "string",
        "description": "Temperature unit.",
        "enum": [
          "celsius",
          "fahrenheit"
        ],
        "default": "fahrenheit"
      }
    },
    "required": [
      "location"
    ]
  }
}
Try a tool call:
All required fields present; types correct; enum value valid.
Tool call (what the model would emit)
{
  "name": "get_weather",
  "input": {
    "location": "Tokyo",
    "unit": "celsius"
  }
}
✓ Valid
This call would pass the API's structural validation. Constrained decoding (Ch 19) guarantees this structure at generation time.
Click through the cases. Valid calls (cyan ✓) have all required fields, correct types, and values within range. Invalid calls (rose ✗) fail at the API layer with structured errors; the model gets these as observations and recovers. Constrained decoding (Ch 19) prevents most invalid calls at generation; semantic correctness(e.g., "Atlantis" isn't a real city) still requires tool-level validation (Section 6's idempotency and error-recovery patterns).

Three preset tool schemas (get_weather, search_database, create_event); multiple example tool calls per schema covering valid and invalid cases. Watch what structural validation catches: missing required fields, wrong types, out-of-range values, invalid enum choices, unknown tool names. The widget makes 'what constrained decoding guarantees' tangible, and what tool-level validation still has to handle.

The agent loop

Single tool call

The simplest production pattern is one tool call per query. The user asks a question requiring one piece of external information; the model emits a tool call; the system executes; the result is returned; the model produces its final answer. This is enough for most weather, calculator, and database-lookup scenarios, a large fraction of real production tool use is exactly this shape. The rest needs the loop.

Multi-step loop

When a single tool call cannot answer the question, multi-step math, a research task with sequential lookups, a workflow that branches based on intermediate results, the agent loop iterates. The structure is short and surprisingly general:

while not done:
    response = model.generate(context, tools)
    if response.is_text():
        return response.text   # final answer; terminal
    tool_call = response.tool_call
    result = execute(tool_call)
    context.append(tool_call)
    context.append(result)

Termination comes from one of three places. Natural termination: the model emits a text response with no tool call, signaling that it has enough information to answer. This is the desired outcome. Limit termination: the loop hits a max_iterations cap, typically 1010 to 5050 steps, to prevent runaway loops on poorly-formed queries. Error termination: an unrecoverable error in tool execution, or a permission denial the model cannot pivot around. The loop layer owns retries and backoff for transient failures; the tool layer owns its own correctness.

Parallel tool calls

Anthropic and OpenAI both support emitting multiple tool calls in a single response. When the calls are independent, three different searches for three different facts, they execute in parallel and the results come back together in the next message. This is a meaningful speedup for the right shape of query: a five-step sequential loop with 500500ms per call is 2.52.5s of latency, while the same five calls in parallel are 500500ms. The model is usually good at distinguishing, it emits parallel calls when the calls are independent and serial calls when one depends on another. Do not force parallelism: if tool B needs tool A’s output, parallelizing them just wastes B’s call.

loop until terminal: modelaction(s)executeobservation(s)model\text{loop until terminal: } \text{model} \to \text{action(s)} \to \text{execute} \to \text{observation(s)} \to \text{model} \to \ldots

(21.agent-loop)

Agent loop trace

Interactive
Agent loop trace
A real multi-step tool-use sequence · step through to see the loop
User asks
"What's the weather in Tokyo right now, and what would that be in Celsius?"
Step 0 of 7 · waiting
Conversation
Click Next ▶ to begin the trace.
What's happening
Click Next to begin the trace.
Walk through the loop with Next ▶. Watch the conversation build: Thought → Action → Observation → Thought → Action → Observation → Final. Each Action is a structured JSON tool call (the API convention from Section 2); each Observation grounds the next Thought in real data. This is Chapter 20's ReAct pattern made production: the loop that every modern agent runs on.

Step through a real multi-step agent loop. User asks for Tokyo's weather in Celsius; the model emits a Thought, calls get_weather, receives an Observation, emits another Thought, calls calculator, receives another Observation, then produces a Final answer. The conversation builds up step by step. Each Action is a structured JSON tool call; each Observation grounds the next Thought. This is Ch 20's ReAct pattern in production form, the loop every modern agent runs on.

The agent loop with a mock model and two mock tools is short enough to read end-to-end:

Multi-tool routing

The selection problem

At 5 to 10 tools, the model reads every description and picks the right one. Selection accuracy is high; this is the default mode. At 50 or more tools, the descriptions consume too much context, even at 200 characters each, 5050 tools is 10,00010{,}000 characters of schema per call, every call, in addition to the conversation. At 500 or more tools, the model gets confused; similar tools collide; accuracy drops below the threshold where the agent loop can recover. Production agents with large tool catalogs need a routing strategy.

Production patterns

Description-based routing is the default and works at 555050 tools. The model sees all descriptions and picks; no preprocessing needed. Hierarchical routing classifies the request into a tool category first, then picks a specific tool within that category, a useful structure at 5050500500 tools, especially when the catalog has natural groupings (search tools, write tools, communication tools). Tool retrieval, RAG for tools, embeds each tool’s description; at each step the system retrieves the top-NN most-relevant tools and presents only those. This is the required pattern at 100+100+ tools, and the same vector-store machinery that Chapter 22 covers for document RAG applies directly. RBAC and permissions hide tools the user or agent does not have access to; this is non-negotiable for production safety, regardless of catalog size.

The failure modes are predictable. Overlapping descriptions, search_documents vs find_files, produce inconsistent selection, with the model picking whichever description matches the query’s surface phrasing rather than its intent. Vague descriptions, “fetch data”, give the model no signal about which data, so the routing falls through to whichever tool was listed first. Hallucinated tools: the model emits a call for a tool that does not exist, happen rarely but reliably; the recovery pattern is to return a structured error listing valid tool names, and the model almost always picks one of those on the next turn.

Observation handling and error recovery

Observations vary in size

A web search might return 5050KB of HTML. A database query might return ten thousand rows. A calculator returns one number. Feeding all of that back into the model’s context unmodified would burn the context window in two or three steps. Truncation caps the observation at NN tokens and appends ...truncated; cheap and good enough for many cases. Summarization uses a smaller model to compress the observation before feeding it back, useful when the raw output is structured (search results, log lines) and the summary preserves the parts that matter. Structured extraction returns only the relevant fields, {title, snippet} per search result rather than full HTML; {rows_returned, top_5} rather than the full result set. The right strategy depends on the tool; the discipline is to not let observation size grow without limit.

Errors are observations too

When a tool fails, return the error message as the observation. The model often recovers gracefully:

Action: get_weather(location="Atlantis")
Observation: Error: location not found.
Thought: Atlantis is not a real location. Let me ask the user to clarify.
Final answer: I couldn't find weather for "Atlantis". Could you specify a different city?

The common error patterns are tractable in roughly this order of difficulty. Bad parameters are caught at schema validation and rarely escape to the tool; constrained decoding from Chapter 19 prevents most of them. Missing data: the tool returns empty, is handled by the model, which either rephrases the query or asks the user. Service unavailable transient failures are handled at the loop layer with retry and backoff; the model never sees them unless the retries are exhausted. Permission denied errors are surfaced to the model, which can pivot to a different approach or ask the user for elevated access. Rate limiting is similar, the model can decide to wait, pivot to a less-rate-limited tool, or give up and report.

The structural pattern is the same in every case: an error becomes an observation, the observation enters the loop, and the model decides what to do next. This is agent resilience handled at the loop layer, not the tool layer. Tools should fail informatively; the loop should treat failure as data.

MCP and modern protocols

The interoperability problem

Through 2023 and most of 2024, every LLM integration was a custom build. Connecting Claude to a Postgres database meant writing custom function-calling glue for Claude’s tool API; connecting GPT to the same database meant writing the same glue for OpenAI’s API; the work did not transfer. Multiply across providers, across tools, across teams, and the duplicate effort across the ecosystem was substantial. The conditions were exactly right for a standard.

Anthropic’s Model Context Protocol

MCP (Anthropic, late 2024) is a standard interface between LLM clients and tool/data servers. Servers expose tools, resources, and prompts through the protocol; any MCP-compatible client can talk to any MCP-compatible server. A database team writes one MCP server; any MCP-compatible client, Claude Desktop, a VS Code plugin, a third-party agent framework, can use it without bespoke integration. The transferability is the breakthrough; one tool server, many LLM clients.

The protocol mechanics are deliberately small. MCP runs JSON-RPC over stdio or HTTP. Capabilities negotiation lets a client ask the server “what tools, resources, and prompts do you offer?” and the server replies with a manifest. Tool invocation uses a tools/call method with a name and an argument object; the server executes and returns the result. Resource access exposes read-only data via URIs, useful for context (files, documents, database snapshots). Prompt templates let the server provide reusable prompt scaffolding that the client can fetch and present to the model.

Adoption in early 20252025 is broad enough that engineers building tool-using systems will encounter MCP regularly. Claude Desktop ships MCP clients out of the box. VS Code, Cursor, and other IDEs include MCP plugins. Third-party MCP servers exist for Git, GitHub, Slack, Postgres, Notion, Linear, Asana, and dozens more. The trajectory is the trajectory of LSP (Language Server Protocol) for code editing, an emerging standard whose ecosystem effect is the point.

Exercises

Four exercises that lock in the production tool-use stack. Each is a self-contained problem with a starting template; hints are collapsed by default; try the problem first.

The exercises trace the chapter’s arc: define a schema (Ex 1) → run the agent loop (Ex 2) → route across many tools (Ex 3) → handle side effects safely (Ex 4).

Exercise 1 (easy): Define a tool schema

Convert a Python function signature into a JSON-Schema-style tool schema. Cover name, description, required vs optional parameters, and at least one enum.

Hint

A tool schema needs:

  • name: a stable identifier (snake_case typical)
  • description: tells the model when to use this tool
  • input_schema.properties: the parameter definitions
  • input_schema.required: list of required parameter names

For each parameter, include:

  • type: “string” / “integer” / “number” / “boolean”
  • description: tells the model what value to pass
  • enum: if the value must be one of a closed set
  • minimum / maximum: for numeric ranges
  • default: for optional parameters
Solution

title is required (the function has no default for it); duration_minutes and urgency are optional with defaults, and urgency gets an enum since the docstring names a closed set of values.

schedule_meeting_schema = {
    "name": "schedule_meeting",
    "description": "Schedule a meeting on the user's calendar. Use when the user asks to book, set up, or add a meeting.",
    "input_schema": {
        "type": "object",
        "properties": {
            "title": {"type": "string", "description": "The meeting's title or topic."},
            "duration_minutes": {
                "type": "integer",
                "description": "Length in minutes.",
                "minimum": 5,
                "maximum": 480,
                "default": 30,
            },
            "urgency": {
                "type": "string",
                "description": "How urgent the meeting is.",
                "enum": ["low", "normal", "high"],
                "default": "normal",
            },
        },
        "required": ["title"],
    },
}

Exercise 2 (medium): Implement the agent loop

Build the agent loop pattern: a function that takes a mock model, a tool registry, and a user message; iterates think → act → observe → repeat until the model returns a final answer or hits max_iterations.

Hint

The loop:

  1. Send messages to the model (mocked here)
  2. Check stop_reason:
    • If "end_turn": return the text, done
    • If "tool_use": extract the tool call; execute it; append the result; loop
  3. Track iterations; break at max_iterations to prevent infinite loops

For the mock model: simulate a multi-step response by counting prior tool uses in messages.

Production loops also handle: parallel tool calls, partial errors (some tools succeed, others fail), and observation truncation.

Solution

Append the assistant turn unconditionally, branch on stop_reason, and for tool_use execute every block and append a single user message carrying all the tool_results before looping again.

def agent_loop(model, tools, user_message, max_iterations=10):
    messages = [{"role": "user", "content": user_message}]

    for iteration in range(max_iterations):
        response = model(messages, list(tools.keys()))
        messages.append({"role": "assistant", "content": response["content"]})

        if response["stop_reason"] == "end_turn":
            for block in response["content"]:
                if block.get("type") == "text":
                    return block["text"]
            return None

        tool_results = []
        for block in response["content"]:
            if block.get("type") == "tool_use":
                fn = tools[block["name"]]
                result = fn(**block["input"])
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block["id"],
                    "content": str(result),
                })
        messages.append({"role": "user", "content": tool_results})

    return None   # hit max_iterations

Running this against mock_model and TOOLS gives Final answer: Tokyo is 65°F (about 18.3°C)., matching the expected output.

Exercise 3 (medium): Multi-tool routing via embeddings

Implement embedding-based tool retrieval for a large tool catalog. Given a user query and a catalog of 20 tools (with mock embeddings), return the top-5 most relevant tools to present to the model.

Hint

The pattern:

  1. Pre-embed each tool’s description (offline; cached)
  2. At query time, embed the user’s request
  3. Compute cosine similarity between query and each tool’s embedding
  4. Return the top-K most similar tools

For the exercise: use mock embeddings (random 8-dim vectors, but seeded to be reproducible). In production: use a small embedding model like text-embedding-3-small or all-MiniLM-L6-v2.

Cosine similarity: cos(q,t)=fracqcdottqt\\cos(q, t) = \\frac{q \\cdot t}{\\|q\\| \\|t\\|}

Solution

Cosine similarity is the dot product normalized by both vector norms; retrieval embeds the query, scores every tool, and sorts descending.

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def retrieve_tools(query, tools, top_k=5):
    q = embed_query(query)
    scored = [(tool, cosine_similarity(q, tool["embedding"])) for tool in tools]
    scored.sort(key=lambda pair: pair[1], reverse=True)
    return scored[:top_k]

Running this on the four sample queries confirms the exercise’s own observation: with these random 8-dim embeddings the top-5 lists are noise (e.g. “what’s the weather in Paris” surfaces deploy and git_status ahead of get_weather, and “schedule a meeting next week” tops out at search_database). The mechanics, top-k by cosine similarity, are correct; only real embeddings would make the rankings semantically sensible.

Exercise 4 (hard): Idempotent tool dispatch

Implement a tool dispatcher that handles side-effect-causing tools safely. Use idempotency keys to prevent duplicate executions on retry, and an audit log to track every call.

Hint

Idempotent dispatch pattern:

  1. Each tool call includes an idempotency key (typically a UUID)
  2. Maintain a cache keyed by (tool_name, idempotency_key)
  3. Before executing: check the cache; if a prior call with the same key exists, return the cached result without re-executing
  4. After executing: store the result in the cache
  5. Audit log: append every call (whether executed or cached) for review

This pattern is essential for tools with side effects (sending emails, creating tickets, charging cards). Without it, an agent loop that retries a failed step could send duplicate emails.

Solution

Look up (tool_name, idem_key) in the cache first; a hit returns the cached result and logs "cached" without touching self.tools again. A miss looks up and calls the function, stores the result, and logs "executed".

def dispatch(self, tool_name, args, idem_key=None):
    if idem_key is None:
        idem_key = str(uuid.uuid4())

    cache_key = (tool_name, idem_key)
    if cache_key in self.cache:
        self.audit.append({"tool": tool_name, "idem_key": idem_key, "status": "cached", "ts": datetime.now()})
        return self.cache[cache_key]

    if tool_name not in self.tools:
        error = {"status": "error", "message": f"Unknown tool: {tool_name}"}
        self.audit.append({"tool": tool_name, "idem_key": idem_key, "status": "error", "ts": datetime.now()})
        return error

    fn = self.tools[tool_name]
    result = fn(**args)
    self.cache[cache_key] = result
    self.audit.append({"tool": tool_name, "idem_key": idem_key, "status": "executed", "ts": datetime.now()})
    return result

With this, the retry under "email-001" returns the cached {'status': 'sent', 'id': 1} and sent_emails stays at length 1; the "email-002" call executes fresh and sent_emails reaches length 2. The audit log has 3 entries: executed, cached, executed.

The full picture

Computer use: the frontier

Anthropic’s computer use (late 2024) generalizes tool use to any application’s UI. The “tools” become a small primitive set: Screenshot to see the screen, Mouse to click and drag, Keyboard to type and press, Wait to let animations settle. The model becomes a “generalist computer operator”: it can drive any application by looking at the screen and operating mouse and keyboard. The agent loop pattern is identical; only the tools have changed.

The implications are substantial and the caveats are equally substantial. No custom integration needed for legacy apps: the model can use anything a human can use, which opens up a long tail of internal tools and enterprise software that never had a public API. Slow and error-prone today: each step requires a screenshot, a model call, and an action, so a five-step UI workflow that a human does in 1010 seconds takes a computer-use agent several minutes. High-stakes by default: a misaligned model with computer use can delete files, send emails, and click “buy now,” so the production deployment needs careful guardrails. Improving rapidly: the model’s UI-understanding accuracy has climbed quickly across 20242024 and into 20252025, and the latency tax has dropped as serving stacks specialized for the workload.

What’s next in Part VII

Tool use is the capability that turns a reasoning model into an agent. The Thought-Action-Observation loop from ReAct (Yao 2022) became production engineering across 2023202320242024: structured tool calls with JSON Schema, constrained decoding for reliability, parallel calls for speed, multi-tool routing for scale, MCP for interoperability, computer use for legacy applications. Every commercial agent built today rests on this stack. The engineering disciplines this chapter covered, schema design, loop control, observation handling, error recovery, and idempotency, are what separate a production system from a demo.

Chapter 22 opens next: retrieval-augmented generation. Retrieval is one of the most useful tools: the model looks up information it does not have memorized. RAG has its own engineering depth (chunking, embedding, vector search, reranking), so it gets its own chapter. Chapter 23 extends the capability stack to multimodal inputs and outputs: vision, audio, video, the input modalities that make computer use practical and that unlock the next set of agent applications. Then Part VIII covers safety, interpretability, and evaluation, the disciplines that turn capable systems into trustworthy ones. Then Part IX assembles everything into full agent architectures, where reasoning, tool use, retrieval, and multimodal compose into the complete systems that this chapter has been quietly building toward.

The capability stack is being assembled. Reasoning gave the model time to think; tool use gives it agency; retrieval will give it knowledge; multimodal will extend it beyond text. The chapter that turns reasoning into agency is done; the chapter that gives that agency a knowledge base opens next.