deepseek harness的subAgent机制(Codex)
USER:
读下:https://blog.csdn.net/Code_LT/article/details/163849301?spm=1011.2415.3001.5331 (下文中的gpt学习.md)
并研究deepseek harness的subAgent机制。
Codex:
一、整体架构
SubAgent 是一个完整的 capability seam:
model-facing tool
|
v
ctx.subagents
|
+-- spawn
+-- fork
+-- acp
+-- codex
+-- claude-code
+-- dsh-sdk
核心服务见 subagent/src/index.ts,公共类型见 types.ts。
它不是 Agent Loop 的内部特殊分支,而是:
- Service Definition:
ctx.subagents - Provider:不同运行方式
- Consumer:
subagent、send_message、interrupt_agent、list_agents、report
多个 provider 可以同时注册,模型通过不同的工具名使用它们;模型不会直接选择 provider。
二、One-shot:一次性委派
调用:
ctx.subagents.start(provider, request)
返回:
{
id,
localAgent,
result,
dispose
}
它的语义是:
start
-> 创建一个 child
-> 执行一次 prompt/turn
-> 返回一个 result
-> 调用方 dispose
SubagentRun 的定义在 types.ts。
重要边界:
- provider 在
start()完成前拥有创建资源; start()成功后,资源所有权转移给调用方;- child-level 模型失败通常不会让
resultreject,而是返回stopReason: 'error'; - 调用方必须调用
dispose(); - 一次性运行没有 follow-up、steering 或 cold resume。
模型层面的 backgroundMode: one-shot 仍然可以后台运行,但它使用普通 ctx.jobs,不是 continuable Activation。
三、Continuable:持久化子 Agent
调用:
ctx.subagents.startContinuable(...)
返回的不是 SubagentRun,而是:
{
childId,
messageId
}
返回时只保证:
初始消息已经被 child inbox 接受。
它不等待模型请求开始,也不等待消息进入 Session Log。实现见 continuation.ts。
模型可以把它理解成:
一个持久化 Child Session
|
+-- 当前最多一个 Activation
|
+-- 一个 Agent
+-- 一个 FIFO inbox
+-- 若干 owned child Activations
Activation 不是一次请求,也不是结果对象。它只是:
当前 child Agent 被加载在本进程中的驻留周期。
因此:
- 一个 Activation 可以执行多轮;
- child 空闲后 Activation 可以被释放;
- Session 仍然保留;
- 后续
send_message可以重新 cold resume; - 同一个
childId跨多个 Activation 保持不变。
这也是当前实现最关键的设计。
四、Follow-up 路由
followup() 根据 child 当前是否驻留来决定路径:
child 有 Activation 且正在运行
-> 进入同一个 Agent inbox
child 有 Activation 但 waiting
-> 唤醒同一个 Agent
child 没有 Activation
-> 从持久化 Session cold resume
-> 恢复 descriptor / persona / toolFilter / model
-> 投递下一轮
实现见 continuation.ts 和 continuation.ts。
所有 follow-up 都是后续 FIFO turn:
- 不会加入当前正在执行的 turn;
- 不会修改已经 claim 的消息;
- 不会重新调用 provider;
- provider 只参与第一次 continuable creation 的准备。
五、Provider 实际拥有的东西很少
continuable provider 只需要实现:
prepareContinuable(request): Promise<ContinuableCreateSpec>
返回值当前主要是:
{
seed?: SessionEvent[]
}
它不返回:
- Agent
- AgentHandle
- result
- dispose
- resume 方法
- prompt 投递器
这些全部由 continuation manager 管理。类型定义在 types.ts。
因此,历史上“provider 负责完整子 Agent 生命周期”的理解已经不准确。现在更准确的说法是:
Provider 负责传输和初始创建差异;continuation manager 负责持久化 child 的完整生命周期。
六、Cold Resume 的真正依据:Descriptor
每个 session-backed child 都会写入一个模型不可见的:
subagent/descriptor
当前版本:
SUBAGENT_DESCRIPTOR_VERSION = 2
continuable descriptor 保存:
- provider
- label
- child model provider
- child model
- persona
- toolFilter
- mode
它不保存:
subagentDepthoutputSchema- 单次 Activation 的
maxTokens - report 工具本身
恢复过程是:
SessionPersistence.inspect(childId)
|
校验 parentSession
|
fold subagent/descriptor
|
ctx.agents.resume()
|
applyChildComposition()
|
followup()
fork child 还要使用 seedLength,只从 child 自己的 suffix 中寻找 descriptor,避免把祖先 Session 的 descriptor 当成自己的。
这体现了一个很强的原则:
持久化 child 的身份和恢复所需组合必须从 child 自己的日志重建,不能依赖父级当前配置。
七、权限和深度继承
进程内 child 会通过 child-agent.ts 统一组合:
- 继承父级 preset;
- 添加
subagent:delegation作用域声明; - 应用 child persona;
- 应用 toolFilter。
权限不是简单复制父级当前状态,而是在委派边界捕获:
captureDelegatedPolicyOverrides(parent)
行为:
- 父级显式 sandbox override 会被写入 child log;
- approval capability 存在时,child approval 固定为
'never'; - 父级之后改变 sandbox,不会追溯影响已经创建的 child;
- cold resume 读取 child 自己已经持久化的策略。
深度则是绝对深度:
childDepth = parentDepth + 1
权威值是:
SessionHeader.delegationDepth
所以 child 恢复后不会被错误当成 top-level agent。
八、Report 和 Settlement Notice 是两条不同通道
显式 report:
child -> direct parent
child 自己决定是否发送、发送几次、发送什么内容。
有两种模式:
quiet:注入 parent,不启动 parent turn;wakeup:进入 parent 的下一轮。
实现见 tool-subagent-report/src/index.ts。
它有严格限制:
- 只在 continuable child scope 中注册;
- child 不能指定 recipient;
- recipient 从 durable
parentSession推导; - 不能伪造 sender;
- nested child 只向 direct parent 上报一层;
- report 不会结束 child turn;
- report 不是 final result。
另一条是 runtime 自动产生的 settlement notice:
continuation manager -> direct parent
每个已经成功返回 childId 的 child,在 Activation 结束时都会收到一条通知,包含:
- stop reason;
- child 最终 assistant 内容;
- 是否完成、被中断、失败或达到 token 上限。
它的来源单独标记为:
{ kind: 'subagent-settled' }
避免把 runtime 写的总结误认为 child 自己说的话。实现见 continuation.ts。
这两者必须分开:
report = child 主动选择的内容
settlement = runtime 对 child 结局的事实通知
九、Interrupt 不是 Dispose
interrupt_agent 只中断当前 turn:
Agent.cancel(cause, { keepInbox: true })
它不会:
- 销毁 Activation;
- 删除 child;
- 丢弃未 claim 的 follow-up;
- 影响已经发布的 descendants。
被中断的当前 turn 不会自动重排;未 claim 的 follow-up 会保留,等待下一次 waking send_message。
权限上:
- direct parent 可以中断 child;
- live ancestor 可以中断 descendant;
- sibling、陌生 Agent、self 会被拒绝;
- stale Agent 对象也会被拒绝;
- 对不存在、one-shot、自然完成的 child 是 accepted no-op。
相关测试集中在 tool-subagent-control.spec.ts。
十、Provider 差异
| Provider | 运行方式 | 继承父级 completed history | continuable |
|---|---|---|---|
spawn |
同进程新 Agent | 否 | 支持 |
fork |
同进程,带 completed prefix | 是 | seam 支持,但 shipped 配置不用 |
acp |
子进程 ACP | 否 | 不支持 |
codex |
Codex app-server | 否 | one-shot |
claude-code |
Claude Agent SDK/CLI | 否 | one-shot |
dsh-sdk |
另一个 DSH 进程 | 否 | one-shot |
spawn 与 fork 支持:
outputSchema
depthLimit
toolFilter
persona
远程 provider 都是:
NO_START_CAPABILITIES
inheritsParentContext = false
所以远程 provider 不能由父级强制:
- output schema;
- maxDepth;
- persona;
- toolFilter。
必须设置:
maxDepth: provider-managed
十一、Fork 为什么生产配置固定 one-shot
fork 的核心价值是复用父级 completed history 的 token/KV prefix。
但 continuable child 会额外拥有:
- child-local
reporttool schema; tool:reportprompt section。
这些内容位于继承历史之前,会破坏 prefix reuse。因此示例和 bundle 都配置为:
provider: fork
backgroundMode: one-shot
具体配置见 examples/acp-agent/cordis.yml 和 packages/bundle/base/cordis.patch.yml。
所以不是 fork 技术上不能 continuable,而是:
当前 report 作用域设计会破坏 fork 最重要的 prefix reuse 优势。
十二、对 gpt学习.md 历史结论的修正
历史内容中这些判断仍然成立:
- SubAgent 是 capability seam;
- provider abstraction 很重要;
- Session Log 是事实账本;
- model-visible 内容必须 logged;
- Claude Code/Codex 可以作为外部 provider;
- Agent 生命周期应该通过扩展点组织。
但需要修正四点:
-
SubAgent 不只是“一次调用 + 一个结果”。
Continuable child 是持久化会话,不返回SubagentRun.result。 -
Remote provider 不是可恢复 child。
ACP、Codex、Claude Code、DSH SDK 当前都只能做 one-shot。 -
Report 不是 final result。
它是 child 主动选择的回传消息;最终结局由 settlement notice 独立表达。 -
Child 不会自动继承父级全部上下文。
fork 只继承 completed history;工具、preset、persona、sandbox、approval 都需要独立组合和持久化。
十三、对你自己的 Agent/Memory 系统最值得借鉴的点
我认为最值得迁移的不是“做一个 SubAgent API”,而是这四个设计:
-
把短任务和持久会话分成两种对象。
不要让一个Task同时承担后台任务、可恢复会话和多轮 Agent。 -
把“驻留状态”与“持久身份”分开。
Activation可以消失,但 child Session 和 child id 不变。 -
把恢复所需配置写进 child 自己的日志。
不要 cold resume 时重新读取父级当前配置。 -
把 child 输出、runtime 结局、parent 控制分成独立消息来源。
这样 transcript 才不会把系统生成的内容错误归因给模型。
验证状态
我审阅了 continuation、inheritance、report、control、list-agents 相关 focused tests,测试覆盖非常完整,尤其覆盖:
- inbox acceptance;
- cold resume;
- FIFO;
- depth;
- policy snapshot;
- report delivery;
- settlement notice;
- interrupt authorization;
- child-first disposal;
- descriptor corruption;
- list diagnostics。
但本轮没有取得实际 pass/fail 运行结果:当前 workspace 的 pnpm 链接不完整,根目录缺少可用 .bin 和常规依赖链接,直接运行 focused Vitest 会卡在依赖解析/安装阶段。因此上面的结论以源码、配置和测试断言为依据,没有把未完成的测试运行误报成通过。
USER:
读下 https://blog.csdn.net/Code_LT/article/details/163879232?spm=1011.2415.3001.5331(下文中的subAgent学习.md),你看看里面的内容是否正确,帮我总结
Codex:
我读完了 subAgent学习.md。总体判断:主线基本正确,核心心智模型已经接近当前源码;大约 85%~90% 的内容可以保留。 主要问题不是方向错误,而是少数地方把实现的保障说得过于绝对。
核心总结
DeepSeek Harness 的 SubAgent 不是单纯的“调用一个子 Agent 的工具”,而是由以下部分组成的运行时:
Provider Registry
+ Child Session
+ Activation Manager
+ Agent Inbox
+ Parent/Child Control
+ Report Channel
+ Lifecycle Events
最重要的区分是:
one-shot
= 一次性委派,有 result,结束后 dispose,没有 follow-up/resume
continuable
= 有 durable child Session 的多轮子 Agent,会通过 Inbox 接收后续消息
Continuable 子 Agent 的实际模型是:
Durable Session
-> 可选的 process-local Activation
-> AgentHandle
-> Agent
-> Inbox
其中:
Session保存身份、历史、descriptor、policy 和 Inbox 事件。Activation表示当前是否加载在进程内。Agent负责真正消费 Inbox、执行模型和工具。AgentHandle表达生命周期所有权。Inbox是 Agent 唯一的 FIFO 消息队列。Provider在 continuable 模式下只提供创建所需的 detached seed,生命周期由 continuation manager 管理。
因此:
Agent identity != Agent residency
释放 Activation 不会删除 Child Session;以后可以通过 cold resume 创建新的 Activation。
稿件中基本正确的部分
ctx.subagents已经不只是 provider registry,也承担 continuable child 的编排。- one-shot 和 continuable 是两种不同生命周期。
send_message是加入下一轮 FIFO 消息,不是实时 steering。interrupt_agent只取消当前执行,保留未消费 Inbox。report是 Child 主动发送的消息,不等于完成,也不等于 one-shot result。- settlement notice 和 report 使用不同 provenance,语义区分正确。
spawn、fork、Claude Code、Codex、ACP、DSH SDK 的 provider 分层理解基本正确。fork只继承已完成的 conversation prefix,不等于继承权限、工具或整个 Parent scope。- delegation policy 在创建时捕获并写入 Child Session,cold resume 不重新读取 Parent 当前 policy。
Session + Inbox + Activation类似 Event Sourcing、Actor 和进程驻留模型,这个类比很有帮助。
需要修正的重点
startContinuable()的 descriptor 时序写错了。
稿件多处写成:
reserve childId
-> 写入 descriptor
-> prepareContinuable()
实际是:
reserve childId
-> 在内存中 snapshot descriptor
-> capture delegated policy
-> await provider.prepareContinuable()
-> 把 seed + descriptor 组成 creation seed
-> ctx.agents.create()
-> 再进入 Child Session
也就是说,descriptor 在 prepareContinuable() 前只是被构造和校验,并没有已经持久化到 Child Session。源码见 continuation.ts:403 和 descriptor-seed.ts:23。
- Inbox 的崩溃恢复能力被写得过强。
正确的是:
已经写入 Session Log 的 Inbox mutation
可以在 resume 时重建
但不是:
只要 followup 被接受,就一定不会丢
测试明确覆盖了“accepted but unlogged message 没有自动 replay”的情况:如果消息已被内存接受,但进程在它进入 Session Log 前退出,恢复时可能不存在。见 continuation.spec.ts:1062 和 inbox.ts:25。
- Settlement notice 不是“这一轮执行结束”。
一个 Activation 可以执行多轮 FIFO turn。Settlement notice 表示:
当前 Activation / residency epoch 结束
而不是每一轮结束都会发送。建议把:
Child #123 这一轮执行结束了
改成:
Child #123 的当前 Activation 已结束
- Parent 不会自动收到完整 transcript,但不是完全没有自动输出。
Child 的完整工具历史、reasoning、中间过程不会自动复制给 Parent;但 Activation 结束时,runtime 会自动发送 settlement notice,其中可以包含最终 assistant 内容。
更准确的说法是:
不会自动复制完整 transcript、tool output 和 reasoning;
但 runtime 会自动发送一条结算通知。
- SettlementWatcher 更准确地说是“状态推导器”,不是独立状态机。
稿件把它称为“小型状态机”作为类比没问题,但实现并没有维护一套独立状态,而是根据:
Agent.status
accepted.size
ownedChildren.size
动态推导:
running
waiting
settled
源码见 continuation.ts:850。
- Cold resume 后模型看到的是派生 history,不是原始完整 Event Log。
稿件写:
完整有效 Session History + 新消息
容易误解。更准确的是:
从 Session Log 派生出的 model-visible history
+ 新的 user message
descriptor、policy、Inbox mutation 等事件可能只服务 runtime,不进入模型上下文;compaction 也可能改变模型实际看到的历史。
childId一开始只是预留身份,不代表 Child 已经成功建立。
如果 materialization 或初始 Inbox admission 在成功返回前失败,整个创建会回滚,调用方不会得到有效的 Child。只有 startContinuable() 成功返回 { childId, messageId } 后,才可以把它视为已建立的 continuable child。
- Capability flags 的适用范围需要限定。
outputSchema、depthLimit、toolFilter、persona 这些静态 capability flags 主要检查 one-shot start()。Continuable 是否支持,判断标准是 provider 是否实现 prepareContinuable(),不是简单复用这些 flags。
一句话版
DeepSeek Harness 的 SubAgent 是把一次性委派和可继续会话统一到同一个 Service 下:one-shot 通过
SubagentRun返回一次性结果;continuable 则通过 durable Session、process-local Activation 和 Agent Inbox 实现多轮交互。Provider 负责后端差异和初始创建数据,Continuation Manager 负责身份、排队、权限、恢复和释放;Session Log 提供恢复依据,但只有已经写入日志的 Inbox 状态才具备崩溃恢复保障。
另外,这份文件实际上是“两轮研究稿”叠在一起:前半部分偏架构理解,后半部分偏源码走读,内容有不少重复。作为个人研究笔记没问题;如果以后整理成正式文档,建议保留后半部分的源码时序,把前半部分压缩成一页总览。
USER:
继续研究下里面提到的两个更深的问题:
① Child 到底继承 Parent 哪些 Service / Tool / Prompt / Memory,Cordis Scope 是怎么做到“共享 composition,但局部 shadow”的;
② spawn/fork/continuable 和 Session Surface / Compaction 怎么互动,尤其是 fork 为什么能共享历史但又不能简单共享 Parent 当前 Context。
Codex:
先给结论:subAgent学习.md 的主线判断基本正确,但需要把三个概念彻底分开:
Composition:Child 能使用哪些 Service / Tool / Prompt
History:Child 的模型能看到哪些 Session 消息
Runtime:哪个 Agent、Scope、Inbox、Activation 正在运行
DSH 只在创建时复用部分 composition,fork 只复制一次稳定的历史 prefix;它们都不会复制 Parent 的 live runtime。
一、Child 继承什么
核心入口是 applyChildComposition:
childCtx.agentPresets.composeFrom(childCtx, parent.ctx)
childCtx.systemPrompt.context(...)
childCtx.systemPrompt.section(...) // optional persona
childCtx.tools.restrict(...) // optional filter
关键点是 composeFrom():
standingMountFor(parentCtx)
bindScopeParent(childScopeKey, parentStandingScopeKey)
见 composeFrom。
它不是:
Child Scope -> Parent Agent Scope
而是:
Parent Agent Scope -> Parent Preset Standing Scope
Child Agent Scope -> 同一个 Preset Standing Scope
因此正确的结构是:
Child Agent Scope
│
▼
Parent Preset Standing Scope
│
▼
Global / Host Scope
Child 不会继承 Parent 自己的临时 scope registrations、Parent 的 tool restriction 或 Parent 当前 persona。它们只有在显式作为 Child composition 参数传入时才会出现。
Cordis 与 dsh-scope 是两层机制
这点非常重要:
- Cordis
Context.extend()使用 JavaScript 原型链,让子 Context 读取父 Context 的属性;子 Context 的 own property 可以 shadow 父属性。 - Cordis Fiber 管理插件生命周期、Service 注册、依赖解析和销毁。
dsh-scope另外维护scopeParents,给 Tool、Prompt、Event 等 scope-aware registry 做可见性路由。
也就是说,dsh-scope 的父链不是通用的 Service 复制机制。官方文档也明确说,只有实现了 scope-aware API 的注册表才会继承;任意 Cordis Service 不会因为 Context 带了 scope tag 就自动隔离或继承。见 dsh-scope README。
继承矩阵
| 内容 | Child 的实际行为 |
|---|---|
| Host/global Service | 通常可见,但这是共享运行时可达性,不是从 Parent 复制 |
| Preset 的 Tool / Prompt 注册 | 可见,多个 Agent 共享同一个 standing composition |
| Parent Agent 自己的局部 Service | 不自动可见 |
| Parent 自己的 Tool restriction | 不自动继承 |
| Child-local Service / Tool | 新建,只属于 Child |
| Provider / Model / maxTokens | 默认从 Parent route 继承,可被 request override |
| Sandbox / approval policy | 在 delegation 时快照成 Child 自己的 session events |
| Conversation / Memory | spawn 不继承,fork 复制一次历史 prefix |
| Parent 后续新增内容 | Child 不可见 |
Tool 的 shadow 规则
ToolRuntime.view() 会按:
global
→ preset standing scope
→ child scope
合并工具;近层同名工具覆盖远层同名工具。见 tools.view。
Child 的 toolFilter 只过滤它继承来的工具。多个 restriction 会相交,但 Child 自己注册的工具不会被自己的 restriction 删除,例如 continuable Child 的 report 工具和 structured-output 工具。
因此:
Parent 禁止 bash
不等于:
Child 也禁止 bash
Child 只有在创建请求中显式收到相同 toolFilter 时才会禁止 bash。
同时,Tool restriction 只是可见性和执行查找过滤,不是安全边界。真正的 sandbox、approval、filesystem policy 仍由对应 capability 和 policy service 执行。
Prompt 的 shadow 规则
System prompt assembly 会合并 global 和 scope-chain 的 sections,同名时近层覆盖远层。见 systemPrompt.assemble。
Child 通常得到:
Global prompt
+ Parent preset prompt sections
+ Child-local subagent:delegation context
+ Child persona shadow
+ Child-local tool guidance
其中:
deployment:persona可以由 Child persona 覆盖;subagent:delegation只注册在 Child;- prompt variables 也按 scope shadow;
complete: true的 persona 可以使 Child 只使用该完整 system prompt;- dynamic runtime context 会在 Child 的 request 中重新计算,并在 shipped loop 中作为
user/messagesnapshot 写入 Child Session。
所以 Prompt 是“共享注册内容,按 Child 重新 assembly”,不是把 Parent 已经生成好的 system prompt 字符串永久复制给 Child。
二、Memory、Session Surface 与 fork
DSH 没有一个独立的“Parent Memory 对象”可以直接传给 Child。真正的权威来源是 Session Event Log:
Session Log
↓
Surface Fold
↓
Session.deriveMessages()
↓
模型历史
deriveMessages() 只投影:
user/message
assistant/message
tool/result
turn/*、assistant/chunk、request/header、policy event、descriptor、compaction metadata 等仍然保存在 log 中,但不直接进入模型消息。见 Session.deriveMessages。
spawn
spawn provider 不提供 seed:
Child Session = empty
Child Agent = new
Child Scope = new
Child Surface = empty
它仍然会加入 Parent 当前使用的 preset composition,但没有 Parent transcript。
fork
fork provider 的 seed 规则是:
lastEnd = parent.session.events.findLast(e => e.type === 'turn/end')
seed = parent.session.events.slice(0, lastEnd.seq + 1)
因此它复制的不是简单的 deriveMessages() 数组,而是:
从 seq 0 开始的完整、连续、可验证的 raw event prefix
Child Session 构造时会重新校验、深拷贝 seed,并追加 session/end-seed。见 Session constructor。
所以 fork 的准确语义是:
复制 Parent 截止 fork 时最后一个完整 turn 的事件历史
+
Child 自己重新组装 runtime
不是实时共享历史。
如果 Parent 当前正在执行:
turn/start
assistant/tool-call
但还没有:
tool/result
turn/end
这个 turn 就不会进入 fork seed。否则 Child 会得到一个未闭合、无法正常 replay 的 Session。
continuable
continuable 不是和 spawn/fork 并列的第三种 Provider。它是由 SubagentContinuationManager 管理的生命周期模式。
Provider 只实现:
prepareContinuable(): Promise<{ seed?: SessionEvent[] }>
之后由 continuation manager 负责:
稳定 childId
→ durable Session
→ Agent
→ Scope
→ Inbox
→ Activation
→ cold resume
→ dispose
第一次创建时:
- spawn 的
seed为空; - fork 的
seed是 Parent 已完成 turn prefix; - manager 额外写入 Child descriptor;
- initial prompt 进入 Child Inbox;
startContinuable()在 Inbox 接受后返回,不等待模型完成。
之后:
Activation running -> 直接排入 Inbox
Activation waiting -> 唤醒原 Agent
Activation 不存在 -> 从持久化 Session cold resume
见 startContinuable 和 coldResume。
因此 continuable Child 的“Memory”是:
同一个 durable Child Session
+ 多次追加自己的 turn
而不是每次 followup 都重新 fork Parent。
三、为什么 fork 能共享历史,却不能共享 Parent 当前 Context
因为两者的稳定性完全不同。
Parent 当前 Context 可能包含:
当前 Agent Scope
当前 tool registrations
当前 prompt providers
当前 policy overrides
当前 Inbox
当前 AbortController
当前正在执行的 tool
当前未完成 turn
临时 Cordis fibers
动态 runtime context
这些东西不能作为一个整体复制。
最关键的原因有五个:
-
未完成 turn 不可重放
当前 tool call 可能只有 assistant call,没有 result 和
turn/end。复制它会产生不平衡 Session。 -
Scope 的所有权不同
Parent Scope 由 Parent Agent 生命周期拥有。Child 必须有自己的 Scope、自己的 disposer 和自己的事件路由。
-
Service 不是纯数据
Cordis Service 可能包含连接、监听器、worker、缓存、AbortSignal、Fiber disposer。复制对象并不能复制正确的生命周期。
-
权限不能随知识自动继承
Parent 当前的 tool restriction、sandbox、approval 状态可能已经发生变化。把历史和当前 authority 绑定在一起会让 Child 使用不清晰的权限状态。
-
Parent 未来会继续变化
fork 后 Parent 可能继续追加消息、执行工具或 compaction。Child 若共享 Context,就会出现两个 Agent 同时改变同一份可变状态。
所以 inheritsParentContext = true 只能理解为:
inherits completed conversation seed
不能理解为:
inherits Parent Service / Tool / Permission / Scope / Runtime
四、Compaction 如何参与
Compaction 不是删除 raw log,而是一个持久化日志事务:
compaction/start
compaction/summary
user/message(surfaceOp = replace)
compaction/end
新的 user/message 会替换 Surface 上的一段旧节点;旧事件仍在 raw log 中。见 commitCompactionBody。
因此:
Parent 先 compaction,再 fork
Child 复制 Parent 当时的 raw event prefix。Child 重新 fold 后看到的是:
summary checkpoint
+ 未被压缩的 retained tail
不是被 shadow 的原始消息。
fork 后 Parent 再 compaction
Child 已经拥有 detached seed,Parent 的 Surface replacement 不会影响 Child。
Child 自己 compaction
只改变 Child 自己的 Surface 和 replaceGeneration,不会自动把 summary 写回 Parent。
Child 若要让 Parent 知道结果,必须通过显式通信:
report tool
quiet inject
wakeup followup
settlement notice
这些才会以新的 Parent Session message 或 notice 进入 Parent 的历史。
还有一个细节:如果 fork seed 中已经包含 Parent 之前写入的 runtime-context snapshot,Child 会继承那条历史消息;但 Child 的 RuntimeContextProjection 是自己的实例,之后会根据 Child 当前 Scope 和 Session 重新判断是否需要追加新 snapshot,不会共享 Parent 的 projection 对象。
五、一个完整时序
Parent:
turn 1 完成
turn 2 正在调用工具
fork:
只复制 turn 1 的完整 event prefix
不复制 turn 2
创建 Child Session
创建 Child Agent / Scope / Inbox
加入 Parent 的 preset standing scope
应用 Child persona / toolFilter
Child 接收新任务
之后:
Parent 继续写入 turn 2
Parent 修改 runtime context
Parent 执行 compaction
Parent 注册或撤销局部工具
结果:
已创建的 Child 都不会自动看到这些变化
因此,对 subAgent学习.md 最需要收紧的表述是:
Child 继承的不是 Parent Context,而是 Parent 当前 preset 的 composition,以及 fork 在创建瞬间截取的、已经完成且可重放的 Session event prefix。
Child 的 Service visibility、Tool visibility、Prompt assembly、Session history 和 runtime ownership,始终是四套不同机制。
更多推荐




所有评论(0)