Multi-Agent Worktrees & Parallel Subagents
You asked one agent to migrate a 300-file codebase from one ORM to another. By file ninety the context window (Lesson 1) is saturated, it's forgotten the conventions it set on file ten, and the whole run is sequential — eight hours of wall-clock for work that's embarrassingly parallel. One context can't hold a migration, and one agent can't parallelize itself. The answer is an orchestrator that fans the work out to isolated subagents and fans the results back in.
Core Architectural Concepts & Trade-offs
The pattern is orchestrator/worker. A lightweight orchestrator doesn't do the work — it decomposes the task into independent units, routes each to a fresh worker subagent with its own clean context window, and aggregates the results. Each worker gets the full window for one file instead of a shrinking slice of the whole job, so quality stays high deep into a large task. This is divide- and-conquer applied to attention budget: you're not making the model smarter, you're refusing to dilute it.
The hard problem when workers run in parallel is state collision. Two agents editing the same working tree at once corrupt each other's changes. git worktree is the clean primitive: each worker gets its own checked-out copy of the repo on its own branch, mutates files in genuine isolation, and the orchestrator merges the branches at the end. No locks, no shared mutable filesystem, no interleaved writes — isolation by construction. The cost is real (disk and setup per worktree), so you spin them up only for agents that actually mutate files concurrently.
Parallelism turns latency from sum into max. Ten files migrated serially is ten round-trips end to end; ten workers fanned out with Promise.all finish in roughly the time of the slowest single file. The same shift you make with parallel tool calls (Lesson 4), now at the agent level. The ceiling is concurrency limits and cost — N workers is N times the token spend at that instant — so you cap the fan-out and treat a budget as a hard ceiling, not a hope.
The trade-offs are coordination and partial failure. Decomposition has to produce genuinely independent units — if file B depends on file A's new interface, they can't run blind in parallel, and naive splitting produces merge conflicts the orchestrator has to resolve. And in a fan-out, one worker dying must not sink the batch: collect results defensively, filter the failures, and retry or report them rather than letting one timeout abort the other nine. Fan-out buys speed; disciplined aggregation is what makes the speed trustworthy.
Parallel Subagent Routing
An orchestrator that fans independent units out to worker agents with the Vercel AI SDK, each worker isolated, results aggregated with bounded concurrency and per-worker failure handling.
// Orchestrator decomposes; workers run in parallel on isolated worktrees.
import { anthropic } from "@ai-sdk/anthropic"
import { generateText, tool } from "ai"
import { z } from "zod"
import pLimit from "p-limit"
const limit = pLimit(6) // hard concurrency cap → bound cost + rate limits
// One worker: a fresh context window scoped to a single file, in its own
// git worktree so concurrent file writes can never collide.
async function migrateFile(file: string) {
const branch = `migrate/${file.replace(/\W/g, "-")}`
const dir = await createWorktree(branch) // isolated checkout
const { text } = await generateText({
model: anthropic("claude-opus-4-8"),
system: MIGRATION_CONVENTIONS, // shared, cacheable prefix
prompt: `Migrate ${file} from Sequelize to Drizzle. Edit in place.`,
tools: {
readFile: tool({ /* scoped to `dir` */ }),
writeFile: tool({ /* scoped to `dir` */ }),
},
maxSteps: 12,
})
return { file, branch, summary: text }
}
export async function runMigration(files: string[]) {
// Fan out under the cap. settled, not all → one failure can't sink the batch.
const results = await Promise.allSettled(
files.map((f) => limit(() => migrateFile(f))),
)
const done = results.filter((r) => r.status === "fulfilled").map((r) => r.value)
const failed = files.filter((_, i) => results[i].status === "rejected")
await mergeBranches(done.map((d) => d.branch)) // fan back in
return { migrated: done.length, failed } // report, don't swallow
}pLimit caps the blast radius, the per-file worktree guarantees isolation, and allSettled means a single worker's failure is a line in the report — not an aborted migration.
Fan Out, Fan In
Fanning out raises a new problem: these agents are stateless and ephemeral. Production needs them to survive a crash — a stateful agent runtime with Durable Objects & Redis. Or browse the full roadmap.