Post

Claude Code - Multi Agent

Claude Code - Multi Agent

Multi Agent

multi agent overview

Sub-Agent

隔离
tool隔离
  • 所有subagent通用黑名单
    • 能派新sub agent的工具
    • 能主动问用户问题的工具
    • 能切换规划模式的工具
    • 能停止其他任务的工具
  • 自定义agent多套一层黑名单
  • 后台异步agent走白名单
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // src/tools/AgentTool/agentToolUtils.ts:70
    export function filterToolsForAgent({ tools, isBuiltIn, isAsync, permissionMode }): Tools {
      return tools.filter(tool => {
          if (tool.name.startsWith('mcp__')) return true// MCP 工具全放行
          if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false // 黑名单
          if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false // custom agent 黑名单
          if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) return false
          return true
      })
    }
    
    context隔离
  • 读文件的缓存 要复制一份给sub agent
  • 改全局状态 这件事对sub agent 直接关闭
  • 注册后台任务 sub agent保留这个接口
  • 给每个sub agent发独立 ID、深度代代+1
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
      // src/utils/forkedAgent.ts:345
      export function createSubagentContext(parentContext, overrides): ToolUseContext {
          return {
              // 决策一:文件读缓存克隆一份
              readFileState: cloneFileStateCache(parentContext.readFileState),
              // 决策二:写全局状态直接设为空操作
              setAppState: () => {},
              // 决策三:但任务注册的通路例外保留
              setAppStateForTasks: parentContext.setAppStateForTasks ?? parentContext.setAppState,
              // 决策四:独立 ID + 深度 +1
              agentId: overrides?.agentId ?? createAgentId(),
              queryTracking: {
                  chainId: randomUUID(),
                  depth: (parentContext.queryTracking?.depth ?? -1) + 1,
              },
              // ...其他字段略
          }
      }
    
    父子agent通信
    父 -> 子
  • 消息队列 + 异步通知
  • sub agent 档案
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    // src/tasks/LocalAgentTask/LocalAgentTask.tsx:116
    export type LocalAgentTaskState = TaskStateBase & {
      type: 'local_agent';
      agentId: string;               // 子 agent 唯一 ID
      prompt: string;                // 初始任务
      agentType: string;
      status: TaskStatus;            // pending/running/completed/failed/killed
      result?: AgentToolResult;      // 完成后的结果
      progress?: AgentProgress;      // 进度
      isBackgrounded: boolean;       // 是否已转后台
      pendingMessages: string[];     // 信箱:父 agent 扔进来的待处理消息
      messages?: Message[];
    };
    
  • 消息投递
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // src/tools/SendMessageTool/SendMessageTool.ts:800
    const task = appState.tasks[agentId]
    if (isLocalAgentTask(task) && !isMainSessionTask(task)) {
      if (task.status === 'running') {
          queuePendingMessage(agentId, input.message, context.setAppStateForTasks)
          return { data: { success: true, message: 'Message queued...' } }
      }
      // 任务已停止,自动唤醒从 transcript 里恢复
      const result = await resumeAgentBackground({ agentId, prompt: input.message, ... })
    }
    
1
2
3
4
5
6
7
// src/tasks/LocalAgentTask/LocalAgentTask.tsx:162
export function queuePendingMessage(taskId, msg, setAppState): void {
	updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => ({
		...task,
		pendingMessages: [...task.pendingMessages, msg]
	}));
}
子 -> 父
  • 将完成通知拼成XML, 伪装成用户消息, 放进父agent的对话中 ```xml
agent-a1b /tmp/xxx.txt completed Agent "Investigate auth bug" completed Found null pointer in src/auth/validate.ts:42... 12345 8 34567
1
2
3
4
5
6
7
8
9
```ts
// src/tasks/LocalAgentTask/LocalAgentTask.tsx:197
const message = `<${TASK_NOTIFICATION_TAG}>
				<${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>
				<${OUTPUT_FILE_TAG}>${outputPath}</${OUTPUT_FILE_TAG}>
				<${STATUS_TAG}>${status}</${STATUS_TAG}>
				<${SUMMARY_TAG}>${summary}</${SUMMARY_TAG}>${resultSection}${usageSection}
				</${TASK_NOTIFICATION_TAG}>`;
enqueuePendingNotification({ value: message, mode: 'task-notification' });
auto-background
  • 父agent同步等待sub agent完成(一个阈值时间内), 超过阈值, sub agent转后台执行
    1
    2
    3
    4
    5
    6
    7
    8
    
    // src/tools/AgentTool/AgentTool.tsx:72
    function getAutoBackgroundMs(): number {
    if (isEnvTruthy(process.env.CLAUDE_AUTO_BACKGROUND_TASKS) 
        || getFeatureValue_CACHED_MAY_BE_STALE('tengu_auto_background_agents', false)) {
      return 120_000;  // 2 分钟
    }
    return 0;
    }
    

    Fork Agent

  • 利用到prompt cache, fork sub agent和父agent的system prompt一致
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    // src/utils/forkedAgent.ts:57
    export type CacheSafeParams = {
      /** System prompt - 必须跟父完全一致 */
    	systemPrompt: SystemPrompt
      /** User context - 拼接在消息前,影响缓存 */
      userContext: { [k: string]: string }
      /** System context - 拼接在 system prompt 后,影响缓存 */
      systemContext: { [k: string]: string }
      /** 工具池、模型等所在的上下文 */
      toolUseContext: ToolUseContext
      /** 父 agent 的消息前缀,用于缓存共享 */
      forkContextMessages: Message[]
    }
    
  • fork agent的system prompt和父agent一致, 不用自己生成
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // src/tools/AgentTool/forkSubagent.ts:60
    export const FORK_AGENT = {
      agentType: FORK_SUBAGENT_TYPE,
      tools: ['*'],             // 用父的完整工具池
      maxTurns: 200,
      model: 'inherit',          // 继承父的模型
      permissionMode: 'bubble',  // 权限弹窗浮到父终端
      source: 'built-in',
      getSystemPrompt: () => '', // 返回空串!
    } satisfies BuiltInAgentDefinition
    
  • fork 和 coordinate互斥
1
2
3
4
5
6
7
8
9
// src/tools/AgentTool/forkSubagent.ts:32
export function isForkSubagentEnabled(): boolean {
	if (feature('FORK_SUBAGENT')) {
		if (isCoordinatorMode()) returnfalse// 互斥!
		if (getIsNonInteractiveSession()) return false
		return true
	}
	return false
}

Coordinator-Worker

  • coordinate模式要显式打开
1
2
3
4
5
6
7
// src/coordinator/coordinatorMode.ts:36
export function isCoordinatorMode(): boolean {
  if (feature('COORDINATOR_MODE')) { // 编译时的功能开关
    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) // 环境变量 CLAUDE_CODE_COORDINATOR_MODE=1
  }
  return false
}
  • 主agent只做派 worker, 收结果, 合成答案; system prompt支持
1
2
3
4
5
6
7
8
9
10
11
12
You are Claude Code, an AI assistant that orchestrates software engineering 
tasks across multiple workers.

## 1. Your Role
You are a **coordinator**. Your job is to:
- Help the user achieve their goal
- Direct workers to research, implement and verify code changes
- Synthesize results and communicate with the user
- Answer questions directly when possible, don't delegate work 
  that you can handle without tools

你的身份是协调者,你的工作是指挥 worker 去做研究、实现、验证,然后自己合成结果跟用户交流。能自己回答的问题不要派人去做。
  • coordinator的builtin工具
    1
    2
    3
    4
    5
    6
    7
    
    // src/coordinator/coordinatorMode.ts:29
    const INTERNAL_WORKER_TOOLS = new Set([
      TEAM_CREATE_TOOL_NAME,       // 创建 worker 团队
      TEAM_DELETE_TOOL_NAME,       // 解散团队
      SEND_MESSAGE_TOOL_NAME,      // 给 worker 发消息
      SYNTHETIC_OUTPUT_TOOL_NAME,  // 合成最终输出给用户
    ])
    
  • 支持并行创建worker, 部分system prompt ```md Parallelism is your superpower. Workers are async. Launch independent workers concurrently whenever possible, don’t serialize work that can run simultaneously and look for opportunities to fan out.

并行是你的超能力,worker 全是异步的,能并行的绝不串行,多找机会一口气派一堆出去。 ```

  • 持续派活时, 是resume old agent 还是 spawn new agent
    • 新任务跟 worker 现有上下文高度相关(比如刚查的文件现在要改),续命老 worker
    • 新任务跟 worker 现有上下文没关系,或者之前 worker 的工作走偏了,派新 worker
This post is licensed under CC BY 4.0 by the author.