Chapter 04: Tools — Navigation, Editing, Verification
Add structured tools for navigating, editing, and verifying code. Move beyond raw bash to safe, predictable operations.
TUTOR WITH THEFOCUS.AI
Copy this prompt into Claude, ChatGPT, or any external AI assistant. It points the assistant to the course instructions and links it to your student profile to track your progress and customize observations.
You are not enrolled yet. Enroll to generate a Student ID to track lesson completions and store learning notes.
Chapter 04: Tools — Navigation, Editing, Verification
What We’re Building
A complete toolset for code work. Instead of relying on the bash tool for everything (dangerous and imprecise), we’ll create structured tools that give the agent safe, predictable ways to:
- Navigate: List files, read files, search for code patterns
- Edit: Create files, replace text with uniqueness checking
- Verify: Run tests, check linting, see git diffs
Step 1: Design the Tools
Start your agent and paste this prompt:
We need to add tools that are appropriate for navigating, editing, and verifying a code base. Look through the existing tools.ts and look through the existing session_logs to see what could be improved, and suggest a bunch of tools that we could build. Ask me clarifying questions one at a time before we start to build anything.
The agent will analyze your current toolset and session patterns, then ask clarifying questions. Here’s the conversation you’ll have:
**Clarifying Question:**
For the edit_file capability, relying on "exact string matching" for search-and-replace
can be brittle if the model hallucinates a single whitespace character.
Would you prefer we implement a robust "search and replace" tool (requiring exact matches),
or should we look into a "line-number based" editing tool?
Respond:
is there a better diff style tool we could incorporate?
Then the agent will ask about bash vs. structured tools:
For the Execution/Verification part (run_command), do you want to keep the raw bash tool
(which effectively allows doing anything), or should we wrap specific common commands
into their own typed tools to encourage safer behavior?
Respond:
lets keep the bash tool for now, but make sure that it’s used only as a last resort
Step 2: Implement the Tools
The agent will now implement the full toolset. Here’s what you’ll end up with:
Navigation Tools
list_files — List files in a directory:
{
type: "function",
function: {
name: "list_files",
description: "List files in a directory",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "Path to list (default: .)" },
},
required: ["path"],
},
},
}
read_file — Read file content with line limits:
{
name: "read_file",
description: "Read the content of a file",
parameters: {
properties: {
path: { type: "string", description: "Path to file" },
startLine: { type: "number", description: "Line to start from (default: 1)" },
limit: { type: "number", description: "Lines to read (default: 500)" },
},
required: ["path"],
},
}
search_files — Search with ripgrep:
{
name: "search_files",
description: "Search for a string pattern in files (using ripgrep)",
parameters: {
properties: {
query: { type: "string", description: "String to search for" },
path: { type: "string", description: "Directory to search in (default: .)" },
includeNodeModules: { type: "boolean", description: "Include node_modules (default: false)" },
},
required: ["query"],
},
}
Editing Tools
write_file — Create or overwrite:
{
name: "write_file",
description: "Create or overwrite a file with full content",
parameters: {
properties: {
path: { type: "string" },
content: { type: "string", description: "Full content of the file" },
},
required: ["path", "content"],
},
}
replace_in_file — Smart diff-style replacement:
{
name: "replace_in_file",
description: "Replace a unique string in a file with a new string. Fails if the string is not found or not unique.",
parameters: {
properties: {
path: { type: "string" },
oldText: { type: "string", description: "Exact text to replace" },
newText: { type: "string", description: "New text to insert" },
},
required: ["path", "oldText", "newText"],
},
}
The replace_in_file tool is key — it checks that oldText appears exactly once in the file before replacing it. This prevents the “hallucinated whitespace” problem that plagues simple search-and-replace.
Verification Tools
run_check — Run tests and linting:
{
name: "run_check",
description: "Run 'mise run check' to verify TypeScript and tests",
parameters: { type: "object", properties: {}, required: [] },
}
git_diff — Show unstaged changes:
{
name: "git_diff",
description: "Show unstaged changes (git diff)",
parameters: { type: "object", properties: {}, required: [] },
}
Step 3: Update the System Prompt
The agent will update your system prompt to guide the new workflow. Here’s the prompt structure:
# Workflow Rules
1. **Explore First**: Don't guess file paths. List and read files to understand context.
2. **Verify Your Work**: After modifying files, ALWAYS run "run_check" to verify.
3. **Atomic Changes**: Make small changes and verify them.
4. **Fix Issues**: If 'run_check' fails, analyze the error and fix immediately.
5. **Clean Code**: Maintain clean, type-safe code.
6. **No Auto-Commit**: NEVER automatically commit changes.
Step 4: Add Parallel Tool Execution
The agent should handle multiple tool calls in a single response:
Also, lets make sure that we can handle multiple parallel tool calls
This uses Promise.all in the agent loop to execute independent tool calls simultaneously:
const toolResults = await Promise.all(
m.tool_calls.map(async (toolCall: any) => {
const args = JSON.parse(toolCall.function.arguments);
let result = await executeTool(toolCall.function.name, args);
return { role: "tool", tool_call_id: toolCall.id, content: result };
}),
);
Step 5: Tool Implementation Details
The replace_in_file Function
export async function replaceInFile(
filePath: string,
oldText: string,
newText: string,
): Promise<string> {
const content = await readFile(filePath, "utf-8");
if (!content.includes(oldText)) {
return `Error: 'oldText' not found in ${filePath}`;
}
if (content.split(oldText).length - 1 > 1) {
return `Error: 'oldText' is not unique in ${filePath}. Provide more context.`;
}
const newContent = content.replace(oldText, newText);
await writeFile(filePath, newContent, "utf-8");
return `Successfully replaced text in ${filePath}`;
}
The search_files Function with ripgrep
export async function searchFiles(
query: string,
searchPath: string = ".",
includeNodeModules: boolean = false,
): Promise<string> {
let result: string;
if (includeNodeModules) {
result = await $`rg -n --no-heading -u ${query} ${searchPath}`.text();
} else {
result = await $`rg -n --no-heading ${query} ${searchPath}`.text();
}
const lines = result.trim().split("\n");
if (lines.length > 50) {
return (
lines.slice(0, 50).join("\n") +
`\n... (and ${lines.length - 50} more matches)`
);
}
return result || "No matches found.";
}
Note: ripgrep (rg) is used for searching. The -n flag adds line numbers, --no-heading groups by line. Exit code 1 means “no matches” (not an error).
Step 6: Test the Toolset
mise run check
Then start your agent and try:
read src/tools/index.ts
search for "runTurn" in the src directory
what files are in src/tools/
add a comment to the top of src/agent.ts explaining what the file does
run the check to make sure everything passes
Tool Dispatch Architecture
All tools are registered in src/tools/index.ts:
export async function executeTool(
name: string,
args: any,
bashExecutor: CommandExecutor = defaultBashExecutor,
agentRunner?: AgentRunner,
agentName = "default",
): Promise<string> {
switch (name) {
case "bash":
return runBash(args.command, bashExecutor);
case "list_files":
return listFiles(args.path);
case "read_file":
return readFileContent(args.path, args.startLine, args.limit);
case "write_file":
return writeFileContent(args.path, args.content);
case "replace_in_file":
return replaceInFile(args.path, args.oldText, args.newText);
case "search_files":
return searchFiles(args.query, args.path, args.includeNodeModules);
case "run_check":
return runBash("mise run check", bashExecutor);
case "git_diff":
return gitDiff(bashExecutor);
default:
throw new Error(`Unknown tool: ${name}`);
}
}