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 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 的完整实现。
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.tsCoordinator 先改变主 Agent
Coordinator mode 的入口是环境/feature gate。isCoordinatorMode() 不是复杂逻辑,但它决定后面 system prompt、工具池和 UI 行为是否进入主控模式。
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> 定义成内部信号,而不是用户消息。
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 的角色定义。
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 只保留编排相关工具:Agent、TaskStop、SendMessage 和 synthetic output。
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 工具留了例外,因为主控需要直接订阅外部事件。
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 真实可用能力。
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}Coordinator prompt 要求主 agent 只把研究、实现、验证等适合并行的任务交出去。
AgentTool 接收 description、prompt、subagent_type、background/isolation 等输入。
worker 获得自包含 prompt、自己的工具池、权限和 agent 配置。
task state 保存 agentId、prompt、进度、pendingMessages、输出路径。
worker 完成或有状态变化时生成 task-notification。
主 agent 读取内部通知后决定回答、继续发消息、停止或读输出。
AgentTool 把委派写入 Task Runtime
AgentTool 的输入 schema 把“给人看的描述”和“给 worker 的任务”分开。description 用于 UI/task 识别,prompt 才是 worker 执行指令,subagent_type 选择 agent 定义。
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。shouldRunAsync 把 isCoordinator 纳入条件,所以主控模式不需要每次显式写 run_in_background: true 才后台运行。
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。
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 时才把父上下文放进来。
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 的 tools、disallowedTools、skills、MCP servers 和 hooks 继续裁剪/增强。
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 使用不适合异步场景的工具。
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。
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}权限还有一个运行时分支:async worker 默认避免弹出交互式权限框;bubble mode 或显式允许时,权限请求才会冒泡到父终端。
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 作为内部消息回到主循环。
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.tasks。LocalAgentTaskState 保存 agentId、prompt、progress、pendingMessages、messages 等字段;sidechain transcript 和 output file 则让 worker 结果可以跨 UI 和恢复路径读取。
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 也能展示异步进度。
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})SendMessage 是续写,不是新开一个 worker
SendMessageTool 先把 to 解析成 agent name 或 agentId。如果目标 worker 正在运行,它不会打断当前执行,而是把消息加入 pendingMessages。
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 的上下文。
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()。
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 模式可以等待。
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}task-notification 把内部状态送回主循环。
主 agent 对照原任务判断证据、实现或验证是否缺口。
同一 worker 仍有上下文时,用 SendMessage 追加指令。
方向错误或任务无价值时停止 running task。
需要完整结果时读取 output_file 或调用 TaskOutput。
主 agent 对多个 worker 的结果去重、取舍、汇总。
Fork Subagent 是另一条路径
forkSubagent.ts 容易和 coordinator 混在一起。fork 的目标是继承父会话上下文,并尽量复用 prompt cache;coordinator 的普通 worker 则强调自包含 prompt、异步 task 和通知回流。源码里甚至在 coordinator mode 下禁用 fork。
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 做判断的全自动调度器。
