s02
Tools
Tools & ExecutionOne Handler Per Tool
237 LOC4 toolsTypeScriptTool dispatch map
The loop stays the same; new tools register into the dispatch map
s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12
"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
- Each tool gets a handler function. Path sandboxing prevents workspace escape.
function safePath(relativePath: string): string {
const filePath = resolve(WORKDIR, relativePath);
const normalizedWorkdir = `${WORKDIR}${process.platform === "win32" ? "\\" : "/"}`;
if (filePath !== WORKDIR && !filePath.startsWith(normalizedWorkdir)) {
throw new Error(`Path escapes workspace: ${relativePath}`);
}
return filePath;
}
- The dispatch map links tool names to handlers.
const TOOL_HANDLERS = {
bash: (input) => runBash(String(input.command ?? "")),
read_file: (input) => runRead(String(input.path ?? ""), Number(input.limit ?? 0) || undefined),
write_file: (input) => runWrite(String(input.path ?? ""), String(input.content ?? "")),
edit_file: (input) =>
runEdit(String(input.path ?? ""), String(input.old_text ?? ""), String(input.new_text ?? "")),
};
- In the loop, look up the handler by name. The loop body itself is unchanged from s01.
for (const block of response.content) {
if (block.type !== "tool_use") continue;
const handler = TOOL_HANDLERS[block.name as ToolUseName];
const output = handler
? handler(block.input as Record<string, unknown>)
: `Unknown tool: ${block.name}`;
results.push({
type: "tool_result",
tool_use_id: block.id,
content: output,
});
}
Add a tool = add a handler + add a schema entry. The loop never changes.
What Changed From s01
| Component | Before (s01) | After (s02) |
|---|---|---|
| Tools | 1 (bash only) | 4 (bash, read, write, edit) |
| Dispatch | Hardcoded bash call | TOOL_HANDLERS dict |
| Path safety | None | safe_path() sandbox |
| Agent loop | Unchanged | Unchanged |
Try It
cd learn-claude-code
cd agents-ts
npm install
npm run s02
Read the file package.jsonCreate a file called greet.ts with a greet(name: string) functionEdit greet.ts to add a JSDoc commentRead greet.ts to verify the edit worked