Copied Fork 不是原样复制父 Rollout:它先构造对 child 合适的模型历史,过滤 Reasoning/工具/旧通信和非最终 assistant,再由 child Create 路径写自己的 SessionMeta。
本节解决的不是“把对象存一下”这种抽象问题,而是把问题限定在:研究 Agent Fork 的内容筛选、指令替换和 copied persistence,不讨论引用式冻结。输入:父模型上下文、FullHistory/LastNTurns、子角色配置与 destination history mode;状态所有者:AgentControl spawn + Session ForkPersistence::Copied;成功结果:拥有新 ThreadId/SessionMeta 的 child Rollout 和经过筛选的 Context History。先把这些边界钉住,后面的顺序、失败和恢复才不会混成一句“持久化失败后重试”。
状态边界
| 问题 | 本节答案 |
|---|---|
| 输入 | 父模型上下文、FullHistory/LastNTurns、子角色配置与 destination history mode |
| 状态所有者 | AgentControl spawn + Session ForkPersistence::Copied |
| 成功产物 | 拥有新 ThreadId/SessionMeta 的 child Rollout 和经过筛选的 Context History |
| 研究范围 | 研究 Agent Fork 的内容筛选、指令替换和 copied persistence,不讨论引用式冻结 |
正常路径:先看顺序点
这条路径可以压缩成五步:
- 父 Thread materialize+flush。
- 加载 Legacy 全史或 Paginated latest model context。
- 可选截取最后 N Turns。
- 筛选 item 并替换子指令。
- 创建 child Meta 后写 copied prefix/settings。
图中的箭头不是“可能调用”的依赖图,而是源码中决定可见性和所有权转移的先后关系。前一步没有确认时,后一步不能替它作出更强的成功承诺。
源码机制拆解
Fork 前先固定父前缀
父 record path 可能异步排队,因此 spawn_forked_thread 先 ensure_rollout_materialized 和 flush,再从 Store 取历史,避免 child 漏掉刚完成的父消息。
只保留适合 child 的长期语义
普通 Message 保留 system/developer/user 与 phase=FinalAnswer 的 assistant;AdditionalTools、AgentMessage、Reasoning、Call/Output、Web/Image、旧 inter-agent communication 被丢弃。Compacted 与必要 Event/Meta 可保留。
截断 Fork 清除 diff 基线
只有 FullHistory 才可能保留 TurnContext/WorldState。LastNTurns 丢掉部分 prompt prefix,child 第一 Turn 必须重新注入上下文,不能沿用父 reference baseline。
父协作提示和指令需要消毒
V2 会过滤父 root/subagent usage hint;父 developer instruction fragment 被替换为 child 角色指令。Compacted.replacement_history 内部也执行同一 retain_mut 规则,不能只清洗顶层旧行。
新物理 Thread 先写 child SessionMeta
Forked history 中可能还含父 SessionMeta 作为历史记录,但 Create recorder 的首行是新 child meta,携带新的 ThreadId、parent/forked_from、source 与配置。Loader 永远以首行确认当前身份。
Python 风格伪代码
下面的伪代码只保留设计职责、状态和失败顺序;它不逐行翻译 Rust,也不借 Python 语法虚构源码中不存在的事务:
async def copied_fork(parent, options, child_config):
await parent.ensure_materialized()
await parent.flush()
items = await load_model_context_for_mode(parent.id, parent.history_mode)
if options.last_n_turns is not None:
items = truncate_to_last_n_effective_turns(items, options.last_n_turns)
preserve_baseline = False
else:
preserve_baseline = not newest_checkpoint_is_legacy(items)
filtered = []
for item in items:
item = sanitize_compacted_and_top_level_messages(item, child_config)
if keep_for_child(item, preserve_baseline):
filtered.append(item)
child = await create_new_thread(
session_meta=new_child_meta(parent_id=parent.id, config=child_config),
initial_history=filtered,
fork_persistence=COPIED,
)
await child.persist(filtered + [child.effective_settings_event()])
await child.ensure_materialized()
return child
阅读时要特别看三处:哪个对象拥有可变状态,哪一个 await 是可观察屏障,以及失败后保留的是已提交前缀、未提交后缀,还是完全独立的外部副作用。
失败、取消与恢复
| 故障点 | 已留下的状态 | 可观察结果 | 恢复责任 |
|---|---|---|---|
| Fork 前未 flush | 父尾部只在内存队列 | child 历史截断过早 | 先 materialize+flush |
| 只过滤顶层、漏 compact replacement | 旧工具/父提示藏在 checkpoint | child 仍继承不合适内容 | 两层使用同一 sanitizer |
| LastNTurns 保留 TurnContext baseline | 前缀已被裁掉 | 首 Turn 只发 diff | 截断模式删除 TurnContext/WorldState |
| 父 SessionMeta 成为 child canonical | 身份/path 不匹配 | 恢复错误 Thread | Create 首行写新 child meta |
这里没有统一的“回滚一切”。内存状态、日志行、SQLite 投影、父子拓扑和工具造成的文件/网络变化分别有自己的提交点。恢复代码只能根据已经存在的权威证据继续,不能用较弱的投影替较强的事实背书。
必须保持的不变量
- child ThreadId 与 canonical SessionMeta 全新
- Copied Fork 不继承工具执行中间态
- FullHistory 才可能保留 reference baseline
- 父角色指令与 usage hint 不原样泄漏到 child
这些不变量比“最终能 Resume”更严格:正常路径要成立,Writer 竞争、任务取消、坏尾行、投影落后和旧格式兼容时也必须成立。
设计取舍
复制使 child Rollout 自包含、删除简单,却复制历史字节;筛选还意味着它不是父日志的取证副本,而是为 child 推理构造的语义快照。
源码可以直接证明字段、分支、调用顺序和测试期望;“为什么这样设计”的表述是基于这些事实作出的工程归纳,不把它包装成未公开的产品承诺。
Mini Codex 复刻
ForkFilter 作为纯函数输入 parent items + child role + mode,输出 child history;ChildRecorder 永远自行创建 meta header。
复刻时先验证协议不变量,再补性能优化。一个能在故障注入下说明“留下了什么”的小实现,比一个只在正常路径调用 save() 的演示更接近真实 Runtime。
源码导航
- codex-rs/core/src/agent/control/spawn.rs:加载、截断、keep_forked_rollout_item、指令/usage hint 消毒
- codex-rs/core/src/session/mod.rs:ForkPersistence::Copied 的初始历史写入
- codex-rs/core/src/session/session.rs:child SessionMeta 创建字段
相邻测试也很重要:
- codex-rs/core/src/agent/control_tests.rs:工具/Reasoning 过滤、compacted history 消毒与 child 指令
- codex-rs/core/src/thread_rollout_truncation_tests.rs:LastNTurns 与 rollback marker 的截断
本节结论
Copied Fork 不是原样复制父 Rollout:它先构造对 child 合适的模型历史,过滤 Reasoning/工具/旧通信和非最终 assistant,再由 child Create 路径写自己的 SessionMeta。
评论
登录后即可评论