---
title: "Chapter 04: Tools — Navigation, Editing, Verification"
description: "Add structured tools for navigating, editing, and verifying code. Move beyond raw bash to safe, predictable operations."
type: lesson
order: 4
chapter: "02-core"
---

# 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:

```typescript
{
  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:

```typescript
{
  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:

```typescript
{
  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:

```typescript
{
  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:

```typescript
{
  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:

```typescript
{
  name: "run_check",
  description: "Run 'mise run check' to verify TypeScript and tests",
  parameters: { type: "object", properties: {}, required: [] },
}
```

**`git_diff`** — Show unstaged changes:

```typescript
{
  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:

```markdown
# 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:

```typescript
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

```typescript
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

```typescript
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

```bash
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`:

```typescript
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}`);
  }
}
```

---

[← Chapter 03](/build-ai-coding-agent/01-setup/03-scaffolding/) · [Next: Chapter 05 →](/build-ai-coding-agent/02-core/05-cost-context/)
