Build a Tool-Calling Agent

Su Suriya Ravichandran Updated 10 Nov 2026
6 min read ·Lesson 197 of 200

The Goal

An agent is a program in which the model decides which actions to take, the program performs them, and the model continues with the results until the task is complete. A tool-calling agent makes that concrete: Claude asks for a tool, the application runs it, and the result goes back to Claude, which may ask for another tool or give the final answer. The loop is the agent, and the tools are what it can do.

This project builds a small agent with two tools, a price lookup and an addition, and it runs the loop with the controls the earlier lessons established: a step limit, validated tool inputs, and a log of every action. The model is scripted in the demonstration, so the sequence of steps is known, and the same loop runs against the live API unchanged.


What You Will Build

An run_agent function that:

  1. sends the question and the tool definitions,
  2. runs each tool the model requests and returns its result,
  3. continues the conversation until the model stops asking for tools,
  4. stops after a fixed number of steps, however the model behaves,
  5. returns the final answer together with a log of every step.

Project Structure

tool_agent/
├── tools.py    # the tools the agent may use, and their definitions
├── agent.py    # the loop
└── demo.py     # a scripted client that asks for two tools, then answers

Step 1: The Tools

Each tool is a plain function with a fixed behavior, and a definition that tells the model what it does and what inputs it takes. The functions accept only the inputs they declare, so an unexpected argument is an error rather than an action (Lesson 94):

"""tools.py: the tools this agent may use, and the definitions the model sees."""

PRICES = {"django-101": 4999, "sql-basics": 2999}


def lookup_price(course: str) -> int:
    if course not in PRICES:
        raise ValueError(f"no price for course {course}")
    return PRICES[course]


def add(a: int, b: int) -> int:
    return a + b


TOOLS = {"lookup_price": lookup_price, "add": add}

TOOL_DEFINITIONS = [
    {
        "name": "lookup_price",
        "description": "Returns the price in rupees of a course, by its slug such as django-101.",
        "input_schema": {
            "type": "object",
            "properties": {"course": {"type": "string"}},
            "required": ["course"],
            "additionalProperties": False,
        },
    },
    {
        "name": "add",
        "description": "Adds two whole numbers and returns the sum.",
        "input_schema": {
            "type": "object",
            "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
            "required": ["a", "b"],
            "additionalProperties": False,
        },
    },
]

The definitions are the only description of the tools the model receives, so they state the inputs and their meaning clearly (Lessons 82 and 93). The additionalProperties setting tells the model, and the schema, that other fields are not accepted.


Step 2: The Loop

The loop follows the pattern from Lesson 88. Each response is appended unchanged, each requested tool is run, its result is returned in one user message, and the loop continues. A step limit ends the loop if the model keeps asking for tools, and an unknown tool name is reported back as an error rather than crashing the agent:

"""agent.py: the loop that runs tools until the model gives a final answer."""

from tools import TOOLS, TOOL_DEFINITIONS

MODEL = "claude-sonnet-5-5"
MAX_STEPS = 5


def text_of(response) -> str:
    return "".join(block.text for block in response.content if block.type == "text")


def run_agent(client, question: str) -> tuple[str, list[str]]:
    messages = [{"role": "user", "content": question}]
    log: list[str] = []
    for step in range(1, MAX_STEPS + 1):
        response = client.messages.create(
            model=MODEL, max_tokens=500, tools=TOOL_DEFINITIONS, messages=messages,
        )
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            return text_of(response), log

        results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            tool = TOOLS.get(block.name)
            try:
                output = tool(**block.input) if tool else f"unknown tool {block.name}"
            except (ValueError, TypeError) as error:
                output = f"error: {error}"
            log.append(f"step {step}: {block.name}({block.input}) -> {output}")
            results.append({"type": "tool_result", "tool_use_id": block.id, "content": str(output)})
        messages.append({"role": "user", "content": results})
    return "[stopped: the agent reached its step limit]", log

How It Works

  1. Each response is appended before anything else happens, so the thinking and tool-use blocks are preserved as the API requires (Lesson 153).
  2. Tools run one at a time, in the order requested, and every tool call receives a result with its identifier, as Lesson 87 requires.
  3. Failures become results. A tool that raises a ValueError returns an error message to the model, which can then correct itself, following Lesson 90.
  4. The step limit bounds the loop. A model that keeps requesting tools reaches the limit, and the agent returns a message saying so, rather than running without end.
  5. The log records every action, with its step, its input, and its output, which is the audit trail the security lessons ask for (Lesson 180).

Step 3: A Scripted Demonstration

The demonstration client returns three responses in sequence: a request for a price, a request for a sum, and a final answer. The blocks have the attributes the loop reads, so the agent cannot tell the script from the live model:

"""demo.py: an agent run with a scripted model that asks for two tools and then answers."""

from types import SimpleNamespace

from agent import run_agent


def tool_use(identifier, name, args):
    return SimpleNamespace(type="tool_use", id=identifier, name=name, input=args)


SCRIPT = [
    SimpleNamespace(stop_reason="tool_use", content=[tool_use("t1", "lookup_price", {"course": "django-101"})]),
    SimpleNamespace(stop_reason="tool_use", content=[tool_use("t2", "add", {"a": 4999, "b": 500})]),
    SimpleNamespace(stop_reason="end_turn",
                    content=[SimpleNamespace(type="text",
                                             text="The total with the workshop fee is 5499 rupees.")]),
]


class ScriptedClient:
    def __init__(self):
        self.messages = self
        self.turn = 0

    def create(self, **request):
        response = SCRIPT[self.turn]
        self.turn += 1
        return response


if __name__ == "__main__":
    answer, log = run_agent(ScriptedClient(), "What is the Django course price plus a 500 workshop fee?")
    for line in log:
        print(line)
    print("final:", answer)

Output:

step 1: lookup_price({'course': 'django-101'}) -> 4999
step 2: add({'a': 4999, 'b': 500}) -> 5499
final: The total with the workshop fee is 5499 rupees.

How It Works

  1. The first step runs the price lookup, which returns 4999 for the Django course, and the log shows the input and the output.
  2. The second step adds the workshop fee to that price, using the value the model received from the first step. The model chained the tools by carrying a result forward, which is the core of agent behavior.
  3. The third response has no tool use, so the loop returns its text as the final answer, along with the log of the two actions.
  4. The arithmetic is done by a tool, not by the model, which is the reason tools exist for work that must be exact (Lesson 100).

Going Live

The scripted client is replaced by Anthropic(), and run_agent is called in the same way. The live model chooses its own steps, so the log may differ from the script, but the loop, the step limit, and the logging are the same.


Safety for a Live Agent

A live agent can call any tool it is given, so the tools should be chosen with the permission rules of Lesson 176, and their inputs should be validated as Lesson 177 described. A tool that changes data should require confirmation before it runs. The step limit and the log are the controls that let an operator stop and understand a misbehaving agent, and they should be reviewed as part of testing (Lesson 183).

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Build a Tool-Calling Agent and get answers drawn from it.

Signed-in readers only.