AgentHarness 课程
s02

Tools

Tools & Execution

一个工具一个 Handler

157 LOC4 toolsTool dispatch map
The loop stays the same; new tools register into the dispatch map

L01 > [ L02 ] L03 > L04 > L05 > L06 | L07 > L08 > L09 > L10 > L11 > L12

"Adding a tool means adding one handler" -- the loop stays the same; new tools register into the dispatch map.

Harness layer: Tool dispatch -- expanding what the model can reach.

Problem

With only bash, the agent shells out for everything. cat truncates unpredictably, sed fails on special characters, and every bash call is an unconstrained security surface. Dedicated tools like read_file and write_file let you enforce path sandboxing at the tool level.

The key insight: adding tools does not require changing the loop.

Solution

+--------+      +-------+      +------------------+
|  User  | ---> |  LLM  | ---> | Tool Dispatch    |
| prompt |      |       |      | {                |
+--------+      +---+---+      |   bash: run_bash |
                    ^           |   read: run_read |
                    |           |   write: run_wr  |
                    +-----------+   edit: run_edit |
                    tool_result | }                |
                                +------------------+

The dispatch map is a dict: {tool_name: handler_function}.
One lookup replaces any if/elif chain.

How It Works

  1. Each tool gets a handler function. Path sandboxing prevents workspace escape.
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]
    return "\n".join(lines)[:50000]
  1. The dispatch map links tool names to handlers.
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"]),
}
  1. In the loop, look up the handler by name. The loop body itself is unchanged from L01.
for block in response.content:
    if block.type == "tool_use":
        handler = TOOL_HANDLERS.get(block.name)
        output = handler(**block.input) if handler \
            else f"Unknown tool: {block.name}"
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": output,
        })

Adding tools vs. the loop: handler + schema; loop unchanged

Add a tool = add a handler + add a schema entry. The loop never changes. Here is what that means.

What “the loop never changes” refers to
It is the same L01-shaped skeleton: send messages (including the current tool definitions) to the model → if the response contains tool_use, read tool name and arguments → dispatch to the handler → append a tool_result → call the model again until there is no more tool_use.
That control flow does not depend on whether you have three tools or thirty, so you should not rewrite the main pipeline for every new capability.

PieceNameRole
Schema (tool definition)One entry in the API tools listTells the model: tool name, what it does, parameter shape (often JSON Schema). Without it, the model does not know the capability exists.
Handlere.g. run_read, run_writeWhen the model chooses that tool, which code runs (read/write, sandbox checks, etc.) and returns the string fed back as tool_result.

Together: the model gets one more optional action (register a schema in definitions), and execution gets one more implementation (register a handler in the dispatch map). The main loop stays a generic pipe—extend the tool list and the handler table; do not fork the core “build messages / check stop_reason” structure for each new tool.

Metaphor: the loop is a fixed conveyor; tools are stations—a new station is a placard (schema) plus the worker (handler).

What Changed From L01

ComponentBefore (L01)After (L02)
Tools1 (bash only)4 (bash, read, write, edit)
DispatchHardcoded bash callTOOL_HANDLERS dict
Path safetyNonesafe_path() sandbox
Agent loopUnchangedUnchanged

Try It

cd learn-claude-code
python agents/s02_tool_use.py
  1. Read the file requirements.txt
  2. Create a file called greet.py with a greet(name) function
  3. Edit greet.py to add a docstring to the function
  4. Read greet.py to verify the edit worked