createSubagentContext 解析
目标
这页回答的是:
子 agent 的上下文到底怎么从父
ToolUseContext派生出来?
文件:
src/utils/forkedAgent.ts
一句话结论
createSubagentContext(...) 不是简单复制父上下文,而是:
默认隔离所有可变状态,只在必要处显式共享。
这是整套多 agent 架构能在同一个 runtime 里不串台的关键。
先说最重要的设计原则
这个函数的默认态度非常保守:
- readFileState:克隆,不共享
- abortController:默认新建 child controller
- setAppState:默认 no-op
- UI callback:默认全部禁用
- 各种 trigger / Set:默认全新建
- queryTracking:新 chain
也就是说:
子 agent 默认不是父 agent 的“继续执行”,而是一个隔离上下文里的新执行体。
关键实现拆解
1. abortController 的策略
实现:
const abortController =
overrides?.abortController ??
(overrides?.shareAbortController
? parentContext.abortController
: createChildAbortController(parentContext.abortController))含义
优先级是:
- 显式 override
- 如果要求共享,就直接共用父 controller
- 否则默认创建一个 child controller
为什么这设计很重要
这意味着:
- 默认父 abort 会传播给子 agent
- 但子 agent 自己也可以有独立 abort 生命周期
- 特殊交互型子 agent 可以显式共享父 controller
这比“全独立”或“全共享”都细。
2. getAppState 的策略
实现核心:
const getAppState = overrides?.getAppState
? overrides.getAppState
: overrides?.shareAbortController
? parentContext.getAppState
: () => {
const state = parentContext.getAppState()
...
shouldAvoidPermissionPrompts: true
}含义
默认情况下,子 agent 的 getAppState() 不是直接照搬父级,
而是包一层,把:
shouldAvoidPermissionPrompts: true强行打开。
这说明什么
默认子 agent 被当成:
- 不该弹权限 UI 的执行体
- 更适合后台 / 非交互子任务
只有当它共享 abortController(也就是更像交互型子 agent)时,才直接沿用父级的 getAppState()。
这和 runAgent 里 async / sync / bubble mode 的设计是一致的。
3. readFileState 默认克隆
实现:
readFileState: cloneFileStateCache(
overrides?.readFileState ?? parentContext.readFileState,
)含义
不管你传 override 还是直接用 parent,最后都会 clone。
为什么值钱
这说明作者非常明确地不想让:
- 子 agent 的文件读取缓存
- 直接污染父 agent 的缓存状态
但又不想完全丢掉已有缓存价值,所以用 clone。
这是典型的“继承已有知识,但隔离可变副作用”。
4. setAppState 默认是 no-op
实现:
setAppState: overrides?.shareSetAppState
? parentContext.setAppState
: () => {}含义
默认子 agent 不能随便写父级 UI / 会话状态。
只有显式要求共享时,才允许直接写父状态。
但注意还有一个关键通道
setAppStateForTasks:
parentContext.setAppStateForTasks ?? parentContext.setAppState这意味着:
- 普通状态更新默认隔离
- 任务注册 / kill / 后台基础设施 仍然可以触达 root store
这个区分非常妙。
也就是:
子 agent 默认不能乱改父 UI, 但它需要登记后台任务、shell task、cleanup 的时候,仍然必须能碰到根状态。
5. denial tracking 的处理
实现:
localDenialTracking: overrides?.shareSetAppState
? parentContext.localDenialTracking
: createDenialTrackingState()含义
如果子 agent 不共享 setAppState,那它就拿一个本地 denial tracking state。
为什么要这样
因为 async 子 agent 往往 setAppState 是 no-op。 如果 denial tracking 还依赖共享 store,那权限拒绝计数就根本积累不起来。
这就是很典型的“主状态隔离后,要补一个本地可变状态兜底”。
6. UI callback 全禁用
实现里默认:
addNotification: undefinedsetToolJSX: undefinedsetStreamMode: undefinedsetSDKStatus: undefinedopenMessageSelector: undefined
含义
默认子 agent 不操纵父界面。
这非常合理:
- 否则多 agent 并发时 UI 会互相抢控制权
- 一个后台子 agent 不应该突然改主界面的 spinner / JSX
所以子 agent 默认是“无界面执行体”。
7. messages 默认替换成自己的
实现:
messages: overrides?.messages ?? parentContext.messages这说明它允许:
- 普通情况下继承一份消息视图
- runAgent 调用时显式传入子 agent 的
initialMessages
真正关键不在这句,而在调用方通常会给它新的 messages。
也就是说:
- 接口支持继承
- 实际 agent runtime 通常传入自己那份子消息序列
8. contentReplacementState 默认克隆父级
实现:
contentReplacementState:
overrides?.contentReplacementState ??
(parentContext.contentReplacementState
? cloneContentReplacementState(parentContext.contentReplacementState)
: undefined)这是非常关键的一点
注释说得很明白:
这样做是为了 cache-sharing forks。
因为 fork 子 agent 会处理父消息里已经出现过的 tool_use_id。 如果给它 fresh state,它会做出不同的 replacement decision, 导致 prompt wire prefix 改变,cache miss。
所以这里必须 clone,而不是 fresh。
这说明什么
createSubagentContext 不只是做“功能隔离”,还在参与:
- prompt cache 稳定性
- transcript / replacement 一致性
这是很高级的工程点。
9. queryTracking 会新开链
实现:
queryTracking: {
chainId: randomUUID(),
depth: (parentContext.queryTracking?.depth ?? -1) + 1,
}含义
每个子 agent:
- 都有自己的 query chain id
- depth 相对父级 +1
这给:
- analytics
- tracing
- agent tree 可视化
提供了结构化基础。
10. 哪些东西是直接继承的?
会直接沿用或基本沿用的有:
options(除非 override)fileReadingLimitsuserModifiedupdateAttributionState
特别值得注意
updateAttributionState 被作者认为是:
- scoped
- functional updater 安全
- 可以共享
这说明不是所有共享都危险,关键在于状态模型是否天然可组合。
总结成一句人话
createSubagentContext() 干的不是“复制一个 context”,而是:
把父 agent 的上下文拆成三类:
- 必须隔离的可变状态
- 可以克隆继承的状态
- 必须保留到 root 的基础设施通道
然后重新拼成一个子 agent 能安全运行的 ToolUseContext。
这页最值得记住的 5 点
- 默认隔离,不默认共享
- readFileState 用 clone,不直接共用
- setAppState 默认 no-op,但 setAppStateForTasks 保留 root 通道
- contentReplacementState 默认 clone,是为了 prompt cache / replacement 一致性
- 子 agent 默认不拥有父 UI 控制权
当前结论
如果说:
AgentTool.call()决定要不要起子 agentrunAgent()决定怎么跑子 agent
那 createSubagentContext() 真正定义的是:
这个子 agent 与父 agent 的边界条件。
这东西不花哨,但非常核心。
多 agent 系统最难的从来不是“再调一次模型”,
而是别让一窝 agent 把同一个 runtime 挠成一团。createSubagentContext() 干的就是这件事。