Vercel Logo

Explorer Subagent

The explorer is the simplest subagent to build, and the most useful to start with.

It can read files. It can search. It can do nothing else. No write, no bash, no asking the user. It investigates a question, summarizes what it found, and disappears.

That sounds like a constraint. It's the feature. The explorer can't drift, can't make accidental changes, and can't burn down your project with a creative find -exec. It does one thing, and when it finishes, the parent gets back a clean answer instead of forty steps of intermediate file reads.

Outcome

A parent-facing task tool spawns a fresh ToolLoopAgent with read and grep only, a cheap model, and a 5-step budget. The parent can delegate research and get a text summary back.

Fast Track

  1. Define a task tool whose schema accepts a description for the subagent
  2. Inside execute, instantiate a new ToolLoopAgent with read and grep only
  3. Use claude-haiku-4-5 and stopWhen: stepCountIs(5)
  4. Return the subagent's text response back to the parent, wrapped in a try/catch

Hands-on Exercise 6.2

Add a delegation seam from the parent into an explorer subagent.

Requirements:

  1. Add a task tool to your tool registry
  2. The schema takes a description: string that the parent uses to tell the subagent what to investigate
  3. Inside execute, create a new ToolLoopAgent with read and grep (no bash, no askUser)
  4. Pick a fast model (claude-haiku-4-5) and cap steps at 5
  5. Return the explorer's text response, with errors caught and returned as a string

Implementation hints:

  • The explorer is instantiated per call. Don't reuse one. Each delegation gets a fresh context window
  • Reuse the read and grep tools you already have. They're closed over the sandbox the parent uses, which is what you want
  • Wrap explorer.generate(...) in try/catch and return "Subagent error: ${e.message}" instead of letting the exception propagate. The parent expects a string back from any tool

The task tool

src/tools.ts (additions)
import { ToolLoopAgent, stepCountIs, tool } from "ai";
import { z } from "zod";
import type { Sandbox } from "./sandbox";
 
export function createTaskTool(sandbox: Sandbox, parentTools: {
  read: ReturnType<typeof createReadTool>;
  grep: ReturnType<typeof createGrepTool>;
}) {
  return tool({
    description: `Delegate research to a read-only subagent.
WHEN TO USE: investigating a codebase, finding patterns, gathering context
  across many files.
WHEN NOT TO USE: making changes (the subagent cannot write or run commands).
DO NOT USE FOR: tasks that need decisions or askUser interactions.`,
    inputSchema: z.object({
      description: z.string().describe("What the subagent should investigate"),
    }),
    execute: async ({ description }) => {
      const explorer = new ToolLoopAgent({
        model: "anthropic/claude-haiku-4-5",
        instructions: `You are an explorer agent. Investigate and report back concisely.
Working directory: ${sandbox.workingDirectory}`,
        tools: { read: parentTools.read, grep: parentTools.grep },
        stopWhen: stepCountIs(5),
      });
 
      try {
        const { text, steps } = await explorer.generate({ prompt: description });
        return text
          ? `[Explorer: ${steps.length} steps]\n${text}`
          : "(no response from subagent)";
      } catch (e: any) {
        return `Subagent error: ${e.message}`;
      }
    },
  });
}

A few design choices worth pointing at:

  • Fresh agent per call. The explorer doesn't survive across calls. Each task gets its own context window, which is the whole point of delegating
  • No bash, no askUser. The explorer can read and search. It cannot modify the project or pause for user input. The parent stays in charge of decisions
  • Haiku, not Sonnet. Exploration is reading and summarizing, not deep reasoning. A faster, cheaper model is the right fit
  • Five steps. Enough to look at a handful of files and report back. If the explorer needs more, the parent should break the task into smaller pieces
  • Errors return as strings. Tools return strings to the model. An uncaught exception breaks the tool loop. Returning the error text lets the parent decide what to do

Wire it into the parent

index.ts
const tools = {
  read: createReadTool(sandbox),
  grep: createGrepTool(sandbox),
  bash: createBashTool(sandbox, createApproval({ mode: "interactive" })),
};
 
const tools_with_task = {
  ...tools,
  task: createTaskTool(sandbox, { read: tools.read, grep: tools.grep }),
};
 
const agent = new ToolLoopAgent({
  // ...
  tools: tools_with_task,
});

The parent now has four tools: read, grep, bash, and task. The first three are direct. The fourth delegates.

Add logs while you debug

When a subagent returns nothing or returns the wrong thing, you have no idea what happened inside its run. While developing, log the subagent's step count and text length from inside the task tool. Without that, you'll be staring at confused parent output with no idea whether the subagent ran for one step or five, found anything, or quietly failed.

Try It

Ask the parent something that the explorer is well-suited for:

Terminal
bun run index.ts . "Delegate to a subagent: find every place this project uses zod and tell me which files import from it."

The parent should call task with that description. The explorer should run, find the imports, and return a summary. The parent should pass that summary back to you.

For comparison, run the same task without the explicit delegation instruction:

Terminal
bun run index.ts . "Find every place this project uses zod and tell me which files import from it."

The parent may or may not delegate. With strong tool descriptions, it might call grep directly. That's fine. The delegation tool earns its keep when the search has to go through many files and the parent doesn't want all that text in its context.

Terminal
npx tsc --noEmit

Commit

git add src/tools.ts index.ts
git commit -m "feat(subagents): add explorer via task tool"

Done-When

  • createTaskTool exists and returns a task tool
  • The task tool spawns a fresh ToolLoopAgent per call
  • The explorer has read and grep only
  • The explorer uses claude-haiku-4-5 and stops at 5 steps
  • Errors return as strings, not exceptions
  • Parent can delegate research and get a clean summary back
  • npx tsc --noEmit passes
Parallel explorers

A single explorer is a coroutine. Spawning two in parallel from the parent's tool loop is real parallelism. Try changing the task tool's schema to accept an array of descriptions and run them with Promise.all. Now the parent can investigate three different parts of the codebase at once and synthesize the results. What changes about the parent's prompt to take advantage of this?

Solution

src/tools.ts (createTaskTool)
import { ToolLoopAgent, stepCountIs, tool } from "ai";
import { z } from "zod";
import type { Sandbox } from "./sandbox";
 
export function createTaskTool(
  sandbox: Sandbox,
  parentTools: { read: any; grep: any },
) {
  return tool({
    description: `Delegate research to a read-only subagent.
WHEN TO USE: investigating a codebase, finding patterns, gathering context.
WHEN NOT TO USE: making changes (the subagent cannot write or run commands).
DO NOT USE FOR: tasks that need decisions or askUser interactions.`,
    inputSchema: z.object({
      description: z.string().describe("What the subagent should investigate"),
    }),
    execute: async ({ description }) => {
      const explorer = new ToolLoopAgent({
        model: "anthropic/claude-haiku-4-5",
        instructions: `You are an explorer agent. Investigate and report back concisely.
Working directory: ${sandbox.workingDirectory}`,
        tools: { read: parentTools.read, grep: parentTools.grep },
        stopWhen: stepCountIs(5),
      });
 
      try {
        const { text, steps } = await explorer.generate({ prompt: description });
        return text
          ? `[Explorer: ${steps.length} steps]\n${text}`
          : "(no response from subagent)";
      } catch (e: any) {
        return `Subagent error: ${e.message}`;
      }
    },
  });
}

Was this helpful?

supported.