Claude Code 源码解剖

Claude Code 的多 Agent 机制

从 AgentTool、Fork、Coordinator、Teams、TaskList、Mailbox、权限同步和 Ultraplan 拆解 Claude Code 如何把单个 Agent Loop 扩展成可并行、可隔离、可审批的多 Agent 运行时。

第 15 章 24 分钟

Claude Code 的多 Agent 机制

这一章要解决的问题很直接:Claude Code 为什么不是简单地“再开几个聊天线程”,而是做了一套 Agent 派生、后台生命周期、团队任务图、消息邮箱和远程计划系统?

源码里的答案可以压成一句话:多 Agent 的核心不是让模型互相聊天,而是把任务、身份、权限、通信和生命周期外化成运行时状态。模型负责理解和执行,运行时负责隔离、调度、审批和收尾。

核心直觉

Claude Code 的多 Agent 不是一个功能点,而是一组递进能力:AgentTool 负责派生,runWithAgentContext() 负责身份隔离,Fork 负责复用父上下文,Coordinator 负责分阶段编排,Teams 负责平面协作,Ultraplan 负责把计划阶段远程化。

先看源码入口

这一章要追的入口比较多,但可以按“层级派生”和“平面协作”分成两组。

源码定位关键符号负责什么
tools/AgentTool/AgentTool.tsxAgentToolbaseInputSchemashouldRunAsync所有 Agent 派生的统一入口,动态暴露 subagent_typemodelrun_in_backgroundisolationteam_name 等字段
utils/agentContext.tsAsyncLocalStorage@@INLINE_0@@runWithAgentContext()在同进程并发 Agent 之间隔离 agentIdagentTypeinvokingRequestId
tools/AgentTool/forkSubagent.tsisForkSubagentEnabled()FORK_AGENTbuildForkedMessages()Fork 模式,继承父会话上下文和系统提示词,最大化 Prompt Cache 共享
coordinator/coordinatorMode.tsisCoordinatorMode()getCoordinatorUserContext()协调者模式,主 Agent 只负责规划、综合和派发 Worker
tools/TeamCreateTool/TeamCreateTool.tsteam_nameagent_type创建 Team,写入 TeamFile,并绑定对应 TaskList
utils/swarm/teamHelpers.tsTeamFilemembers[]团队配置文件,记录队友身份、模型、权限模式、后端类型和活动状态
utils/swarm/spawnMultiAgent.tsspawnTeammate()根据 tmux、iTerm2、In-Process 后端生成队友,并把 CLI 参数和初始指令传过去
tools/SendMessageTool/SendMessageTool.tsSendMessageToolto: "*" / uds: / bridge:队友间消息路由,支持名称、广播、UDS 和 Remote Control bridge
utils/tasks.tsuseTaskListWatcher.tsTaskfindAvailableTask()claimTask()Teams 的共享任务图和最小调度器
utils/swarm/permissionSync.tspending/resolved/队友危险工具调用交给 Leader 代理审批
commands/ultraplan.tsxutils/ultraplan/ccrSession.tslaunchUltraplan()pollForApprovedExitPlanMode()把计划阶段上传到 CCR 远程容器运行,并轮询 ExitPlanMode 状态

这些入口共同说明:Claude Code 的多 Agent 不是单独靠 prompt 约束出来的,它在运行时有明确的数据结构和状态转移。

总流程图

flowchart TD A["模型调用 AgentTool"] --> B{"选择派生模式"} B --> C["标准子 Agent<br/>新上下文 + Agent 定义"] B --> D["Fork Agent<br/>继承父消息 + cache-safe 前缀"] B --> E["Coordinator Worker<br/>协调者派发后台任务"] B --> F["Team Teammate<br/>平面团队成员"] C --> G["runAgent()"] D --> H["buildForkedMessages()<br/>占位 tool_result + per-child 指令"] E --> I["registerAsyncAgent()<br/>后台进度和完成通知"] F --> J["spawnTeammate()<br/>tmux / iTerm2 / In-Process"] J --> K["TeamFile<br/>~/.claude/teams/{team}/config.json"] J --> L["TaskList<br/>~/.claude/tasks/{team}/"] J --> M["Mailbox<br/>~/.claude/teams/{team}/inboxes/*.json"] L --> N["findAvailableTask()<br/>pending + no owner + blockers done"] N --> O["claimTask()<br/>写入 owner"] O --> P["队友执行任务"] P --> Q["TaskCompleted / TeammateIdle"] Q --> N P --> R{"需要危险工具?"} R -->|是| S["permissionSync<br/>pending -> Leader 审批 -> resolved"] R -->|否| T["继续执行"] S --> T A --> U{"用户触发 ultraplan?"} U -->|是| V["teleportToRemote()<br/>CCR 远程容器"] V --> W["pollForApprovedExitPlanMode()<br/>running / needs_input / plan_ready"]

这张图里最容易被忽略的是 Teams 这条线。标准子 Agent 和 Fork 仍然是“父派生子”的层级结构;Teams 则变成平面团队,靠 TaskList、Mailbox 和权限同步协作。

第一层:AgentTool 是统一派生入口

所有派生都先经过 AgentTool。它不是一个静态工具定义,而是会根据 Feature Flag 和运行时状态动态组合 schema。

const baseInputSchema = lazySchema(() => z.object({
  description: z.string(),
  prompt: z.string(),
  subagent_type: z.string().optional(),
  model: z.enum(['sonnet', 'opus', 'haiku']).optional(),
  run_in_background: z.boolean().optional()
}))

这段代码证明:标准的 Agent 派生至少包含任务描述、完整 prompt、可选 Agent 类型、模型和后台运行开关。原文还说明,当 Swarm 特性启用时,schema 会继续合并 nameteam_namemode;当 Fork 模式启用时,run_in_background 会被移除,因为 Fork 默认后台化。

这个动态 schema 很关键。模型不是看到一份“全功能说明书”,而是只看到当前运行条件下允许使用的参数。能力暴露本身就是控制面。

同进程隔离靠 AsyncLocalStorage

后台 Agent 可能和主 Agent 在同一进程里并发运行。如果身份信息放在全局 AppState,Agent A 的事件可能被 Agent B 的上下文覆盖。源码用 AsyncLocalStorage 隔离每条异步执行链。

const agentContextStorage = new AsyncLocalStorage<AgentContext>()

export function runWithAgentContext<T>(
  context: AgentContext,
  fn: () => T,
): T {
  return agentContextStorage.run(context, fn)
}

这段代码证明:Claude Code 把 Agent 身份当成异步上下文,而不是普通全局变量。AgentContextagentType 区分 subagentteammate,并带有 agentIdsubagentNameteamNameisTeamLeadinvokingRequestId 等字段。

invokingRequestId 还有一个细节:它只在 spawn 或 resume 后的第一个 API 事件消费一次。这是一条“稀疏边”,用于把某次后台任务和触发它的请求连起来,又避免每个后续事件重复携带同一个来源。

第二层:标准子 Agent、Fork 与 Coordinator

AgentTool 后面至少有三条分支,它们的差异不在“会不会另起一个模型调用”,而在上下文继承、工具池、生命周期和缓存策略。

模式上下文系统提示词执行方式适合场景
标准子 Agent全新对话,只看到父 Agent 传入的 prompt来自 Agent 定义前台或后台独立搜索、验证、分析
Fork Agent继承父会话消息继承父级已渲染提示词强制后台需要完整上下文的并行探索
Coordinator WorkerWorker 独立,Coordinator 负责综合Worker 工具上下文由协调者生成强制后台大任务分阶段研究、实现、验证

标准子 Agent:隔离优先

标准子 Agent 的默认类型来自 general-purpose。当调用没有显式传 subagent_type,且 Fork 模式关闭时,源码会回退到通用 Agent。

const effectiveType = subagent_type
  ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType)

这段代码证明:标准子 Agent 默认是一个隔离的执行单元。它不继承父对话历史,只拿到父 Agent 明确写入 prompt 的任务。这带来一个好处:子 Agent 不会被主对话里的噪音拖累;也带来一个代价:父 Agent 必须把任务边界说清楚。

分叉 Agent:用同一段前缀换并行

Fork 模式的关键不是“后台跑”,而是继承父上下文并保持 API 请求前缀稳定。

export const FORK_AGENT = {
  agentType: FORK_SUBAGENT_TYPE,
  tools: ['*'],
  maxTurns: 200,
  model: 'inherit',
  permissionMode: 'bubble',
  source: 'built-in',
  baseDir: 'built-in',
  getSystemPrompt: () => '',
}

这段代码证明三点:

  • model: &#39;inherit&#39; 表示 Fork 子进程沿用父模型,避免上下文窗口和行为策略漂移。
  • getSystemPrompt: () =&gt; &#39;&#39; 不是没有提示词,而是使用父级已经渲染好的系统提示词。
  • permissionMode: &#39;bubble&#39; 表示权限结果会向父层冒泡,而不是让 Fork 子进程自己随意放行。

buildForkedMessages() 的设计更能说明 Fork 的工程目标。它会保留父消息里的 assistant 内容,为所有正在启动的 tool_use 构造固定的占位 tool_result,再只在最后追加每个子 Agent 不同的指令。

父历史消息
assistant: 多个 tool_use
user: 固定占位 tool_result
user: 当前 Fork 的专属任务指令

这证明 Fork 在追求两件事:完整上下文Prompt Cache 命中。如果每个 Fork 从头构造不同前缀,成本和延迟都会膨胀;保持稳定前缀则可以把差异压到最后的 per-child 指令。

源码还禁止 Fork 内继续 Fork:

if (
  toolUseContext.options.querySource === `agent:builtin:${FORK_AGENT.agentType}`
  || isInForkChild(toolUseContext.messages)
) {
  throw new Error('Fork is not available inside a forked worker.')
}

这段代码证明:Fork 是一层并行展开,不是无限递归树。检查同时依赖 querySource 和消息中的 @@INLINE_0@@ 标签,是为了在压缩或消息变形后仍然保留递归防护。

协调者模式:永远不要委托理解

Coordinator Mode 通过 CLAUDE_CODE_COORDINATOR_MODE 环境变量和 COORDINATOR_MODE 构建特性开启。

export function isCoordinatorMode(): boolean {
  if (feature('COORDINATOR_MODE')) {
    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
  }
  return false
}

协调者模式把主 Agent 变成一个不直接编码的编排者。源码提示词把流程拆成四段:

阶段执行者关键要求
ResearchWorker并行调查代码库和风险点
SynthesisCoordinator主 Agent 必须亲自阅读、理解和归纳
ImplementationWorker按协调者规格修改代码
VerificationWorker独立验证实现正确性

原文特别强调一句规则:不要写 “based on your findings”。这不是文字洁癖,而是在防止协调者把理解责任继续下放。Worker 可以收集材料,最终设计判断必须回到 Coordinator。

Worker 完成后,结果不是作为普通 assistant 消息注入,而是用 @@INLINE_0@@ 作为用户角色消息传回协调者。

<task-notification>
  <task-id>{agentId}</task-id>
  <status>completed|failed|killed</status>
  <summary>{summary}</summary>
  <result>{result}</result>
  <usage>
    <total_tokens>N</total_tokens>
    <tool_uses>N</tool_uses>
    <duration_ms>N</duration_ms>
  </usage>
</task-notification>

这段结构证明:Claude Code 需要让协调者明确区分“用户说的话”和“Worker 返回的运行时事件”。否则主 Agent 可能把 Worker 结果当成新的用户指令,导致角色混乱。

第三层:Teams 是平面结构,不是递归树

Teams 解决的问题和 AgentTool 子 Agent 不同。它不是父亲不断派孩子,而是创建一个平面团队,让成员围绕同一个任务图协作。

const inputSchema = lazySchema(() =>
  z.strictObject({
    team_name: z.string().describe('Name for the new team to create.'),
    description: z.string().optional(),
    agent_type: z.string().optional(),
  }),
)

这段 TeamCreateTool schema 证明:团队创建的最小输入是团队名,外加可选描述和 Leader 类型。Team 创建后会持久化 TeamFile,并初始化对应的 TaskList。

源码还明确禁止队友再生成队友:

if (isTeammate() && teamName && name) {
  throw new Error(
    'Teammates cannot spawn other teammates - the team roster is flat.',
  )
}

这段代码是 Teams 的架构边界。队友名册是平面数组,协调权集中在 Leader;如果允许队友继续嵌套派生队友,任务图、权限审批和终端后端都会变得难以追踪。

团队配置文件是运行时配置

原文给出的 TeamFile 结构大致如下:

{
  name: string,
  description?: string,
  createdAt: number,
  leadAgentId: string,
  members: [{
    agentId: string,
    name: string,
    agentType?: string,
    model?: string,
    prompt: string,
    color: string,
    planModeRequired: boolean,
    tmuxPaneId?: string,
    sessionId?: string,
    backendType: BackendType,
    isActive: boolean,
    mode: PermissionMode,
  }]
}

这段结构证明:Team 不是一个内存里的临时数组。它有持久配置、成员身份、终端颜色、后端类型、权限模式和活跃状态。存储路径是 ~/.claude/teams/{teamName}/config.json

队友 ID 采用 {name}@{teamName} 格式。这不是 UI 小细节,而是日志、Mailbox、权限请求和事件追踪都能直接看出成员归属。

团队调度内核:任务表不是待办清单

如果只看到 SendMessageTool,很容易误解成“Agent 之间互发消息就叫协作”。源码里真正驱动 Teams 的是 TaskList。原文明确写到:Team = TaskList。团队和任务表是同一个运行时对象的两个视图。

任务结构不是普通 Todo,而是 DAG 节点。

{
  id: string,
  owner?: string,
  status: 'pending' | 'in_progress' | 'completed',
  blocks: string[],
  blockedBy: string[],
}

这段代码证明:blocksblockedBy 把任务列表升级成依赖图。Leader 可以一次性声明“auth 和 payment 完成后再跑集成测试”,而不是在自然语言里反复提醒每个队友不要抢错任务。

useTaskListWatcher.ts 里的 findAvailableTask() 才是 Swarm 的最小调度器。它筛选的条件是:

  • status === &#39;pending&#39;
  • owner 为空
  • blockedBy 中的任务都已完成

找到候选任务后,运行时先 claimTask() 写入 owner,再把任务格式化成 prompt 交给 Agent。提交失败时释放 claim。

sequenceDiagram participant L as Leader participant TL as TaskList participant W1 as Worker auth participant W2 as Worker payment L->>TL: 创建 auth / payment / integration-test Note over TL: integration-test.blockedBy = [auth, payment] W1->>TL: findAvailableTask() TL-->>W1: auth 可执行 W1->>TL: claimTask(auth, owner=W1) W2->>TL: findAvailableTask() TL-->>W2: payment 可执行 W2->>TL: claimTask(payment, owner=W2) W1->>TL: TaskUpdate(auth, completed) W1->>TL: findAvailableTask() TL-->>W1: integration-test 仍被 payment 阻塞 W2->>TL: TaskUpdate(payment, completed) W1->>TL: findAvailableTask() TL-->>W1: integration-test 已解除阻塞 W1->>TL: claimTask(integration-test, owner=W1)

这个流程证明:Teams 的并行来自共享状态和原子 claim,而不是来自“大家互相商量”。模型不用自己维护全局锁,运行时把冲突编码进任务状态。

文件邮箱:文件系统消息队列

队友间通信使用 SendMessageToolto 字段根据 Feature Flag 支持多种寻址。

to: z.string().describe(
  feature('UDS_INBOX')
    ? 'Recipient: teammate name, "*" for broadcast, "uds:<socket-path>" for a local peer, or "bridge:<session-id>" for a Remote Control peer'
    : 'Recipient: teammate name, or "*" for broadcast to all teammates',
)

这段代码证明:消息路由有三层能力。

寻址方式语义用途
tester指定队友名点对点通知
*广播给所有队友,排除发送者Leader 发布全局状态
uds:@@INLINE_0@@Unix Domain Socket 本地 peer低延迟本机通信
bridge:@@INLINE_0@@Remote Control peer跨会话控制

普通 Teams 通信落到文件系统邮箱:

~/.claude/teams/{teamName}/inboxes/{agentName}.json

消息结构大致如下:

type TeammateMessage = {
  from: string,
  text: string,
  timestamp: string,
  read: boolean,
  color?: string,
  summary?: string,
}

这段结构证明 Mailbox 是 durable queue。它不是内存事件,一旦队友进程崩溃或终端断开,消息仍然可检查。原文还提到写入时使用 async lockfile 和指数退避,说明文件邮箱不是“随手写 JSON”,而是考虑了并发写入。

控制消息被嵌在 text 字段里,包括 idleshutdown_requestshutdown_responseplan_approval_response。其中 idle 通知尤其重要:

type IdleNotificationMessage = {
  type: 'idle',
  teamName: string,
  agentName: string,
  agentId: string,
  idleReason: 'available' | 'error' | 'shutdown' | 'completed',
  summary?: string,
  peerDmSummary?: string,
  errorDetails?: string,
}

这段结构证明:队友空闲不是“没有输出”,而是一个显式运行时事件。Leader 可以通过 idle reason 判断队友是可继续领任务、执行出错、准备关闭,还是已经完成。

回合结束事件:TaskCompleted 与 TeammateIdle

Teams 不是纯 pull,也不是纯 push。队友会主动从 TaskList 领取工作,同时回合结束后触发事件。

原文定位在 query/stopHooks.ts:当当前执行者是 teammate,普通 Stop hooks 之后还会运行两类专用事件:

事件触发时机影响
TaskCompleted当前队友拥有的 in_progress 任务完成更新任务状态,可能解除其他任务的 blocker
TeammateIdle队友进入空闲通知 Leader,并回到 TaskList 查找新任务

这说明 Teams 的调度闭环是:

flowchart LR A["队友空闲"] --> B["findAvailableTask()"] B --> C{"有可执行任务?"} C -->|有| D["claimTask()"] D --> E["执行任务"] E --> F["TaskCompleted"] F --> G["解除 blocker"] G --> A C -->|没有| H["TeammateIdle"] H --> I["Mailbox 通知 Leader"]

这也是为什么 Teams 更像一个小型分布式调度器,而不是多窗口聊天。

权限同步:危险操作集中到 Leader

队友运行在独立进程时,不能让每个队友自己弹权限确认。源码把权限请求落成共享目录里的 pending/resolved 文件。

~/.claude/teams/{teamName}/permissions/
  pending/
  resolved/

权限流程是:

flowchart TD A["Worker 遇到权限检查"] --> B["创建 SwarmPermissionRequest<br/>toolName + input + suggestions"] B --> C["写入 pending/{requestId}.json"] C --> D["发送 Mailbox 给 Leader"] D --> E["Leader 终端展示给用户"] E --> F{"用户审批?"} F -->|允许| G["写入 resolved/{requestId}.json: allow"] F -->|拒绝| H["写入 resolved/{requestId}.json: deny"] G --> I["Worker 轮询 resolved 后继续"] H --> J["Worker 收到拒绝并停下或换路"]

这段流程证明:Teams 的自治没有越过权限系统。真正危险的工具调用仍然在 Leader 终端集中审批,这样用户不用在多个 tmux pane 里追权限弹窗,也避免后台队友悄悄放权。

三种后端:tmux、iTerm2 与 In-Process

Teams 的物理执行后端统一在 PaneBackendTeammateExecutor 接口之后。原文列出三种后端:

后端进程模型通信机制场景
Tmux独立 CLI 进程,tmux 分屏文件 MailboxLinux/macOS 默认路径
iTerm2独立 CLI 进程,iTerm2 分屏文件 MailboxmacOS 原生终端体验
In-Process同进程执行,用 AsyncLocalStorage 隔离AppState 内存队列没有 tmux/iTerm2 时回退

In-Process 队友仍然有自己的上下文:

type TeammateContext = {
  agentId: string,
  agentName: string,
  teamName: string,
  parentSessionId: string,
  isInProcess: true,
  abortController: AbortController,
}

runWithTeammateContext<T>(context, fn: () => T): T

这段代码证明:In-Process 只是物理运行位置变了,不等于共享一坨全局内存。身份、取消控制、消息队列和 UI 缓冲仍然被隔离。

团队记忆:共享但不能混进个人记忆

Teams 还有一个容易被忽略的状态面:Team Memory。原文定位在:

~/.claude/projects/{project}/memory/team/MEMORY.md

它和个人记忆 ~/.claude/projects/{project}/memory/ 独立。团队成员共享 team memory,但不能把个人记忆自动提升成团队记忆。这个边界在后续记忆系统里更重要:共享知识需要用户明确选择,不能由后台整理任务擅自升级。

路径安全也很重。原文提到 memdir/teamMemPaths.ts 的补丁覆盖了 null byte、URL 编码遍历、Unicode 正规化攻击、反斜杠遍历、符号链接循环和 realpath 逃逸验证。团队记忆是共享写入面,一旦路径验证放松,就会从“知识共享”变成“跨目录写文件”。

远程计划:把计划阶段传送到远程

本地多 Agent 仍然占用用户终端。Ultraplan 解决的是另一个问题:把计划阶段上传到 CCR 远程容器运行,让用户终端继续可用。

源码入口集中在:

文件关键符号行为
utils/ultraplan/keyword.tsfindUltraplanTriggerPositions()replaceUltraplanKeyword()检测自然语言里的 ultraplan,排除代码、路径、引用等上下文
commands/ultraplan.tsxlaunchUltraplan()检查资格、构造提示词、调用 teleportToRemote()、注册远程任务
utils/ultraplan/ccrSession.tspollForApprovedExitPlanMode()ExitPlanModeScanner每 3 秒轮询远程会话,扫描计划审批状态
state/AppStateStore.tsultraplanLaunchingultraplanSessionUrlultraplanPendingChoice本地 UI 状态机

Ultraplan 的本地状态大致是:

ultraplanLaunching?: boolean
ultraplanSessionUrl?: string
ultraplanPendingChoice?: {
  plan: string
  sessionId: string
  taskId: string
}
ultraplanLaunchPending?: {
  blurb: string
}
isUltraplanMode?: boolean

这段结构证明:本地端不执行计划,只管理远程会话 URL、启动防重、计划回传和用户选择。

远程轮询有固定参数:

const POLL_INTERVAL_MS = 3000
const MAX_CONSECUTIVE_FAILURES = 5
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000

这证明 Ultraplan 是一个有超时和失败阈值的后台任务,而不是把本地 Loop 无限挂到远程。

ExitPlanModeScanner 的扫描结果也很清晰:

type ScanResult =
  | { kind: 'approved'; plan: string }
  | { kind: 'teleport'; plan: string }
  | { kind: 'rejected'; id: string }
  | { kind: 'pending' }
  | { kind: 'terminated'; subtype: string }
  | { kind: 'unchanged' }

这段类型证明:Ultraplan 不是只有“完成/失败”。它区分已批准远程执行、传送回本地、被拒绝后继续修改、等待审批、终止和无变化。

状态和数据结构

状态或结构关键字段影响
AgentContextagentTypeagentIdinvokingRequestId决定事件归属、后台任务来源和同进程隔离
FORK_AGENTmodel: &#39;inherit&#39;tools: [&#39;*&#39;]permissionMode: &#39;bubble&#39;保持父上下文能力,同时把权限向父层冒泡
TeamFileleadAgentIdmembers[]backendTypemode决定队友身份、后端、权限模式和生命周期
TaskstatusownerblocksblockedBy把 Todo 变成可 claim 的 DAG 节点
TeammateMessagefromtextreadsummary文件系统 Mailbox 的最小消息单元
IdleNotificationMessageidleReasonsummaryerrorDetailsLeader 判断队友是否可继续调度
SwarmPermissionRequesttoolNameinputsuggestions把队友权限请求转交 Leader 审批
UltraplanPendingChoiceplansessionIdtaskId远程计划批准后,本地决定继续远程还是传回终端

设计取舍

第一,隔离优先于共享。 标准子 Agent 从新上下文开始,Fork 虽然继承上下文但禁止递归,In-Process teammate 虽在同进程运行仍用 AsyncLocalStorage 隔离身份。这说明 Claude Code 不把“共享一切”当成协作捷径。

第二,调度状态外化。 Teams 没有让模型用自然语言维护谁该做什么,而是把任务写入 ~/.claude/tasks/{team-name}/,用 ownerblockedBystatus 做调度。这让并行协作可以被文件系统观察、恢复和调试。

第三,通信选择 durable mailbox。 文件系统 Mailbox 比 IPC 慢,但崩溃后可检查、跨进程简单、适合终端工具调试。对 Agent 协作而言,秒级延迟通常可接受,消息可恢复更重要。

第四,权限集中到 Leader。 队友可以并行工作,但危险工具调用不能分散审批。pending/resolved/ 把审批变成一个可轮询、可恢复的状态机。

第五,远程计划不等于远程放权。 Ultraplan 把探索和计划上传到 CCR,但 ExitPlanMode 仍然要经过审批;计划还可以传回本地执行。这把“更强规划能力”和“最终执行控制”分开。

读源码抓手

读源码抓手

读多 Agent 不要从“有哪些 Agent 类型”开始,而要沿着状态怎么流动追。

建议路线:

  • 先看 tools/AgentTool/AgentTool.tsx 的 schema 组合和 shouldRunAsync 分支,理解能力怎么暴露给模型。
  • 再看 utils/agentContext.ts,确认 Agent 身份如何进入事件、后台任务和同进程执行链。
  • 接着读 tools/AgentTool/forkSubagent.ts,重点看 FORK_AGENTbuildForkedMessages() 如何保持 cache-safe 前缀。
  • 然后跳到 tools/TeamCreateTool/TeamCreateTool.tsutils/swarm/teamHelpers.ts,看 TeamFile 如何落盘。
  • 再追 utils/tasks.tsuseTaskListWatcher.tsfindAvailableTask()claimTask(),这是 Teams 的调度核心。
  • 最后读 tools/SendMessageTool/SendMessageTool.tsutils/swarm/permissionSync.tsutils/ultraplan/ccrSession.ts,分别看通信、审批和远程计划。

小结

小结

- 多 Agent 的关键不是“多开几个模型”,而是运行时能否隔离身份、控制权限、持久化任务和回收结果。 - 标准子 Agent 用新上下文换干净边界,Fork 用父上下文换缓存和并行,Coordinator 用 Worker 换阶段化执行。 - Teams 的核心是 TaskList + claim + Mailbox + stop hooks,消息只是协作补充,不是主调度面。 - 文件系统在这里不是落后方案,而是 durable、可调试、跨进程友好的协作基底。 - Ultraplan 把计划远程化,但仍保留审批、超时、失败阈值和传回本地的控制面。