claude-code · Agent 架构 · 2026-06-06

Claude Code 源码解析(04)Coordinator:主控 Agent 如何编排 Subagent

这篇源码解析基于本地 Claude Code 源码,拆解 coordinator mode 如何把主 agent 变成编排者:什么时候启动 worker,worker 能看到什么上下文,AgentTool 如何把子 agent 写入 task state 和 transcript,SendMessage / TaskStop / TaskOutput 如何继续、停止和召回 worker 输出,以及默认异步编排和可选 fork/worktree/remote 隔离的边界。

聚焦
Coordinator / Subagent 编排:coordinator mode、worker prompt、AgentTool 异步任务、SendMessage 续写、TaskStop 停止、TaskOutput 召回输出、worker 工具/权限/上下文隔离与 worktree/fork 边界
源码
assets/projects/claude-code
版本
4b9d30f
查看仓库

先给结论

Claude Code 的 coordinator 不是“让模型知道可以开多个助手”。它把主 agent 的运行模式改成编排者:主 agent 负责判断任务是否值得委派、给 worker 写自包含任务、读取结果、继续追问、停止偏离方向的 worker,并最终面向用户综合;worker 负责研究、实现或验证这些可并行子任务。

Coordinator 编排全景:主 agent 不直接等待 worker,而是通过 task runtime 接收异步通知,再决定继续、停止或综合。
Coordinator 编排全景:主 agent 不直接等待 worker,而是通过 task runtime 接收异步通知,再决定继续、停止或综合。

默认路径

Coordinator mode 下,主 agent 用 AgentTool 创建 worker。worker 作为 async local_agent task 运行,结果以 task-notification 回流;主 agent 再用 SendMessage、TaskStop 或 output_file/TaskOutput 控制后续。

可选路径

fork subagent、worktree isolation、remote isolation、命名 teammate、agent frontmatter MCP/skill/hook、后台输出整理都是增强路径,不是 coordinator 默认编排的前提。

源码证据地图

这条线只讨论主控 agent 如何调度 worker,不展开普通工具执行、安全沙箱、MCP 协议和 context compact 的完整实现。

text
1# 1 assets/projects/claude-code/src/coordinator/coordinatorMode.ts2# 2 assets/projects/claude-code/src/utils/systemPrompt.ts3# 3 assets/projects/claude-code/src/constants/tools.ts4# 4 assets/projects/claude-code/src/utils/toolPool.ts5# 5 assets/projects/claude-code/src/tools/AgentTool/AgentTool.tsx6# 6 assets/projects/claude-code/src/tools/AgentTool/runAgent.ts7# 7 assets/projects/claude-code/src/tools/AgentTool/agentToolUtils.ts8# 8 assets/projects/claude-code/src/tools/AgentTool/loadAgentsDir.ts9# 9 assets/projects/claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx10# 10 assets/projects/claude-code/src/utils/attachments.ts11# 11 assets/projects/claude-code/src/tools/SendMessageTool/SendMessageTool.ts12# 12 assets/projects/claude-code/src/tasks/stopTask.ts13# 13 assets/projects/claude-code/src/tools/TaskOutputTool/TaskOutputTool.tsx14# 14 assets/projects/claude-code/src/tools/AgentTool/forkSubagent.ts

Coordinator 先改变主 Agent

Coordinator mode 的入口是环境/feature gate。isCoordinatorMode() 不是复杂逻辑,但它决定后面 system prompt、工具池和 UI 行为是否进入主控模式。

ts
1// assets/projects/claude-code/src/coordinator/coordinatorMode.ts2export function isCoordinatorMode(): boolean {3  if (feature('COORDINATOR_MODE')) {4    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)5  }6  return false7}

真正的机制在 prompt。getCoordinatorSystemPrompt() 把主 agent 定义成 coordinator,要求它 direct workers、synthesize results、communicate with the user。这个提示还把 <task-notification> 定义成内部信号,而不是用户消息。

ts
1// assets/projects/claude-code/src/coordinator/coordinatorMode.ts2export function getCoordinatorSystemPrompt(): string {3  return `You are Claude Code, an AI assistant that orchestrates4software engineering tasks across multiple workers.5 6## 1. Your Role7You are a **coordinator**. Your job is to:8- Direct workers to research, implement and verify code changes9- Synthesize results and communicate with the user`10}

utils/systemPrompt.ts 在 coordinator mode 下会直接替换系统提示。这不是在普通 coding prompt 后面补一句“你可以找人帮忙”,而是切换主 agent 的角色定义。

ts
1// assets/projects/claude-code/src/utils/systemPrompt.ts2if (3  isCoordinatorMode() &&4  !mainThreadAgentDefinition &&5  !overrideSystemPrompt6) {7  return asSystemPrompt([getCoordinatorSystemPrompt()])8}

主线程工具池被收窄

主 agent 进入 coordinator 后,不再像普通 coding agent 那样直接拿完整工具池。COORDINATOR_MODE_ALLOWED_TOOLS 只保留编排相关工具:AgentTaskStopSendMessage 和 synthetic output。

ts
1// assets/projects/claude-code/src/constants/tools.ts2export const COORDINATOR_MODE_ALLOWED_TOOLS = new Set([3  AGENT_TOOL_NAME,4  TASK_STOP_TOOL_NAME,5  SEND_MESSAGE_TOOL_NAME,6  SYNTHETIC_OUTPUT_TOOL_NAME,7])

过滤发生在 tool pool 合并阶段。代码还给 PR subscription 这类 MCP 工具留了例外,因为主控需要直接订阅外部事件。

ts
1// assets/projects/claude-code/src/utils/toolPool.ts2export function applyCoordinatorToolFilter(tools: Tools): Tools {3  if (!isCoordinatorMode()) return tools4  return tools.filter(tool =>5    COORDINATOR_MODE_ALLOWED_TOOLS.has(tool.name) ||6    isPrSubscriptionTool(tool.name),7  )8}

这个设计的效果是:主 agent 更像调度者,不是“自己改文件同时让 worker 改文件”。它能 spawn、继续、停止、读结果,但默认不把大量文件编辑工具留在主线程上。

Worker 能力是动态注入的

Coordinator prompt 不是静态列出所有 worker 工具。getCoordinatorUserContext() 会根据当前会话的 async-agent allowed tools、MCP clients、scratchpad 生成 user context,告诉主 agent worker 真实可用能力。

ts
1// assets/projects/claude-code/src/coordinator/coordinatorMode.ts2export function getCoordinatorUserContext(3  mcpClients: ReadonlyArray<{ name: string }>,4  scratchpadDir?: string,5): { [k: string]: string } {6  if (!isCoordinatorMode()) return {}7  const workerTools = Array.from(ASYNC_AGENT_ALLOWED_TOOLS)8    .filter(name => !INTERNAL_WORKER_TOOLS.has(name))9    .sort()10    .join(', ')11  let content = `Workers spawned via the ${AGENT_TOOL_NAME} tool have access to these tools: ${workerTools}`12  return { workerToolsContext: content }13}
Worker 生命周期:能力说明进入 context,AgentTool 创建 task,worker 独立 query,结果以通知回流。
Worker 生命周期:能力说明进入 context,AgentTool 创建 task,worker 独立 query,结果以通知回流。
1主 agent 判断是否委派

Coordinator prompt 要求主 agent 只把研究、实现、验证等适合并行的任务交出去。

2AgentTool 创建 worker

AgentTool 接收 description、prompt、subagent_type、background/isolation 等输入。

3runAgent 重组上下文

worker 获得自包含 prompt、自己的工具池、权限和 agent 配置。

4LocalAgentTask 记录状态

task state 保存 agentId、prompt、进度、pendingMessages、输出路径。

5task-notification 回流

worker 完成或有状态变化时生成 task-notification。

6主 agent 综合或继续

主 agent 读取内部通知后决定回答、继续发消息、停止或读输出。

AgentTool 把委派写入 Task Runtime

AgentTool 的输入 schema 把“给人看的描述”和“给 worker 的任务”分开。description 用于 UI/task 识别,prompt 才是 worker 执行指令,subagent_type 选择 agent 定义。

ts
1// assets/projects/claude-code/src/tools/AgentTool/AgentTool.tsx2const baseInputSchema = lazySchema(() => z.object({3  description: z.string().describe('A short (3-5 word) description of the task'),4  prompt: z.string().describe('The task for the agent to perform'),5  subagent_type: z.string().optional(),6  run_in_background: z.boolean().optional(),7}))

Coordinator mode 下,worker 默认走 async task。shouldRunAsyncisCoordinator 纳入条件,所以主控模式不需要每次显式写 run_in_background: true 才后台运行。

ts
1// assets/projects/claude-code/src/tools/AgentTool/AgentTool.tsx2const shouldRunAsync =3  (run_in_background === true ||4    selectedAgent.background === true ||5    isCoordinator ||6    forceAsync ||7    assistantForceAsync ||8    (proactiveModule?.isProactiveActive() ?? false)) &&9  !isBackgroundTasksDisabled

创建 worker 时,registerAsyncAgent() 会把 agentId、description、prompt、selectedAgent、toolUseId 等写入 AppState.tasks。后续进度展示、通知、停止、继续和输出读取都依赖这个 task state。

ts
1// assets/projects/claude-code/src/tools/AgentTool/AgentTool.tsx2const agentBackgroundTask = registerAsyncAgent({3  agentId: asyncAgentId,4  description,5  prompt,6  selectedAgent,7  setAppState: rootSetAppState,8  toolUseId: toolUseContext.toolUseId,9})

子 Agent 不是完整复制主会话

普通 worker 默认只拿到自包含 prompt message,不自动继承完整用户对话。runAgent.ts 只有在 fork path 提供 forkContextMessages 时才把父上下文放进来。

ts
1// assets/projects/claude-code/src/tools/AgentTool/runAgent.ts2const contextMessages: Message[] = forkContextMessages3  ? filterIncompleteToolCalls(forkContextMessages)4  : []5const initialMessages: Message[] = [...contextMessages, ...promptMessages]

这解释了 coordinator prompt 为什么要求任务说明具体。主 agent 如果只写“按刚才说的修一下”,普通 worker 可能根本不知道“刚才”是什么。worker prompt 必须包含目标、文件、约束、验收方式和需要返回的结果形态。

普通 coordinator worker

自包含 prompt + agent 定义 + 裁剪后的 user/system context + 重新计算的工具/权限。适合并行研究、实现、验证。

fork subagent

继承父会话消息和父 system prompt,强调 cache-friendly 前缀和一次性 directive;它不是 coordinator 默认路径。

工具、权限和扩展点会重算

worker 的工具池不是简单继承主 agent。AgentTool 会基于 worker 的 permission mode 重新 assemble tools,runAgent 再根据 agent definition 的 toolsdisallowedTools、skills、MCP servers 和 hooks 继续裁剪/增强。

ts
1// assets/projects/claude-code/src/tools/AgentTool/AgentTool.tsx2const workerPermissionContext = {3  ...appState.toolPermissionContext,4  mode: selectedAgent.permissionMode ?? 'acceptEdits',5}6const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools)

异步 agent 还会被 ASYNC_AGENT_ALLOWED_TOOLS 限制,避免后台 worker 使用不适合异步场景的工具。

ts
1// assets/projects/claude-code/src/tools/AgentTool/agentToolUtils.ts2if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) {3  if (isAgentSwarmsEnabled() && isInProcessTeammate()) {4    if (toolMatchesName(tool, AGENT_TOOL_NAME)) return true5  }6  return false7}

agent definition 本身也不只是 prompt。它可以声明工具、禁用工具、skills、MCP servers、hooks、permissionMode、maxTurns、background 和 isolation。

ts
1// assets/projects/claude-code/src/tools/AgentTool/loadAgentsDir.ts2export type BaseAgentDefinition = {3  agentType: string4  whenToUse: string5  tools?: string[]6  disallowedTools?: string[]7  skills?: string[]8  mcpServers?: AgentMcpServerSpec[]9  hooks?: HooksSettings10  permissionMode?: PermissionMode11  isolation?: 'worktree' | 'remote'12}
隔离不是只有 worktree:上下文、工具、权限、MCP、hooks、skills 都会在 runAgent 中重新组装。
隔离不是只有 worktree:上下文、工具、权限、MCP、hooks、skills 都会在 runAgent 中重新组装。

权限还有一个运行时分支:async worker 默认避免弹出交互式权限框;bubble mode 或显式允许时,权限请求才会冒泡到父终端。

ts
1// assets/projects/claude-code/src/tools/AgentTool/runAgent.ts2const shouldAvoidPrompts =3  canShowPermissionPrompts !== undefined4    ? !canShowPermissionPrompts5    : agentPermissionMode === 'bubble'6      ? false7      : isAsync

结果不是 return,而是通知回流

异步 worker 结束时不会直接替主 agent 回答用户。LocalAgentTask 会生成 <task-notification>,里面包含 task id、输出文件路径、状态、摘要、结果和 usage。这个 XML 作为内部消息回到主循环。

ts
1// assets/projects/claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx2const message = `<${TASK_NOTIFICATION_TAG}>3<${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>4<${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}>5<${STATUS_TAG}>${status}</${STATUS_TAG}>6<${SUMMARY_TAG}>${summary}</${SUMMARY_TAG}>${resultSection}${usageSection}7</${TASK_NOTIFICATION_TAG}>`

这是一种很实用的桥接:从消息通道看,它像 user-role input;从 prompt 语义看,它不是用户需求,而是内部任务事件。主 agent 要读取它,再决定是否继续。

进度、输出和 pending message 存在哪

运行态状态存在 AppState.tasksLocalAgentTaskState 保存 agentId、prompt、progress、pendingMessages、messages 等字段;sidechain transcript 和 output file 则让 worker 结果可以跨 UI 和恢复路径读取。

ts
1// assets/projects/claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx2export type LocalAgentTaskState = TaskStateBase & {3  type: 'local_agent'4  agentId: string5  prompt: string6  progress?: AgentProgress7  pendingMessages: string[]8  messages?: Message[]9}

runAgent.ts 会记录 sidechain transcript 和 agent metadata。这样 SendMessage 可以恢复 worker,TaskOutput 可以按 taskId 读输出,UI 也能展示异步进度。

ts
1// assets/projects/claude-code/src/tools/AgentTool/runAgent.ts2void recordSidechainTranscript(initialMessages, agentId)3void writeAgentMetadata(agentId, {4  agentType: agentDefinition.agentType,5  ...(worktreePath && { worktreePath }),6  ...(description && { description }),7})
控制面:TaskOutput 读输出,SendMessage 继续 worker,TaskStop 停止 task,notification 把结果送回主 agent。
控制面:TaskOutput 读输出,SendMessage 继续 worker,TaskStop 停止 task,notification 把结果送回主 agent。

SendMessage 是续写,不是新开一个 worker

SendMessageTool 先把 to 解析成 agent name 或 agentId。如果目标 worker 正在运行,它不会打断当前执行,而是把消息加入 pendingMessages

ts
1// assets/projects/claude-code/src/tools/SendMessageTool/SendMessageTool.ts2if (task.status === 'running') {3  queuePendingMessage(4    agentId,5    input.message,6    context.setAppStateForTasks ?? context.setAppState,7  )8  return { data: { success: true, message: `Message queued` } }9}

pending message 的消费点在 dynamic attachments。attachments.ts 会调用 drainPendingMessages(),把队列里的消息变成下一轮发给 worker 的上下文。

ts
1// assets/projects/claude-code/src/tasks/LocalAgentTask/LocalAgentTask.tsx2export function drainPendingMessages(3  taskId: string,4  getAppState: () => AppState,5  setAppState: (f: (prev: AppState) => AppState) => void,6): string[] {7  const task = getAppState().tasks[taskId]8  if (!isLocalAgentTask(task) || task.pendingMessages.length === 0) return []9  const drained = task.pendingMessages10  updateTaskState(taskId, setAppState, t => ({ ...t, pendingMessages: [] }))11  return drained12}

如果 worker 已停止或内存状态里没有这个 task,SendMessage 会尝试 resumeAgentBackground(),从 transcript 恢复同一个 agent 后继续。这就是“续写 worker”的语义,而不是重新开一个没有上下文的新 worker。

TaskStop 和 TaskOutput 构成控制面

TaskStop 解决的是“这个 worker 方向错了、需求变了、或者已经不需要了”。它按 taskId 找到 running task,再调用对应 task implementation 的 kill()

ts
1// assets/projects/claude-code/src/tasks/stopTask.ts2const task = appState.tasks?.[taskId] as TaskStateBase | undefined3if (!task) throw new StopTaskError(`No task found`, 'not_found')4if (task.status !== 'running') {5  throw new StopTaskError(`Task is not running`, 'not_running')6}7const taskImpl = getTaskByType(task.type)8await taskImpl.kill(taskId, setAppState)

TaskOutput 解决的是“我需要主动读取这个 task 的输出”。非阻塞模式下,如果 task 已经完成,就直接读输出;blocking 模式可以等待。

ts
1// assets/projects/claude-code/src/tools/TaskOutputTool/TaskOutputTool.tsx2if (!block) {3  if (task.status !== 'running' && task.status !== 'pending') {4    return {5      data: {6        retrieval_status: 'success',7        task: await getTaskOutputData(task),8      },9    }10  }11}
1收到通知

task-notification 把内部状态送回主循环。

2判断结果是否足够

主 agent 对照原任务判断证据、实现或验证是否缺口。

3SendMessage 继续

同一 worker 仍有上下文时,用 SendMessage 追加指令。

4TaskStop 停止

方向错误或任务无价值时停止 running task。

5读取 output_file 或 TaskOutput

需要完整结果时读取 output_file 或调用 TaskOutput。

6综合给用户

主 agent 对多个 worker 的结果去重、取舍、汇总。

Fork Subagent 是另一条路径

forkSubagent.ts 容易和 coordinator 混在一起。fork 的目标是继承父会话上下文,并尽量复用 prompt cache;coordinator 的普通 worker 则强调自包含 prompt、异步 task 和通知回流。源码里甚至在 coordinator mode 下禁用 fork。

ts
1// assets/projects/claude-code/src/tools/AgentTool/forkSubagent.ts2export function isForkSubagentEnabled(): boolean {3  if (feature('FORK_SUBAGENT')) {4    if (isCoordinatorMode()) return false5    if (getIsNonInteractiveSession()) return false6    return true7  }8  return false9}

所以默认 coordinator worker 的关键不是“复制当前会话”,而是“主 agent 写一份可执行任务说明”。这也是它能并行跑多个 worker 的前提。

可复用设计

  • 主控保留理解权:worker 产物是证据,不是最终答案;主 agent 负责综合和取舍。
  • spawn/notify/continue/stop 四个控制面先稳定:先有 AgentTool、task-notification、SendMessage、TaskStop,再考虑 worktree/remote 等增强。
  • worker prompt 必须自包含:普通 worker 不自动拥有完整主对话,任务说明要包含目标、约束和验收标准。
  • 隔离不只靠文件系统:上下文、工具、权限、MCP、skills、hooks 都是隔离维度。
  • 输出要可寻址:task id、output file、sidechain transcript 让主 agent 能继续、恢复和审计 worker。

边界

默认 coordinator worker 看不到完整用户对话;默认不创建 worktree;默认不是 remote execution;默认不会自动规划出完美分工;多个 worker 写同一批文件也可能制造冲突。它提供的是编排协议和任务运行时,不是替主 agent 做判断的全自动调度器。