Back home

Multi-agent / Claude Code source notes

Claude Code multi-agent turns collaboration into protocol.

I read through the Claude Code multi-agent code again, more slowly this time. Raw parallel model calls are only the surface. The more interesting part is how the system treats collaboration: it avoids the shape of a busy group chat and starts from plain runtime facts. Who belongs to which team, where tasks live, how messages are delivered, how a background agent ends, who approves permissions, and whether failed work can continue.

Team

Create the team before collaboration

In Claude Code, creating a team is not a cosmetic UI step. It writes team config and gives member discovery, task ownership, and message routing a shared namespace.

Spawn

Prompt chooses collaboration; runtime routes

Whether to create a team or teammate is a model choice guided by prompts. AgentTool does not re-decide whether collaboration is needed; it branches mechanically from name, team_name, and related parameters.

Message

Plain replies are not heard by teammates

The teammate system prompt is blunt: use SendMessage. The runtime writes to a pending queue or mailbox, then injects the message into the recipient context on the next turn.

Resume

Ending work still needs handoff

Background agents return as task notifications. Stopped agents are not always dead; SendMessage can try to resume them from transcript state as a new background task.

01 / Runtime shape

Different execution lanes

There are at least two lanes of parallelism. A normal subagent is like a temporary background work packet: it has an agentId, output file, progress, and abort controller. It is good for research, implementation, or verification where the boundary is clear. When it finishes, the result comes back as a task notification.

A team teammate is more like a long-lived project member. It has name@team identity, team config, a mailbox, an idle loop, and logic for claiming tasks. It stays around after a turn finishes, waits between prompts, and keeps useful context across prompts.

There is an important split here: the prompt layer teaches the model when to create a team, when to spawn a teammate, and when a normal subagent is enough. The runtime does not make that product judgment again. By the time execution reaches AgentTool.call, the decision has already been expressed as parameters: team_name plus name means teammate spawn; no name usually means normal subagent, background, sync, or worktree flow.

Both still enter through AgentTool, so from the outside they both look like “start an agent.” In the source, though, their lifecycle, permission handling, message return path, and UI representation are different. That split matters because a background job and a teammate solve different problems.

Two agent lanes

lane 01

Background agent

A monitorable, stoppable, resumable background job. It has an agentId, progress, output file, and task notification. Best for bounded packets like research this area, edit this file, or run this verification.

lane 02

Team teammate

A resident collaborator with name@team identity, team file, mailbox, task claiming, and idle loop. Best for ongoing coordination and a shared task pool.

02 / Protocol

From team creation to result delivery

0

Prompt guidance

The TeamCreate / Agent / SendMessage prompts teach the model the collaboration strategy: create a team for complex work, pass team_name and name for teammates, and use SendMessage because plain text is not delivered.

1

TeamCreate

Writes ~/.claude/teams/{team}/config.json, registers team-lead, and resets the matching task list. The team name becomes the shared coordinate for tasks and inboxes.

2

TaskCreate / TaskUpdate

Turns work into shared tasks. status, owner, and blockedBy are not decorations; they are the protocol that prevents duplicate work.

3

AgentTool

Runtime branches from parameters: team_name plus name triggers teammate spawn. Without name, it usually follows normal subagent lifecycle. Worktree, model, tool pool, and permission mode are resolved here too.

4

SendMessage

Queues messages for running local agents, writes teammate mailboxes, or tries to resume stopped agents from transcript.

5

Notification

Background agents return <task-notification> with status, summary, result, usage, and output path. Teammates send idle notifications to team-lead between turns.

03 / Return flow

The hard part is returning results to the right place.

Conceptual pseudocode abstracted from TeamCreate / AgentTool / SendMessage

multi-agent flowTeamCreate / AgentTool / SendMessage
const team = TeamCreate()
TaskCreate('research parser')
TaskCreate('verify tests')

Agent({
  team_name: team.name,
  name: 'researcher',
  prompt: 'claim a task and report findings'
})

// later, every visible handoff is explicit
await SendMessage({ to: 'researcher', message: 'continue with this spec' })
await waitFor('<task-notification> or idle notification')

04 / Lifecycle

The lifecycle details look boring, and that is exactly why they matter

The background agent lifecycle looks like a task runner: register task, create abort controller, start runAgent, update progress while it runs, then wrap the final text into an AgentToolResult. One small but important detail: it marks the task completed before running handoff classification or worktree cleanup. That means a slow classifier call or a slow git operation does not block consumers waiting on TaskOutput.

The in-process teammate lifecycle is longer. It runs in the same Node.js process, but AsyncLocalStorage carries teammate identity. After each turn it marks itself idle, sends an idle notification, then polls mailbox, pendingUserMessages, and the task list. A shutdown_request is passed back into the model rather than being silently accepted by the runtime.

These details make the system feel less magical. The models coordinate inside a runtime that keeps moving each participant into an observable and recoverable state.

Progress

Background agents track tool uses, tokens, and recent activity so UI and SDK consumers can see what is happening.

Abort

Background jobs, foreground agents, and in-process teammate work have different abort-controller layers.

Ordering

Task completion is recorded before optional notification embellishments, so slow cleanup does not block waiters.

Idle

For a long-lived teammate, idle means waiting for the next message or task claim, not finishing.

05 / Shared state

Where shared state lives

The design deliberately avoids asking agents to inspect each other through terminals. The prompt even says not to use terminal tools to view team activity; use SendMessage and Task tools instead. Terminal output is for humans. Collaboration state needs to live in data structures that can be read, locked, and recovered.

So Claude Code splits shared facts across several surfaces: team config stores members, task list stores work, mailbox stores messages, AppState.tasks stores in-process runtime state, and transcript/output files store resumable execution. Each surface is simple, but together they create the minimum order a multi-agent system needs.

Team config

Names, agent IDs, agent types, model, color, pane metadata, and working directories. It answers who is on this team.

Task list

Status, owner, blocks, and blockedBy. Create, update, and claim paths use lockfiles so agents do not race for the same work.

Mailbox

JSON inboxes keyed by team/name. Messages carry from, text, timestamp, read state, and optional summary; writes are locked.

AppState tasks

In-process state, abort controllers, progress, pending messages, and retained transcript state. It answers who is still alive.

Transcript

The recovery source for stopped background agents and output files. It answers where the job left off.

06 / Conflict control

How it keeps subagents from colliding

The first thing to be precise about: Claude Code has no global write lock around every source file, and AgentTool does not magically decide whether two subagents might edit the same file. The design is more practical. It splits conflict control across layers: prompt-level scheduling, task-list ownership, mailbox delivery, TaskStop / SendMessage correction, and optional worktree isolation for risky writes.

The first layer is still the prompt. The coordinator prompt tells the model that research can run in parallel, write-heavy work over the same files should narrow to fewer workers, and verification can run in parallel when it covers different areas. In other words, simultaneous writing is a planning responsibility for the coordinator; AgentTool.call only executes the branch already expressed through parameters.

Once a team exists, the task list becomes the hard protocol. Tasks have owner, status, and blockedBy. claimTask changes state under a lockfile; already claimed, completed, or blocked tasks are rejected. With the busy check enabled, an agent that still has unfinished work cannot claim another task. The value is concrete: “who is doing what” becomes shared state every teammate can read.

The system therefore aims for early visibility rather than a promise that collisions are impossible: who claimed the task, who is blocked, who went off track, who needs to be stopped, and which write-heavy job should be isolated in a worktree. That boundary matters because the source contains no universal mechanism that prevents two agents from touching the same file. The real guardrail is the stack of prompt scheduling, task protocol, message ordering, and isolation.

Prompt scheduling

Research can fan out; write-heavy work narrows by file area. This is the first layer of edit-conflict control.

Task ownership

owner / status / blockedBy make ownership durable, and claimTask uses lockfiles to prevent duplicate claims.

Flat team topology

A teammate cannot spawn another teammate, so the roster does not become a hard-to-debug tree.

Message ordering

Mailbox writes are locked; the in-process runner prioritizes shutdown and team-lead messages.

Stop / resume

A bad direction can be stopped with TaskStop, then continued or corrected through SendMessage.

Worktree isolation

Risky write tasks can use isolation: worktree so changes happen in a separate checkout.

07 / Coordination

The coordinator turns parallel results into the next step

The previous section was about keeping workers from colliding. That is only the baseline. Quality depends on how the coordinator turns parallel work back into one clear next step. Subagents can bring information back, but their findings do not automatically become a plan.

The coordinator prompt reflects that shape: research can fan out, write-heavy work should narrow, the same worker can continue when context overlap is high, and a fresh worker can verify independently. The coordinator is not merely forwarding messages. Before every handoff, it decides whether the same worker should continue or whether the result should be rewritten as a fresh task for someone else.

When the same worker continues, it usually still has useful context. It may have just read the relevant files, run the failing tests, or found the exact function. Continuing it is reasonable, but the follow-up still needs to restate the important facts: file, line, cause, desired change, and done criteria. That uses the worker’s context without handing understanding back to the worker.

When a fresh worker is spawned, the bar is higher. The new worker does not have the previous worker’s discovery context, so “based on your findings, fix it” is not a real task brief. The coordinator has to turn the previous result into a self-contained instruction.

So the rule is not “never mention what the worker found.” The rule is: do not let that phrase replace synthesis. Parallelism speeds up information gathering; quality comes from reorganizing findings, constraints, and the next action into a clear handoff.

Run research in parallel; narrow write-heavy work to fewer workers.

When continuing the same worker, use its context, but restate the key facts in the follow-up.

When spawning a fresh worker, the prompt must be self-contained and cannot depend on another worker’s history.

Verification is often better as a fresh worker to avoid inherited assumptions.

Failures usually belong with the same worker because it has the error context.

A concrete example

Weak prompt

“Based on your findings, fix it.” This only barely works in the best continuation case, and it is still underspecified.

Better continuation

“Continue with the issue you found in src/auth/validate.ts:42. session.user can be empty when the session expires but the token remains cached; add a null check before user.id, return 401 / Session expired, and run validate.test.ts.”

For a fresh worker

“Fix a null pointer in src/auth/validate.ts:42. Research found that session.user can be undefined when the session expires but the token remains cached. Add a null check, return 401 / Session expired, update tests, and run the relevant suite.”

Permissions are the boundary of collaboration

The permission layer is easy to underestimate. An in-process teammate shares the Node.js process while keeping a separate identity. AsyncLocalStorage carries teammate identity, and tool approval can go through the leader ToolUseConfirm UI so the user can see which worker wants to do what.

If that leader UI is unavailable, there is a mailbox version of permission_request / permission_response. True background agents cannot show UI, so runAgent turns on shouldAvoidPermissionPrompts and the tool pool is filtered to tools suitable for async execution. Execution shape decides permission shape.

That is realistic. Once agents can edit files, run commands, and call MCP tools, they become multiple executors sharing one workspace. Without a permission protocol, they are just creating risk concurrently.

Architecture judgments I took from the source

First, the bottleneck is synthesis. Parallel results need to return to the right context reliably; task notifications, idle notifications, and mailboxes all solve that return flow.

Second, names matter more than UUIDs. The source repeatedly tells agents to message teammates by name because collaboration is organized around human-readable roles and ownership.

Third, long-lived teammates need an idle loop; one-shot subagents need a resume path. One behaves like a teammate, the other like a resumable background job.

Fourth, a useful multi-agent system looks a little conservative: file locks, task state, explicit messages, permission loops, stop and resume. These pieces are less flashy than model capability, but they decide whether the system works on a real project.

Fifth, conflict control is not a magic runtime arbiter. Claude Code relies on scheduling prompts, task ownership, message protocol, and optional isolation to turn hidden concurrency into visible, manageable engineering state.