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
- Define a
tasktool whose schema accepts adescriptionfor the subagent - Inside
execute, instantiate a newToolLoopAgentwithreadandgreponly - Use
claude-haiku-4-5andstopWhen: stepCountIs(5) - 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:
- Add a
tasktool to your tool registry - The schema takes a
description: stringthat the parent uses to tell the subagent what to investigate - Inside
execute, create a newToolLoopAgentwithreadandgrep(nobash, noaskUser) - Pick a fast model (
claude-haiku-4-5) and cap steps at 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
readandgreptools 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
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, noaskUser. 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
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.
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:
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:
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.
npx tsc --noEmitCommit
git add src/tools.ts index.ts
git commit -m "feat(subagents): add explorer via task tool"Done-When
createTaskToolexists and returns atasktool- The task tool spawns a fresh
ToolLoopAgentper call - The explorer has
readandgreponly - The explorer uses
claude-haiku-4-5and stops at 5 steps - Errors return as strings, not exceptions
- Parent can delegate research and get a clean summary back
npx tsc --noEmitpasses
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
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?