对话线程谱系与上下文转移
分叉(fork)、提供方交接(provider handoff)、合并回去(merge-back)和子智能体是各自独立的产品功能,但它们应当共享同一套编排模型。共同的原语不是「分叉」,也不是「摘要」。它是:
thread relationship
+ source point
+ optional context transfer resolution应用应当低成本地保留谱系,并把昂贵的提供方与上下文工作留到某次运行真正需要时,再惰性解析。
产品使用场景
分叉以探索
用户分叉一条已有的应用对话线程(thread),以便探索一条旁支,同时不污染源对话线程的上下文。
source AppThread
-> user forks from stable source point
-> new idle AppThread
-> no provider chosen yet
-> first message chooses provider/model如果分叉上的第一次运行使用与源相同的提供方(provider),并且该提供方支持原生分叉,就走提供方原生的分叉路径。如果使用的是另一个提供方,则在首次分发时物化可移植上下文。
用另一个智能体继续同一条对话线程
用户用另一个提供方继续同一条应用对话线程。这并不是一条新的应用对话线程,但它仍然是一次上下文转移。
AppThread runs 1-5 with Codex
user sends run 6 with Claude
-> resolve context transfer from Codex-backed source state to Claude
-> start Claude provider thread with handoff context + user message应用对话线程仍然是权威的对话。提供方的原生对话线程,是带有明确覆盖范围的底层句柄。
把分叉合并回它的源
用户在分叉里做深入探索,再把结果带回源对话线程。
source thread at source point S
fork explores runs F1-Fn
user sends next message in source thread with "bring this back"
-> build delta from S to fork latest stable point
-> inject delta context with the source-thread user message这并不是对源对话线程做一次完整的提供方切换。这是从分叉回到原来那条对话线程的一次针对性上下文转移。
子智能体
子智能体属于同一类操作,只是创建者和生命周期契约不同。
原生子智能体:
- 提供方通过自己的原生能力派生出子级;
- 应用观察原生的子级引用和事件(event);
- 子级被建模为子执行节点,并在有用时建模为相关联的应用子对话线程。
跨提供方的 T3 子智能体:
- 父智能体调用一个由应用拥有的工具;
- 应用创建一条与父级有关系的子应用对话线程或运行;
- 子级结果作为一次子智能体结果上下文转移,转移回父级。
共享模型应当使用户发起的分叉和由智能体创建的子对话线程呈现为同一种图结构,区别只在于 createdBy 和生命周期策略。
核心概念
对话线程关系
对话线程(thread)关系描述的是:一条目标对话线程或一次运行,相对于另一条对话线程为何存在。
type ThreadRelationshipKind =
| "fork"
| "provider_handoff"
| "merge_back"
| "subagent_spawn"
| "subagent_result";关系属于产品层面,并且与提供方(provider)无关。分叉与提供方原生分叉不是同一件事。提供方原生分叉,只是分叉关系的一种可能的解析策略。
源点
源点描述的是正在被转移的确切状态。
type ContextSourcePoint = {
threadId: ThreadId;
runId?: RunId;
checkpointId?: CheckpointId;
turnItemId?: TurnItemId;
providerThreadRef?: NativeThreadRef;
providerTurnRef?: NativeTurnRef;
};稳定的源点应当优先选取已完成的运行或检查点(checkpoint)。活动运行更难处理,因为提供方的数据块可能还在陆续到达,检查点也可能还不是最终结果。
上下文转移
上下文转移是连接源与目标的持久操作记录。创建它的成本很低,并且可以一直保持未解析,直到第一次被使用。
type ContextTransfer = {
id: ContextTransferId;
type: ThreadRelationshipKind;
sourceThreadId: ThreadId;
targetThreadId: ThreadId;
sourcePoint: ContextSourcePoint;
basePoint?: ContextSourcePoint;
sourceProvider?: ProviderKind;
targetProvider?: ProviderKind;
status:
| "pending"
| "resolved_native"
| "resolved_portable"
| "failed"
| "consumed"
| "superseded";
resolution?: ContextTransferResolution;
createdBy: "user" | "agent" | "system";
createdAt: string;
consumedAt?: string;
};basePoint 用于增量转移,合并回去时尤其如此。在把分叉合并回去时,basePoint 是该分叉最初的源点,sourcePoint 是该分叉最新的稳定点。
上下文交接
上下文交接是代价较高的、已经物化的上下文产物。只有当原生转移不可用或不够用时,才会惰性地创建它。
type ContextHandoff = {
id: ContextHandoffId;
transferId: ContextTransferId;
kind: "portable_context" | "delta_context" | "checkpoint_context";
payload: PortableContextPayload;
createdAt: string;
};上下文交接应当是可审计的应用产物,而不是藏起来的提示词拼接。
解析策略
解析决定目标提供方或目标对话线程如何接收源上下文。
type ContextTransferResolution =
| {
strategy: "native_fork";
providerThreadRef: NativeThreadRef;
}
| {
strategy: "portable_context";
contextHandoffId: ContextHandoffId;
}
| {
strategy: "delta_context";
contextHandoffId: ContextHandoffId;
}
| {
strategy: "checkpoint_context";
contextHandoffId: ContextHandoffId;
};当源的原生引用足够强,并且适配器(adapter)支持时,同一提供方的分叉应当优先使用 native_fork。跨提供方交接和合并回去通常都需要一次上下文交接。
惰性解析
创建分叉的成本应当很低:
thread.fork
-> create target AppThread
-> record thread relationship / ContextTransfer(status=pending)
-> do not create provider session
-> do not create provider thread
-> do not build portable context对待决分叉的首次分发会解析这次转移:
first run on fork chooses provider P
-> find pending fork ContextTransfer
-> if source provider == P and native fork is supported:
resolve native fork
else:
materialize portable context
-> create/resume provider thread
-> send handoff/native context + user message这样既避免在分叉之时强迫用户选择智能体,也避免生成可能永远用不上的摘要。
运行时入口
运行启动时应当有一个与提供方(provider)无关的钩子:
resolveStartContext({
threadId,
provider,
message,
}): StartContextResolution这个钩子检查以该对话线程或该次运行为目标的待决上下文转移,并选择一种策略:
- 无需转移:目标提供方已经拥有当前的覆盖范围。
- 原生转移:提供方相同,且适配器(adapter)支持原生分叉或恢复。
- 可移植转移:构建一次上下文交接,并注入到这次运行中。
- 增量转移:构建
basePoint与sourcePoint之间的变更。 - 不支持:在提供方的工作开始之前显式失败。
提供方适配器负责原生细节。编排器(orchestrator)负责关系、源点、持久的转移记录,以及命令回执。
对于 Codex,原生的 thread/fork 接受一个包含边界点本身的 lastTurnId 边界。当应用的源点是一个已经完成的提供方回合(turn),并且带有原生回合引用时,Codex 适配器会传入该原生 id,让提供方直接在所请求的位置创建分叉。如果没有可用的原生回合引用,适配器会改为对最新的原生状态做分叉,再按此后已经终结的提供方回合数量回滚该分叉:在分页历史上使用 thread/revert,在旧式历史上则显式报告失败,因为 Codex 0.156 无法回退旧式历史。编排器传递的仍然是一个与提供方无关的源点,以及源提供方的回合历史;它并不编码 Codex 的边界策略或回滚策略。
直接回滚检查点(checkpoint)同样要求 Codex 历史是分页的。V2 适配器在回滚之前先检查 historyMode,遇到旧式历史就显式失败,因为 Codex 0.156 无法回退。在分页历史上,它会分页读取 thread/turns/list,找到第一个被移除的回合,然后发送 thread/revert,并带上 beforeTurnId。
数据归属
AppThread 应当存放轻量的浏览谱系:
type AppThreadLineage = {
parentThreadId: ThreadId | null;
relationshipToParent: "fork" | "subagent" | null;
rootThreadId: ThreadId;
// A side chat: a fork shown only inside its parent. The fork planner sets it from `thread.fork`.
sideChat?: boolean;
};操作上的转移细节应当放在 ContextTransfer 里,而不是直接放在 AppThread 上,因为一条对话线程(thread)可以参与许多次转移:
- 由分叉创建;
- 后来切换到另一个提供方;
- 后来合并回它的源;
- 后来派生出子智能体。
检查点与活动运行策略
第一版实现应当优先采用稳定的源点:
- 已完成运行的检查点(checkpoint);
- 显式检查点;
- 覆盖范围已知的空闲提供方(provider)对话线程。
从活动运行进行分叉或交接,应当:
- 使用最近一次已完成的检查点,或者
- 予以拒绝,直到活动运行的语义设计完成。
不要从只流式传输了一部分的提供方状态静默分叉。那样会削弱关联,并使重放和恢复变得困难。
当前实现边界
后端的第一片实现同一提供方(provider)分叉的惰性解析:
thread.fork创建一条带有分叉谱系的目标应用对话线程(thread),以及一条待决的ContextTransfer。thread.fork不会创建提供方会话(session)、提供方对话线程、提供方回合(turn),也不会创建可移植上下文产物。- 分叉上的首次分发会解析这条待决转移。对于原生引用足够强、并且支持
thread/fork的 Codex 到 Codex 分叉,编排器使用提供方原生的分叉路径,先记录resolved_native,再记录consumed。 - 跨提供方的分叉转移,以及其他非原生的分叉转移,会在目标对话线程首次分发时,惰性物化一份可移植的
ContextHandoff。 - 对于显式的运行分叉或检查点分叉,活动中的源运行会被拒绝;
latest_stable会解析到最近一次已完成、并且已经建立检查点的运行。
这使得运行时与该架构保持一致,同时让可移植上下文保持明确,并可通过 ContextTransfer 和 ContextHandoff 记录进行审计。
与提供方切换的关系
提供方(provider)切换是这样一种上下文转移:sourceThreadId === targetThreadId,并且目标提供方不同于当前活动的提供方。
回到先前用过的提供方,同样是一次上下文转移,通常是从该提供方最后覆盖到的运行范围,到当前应用对话线程位置之间的一段增量。
提供方切换与上下文交接这篇文档描述该特定功能如何选择策略。本文档定义的是提供方切换、分叉、合并回去和子智能体所共用的更广泛模型。