Site

App: Simple Agent — File-Reading Agent

Tutorial A10.0  •  AI / Learn / Applications

A10.0 What This Teaches

This tutorial builds an agent loop: the model calls list_files and read_file tools repeatedly until it can answer a question about the project. It covers:

A10.1 What an Agent Is

An agent runs in a loop. Each iteration:
  1. Call the model with the current message history and available tools
  2. If stop_reason == "end_turn", return the final answer and stop
  3. If stop_reason == "tool_use", find all tool calls in the response, execute each one, collect the results
  4. Append the assistant message and the tool results to the history
  5. Go back to step 1
The model decides how many tool calls to make. A simple question might need one; a complex question might need five. The loop runs until the model says it is done.

A10.2 The Tools

Two tools give the agent read-only access to the local file system.
# agent_tools.py - file-system tools for the agent.
import os

def list_files(directory: str) -> str:
    # returns filenames as a comma-separated string
    try:
        names = os.listdir(directory)
        return ", ".join(names) if names else "(empty directory)"
    except FileNotFoundError:
        return f"Error: directory not found: {directory}"

def read_file(path: str) -> str:
    # returns first 500 chars to keep token usage manageable
    try:
        with open(path, "r", encoding="utf-8") as f:
            content = f.read(500)
        return content if content else "(empty file)"
    except FileNotFoundError:
        return f"Error: file not found: {path}"

TOOLS = [
    {
        "name": "list_files",
        "description": "List the files in a directory. Returns filenames as a comma-separated string.",
        "input_schema": {
            "type": "object",
            "properties": {
                "directory": {"type": "string", "description": "Directory path to list"}
            },
            "required": ["directory"],
        },
    },
    {
        "name": "read_file",
        "description": "Read the first 500 characters of a text file.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "File path to read"}
            },
            "required": ["path"],
        },
    },
]

FUNCTION_MAP = {"list_files": list_files, "read_file": read_file}

A10.3 The Agent Loop

The loop runs until the model reaches end_turn or the iteration limit is hit. When the model calls multiple tools in one response, collect all results before sending them back.
# Agent loop skeleton.
MAX_ITERATIONS = 10

def run_agent(question: str) -> str:
    messages = [{"role": "user", "content": question}]

    for iteration in range(MAX_ITERATIONS):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,
        )

        if response.stop_reason == "end_turn":
            return response.content[0].text

        if response.stop_reason != "tool_use":
            return f"Unexpected stop_reason: {response.stop_reason}"

        # collect all tool results from this turn
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = FUNCTION_MAP[block.name](**block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": result,
                })

        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user",      "content": tool_results})

    return "Stopped: max_iterations reached without a final answer."

A10.4 Complete Implementation

# simple_agent.py - file-reading agent using Claude.
# Usage: python simple_agent.py

import os
import anthropic

client = anthropic.Anthropic()
MAX_ITERATIONS = 10

def list_files(directory: str) -> str:
    try:
        names = os.listdir(directory)
        return ", ".join(names) if names else "(empty directory)"
    except FileNotFoundError:
        return f"Error: directory not found: {directory}"

def read_file(path: str) -> str:
    try:
        with open(path, "r", encoding="utf-8") as f:
            content = f.read(500)
        return content if content else "(empty file)"
    except FileNotFoundError:
        return f"Error: file not found: {path}"

TOOLS = [
    {
        "name": "list_files",
        "description": "List the files in a directory. Returns filenames as a comma-separated string.",
        "input_schema": {
            "type": "object",
            "properties": {
                "directory": {"type": "string", "description": "Directory path"}
            },
            "required": ["directory"],
        },
    },
    {
        "name": "read_file",
        "description": "Read the first 500 characters of a text file.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "File path"}
            },
            "required": ["path"],
        },
    },
]

FUNCTION_MAP = {"list_files": list_files, "read_file": read_file}

def run_agent(question: str) -> str:
    messages = [{"role": "user", "content": question}]

    for iteration in range(MAX_ITERATIONS):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            tools=TOOLS,
            messages=messages,
        )

        if response.stop_reason == "end_turn":
            return response.content[0].text

        if response.stop_reason != "tool_use":
            return f"Unexpected stop_reason: {response.stop_reason}"

        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = FUNCTION_MAP[block.name](**block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": result,
                })

        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user",      "content": tool_results})

    return "Stopped: max_iterations reached."

if __name__ == "__main__":
    question = "What Python files are in the current directory and what does the first one do?"
    print(run_agent(question))

A10.5 Safety Limits

An agent without a limit can loop indefinitely. This happens when: Always set max_iterations before running an agent loop. Ten is reasonable for simple file tasks. Complex agents may need more, but start low and raise only when needed - each iteration costs tokens.
# Safety guard at the top of the loop.
for iteration in range(MAX_ITERATIONS):
    ...

return "Stopped: max_iterations reached without a final answer."

A10.6 Sample Session

$ python simple_agent.py

[Agent calls list_files(".")]
  -> app.py, requirements.txt, simple_agent.py

[Agent calls read_file("app.py")]
  -> # app.py - Flask chat server backed by Claude. ...

The current directory contains three Python-related files:
- app.py: A Flask web server that provides a chat interface backed by Claude.
  It defines two routes: GET / serves the chat page and POST /chat accepts
  a message, calls Claude, and returns the reply as JSON.
- requirements.txt: Lists the project dependencies (flask, anthropic).
- simple_agent.py: The agent script itself.
The agent made two tool calls - one to list files and one to read the first file - then composed an answer from the results.

A10.7 Extending the Agent

Add more tools to give the agent more capabilities: Tools with side effects (write, run, delete) need explicit safety checks. Read-only tools (list, read, search) are safe to expose freely.

A10.8 Exercise

Exercise Add a word_count(path) tool that returns the number of words in a file. Then ask the agent: "Which file in the current directory has the most words?"
  1. Define word_count(path) using open() and len(content.split()).
  2. Add the tool schema and add it to FUNCTION_MAP.
  3. Run the agent with the question above.
The agent should call list_files(".") to get the filenames, then call word_count on each one, then report which file has the highest count.

A10.9 Common Mistakes

No max_iterations limit

Without a limit, a buggy tool or an unanswerable question can cause the agent to loop for hundreds of iterations. Each iteration makes two API calls (or more with multiple tools) and costs real money. Always set the limit before testing.

Returning only one tool_result when multiple tools were called

If the model calls two tools in one response, you must return a tool_result for each one. The API validates that every tool_use_id in the assistant message has a matching tool_result in the next user message. Missing one causes a validation error.

Giving the agent write or delete tools without safety checks

An agent with a delete_file tool and a misunderstood question can delete files you did not intend to remove. For any tool with a destructive side effect, print what the agent intends to do and require confirmation before executing.

A10.10 Key Terms

TermMeaning
agent loopA while/for loop that repeatedly calls the model, executes tools, and feeds results back until the model finishes
tool callA request from the model to execute a named function with specific arguments
tool resultThe return value of the executed function, sent back to the model as a tool_result content block
max_iterationsAn upper bound on how many times the agent loop may run; prevents runaway loops
stop_reason"end_turn" means the model is done; "tool_use" means it wants to call a function
end_turnThe stop_reason value that signals the model has finished and produced its final answer
multi-step reasoningThe model answering a question by chaining multiple tool calls, each using the result of the previous one