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))

含义

优先级是:

  1. 显式 override
  2. 如果要求共享,就直接共用父 controller
  3. 否则默认创建一个 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: undefined
  • setToolJSX: undefined
  • setStreamMode: undefined
  • setSDKStatus: undefined
  • openMessageSelector: 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)
  • fileReadingLimits
  • userModified
  • updateAttributionState

特别值得注意

updateAttributionState 被作者认为是:

  • scoped
  • functional updater 安全
  • 可以共享

这说明不是所有共享都危险,关键在于状态模型是否天然可组合。


总结成一句人话

createSubagentContext() 干的不是“复制一个 context”,而是:

把父 agent 的上下文拆成三类:

  1. 必须隔离的可变状态
  2. 可以克隆继承的状态
  3. 必须保留到 root 的基础设施通道

然后重新拼成一个子 agent 能安全运行的 ToolUseContext


这页最值得记住的 5 点

  1. 默认隔离,不默认共享
  2. readFileState 用 clone,不直接共用
  3. setAppState 默认 no-op,但 setAppStateForTasks 保留 root 通道
  4. contentReplacementState 默认 clone,是为了 prompt cache / replacement 一致性
  5. 子 agent 默认不拥有父 UI 控制权

当前结论

如果说:

  • AgentTool.call() 决定要不要起子 agent
  • runAgent() 决定怎么跑子 agent

createSubagentContext() 真正定义的是:

这个子 agent 与父 agent 的边界条件。

这东西不花哨,但非常核心。

多 agent 系统最难的从来不是“再调一次模型”, 而是别让一窝 agent 把同一个 runtime 挠成一团。createSubagentContext() 干的就是这件事。