Multi-Agent 系统完整运行原理
Claude Code 不是单一的"Claude 主循环",而是一个可以孵化、隔离、协作 N 个 agent 的分布式 LLM 运行时。 本文从 AgentDefinition 数据结构,到 runAgent 的 ~30 步流水线,再到 swarm 跨进程团队,逐层拆解每一个设计细节。
01概览:为什么需要多 agent
主 agent 一个人干所有活会撞上 4 个根本问题:上下文窗口、工具数量、模型成本、并发隔离。多 agent 系统是这些问题的集中解法。
1.1 多 agent 解决的 4 类问题
| 问题 | 主 agent 单独干的代价 | 多 agent 的解法 |
|---|---|---|
| 上下文污染 | 读 50 个文件做调研 → 200k context 全是噪声,后续推理跑偏 | 派子 agent 调研,只把 200 字 summary 带回来 |
| 工具范围扩散 | 所有工具 schema 都加载 → ~30k token prompt 浪费 | 子 agent 用 disallowedTools / 自定义 tools 列表收窄 |
| 模型成本失衡 | 主 Opus 跑 grep / find → 杀鸡用牛刀 | Explore 用 Haiku,Verification 用 Opus,主 agent 决策 |
| 无法并行 | 任务串行 → 一个 5 分钟测试卡住整个会话 | run_in_background 派多个 agent 并发 + 通知回流 |
1.2 一句话定义每种 agent
- built-inbuilt-in agent — 源码里硬编码,如 general-purpose、Explore、Plan
- customcustom agent — 用户在
~/.claude/agents/<name>.md写的 markdown,带 frontmatter - pluginplugin agent — 插件包提供的 agent,带 plugin 元信息
- forkfork agent — 实验性:不传 subagent_type 时触发,继承父 agent 全部上下文 + system prompt
- teammateteammate — swarm 模式下的"长驻协作进程",独立 tmux/iterm pane,文件邮箱通信
1.3 一组关键术语
| 术语 | 含义 |
|---|---|
subagent | 由主 agent 通过 AgentTool 派出的"一次性"子 agent,任务完成即销毁 |
fork | 子 agent 的特殊形态:继承父的全部上下文,共享 prompt cache |
teammate | 长期存在的并行 agent,每个跑在自己的进程/pane 里,通过邮箱协作 |
leader | swarm 模式下创建团队、分派任务的主 agent |
worktree | git worktree 创建的独立工作目录,子 agent 改的文件不影响父 |
async / background | 子 agent 立即返回,后台跑,完成后通过 task-notification 通知父 |
sync / foreground | 子 agent 跑完才返回,期间父 agent 阻塞等待 |
02系统架构总览
把多 agent 系统拆成 5 层,从工具调用到通知回流,每一层都有明确职责。
03Agent 类型四大分类
所有 agent 都满足同一个接口 AgentDefinition,但来源不同,加载逻辑、信任级别、可写性都不同。
3.1 四种来源对照
| 类型 | 位置 | 系统提示 | 信任级别 |
|---|---|---|---|
| built-in | 源码 built-in/*.ts |
动态 getSystemPrompt({ toolUseContext }) |
admin-trusted |
| custom | ~/.claude/agents/*.md 或项目级 |
静态闭包 () => promptText |
跟 source 走(user/project/managed/flag) |
| plugin | 插件包 plugin/*/agents/*.md |
静态闭包,带 plugin 元信息 | admin-trusted (受插件签名) |
| fork (合成) | 仅运行时存在(FORK_AGENT) |
占位 → 实际用父的 system prompt | 跟父 agent 一致 |
3.2 内置 agent 完整清单(builtInAgents.ts)
| agentType | 用途 | 默认模型 | 典型工具 |
|---|---|---|---|
general-purpose |
通用调研 + 多步任务 | 跟父(getDefaultSubagentModel) | ['*'] 全部 |
Explore |
只读代码探索(快) | ant: inherit / 外部: haiku |
排除 Edit/Write/NotebookEdit/Agent/ExitPlanMode |
Plan |
实现规划 | — | 不能 Edit/Write |
verifier |
验证主 agent 完成质量(PASS/PARTIAL/FAIL) | — | — |
statusline-setup |
配置 statusline 设置 | — | Read + Edit(局限于 settings.json) |
claude-code-guide |
回答关于 Claude Code 本身的问题 | — | Bash + Read + WebFetch + WebSearch |
built-in < plugin < user < project < flag < policy
即用户在 ~/.claude/agents/general-purpose.md 写一个同名 agent,会覆盖内置的 general-purpose。
policy(企业策略)的优先级最高,可强制覆盖一切。
04AgentDefinition 数据结构
理解 AgentDefinition 是理解整个 agent 系统的钥匙。所有 frontmatter 字段最终都会映射到这里。
4.1 完整 schema
type BaseAgentDefinition = {
agentType: string // 唯一名称,subagent_type 引用
whenToUse: string // 给主 agent 看的"何时用"
tools?: string[] // 白名单,['*'] = 全部
disallowedTools?: string[] // 黑名单
skills?: string[] // 启动时预加载的 skill
mcpServers?: AgentMcpServerSpec[] // agent 私有 MCP 服务器
hooks?: HooksSettings // agent 生命周期内的 hook
color?: AgentColorName // UI 显示颜色
model?: string // 'sonnet' | 'opus' | 'haiku' | 'inherit'
effort?: EffortValue // thinking 配额
permissionMode?: PermissionMode // 'plan' | 'acceptEdits' | 'bypass' | 'bubble'
maxTurns?: number // 回合数上限
requiredMcpServers?: string[] // 不满足则该 agent 不可用
background?: boolean // 强制后台运行
initialPrompt?: string // 拼到第一个 user turn 前
memory?: 'user' | 'project' | 'local' // 持久化记忆作用域
isolation?: 'worktree' | 'remote' // 隔离模式
omitClaudeMd?: boolean // 跳过 CLAUDE.md(Explore/Plan 用)
criticalSystemReminder_EXPERIMENTAL?: string // 每个用户回合都注入的提醒
filename?: string // 自定义 agent 的源文件
baseDir?: string
}
4.2 子类型分化
// 内置:动态生成 system prompt(可读 toolUseContext)
type BuiltInAgentDefinition = BaseAgentDefinition & {
source: 'built-in'
baseDir: 'built-in'
getSystemPrompt: ({ toolUseContext }) => string
callback?: () => void
}
// 自定义:从 markdown frontmatter 解析,prompt 已固化在闭包里
type CustomAgentDefinition = BaseAgentDefinition & {
getSystemPrompt: () => string
source: SettingSource // userSettings | projectSettings | flagSettings | policySettings
filename?: string
}
// 插件:同 custom,加 plugin 元信息
type PluginAgentDefinition = BaseAgentDefinition & {
getSystemPrompt: () => string
source: 'plugin'
plugin: string
}
4.3 frontmatter 示例 → AgentDefinition 映射
# ~/.claude/agents/security-reviewer.md
---
description: "Senior security engineer for OWASP review"
tools: ["Read", "Bash", "Grep"]
disallowedTools: ["Bash(rm:*)"]
model: "opus"
permissionMode: "plan"
maxTurns: 50
mcpServers: ["github", { vault: { command: "vault-mcp" } }]
skills: ["security-checklist"]
color: "red"
---
You are a senior security engineer specializing in code review for OWASP top 10...
解析后的 AgentDefinition:
{
agentType: 'security-reviewer', // 文件名(去 .md)
whenToUse: 'Senior security engineer...', // description
tools: ['Read', 'Bash', 'Grep'],
disallowedTools: ['Bash(rm:*)'], // 带规则模式
model: 'opus',
permissionMode: 'plan',
maxTurns: 50,
mcpServers: ['github', { vault: {...} }],
skills: ['security-checklist'],
color: 'red',
source: 'userSettings',
filename: 'security-reviewer',
getSystemPrompt: () => "You are a senior...", // 闭包持有 markdown 正文
}
05AgentTool 输入输出
AgentTool 是模型与多 agent 系统的唯一接口。它的 schema 由 5+ 个特性 gate 动态裁剪,模型只看到自己 user-type 能用的部分。
5.1 输入字段
| 字段 | 类型 | 用途 |
|---|---|---|
description | string | 3-5 词任务描述,显示在 UI |
prompt | string | 给子 agent 的实际任务文本 |
subagent_type | string? | 选哪个 agent。fork 模式可省略 |
model | 'sonnet'|'opus'|'haiku'? | 覆盖 agent 定义的 model |
run_in_background | boolean? | 后台跑 + 通过 task-notification 回流 |
name | string? | swarm 给 spawn 的 teammate 起名,可被 SendMessage 寻址 |
team_name | string? | swarm 指定团队 |
mode | PermissionMode? | swarm 例如 'plan' 强制 spawn 的 teammate 进 plan 模式 |
isolation | 'worktree'|'remote'? | worktree 隔离 / 远端执行(ant-only) |
cwd | string? | 显式覆盖工作目录(KAIROS gate) |
5.2 输出 4 种形态
// 1. 同步完成:子 agent 跑完了,带回结果
{ status: 'completed', prompt, content: [{ type: 'text', text: ... }], agentId, ... }
// 2. 异步派发:立即返回,后台还在跑
{
status: 'async_launched',
agentId: 'agent_xyz',
description, prompt,
outputFile: '~/.claude/sessions/.../subagents/xyz.jsonl',
canReadOutputFile: true // 主 agent 是否能 Read/Bash 该文件
}
// 3. teammate 派生:swarm 模式,在新 pane 启动
{
status: 'teammate_spawned',
teammate_id, agent_id, name, color,
tmux_session_name, tmux_window_name, tmux_pane_id,
team_name, plan_mode_required
}
// 4. 远端启动:isolation: 'remote'
{
status: 'remote_launched',
taskId, sessionUrl, description, prompt, outputFile
}
5.3 schema 动态裁剪
// AgentTool.tsx:242
export const inputSchema = lazySchema(() => {
// KAIROS gate 控制 cwd 字段
const schema = feature('KAIROS') ? fullInputSchema() : fullInputSchema().omit({ cwd: true })
// background gate 或 fork 启用 → 隐藏 run_in_background(强制 async)
return isBackgroundTasksDisabled || isForkSubagentEnabled()
? schema.omit({ run_in_background: true })
: schema
})
unknown,丢失编译时类型检查。
.omit() 保留具体类型。call() 用显式 AgentToolInput 类型覆盖回完整字段,
所以即使 schema 不暴露,代码仍能安全访问。
5.4 prompt 是动态生成的
AgentTool 的 prompt 不是静态文本,而是 async prompt({ agents, tools, ... }) 函数:
- 从 toolUseContext 拿到所有可用 agents
- 用
filterAgentsByMcpRequirements过滤掉 MCP 不满足的 - 用
filterDeniedAgents过滤掉权限规则禁止的 - 把剩下的 agent 列表(
type: whenToUse (Tools: ...))拼进 prompt
tengu_agent_list_attach,可以改成把列表作为独立 attachment 注入,
让 tool description 保持稳定,大幅提升缓存命中率(节省了 ~10.2% fleet cache_creation tokens)。
06三大执行路径
AgentTool.call() 进来后会根据参数分流到三条完全不同的执行路径。理解这三条路径是理解整个系统的关键。
6.1 teammate 路径(spawnTeammate)
触发条件:team_name && name 同时存在,且 swarm 启用。这条路径不调用 runAgent —— 而是启动一个独立 Claude 进程。
// AgentTool.tsx:441
if (teamName && name) {
// 防御:teammates 不能再 spawn teammates(团队结构是平的)
if (isTeammate() && teamName && name) throw
// 防御:in-process teammate 不能 spawn 后台 agent
if (isInProcessTeammate() && teamName && run_in_background === true) throw
const result = await spawnTeammate({
name, prompt, description, team_name: teamName,
use_splitpane: true,
plan_mode_required: spawnMode === 'plan',
model, agent_type: subagent_type,
}, toolUseContext)
return { status: 'teammate_spawned', ... }
}
6.2 fork 路径
触发条件:省略 subagent_type + FORK_SUBAGENT gate 启用 + 非 coordinator + 非 SDK。
// AgentTool.tsx:482
const effectiveType = subagent_type ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE)
const isForkPath = effectiveType === undefined
if (isForkPath) {
// 递归 fork 防御:fork 子 agent 不能再 fork
if (toolUseContext.options.querySource === 'agent:builtin:fork' ||
isInForkChild(toolUseContext.messages)) throw
selectedAgent = FORK_AGENT
}
6.3 普通 subagent 路径
大多数情况走这条路径。要做 4 重检查:
-
找 agent
agents.find(a => a.agentType === effectiveType) -
权限过滤
filterDeniedAgents检查Agent(name)形式的 deny 规则;allowedAgentTypes来自Agent(x,y)工具白名单。 -
MCP 等待 + 检查
如果 agent 定义了
requiredMcpServers,需要等 pending MCP 连接完成(最多 30s,500ms 轮询), 然后用实际有 tools 的 server 列表(server 必须是 connected + authenticated 才算)做匹配。 -
isolation 处理
worktree创建临时 git worktree;remote(ant-only)调 CCR 远端启动。
07runAgent 流水线
runAgent.ts 是子 agent 真正"启动"的地方,也是整个系统最复杂的函数。它有 ~30 个步骤,每一步都解决一个具体问题。
7.1 流水线全景
7.2 关键决策片段:abortController 选择
// runAgent.ts:530
const agentAbortController = override?.abortController
? override.abortController // 显式覆盖优先
: isAsync
? new AbortController() // 异步:全新独立 controller
: toolUseContext.abortController // 同步:共享父的
这是同步/异步行为差异的根源:
- 同步子 agent:用户按 Ctrl+C 中断主线程,会同时中断子 agent
- 异步子 agent:主线程被中断,子 agent 继续跑,需要显式
kill
7.3 MCP 初始化的 admin-trusted 门控
// runAgent.ts:117
const agentIsAdminTrusted = isSourceAdminTrusted(agentDefinition.source)
if (isRestrictedToPluginOnly('mcp') && !agentIsAdminTrusted) {
// 用户级 agent + MCP 锁定为 plugin-only:跳过 frontmatter MCP
return { clients: parentClients, tools: [], cleanup: async()=>{} }
}
08上下文隔离与共享
子 agent 既要从父继承够用的环境信息(避免重新发现成本),又要保证错误不污染父。createSubagentContext 是这个权衡的关键。
8.1 隔离 vs 共享对照
| 状态 | 同步 subagent | 异步 subagent | fork |
|---|---|---|---|
| readFileState | 克隆 | 克隆 | 克隆 |
| contentReplacementState | 克隆 | 克隆 | 克隆(关键:cache 共享必须克隆) |
| setAppState | 共享 shareSetAppState=true | 独立 | 共享 |
| abortController | 共享父的 | 全新独立 | 取决于 isAsync |
| messages | 独立 仅 promptMessages | 独立 | 完整继承 forkContextMessages |
| system prompt | 独立 用 agent 自己的 | 独立 | 继承父的 |
| tools | 独立 agent 定义 | 独立 | 继承父的 useExactTools |
| thinkingConfig | disabled | disabled | 继承父的 |
| MCP clients | 合并 parent + agent 私有 | 合并 | 合并 |
8.2 permissionMode 覆盖规则
// runAgent.ts:421
// agent 定义了 permissionMode 时覆盖,但有 3 种模式不可被覆盖
if (
agentPermissionMode &&
state.toolPermissionContext.mode !== 'bypassPermissions' &&
state.toolPermissionContext.mode !== 'acceptEdits' &&
!(feature('TRANSCRIPT_CLASSIFIER') && state.toolPermissionContext.mode === 'auto')
) {
toolPermissionContext = { ...toolPermissionContext, mode: agentPermissionMode }
}
- bypassPermissions:用户已经接受了"全开"风险,子 agent 不应该退回更严格
- acceptEdits:用户已经允许编辑,不该回退
- auto(transcript classifier):自动分类器在管控,agent 自己的 mode 不该越权
8.3 shouldAvoidPermissionPrompts 自动设置
// runAgent.ts:446
const shouldAvoidPrompts = canShowPermissionPrompts !== undefined
? !canShowPermissionPrompts
: agentPermissionMode === 'bubble'
? false // bubble 模式总是允许显示提示(冒泡到父终端)
: isAsync // 默认:同步可显示,异步自动拒绝
异步 agent 在后台跑,UI 上没有交互入口,所以不能显示权限对话框 → 默认 deny; bubble 模式特殊,允许把权限提示冒泡到父 agent 的终端处理。
8.4 awaitAutomatedChecksBeforeDialog
if (isAsync && !shouldAvoidPrompts) {
toolPermissionContext = {
...toolPermissionContext,
awaitAutomatedChecksBeforeDialog: true,
}
}
异步 + 允许提示的特殊场景(bubble):权限对话框前先等所有自动检查(分类器、permission hooks)跑完。 因为后台 agent 不该频繁打扰用户,用户只在自动化无法决策时才被打扰。
09工具过滤三层机制
每个 agent 看到的 tools 列表都不同,这是通过三层过滤合成的。
9.1 第 1 层:assembleToolPool 重组
// AgentTool.tsx:840 — worker 总是用自己的 permissionMode 重组
const workerPermissionContext = {
...appState.toolPermissionContext,
mode: selectedAgent.permissionMode ?? 'acceptEdits',
}
const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools)
核心点:worker 的工具池跟 parent 的限制无关。即使父被限制成"只能 Read",子 agent 仍按自己的 permissionMode 重新组装 —— acceptEdits 默认开放 Edit/Write。
9.2 第 2 层:filterToolsForAgent
// agentToolUtils.ts:70
function filterToolsForAgent({ tools, isBuiltIn, isAsync, permissionMode }) {
return tools.filter(tool => {
if (tool.name.startsWith('mcp__')) return true // MCP 全放
// plan mode 例外:ExitPlanMode 必须可用
if (toolMatchesName(tool, EXIT_PLAN_MODE_V2_TOOL_NAME) && permissionMode === 'plan') return true
// 所有 agent 都禁止的:TaskOutput, EnterPlanMode, AskUserQuestion, TaskStop, (非 ant 还禁 Agent)
if (ALL_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
// custom agent 额外禁止
if (!isBuiltIn && CUSTOM_AGENT_DISALLOWED_TOOLS.has(tool.name)) return false
// async 只能用白名单(in-process teammate 例外)
if (isAsync && !ASYNC_AGENT_ALLOWED_TOOLS.has(tool.name)) {
if (isInProcessTeammate()) {
if (toolMatchesName(tool, AGENT_TOOL_NAME)) return true
if (IN_PROCESS_TEAMMATE_ALLOWED_TOOLS.has(tool.name)) return true
}
return false
}
return true
})
}
9.3 三套黑/白名单
| 名单 | 包含 | 用途 |
|---|---|---|
ALL_AGENT_DISALLOWED_TOOLS |
TaskOutput, ExitPlanMode, EnterPlanMode, AskUserQuestion, TaskStop, Agent(非ant), Workflow(非ant) | 所有 agent 都不可用 — 这些是"主线程协调"工具 |
ASYNC_AGENT_ALLOWED_TOOLS |
FileRead, WebSearch, TodoWrite, Grep, WebFetch, Glob, Bash/PowerShell, Edit, Write, NotebookEdit, Skill, ToolSearch, EnterWorktree, ExitWorktree | 异步 agent 的白名单 — 没有 UI 交互的工具才能用 |
IN_PROCESS_TEAMMATE_ALLOWED_TOOLS |
TaskCreate/Get/List/Update, SendMessage, CronCreate/Delete/List | 同进程 teammate 的额外白名单 — 协作所需 |
9.4 第 3 层:resolveAgentTools
// agentToolUtils.ts:122 — 根据 agentDef 的 tools / disallowedTools 收窄
function resolveAgentTools(agentDef, availableTools, isAsync) {
const filtered = filterToolsForAgent({ tools: availableTools, ... })
const denyOnly = filtered.filter(t => !disallowedTools.has(t.name))
// tools 字段处理
if (agentTools === undefined || agentTools[0] === '*') {
return { hasWildcard: true, resolvedTools: denyOnly }
}
// 有 allowlist:只放白名单里的
for (const spec of agentTools) {
const { toolName, ruleContent } = permissionRuleValueFromString(spec)
// 特殊:Agent(name1, name2) 携带 allowedAgentTypes 元信息
if (toolName === AGENT_TOOL_NAME && ruleContent) {
allowedAgentTypes = ruleContent.split(',').map(s => s.trim())
}
const tool = availableToolMap.get(toolName)
if (tool) resolved.push(tool)
}
return { resolvedTools: resolved, allowedAgentTypes, ... }
}
9.5 Agent(x,y) 元信息特殊处理
当 frontmatter 写 tools: ['Agent(researcher,reviewer)'] 时,这是说"允许该 agent 调用 Agent 工具,但只能派 researcher 和 reviewer 两种"。
resolveAgentTools 提取这个元信息,设置到 allowedAgentTypes,后续 AgentTool.call() 会用它过滤可派 agent 列表。
10系统提示组装
每个 agent 的 system prompt 都是动态拼接的,包含 agent 自己的 prompt + 环境细节 + 可选的 hook 上下文。
10.1 普通路径(非 fork)
// AgentTool.tsx:765
const agentPrompt = selectedAgent.getSystemPrompt({ toolUseContext })
// 注入环境细节
const enhancedSystemPrompt = await enhanceSystemPromptWithEnvDetails(
[agentPrompt],
resolvedAgentModel,
additionalWorkingDirectories,
)
// enhancedSystemPrompt 现在含:
// - agent 自己的 prompt
// - cwd / 平台 / 日期 / shell / Git 信息
// - additional working directories
// - 绝对路径要求 / 表情使用约束
10.2 fork 路径(继承父的)
// AgentTool.tsx:729
if (isForkPath) {
if (toolUseContext.renderedSystemPrompt) {
// 已经渲染过的字节,完全继承(用于 cache 命中)
forkParentSystemPrompt = toolUseContext.renderedSystemPrompt
} else {
// fallback:重新构建(可能因 GrowthBook 翻转而和父不一致)
forkParentSystemPrompt = buildEffectiveSystemPrompt({
mainThreadAgentDefinition, toolUseContext, ...
})
}
}
toolUseContext.renderedSystemPrompt 里。fork 子 agent 直接拿这个字节复用,
保证 API 请求前缀字节级一致 → prompt cache 100% 命中。
重新调 getSystemPrompt 可能因 GrowthBook 翻转产生差异,bust cache。
10.3 SubagentStart hook 注入
// runAgent.ts:551
if (additionalContexts.length > 0) {
const contextMessage = createAttachmentMessage({
type: 'hook_additional_context',
content: additionalContexts,
hookName: 'SubagentStart',
hookEvent: 'SubagentStart',
})
initialMessages.push(contextMessage) // 作为第一条用户消息
}
用户配置的 SubagentStart hook 可以注入自定义上下文,比如"当前在 git branch X 上"、"昨天发生了 deploy"等。 这些不是改 system prompt(避免 cache miss),而是作为附件用户消息插在 promptMessages 之前。
10.4 critical system reminder(实验)
criticalSystemReminder_EXPERIMENTAL?: string
agent 定义里如果写了这个字段,内容会在每个用户回合都重新注入(通过 attachment 管线)。 用于"必须每轮都强调"的关键约束,比如"严禁运行 rm -rf"、"必须先 plan 再 implement"。
11同步 vs 异步执行
两种执行模式有完全不同的生命周期、通信方式、UI 行为。理解差异是用对工具的关键。
11.1 决策树:shouldRunAsync
// AgentTool.tsx:827
const shouldRunAsync = (
run_in_background === true || // 用户显式
selectedAgent.background === true || // agent 定义强制
isCoordinator || // coordinator 模式
forceAsync || // fork gate(所有 spawn 都 async)
assistantForceAsync || // KAIROS 助理模式
(proactiveModule?.isProactiveActive() ?? false) // proactive agent
) && !isBackgroundTasksDisabled
11.2 同步路径详解
-
registerAgentForeground
注册 task 状态(可选自动后台化超时);返回 backgroundSignal Promise。
-
runAgent 流式消费
for await (const message of runAgent(...));同时 race backgroundPromise。 -
~2s 超时显示 BackgroundHint
PROGRESS_THRESHOLD_MS = 2000,UI 提示"按 Ctrl+B 后台化"。 -
用户主动后台化(可选)
backgroundPromise resolve → 切换到异步路径,主线程立即返回。
-
正常完成
finalizeAgentTool 提取最后 assistant 消息文本作为结果,作为 tool_result 返回。
11.3 异步路径详解
-
registerAsyncAgent
注册 LocalAgentTaskState(status: 'running');立即返回 taskId 给主 agent。
-
void runWithAgentContext(...)
fire-and-forget 启动一个独立 lifecycle。AsyncLocalStorage 自动捕获父的 workload。
-
tool_result 返回 async_launched
主 agent 拿到
{ status: 'async_launched', agentId, outputFile },可以继续做别的。 -
子 agent 在后台跑
每条 message 都通过 onCacheSafeParams 触发 startAgentSummarization;同时实时写入 outputFile。
-
子 agent 完成
completeAsyncAgent 标记状态;classifyHandoffIfNeeded 跑一次 transcript classifier。
-
enqueueAgentNotification
通过 messageQueueManager 把
<task-notification>XML 入队,priority='later'。 -
queryLoop 下一回合排空
主 agent 的 query loop 在合适时机消费,作为附件喂给主 agent,主 agent 知道"agent X 完成了"。
11.4 异步通知 XML 结构
// LocalAgentTask.tsx:342
<task-notification>
<task-id>agent_xyz</task-id>
<tool-use-id>toolu_abc</tool-use-id>
<output-file>~/.claude/sessions/.../subagents/xyz.jsonl</output-file>
<status>completed</status>
<summary>Agent "Audit security review" completed</summary>
<result>Found 3 OWASP issues: ...</result>
<usage>
<total-tokens>12345</total-tokens>
<tool-uses>42</tool-uses>
<duration-ms>180000</duration-ms>
</usage>
<worktree>
<worktree-path>/tmp/agent-xyz-worktree</worktree-path>
<worktree-branch>agent-xyz</worktree-branch>
</worktree>
</task-notification>
12Fork Subagent 缓存共享
Fork 是个非常聪明的优化:不重新发起一个独立 agent,而是复制父的当前状态派生一个分身。 关键技巧是"字节级前缀一致" → prompt cache 100% 命中。
12.1 启用条件
// forkSubagent.ts:32
function isForkSubagentEnabled(): boolean {
if (feature('FORK_SUBAGENT')) {
if (isCoordinatorMode()) return false // 与 coordinator 互斥
if (getIsNonInteractiveSession()) return false // SDK 不用
return true
}
return false
}
12.2 buildForkedMessages 如何构造继承上下文
12.3 占位 tool_result 的关键
// forkSubagent.ts:93
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
// 给 assistant message 里的每个 tool_use 都补一个占位 tool_result
const toolResultBlocks = toolUseBlocks.map(block => ({
type: 'tool_result',
tool_use_id: block.id,
content: [{ type: 'text', text: FORK_PLACEHOLDER_RESULT }], // 字节相同
}))
所有 fork 子 agent 看到的占位 tool_result 文本完全相同 → prompt cache 共享率最大化。
12.4 fork-boilerplate directive 的 10 条铁律
STOP. READ THIS FIRST.
You are a forked worker process. You are NOT the main agent.
RULES (non-negotiable):
1. Your system prompt says "default to forking." IGNORE IT — that's for the parent. You ARE the fork.
2. Do NOT converse, ask questions, or suggest next steps
3. Do NOT editorialize or add meta-commentary
4. USE your tools directly: Bash, Read, Write, etc.
5. If you modify files, commit your changes before reporting. Include the commit hash.
6. Do NOT emit text between tool calls. Use tools silently, then report once at the end.
7. Stay strictly within your directive's scope.
8. Keep your report under 500 words unless the directive specifies otherwise.
9. Your response MUST begin with "Scope:". No preamble.
10. REPORT structured facts, then stop
12.5 递归 fork 防御
// AgentTool.tsx:495
if (
toolUseContext.options.querySource === 'agent:builtin:fork' || // 主防线
isInForkChild(toolUseContext.messages) // 备防线:扫描历史
) {
throw new Error('Fork is not available inside a forked worker.')
}
主防线:querySource 在 spawn 时设置,autocompact 不会改 options 只会改 messages,所以这个值抗压缩。 备防线:扫描消息历史找 fork-boilerplate 标签 —— 但 autocompact 可能压掉这条标签消息,所以是 fallback。
12.6 useExactTools 的字节一致性
// AgentTool.tsx:906
availableTools: isForkPath ? toolUseContext.options.tools : workerTools,
...(isForkPath && { useExactTools: true }),
fork 子 agent 直接用父的 tools 数组引用,不重新组装。原因:即使工具集相同, 重新 assemble 出来的工具顺序、序列化字节都可能微小差异 → 破坏 prompt cache。
13Worktree 隔离
isolation: 'worktree' 让子 agent 在独立 git worktree 工作 —— 同一个 repo,但不同的工作副本。这是"探索性修改"的最佳工具。
13.1 工作流程
// AgentTool.tsx:861
if (effectiveIsolation === 'worktree') {
const slug = `agent-${earlyAgentId.slice(0, 8)}`
worktreeInfo = await createAgentWorktree(slug)
// worktreeInfo = { worktreePath, worktreeBranch, headCommit, gitRoot }
}
13.2 cwd 自动切换
const cwdOverridePath = cwd ?? worktreeInfo?.worktreePath
const wrapWithCwd = (fn) => cwdOverridePath
? runWithCwdOverride(cwdOverridePath, fn)
: fn()
runWithCwdOverride 用 AsyncLocalStorage 给 agent 整个生命周期注入新的 cwd。
所有 getCwd() 调用(file 工具、shell)都会拿到 worktree 路径。
13.3 自动清理逻辑
// AgentTool.tsx:921
const cleanupWorktreeIfNeeded = async () => {
if (!worktreeInfo) return {}
const { worktreePath, worktreeBranch, headCommit, gitRoot, hookBased } = worktreeInfo
if (hookBased) return { worktreePath } // hook 创建的不自动删
if (headCommit) {
const changed = await hasWorktreeChanges(worktreePath, headCommit)
if (!changed) {
await removeAgentWorktree(worktreePath, worktreeBranch, gitRoot)
return {}
}
}
return { worktreePath, worktreeBranch } // 有改动:保留路径,通知父
}
git worktree remove 的负担。
但如果有未提交改动,会自动保留 + 把路径返回给父 agent,让父决定怎么处理(merge / cherry-pick / 丢弃)。
13.4 fork + worktree 组合
// AgentTool.tsx:869
if (isForkPath && worktreeInfo) {
promptMessages.push(createUserMessage({
content: buildWorktreeNotice(getCwd(), worktreeInfo.worktreePath),
}))
}
注入一条提示给 fork 子 agent:
You've inherited the conversation context above from a parent agent working in{parentCwd}. You are operating in an isolated git worktree at{worktreeCwd}— same repository, same relative file structure, separate working copy. Paths in the inherited context refer to the parent's working directory; translate them to your worktree root. Re-read files before editing if the parent may have modified them since they appear in the context. Your changes stay in this worktree and will not affect the parent's files.
14Swarm 团队架构
Swarm 是 swarm 模式 + 多 Claude 进程 + 文件系统的组合。它把"多 agent"从同进程协作扩展到跨进程、跨 pane、跨机器。
14.1 创建团队 (TeamCreate)
// TeamCreateTool.ts:155 — 创建团队的 5 步
// 1. 写团队配置文件
const teamFile: TeamFile = {
name: finalTeamName,
createdAt: Date.now(),
leadAgentId,
leadSessionId: getSessionId(),
members: [{
agentId: leadAgentId,
name: TEAM_LEAD_NAME, // 'team-lead'
agentType: leadAgentType,
model: leadModel,
joinedAt: Date.now(),
tmuxPaneId: '',
cwd: getCwd(),
subscriptions: [],
}],
}
await writeTeamFileAsync(finalTeamName, teamFile)
// → ~/.claude/teams/<name>/config.json
// 2. 注册 session-end 清理
registerTeamForSessionCleanup(finalTeamName)
// 3. 重置任务列表(team = task list)
const taskListId = sanitizeName(finalTeamName)
await resetTaskList(taskListId)
await ensureTasksDir(taskListId)
// 4. 设置 leader 的 teamName(让 getTaskListId 返回它)
setLeaderTeamName(sanitizeName(finalTeamName))
// 5. 更新 AppState.teamContext
setAppState(prev => ({ ...prev, teamContext: { teamName, leadAgentId, teammates: ... } }))
14.2 TeamFile 完整结构
type TeamFile = {
name: string
description?: string
createdAt: number
leadAgentId: string // "team-lead@<teamName>"
leadSessionId: string // 实际 session ID,供 team discovery
members: [{
agentId: string // "name@teamName" 格式
name: string // 显示名,SendMessage 寻址用
agentType?: string // 角色,如 'researcher'
model?: string
joinedAt: number
tmuxPaneId: string
cwd: string
subscriptions: string[] // 订阅哪些 channel
}]
}
14.3 派 teammate (spawnTeammate)
// AgentTool.tsx:441 — 当 teamName && name 时
const result = await spawnTeammate({
name, // 比如 'researcher'
prompt,
description,
team_name: teamName,
use_splitpane: true,
plan_mode_required: spawnMode === 'plan',
model: model ?? agentDef?.model,
agent_type: subagent_type,
}, toolUseContext)
14.4 平结构(no nested teammates)
// AgentTool.tsx:425 — 防御
if (isTeammate() && teamName && name) {
throw new Error(
'Teammates cannot spawn other teammates — the team roster is flat.'
)
}
- 新 teammate 不知道自己的"父 teammate" — 没有这个字段
- leader 不知道这个新成员是谁派的
- shutdown / re-assign 等操作的责任链断裂
15Backend 抽象层
同样的 spawnTeammate API 在 4 种环境下有完全不同的实现:tmux / iTerm2 / Windows Terminal / 同进程。
TeammateExecutor 接口统一了它们。
15.1 4 种 backend 对照
| Backend | 载体 | 启动新 teammate | 限制 |
|---|---|---|---|
| tmux | tmux pane(独立进程) | spawn 新 pane,在里面跑 claude 命令 | 需要 tmux 已安装 |
| iterm2 | iTerm2 split pane(独立进程) | 通过 it2 CLI 创建 split + 运行 claude | 需要 iTerm2 + it2 工具 |
| windows-terminal | Windows Terminal pane/tab(独立进程) | wt.exe 命令行参数创建 | 需要 Windows Terminal |
| in-process | 同 Node.js 进程,独立 context | fork worker(共享内存) | 不能 spawn background;不能嵌套 |
15.2 PaneBackend 接口(tmux/iterm2/windows-terminal)
type PaneBackend = {
type: BackendType
displayName: string
supportsHideShow: boolean
isAvailable(): Promise<boolean>
isRunningInside(): Promise<boolean>
createTeammatePaneInSwarmView(name, color): Promise<CreatePaneResult>
sendCommandToPane(paneId, command): Promise<void>
setPaneBorderColor(paneId, color): Promise<void>
setPaneTitle(paneId, name, color): Promise<void>
enablePaneBorderStatus(): Promise<void>
rebalancePanes(windowTarget, hasLeader): Promise<void>
killPane(paneId): Promise<boolean>
hidePane(paneId): Promise<boolean>
showPane(paneId, target): Promise<boolean>
}
15.3 TeammateExecutor 接口(高层)
type TeammateExecutor = {
type: BackendType
setContext?(context): void
isAvailable(): Promise<boolean>
spawn(config: TeammateSpawnConfig): Promise<TeammateSpawnResult>
sendMessage(agentId, message): Promise<void>
terminate(agentId, reason?): Promise<boolean> // 优雅
kill(agentId): Promise<boolean> // 强制
isActive(agentId): Promise<boolean>
}
15.4 detection 流程(backends/detection.ts)
- 读
CLAUDE_CODE_TEAMMATE_MODEenv(强制覆盖) - 检查
TMUXenv 变量 → tmux backend - 检查
TERM_PROGRAM === 'iTerm.app'+ it2 CLI 是否安装 → iterm2 - 检查
WT_SESSIONenv → windows-terminal - fallback → in-process
15.5 in-process 的特殊性
// in-process teammate 在同 Node.js 进程跑 worker
// 但有自己的:
- AgentContext (AsyncLocalStorage)
- agentId / agentName / teamName
- abortController
- AppState 视图(通过 ToolUseContext.getAppState 包装)
// 共享的:
- 文件系统 / MCP 连接 / 配置
- AppState 后端(通过 setAppStateForTasks 写)
- 不能 spawn background subagent(他们的生命周期跟 leader 进程绑定 — leader 死则 worker 死)
- 不能再 spawn teammate(平结构限制)
- 但可以用 TaskCreate / SendMessage / CronCreate(IN_PROCESS_TEAMMATE_ALLOWED_TOOLS)
16邮箱通信系统
Swarm 模式下 teammates 之间通过文件邮箱异步通信。这是为什么 swarm 可以跨进程、跨 tmux 窗口工作的核心机制。
16.1 邮箱目录结构
~/.claude/teams/<teamName>/
├── config.json // TeamFile
└── inboxes/
├── team-lead.json // leader 的收件箱
├── researcher.json // teammate "researcher" 的收件箱
├── researcher.json.lock // 锁文件
└── reviewer.json
16.2 邮箱消息结构
type TeammateMessage = {
from: string // 发件人 name
text: string // 消息正文
timestamp: string // ISO 8601
read: boolean // 收件人是否已读
color?: string // 发件人颜色(UI 显示用)
summary?: string // 5-10 词摘要(UI 预览)
}
16.3 写信(writeToMailbox)
// teammateMailbox.ts:134
async function writeToMailbox(recipientName, message, teamName) {
await ensureInboxDir(teamName)
const inboxPath = getInboxPath(recipientName, teamName)
// 必须先创建文件(proper-lockfile 要求文件存在)
// 'wx' = 不存在才写,存在抛 EEXIST(被忽略)
await writeFile(inboxPath, '[]', { flag: 'wx' })
try {
release = await lockfile.lock(inboxPath, LOCK_OPTIONS)
// 拿锁后重读,避免覆盖期间收到的消息
const messages = await readMailbox(recipientName, teamName)
messages.push({ ...message, read: false })
await writeFile(inboxPath, jsonStringify(messages, null, 2), 'utf-8')
} finally {
await release?.()
}
}
16.4 SendMessage 工具
// SendMessageTool.ts
{
to: string, // teammate 名 / "*" 广播 / "uds:<path>"
summary?: string, // UI 预览
message: string | StructuredMessage // 文本或结构化消息
}
// StructuredMessage(用于协议级通信)
{ type: 'shutdown_request', reason? }
{ type: 'shutdown_response', request_id, approve, reason? }
{ type: 'plan_approval_response', request_id, approve, feedback? }
16.5 收信(getTeammateMailboxAttachments)
这是邮箱系统跟 query loop 的连接点。每个 user turn 开始,attachment 管线会:
-
读未读消息
readUnreadMessages(agentName, teamName)返回read: false的消息 -
过滤掉协议消息
shutdown_request 等结构化消息必须留在邮箱中等专门的 InboxPoller 处理, 否则 attachment 管线先消费掉,这些信号永远不会触发对应 UI handler。
-
合并 AppState.inbox 中的待处理消息
这些是 InboxPoller 已经从邮箱拉到内存但还没作为 turn 处理的消息。需要去重(file + AppState 都可能有同条)。
-
构造为 attachment
包成
<teammate-message>XML,作为 user message 注入下一个 turn。 -
标记为已读
从文件邮箱写回 read: true,从 AppState 标记 status: 'consumed'。
16.6 协议消息分流示意
17任务通知回流
这是异步 agent 跟父 agent 的通信机制 —— 子 agent 完成时不直接调用父,而是排队一条 message,让父在自己合适的回合消费。
17.1 通知触发点
// agentToolUtils.ts:625 — runAsyncAgentLifecycle 中
enqueueAgentNotification({
taskId,
description,
status: 'completed', // | 'failed' | 'killed'
setAppState: rootSetAppState,
finalMessage, // 子 agent 最后输出的文本
usage: { totalTokens, toolUses, durationMs },
toolUseId,
worktreePath,
worktreeBranch,
})
17.2 enqueueAgentNotification 内部
// LocalAgentTask.tsx:272 — 防重复
let shouldEnqueue = false
updateTaskState(taskId, setAppState, task => {
if (task.notified) return task // 已通知过,跳过
shouldEnqueue = true
return { ...task, notified: true }
})
if (!shouldEnqueue) return
// 中止 prompt suggestion 推测,因为后台 task 状态变了
abortSpeculation(setAppState)
// 拼 XML 消息
const message = `<task-notification>...</task-notification>`
// 入队 priority='later'
enqueuePendingNotification({ value: message, mode: 'task-notification' })
17.3 priority 系统
// messageQueueManager.ts:151
const PRIORITY_ORDER = {
now: 0, // 立即处理(用户输入)
next: 1, // 下一个 turn 必处理(任务摘要)
later: 2, // 在自然 idle 时处理(task notifications)
}
task-notification 用 'later' 优先级,不抢用户输入。dequeue 时同优先级 FIFO,不同优先级按上面顺序。
17.4 完整通知生命周期
18SubagentStart / SubagentStop Hooks
用户可以在 settings.json 配 hook,在每个 subagent 的关键生命周期点运行自定义脚本。
18.1 触发点
| Hook 事件 | 时机 | 能力 |
|---|---|---|
SubagentStart |
runAgent 进入 query() 之前 | 注入 additionalContexts(作为 user message),不能阻断启动 |
SubagentStop |
子 agent 完成 / 失败 / 中止时 | 由 frontmatter Stop hook 转换;能阻断"标记为完成" |
PreToolUse |
子 agent 内每个 tool 调用前 | 能阻断、改输入 |
PostToolUseFailure |
子 agent 内 tool 失败后 | 提示恢复策略 |
18.2 frontmatter 注册的 Stop hook 自动转换
// runAgent.ts:573 — 关键 isAgent 参数
if (agentDefinition.hooks && hooksAllowedForThisAgent) {
registerFrontmatterHooks(
rootSetAppState,
agentId,
agentDefinition.hooks,
`agent '${agentDefinition.agentType}'`,
true, // ← isAgent: 把 Stop 转成 SubagentStop
)
}
hooks.Stop 时,用户期望"这个 agent 结束时跑"。
但 hook 系统的 'Stop' 事件是"主对话 Stop"。如果不转换,主线程结束才会触发,语义错位。
所以注册时自动改名为 SubagentStop,精确匹配语义。
18.3 admin-trusted gate
const hooksAllowedForThisAgent =
!isRestrictedToPluginOnly('hooks') ||
isSourceAdminTrusted(agentDefinition.source)
跟 MCP 一样:企业策略可以"只允许 plugin 提供的 hooks"。 built-in / plugin / policy agent 算 admin-trusted,放行;user/project 自定义 agent 的 frontmatter hooks 被屏蔽。
18.4 SubagentStart hook 的 additionalContexts
// runAgent.ts:537
const additionalContexts = []
for await (const hookResult of executeSubagentStartHooks(
agentId, agentDefinition.agentType, agentAbortController.signal,
)) {
if (hookResult.additionalContexts?.length) {
additionalContexts.push(...hookResult.additionalContexts)
}
}
if (additionalContexts.length > 0) {
const contextMessage = createAttachmentMessage({
type: 'hook_additional_context',
content: additionalContexts,
hookName: 'SubagentStart',
hookEvent: 'SubagentStart',
})
initialMessages.push(contextMessage)
}
典型用法:hook 脚本输出 {"additionalContexts": ["当前 PR 是 #1234", "测试套件已锁定"]},这些会作为子 agent 第一个用户消息出现。
19端到端示例
用一个真实场景串起所有机制。
场景:用户让模型"重构 auth 模块,做完跑测试 + 安全 review"
-
用户输入
"把 auth 模块重构成基于 JWT,做完后跑测试,然后让安全专家 review" -
主 agent 决策
模型识别这是个并行任务:重构和测试可以串行,但 review 可以跟测试并行。它决定:
- 主 agent 自己做重构
- 派
security-reviewer(用户自定义 agent)做 review,异步后台跑 - 主 agent 跑测试
- 等通知回来再总结
-
主 agent 调用 AgentTool(后台 review)
{ description: "Security review of auth refactor", prompt: "Review the JWT-based auth refactor in src/auth/. Check OWASP top 10...", subagent_type: "security-reviewer", run_in_background: true, isolation: "worktree" // 在隔离副本里看,不污染主分支 } -
AgentTool.call() 走普通路径
- findActiveAgent → 找到 security-reviewer (CustomAgentDefinition, source: 'userSettings')
- 权限过滤通过,MCP 不需要
- isolation: 'worktree' → createAgentWorktree →
/tmp/agent-abc12345 - shouldRunAsync = true → registerAsyncAgent + runAsyncAgentLifecycle
- 立即返回
{ status: 'async_launched', agentId: 'agent_xyz', outputFile: '...' }
-
子 agent 在后台进入 runAgent 流水线
- 用 opus 模型(agentDef.model = 'opus')
- permissionMode: 'plan' → toolPermissionContext.mode 切换
- readFileState 全新空缓存(不是 fork)
- tools 经 resolveAgentTools 收窄成 [Read, Bash, Grep] minus Bash(rm:*)
- system prompt = security-reviewer 自己的 + 环境注入 + cwd = worktree path
- SubagentStart hook 跑了一遍,注入 "在 worktree X 上工作"
- 进入 query loop
-
主 agent 同时跑测试
Bash("npm test")阻塞 60s 跑完测试。期间子 agent 在后台读代码、检查 OWASP 项。 -
主 agent 完成测试,结束当前 turn
queryLoop 准备进入下一 turn。检查 message queue,发现还没有 task-notification(子 agent 还没完成)。 主 agent 给用户回了一个"测试通过"的中间答复。
-
子 agent 完成
runAsyncAgentLifecycle 调:
- completeAsyncAgent(标 task 状态 completed)
- classifyHandoffIfNeeded(没问题,跳过)
- cleanupWorktreeIfNeeded → reviewer 没改文件 → 自动清理 worktree
- enqueueAgentNotification(priority='later')
-
用户问"安全 review 怎么样了?"
queryLoop 进入新 user turn。排空 queue 时拿到 task-notification,作为附件注入。
<task-notification> <task-id>agent_xyz</task-id> <status>completed</status> <summary>Agent "Security review" completed</summary> <result>Found 2 issues: missing CSRF token, JWT secret in env file</result> <usage> <total-tokens>8500</total-tokens> <tool-uses>15</tool-uses> <duration-ms>45000</duration-ms> </usage> </task-notification>主 agent 看到这条通知 + 用户问题,综合回答:"重构完成,测试 12/12 通过。安全 review 发现 2 个问题:..."
关键时间线
20核心设计原则
从这套实现中可以提炼出 8 条可迁移到其他 agent 系统的设计原则。
-
把 agent 当一等公民
用结构化
AgentDefinition表达,而不是把"调用 LLM"当作一个 RPC。 每个 agent 有独立的 system prompt、tool 集合、permission mode、model、MCP server。 这样才能精确控制其能力边界。 -
静态发现 + 动态注册
built-in 在源码里,user/project 在 .md 文件,plugin 在插件包。 按优先级合并(plugin < user < project < flag < policy),让"系统提供安全默认 + 用户覆盖 + 企业兜底"成为标准模式。
-
显式声明工具范围
三层过滤(
filterToolsForAgent+ agent 自己的tools/disallowedTools+ 权限规则)让每个 agent 只看到必要的工具。 这同时降低 prompt 成本(token 数)和安全风险(误用的可能性)。 -
同步 vs 异步是不同的语义,不是性能选项
同步阻塞父 turn,共享 abortController 和 setAppState;异步独立生命周期,通过 task-notification 异步通信。 两条路径有不同的 tool 白名单(异步不能 AskUserQuestion 等交互工具)。把它们当作两套独立的执行模式,而不是"同步 + 后台 flag"。
-
Fork 是为缓存而生的
fork 子 agent 完整继承父的字节级 system prompt、tools、history。 所有 fork 子用同一占位 tool_result,只有 directive 不同 → 前缀 100% cache 命中。 这是把"启动一个新 agent"的成本从~5k token 降到 ~50 token。
-
消息队列 + priority 优先级
异步通知用 priority='later',永远不抢用户输入。这种"排队 + 自然消费"模式比同步回调简单得多, 也避免了"通知洪水"问题。同时支持多个 agent 同时完成时的有序处理。
-
文件系统作为 swarm 的真理来源
TeamFile / 邮箱 / 任务列表都用 JSON 文件 + proper-lockfile。 这让 swarm 模式天然支持 tmux / iTerm2 / Windows Terminal / 远端机器 —— 只要能访问同一文件系统就能协作。 没有任何 IPC、daemon、或网络协议。
-
双管线分流邮箱中的不同消息类型
普通文本走 attachment 管线,变成下个 turn 的 user message;协议消息(shutdown/permission)走 InboxPoller, 路由到专门的 handler。两条管线有不能交叉的硬约束 —— 否则信号会被错误消费导致死锁。
- 统一的工具入口(AgentTool / SendMessage)
- 统一的通信协议(task-notification 附件 + 文件邮箱)
- 统一的状态管理(AppState.tasks + teamContext)
- 独立但统一的隔离机制(worktree + permissionMode)