Skip to content
TechBits
Go back

Building Coding Agent Part 2: Tool Dispatching – Expanding the Agent's Reach

Edit page

Lesson 2: Tool Dispatching – Expanding the Agent’s Reach

Welcome back to our journey of building a coding AI agent from scratch!

Where we left off: In Lesson 1, we stripped away the hype and discovered that an AI agent is essentially just a language model running inside a while loop. The model receives a prompt, decides whether to use a tool or talk to the user, and loops until the task is finished. We gave our agent exactly one tool: bash.

But as we start doing real work, we run into a problem.

The Problem

If our agent relies solely on bash, it has to shell out for absolutely everything. This leads to a few major headaches:

We need dedicated tools to handle file operations reliably and securely. Specifically, we want to give our agent tools to read_file, write_file, and edit_file.

The Solution: The Dispatch Map

You might be thinking, “If we add more tools, do we need to write a massive if/elif chain inside our beautiful, simple agent loop?”

Absolutely not. The key insight of this lesson is: Adding a tool means adding one handler. The loop itself stays exactly the same.

Instead of hardcoding logic into the loop, we use a “Dispatch Map” (a simple Python dictionary) to route tool calls to the correct Python functions.

How It Works (In Code)

Step 1: Build Safe Tool Handlers First, we write dedicated functions for reading, writing, and editing files. To solve the security issue, we introduce a safe_path helper function to enforce “sandboxing”. This ensures the agent can never escape its designated working directory.

def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

def run_read(path: str, limit: int = None) -> str:
    text = safe_path(path).read_text()
    lines = text.splitlines()
    if limit and limit < len(lines):
        lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
    return "\n".join(lines)[:50000]

Step 2: Create the Dispatch Map Next, we link the tool names (which the LLM will output) to the functions we just wrote using a dictionary.

TOOL_HANDLERS = {
    "bash":       lambda **kw: run_bash(kw["command"]),
    "read_file":  lambda **kw: run_read(kw["path"], kw.get("limit")),
    "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
    "edit_file":  lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}

Step 3: Update the Loop (By Doing Nothing!) Inside our core while loop, we simply look up the requested tool by name in our TOOL_HANDLERS dictionary.

for block in response.content:
    if block.type == "tool_use":
        # Look up the tool in our map
        handler = TOOL_HANDLERS.get(block.name)
        
        # Execute it, or return an error if it doesn't exist
        output = handler(**block.input) if handler \
            else f"Unknown tool: {block.name}"
            
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": output,
        })

Notice how clean this is? We just expanded our agent’s capabilities massively, but the control flow of the loop didn’t change at all.

The Result

By equipping our agent with just these four tools (bash, read_file, write_file, and edit_file), we have successfully covered most usecases of our coding tasks. Fewer tools mean less cognitive load for the model, fewer chances of it picking the wrong tool, and a much more reliable agent.


Edit page
Share this post on:

Previous Post
Building Coding Agent Part 3: Visible Planning – Keeping the Agent on Course
Next Post
Building Coding Agent Part 1: The Agent Loop – Bash is All You Need