核心图与数据模型
概述
V2 把编排建模为一张图,其中只包含少量持久实体类型:
Project
AppThread
Run
ExecutionNode tree
ProviderThread handles
Context transfers and handoffs
Checkpoints核心划分如下:
AppThread:T3 Code 中用户可见的对话。Run:应用对话线程(thread)上会计入回合数的、用户可见的回合(turn)。ExecutionNode:一次运行内部的提供方(provider)/运行时工作单元。ProviderThread:提供方原生的对话句柄。ProviderSession:活动或可恢复的提供方进程/运行时。ContextTransfer:一条提供方中立的关系/源点记录,供分叉、提供方切换、合并回写(merge-back)和子智能体使用。ContextHandoff:一份已物化的可移植上下文制品,在原生转移不可用或不足时使用。
提供方特有的生命周期保存在提供方引用和诊断用的原始提供方日志中。应用行为由应用自有的 id、V2 原生编排事件(event)以及图关系驱动。
V2 事件是持久的,因为它们就是应用状态转换日志。原始提供方帧并不是持久的应用状态。它们可以通过可选的关联 id(例如 rawEventId)附带,但规范的持久行是规范化后的 V2 事件。
每条已提交的 V2 事件都有一个由存储分配、单调递增的 sequence。投影(projection)快照和 WebSocket 流以该序号作为游标边界:
read snapshot at sequence N
-> stream committed V2 events where sequence > N该序号属于已存储的事件信封,不属于提供方事件,也不属于领域载荷。提供方原生的顺序通过提供方引用和诊断日志另行保留。客户端(client)应把快照的 snapshotSequence 视为权威游标,并且只应用序号更大的后续事件。
实体摘要
AppThread
id: ThreadId
projectId: ProjectId
title
providerBinding
activeProviderThreadId?
lineage?
status projection
Run
id: RunId
threadId: ThreadId
ordinal: number
status
rootNodeId
attempts[]
providerThreadId
contextHandoffId?
userMessageId
checkpoint?
ExecutionNode
id: NodeId
threadId: ThreadId
runId: RunId | null
parentNodeId: NodeId | null
rootNodeId: NodeId
kind
status
providerThreadId?
providerTurnId?
itemId?
countsForRun
checkpointScopeId?
CheckpointScope
id: CheckpointScopeId
threadId
runId?
nodeId
parentScopeId?
kind
advancesAppRunCount
ProviderSession
id: ProviderSessionId
provider
status
cwd
capabilities
ProviderThread
id: ProviderThreadId
providerSessionId?
appThreadId?
nativeThreadRef?
nativeConversationHeadRef?
coveredRunRange?
contextHandoffIds[]
forkSource?
ContextTransfer
id: ContextTransferId
type
sourceThreadId
targetThreadId
sourcePoint
basePoint?
sourceProvider?
targetProvider?
status
resolution?
ContextHandoff
id: ContextHandoffId
transferId
kind
payload
status
ProviderTurn
id: ProviderTurnId
providerThreadId
nativeTurnRef?
nodeId
runAttemptId?
status
RuntimeRequest
id: RuntimeRequestId
nodeId
providerRequestRef?
kind
statusAppThread
AppThread 是稳定的、面向用户的对话。它拥有消息、运行、检查点(checkpoint)和 UI 状态。它不必永远与某个提供方(provider)原生对话线程(thread)一一对应。
type AppThread = {
id: ThreadId;
projectId: ProjectId;
title: string;
defaultProvider: ProviderKind;
modelSelection: ModelSelection;
runtimeMode: RuntimeMode;
interactionMode: InteractionMode;
branch: string | null;
worktreePath: string | null;
activeProviderThreadId: ProviderThreadId | null;
lineage: AppThreadLineage | null;
createdAt: string;
updatedAt: string;
archivedAt: string | null;
deletedAt: string | null;
};activeProviderThreadId 指向当前支撑该应用对话线程的提供方原生对话。取决于具体操作,分叉、提供方迁移或恢复可以创建新的提供方对话线程,同时保留同一个应用对话线程。
defaultProvider 只是当前为未来运行选定的默认值。历史运行仍保留各自的提供方以及提供方对话线程绑定。
lineage 被有意做成轻量的浏览元数据。供实际操作使用的源点、原生分叉的惰性解析、可移植上下文交接、合并回写(merge-back)增量,以及子智能体结果转移,都存放在 ContextTransfer 行中,因为一个应用对话线程可以随着时间参与不止一次转移。
type AppThreadLineage = {
parentThreadId: ThreadId | null;
relationshipToParent: "fork" | "subagent" | null;
rootThreadId: ThreadId;
};Run
Run 是会计入回合数的、用户可见的回合(turn)。它取代了把提供方(provider)回合 id 当作应用层生命周期边界的做法。
type Run = {
id: RunId;
threadId: ThreadId;
ordinal: number;
provider: ProviderKind;
providerThreadId: ProviderThreadId | null;
userMessageId: MessageId;
rootNodeId: NodeId | null;
activeAttemptId: RunAttemptId | null;
status:
| "queued"
| "starting"
| "running"
| "waiting"
| "completed"
| "interrupted"
| "failed"
| "cancelled"
| "rolled_back";
requestedAt: string;
startedAt: string | null;
completedAt: string | null;
checkpointId: CheckpointId | null;
contextHandoffId: ContextHandoffId | null;
sourcePlanRef?: {
threadId: ThreadId;
planId: ProposedPlanId;
};
};只有 countsForConversation = true 的运行,才会计入用户可见的回合数和检查点(checkpoint)数。
providerThreadId 是这次运行所使用的提供方原生对话。这使得混合提供方的应用对话线程(thread)变得明确:运行 1 可以是 Codex,运行 2 可以是 Claude,运行 3 可以回到原来的 Codex 提供方对话线程。
contextHandoffId 指向这次运行所消费的已物化交接制品。一次运行也可以通过转移的目标字段或源字段,关联到范围更广的 ContextTransfer。交接是载荷;转移是持久的关系与策略记录。
RunAttempt
RunAttempt 表示一次应用运行中的一次提供方(provider)执行尝试。大多数运行恰好只有一次尝试。引导(steering)、重试、提供方恢复或提供方切换恢复,可能会创建不止一次尝试。
type RunAttempt = {
id: RunAttemptId;
runId: RunId;
attemptOrdinal: number;
rootNodeId: NodeId;
provider: ProviderKind;
providerThreadId: ProviderThreadId;
providerTurnId: ProviderTurnId | null;
reason: "initial" | "steering_restart" | "retry" | "provider_recovery";
status:
| "pending"
| "running"
| "completed"
| "interrupted"
| "failed"
| "cancelled"
| "superseded";
startedAt: string | null;
completedAt: string | null;
};运行尝试让应用层引导对那些无法引导活动原生回合(turn)的提供方也同样有效。应用可以中断活动的提供方回合,并在同一个 RunId 下创建一次替代尝试。
只有一次尝试会被选为运行完成和建立检查点时的最终尝试。已取代或已中断的尝试仍保留在执行图中,供审计和调试使用。
ExecutionNode
ExecutionNode 是运行时工作的通用单元。它是提供方(provider)事件(event)与应用行为之间的桥梁。
type ExecutionNode = {
id: NodeId;
threadId: ThreadId;
runId: RunId | null;
parentNodeId: NodeId | null;
rootNodeId: NodeId;
kind:
| "root_turn"
| "assistant_message"
| "reasoning"
| "plan"
| "todo_list"
| "tool_call"
| "approval_request"
| "user_input_request"
| "subagent"
| "hook"
| "system";
status:
| "pending"
| "running"
| "waiting"
| "completed"
| "interrupted"
| "failed"
| "cancelled"
| "rolled_back";
countsForRun: boolean;
providerThreadId: ProviderThreadId | null;
providerTurnId: ProviderTurnId | null;
nativeItemRef: ProviderRef | null;
runtimeRequestId: RuntimeRequestId | null;
checkpointScopeId: CheckpointScopeId | null;
startedAt: string | null;
completedAt: string | null;
};一次运行的根节点是唯一获准完成该次运行的节点。子智能体节点、工具节点、审批节点和计划节点都可以各自独立完成。
CheckpointScope
CheckpointScope 描述一份能够建立检查点(checkpoint)的文件系统状态。根运行和子执行节点都可以拥有检查点作用域。
type CheckpointScope = {
id: CheckpointScopeId;
threadId: ThreadId;
runId: RunId | null;
nodeId: NodeId;
parentScopeId: CheckpointScopeId | null;
providerThreadId: ProviderThreadId | null;
kind: "root_run" | "subagent" | "tool" | "provider_thread" | "manual";
ordinalWithinParent: number;
advancesAppRunCount: boolean;
cwd: string;
createdAt: string;
};根运行作用域具有 advancesAppRunCount = true。子作用域(例如子智能体)具有 advancesAppRunCount = false,并嵌套在父运行的作用域之下。
ProviderSession
ProviderSession 是活动的提供方(provider)运行时进程/提供方会话(session),例如 Codex app-server 进程或 Claude SDK 会话。
type ProviderSession = {
id: ProviderSessionId;
provider: ProviderKind;
status: "starting" | "ready" | "running" | "waiting" | "stopped" | "error";
cwd: string;
model: string | null;
capabilities: ProviderCapabilities;
createdAt: string;
updatedAt: string;
lastError: string | null;
};如果提供方支持,一个提供方会话可以承载一个或多个提供方对话线程(thread)。如果某个提供方的每个进程只支持一个活动对话,V2 仍将其建模为挂在该会话上的一个提供方对话线程。
会话实体是活动或可恢复运行时的持久元数据,而不是运行时句柄本身。内存中的进程/客户端(client)句柄可能会消失,原因包括服务器重启、空闲回收器释放了它,或提供方进程崩溃。无论哪种情况,应用都会把 ProviderSession、ProviderThread 以及原生恢复引用保留在持久状态中,然后通过常规的提供方会话管理器路径重新创建活动运行时。
提供方运行时 id 和序号不得依赖适配器(adapter)的进程内存。如果一个值已经持久化,或者必须在恢复之后继续存在,就由编排器(orchestrator)、关联服务或 id 服务分配,或者从持久的提供方引用推导得出。
ProviderThread
ProviderThread 是提供方(provider)原生的对话句柄。即便它嵌套在子智能体执行节点之下,也仍然可以被寻址。
type ProviderThread = {
id: ProviderThreadId;
provider: ProviderKind;
providerSessionId: ProviderSessionId | null;
appThreadId: ThreadId | null;
ownerNodeId: NodeId | null;
nativeThreadRef: string | null;
nativeConversationHeadRef: ProviderRef | null;
status: "not_loaded" | "idle" | "active" | "archived" | "closed" | "error";
firstRunOrdinal: number | null;
lastRunOrdinal: number | null;
handoffIds: ContextHandoffId[];
forkedFrom: ProviderThreadForkSource | null;
createdAt: string;
updatedAt: string;
};当提供方对话线程(thread)支撑一个一等应用对话线程时,会设置 appThreadId。当提供方对话线程嵌套在执行节点(例如子智能体)之下时,会设置 ownerNodeId。提供方对话线程以后可以被分叉,或被提升为一等应用对话线程。
提供方对话线程的原生运行覆盖范围可以出现间断。例如:运行 1–5 使用 Codex 提供方对话线程 A,运行 6–8 使用 Claude 提供方对话线程 B,运行 9 回到 Codex 对话线程 A,并带有一份覆盖运行 6–8 的交接摘要。此时 Codex 对话线程 A 仍然是同一个提供方对话线程,但在运行 9 之前会有一份显式的 ContextHandoff。
ContextTransfer
ContextTransfer 记录源与目标之间的关系,以及上下文应当如何在二者之间移动。它是用户分叉、提供方(provider)切换、合并回写(merge-back)和子智能体共用的原语。
type ContextTransfer = {
id: ContextTransferId;
type: "fork" | "provider_handoff" | "merge_back" | "subagent_spawn" | "subagent_result";
sourceThreadId: ThreadId;
targetThreadId: ThreadId;
sourcePoint: ContextSourcePoint;
basePoint: ContextSourcePoint | null;
sourceProvider: ProviderKind | null;
targetProvider: ProviderKind | null;
status:
| "pending"
| "resolved_native"
| "resolved_portable"
| "failed"
| "consumed"
| "superseded";
resolution: ContextTransferResolution | null;
createdBy: "user" | "agent" | "system";
createdAt: string;
consumedAt: string | null;
};type ContextSourcePoint = {
threadId: ThreadId;
runId: RunId | null;
checkpointId: CheckpointId | null;
turnItemId: TurnItemId | null;
providerThreadRef: NativeThreadRef | null;
providerTurnRef: NativeTurnRef | null;
};
type ContextTransferResolution =
| { strategy: "native_fork"; providerThreadRef: NativeThreadRef }
| { strategy: "portable_context"; contextHandoffId: ContextHandoffId }
| { strategy: "delta_context"; contextHandoffId: ContextHandoffId }
| { strategy: "checkpoint_context"; contextHandoffId: ContextHandoffId };创建转移本身应当是廉价的。昂贵的上下文交接会惰性物化,时机是目标运行开始、且所选提供方已经确定之时。
ContextHandoff
ContextHandoff 是必须桥接可移植提供方(provider)上下文时创建的一等制品。它最常见于在不同运行之间更换提供方;当提供方恢复失败、并且必须依据应用历史为替代提供方对话线程(thread)播种时,也同样适用。
在更广义的转移模型中,ContextTransfer 记录源、目标和生命周期。当原生转移无法满足该关系时,ContextHandoff 就是一次运行所消费的已物化载荷。
type ContextHandoff = {
id: ContextHandoffId;
transferId: ContextTransferId;
threadId: ThreadId;
targetRunId: RunId;
fromProviderThreadIds: ProviderThreadId[];
toProviderThreadId: ProviderThreadId;
coveredRunOrdinals: {
from: number;
to: number;
};
strategy:
| "delta_since_target_last_seen"
| "full_thread_summary"
| "checkpoint_summary"
| "manual_context";
status: "pending" | "ready" | "failed" | "superseded";
summaryMessageId: MessageId | null;
summaryText: string;
createdByProvider: ProviderKind | null;
createdAt: string;
updatedAt: string;
};交接不只是提示词文本。它是图的一部分,可以被检查、重新生成、取代或审计。
回到该提供方时的首选策略是 delta_since_target_last_seen:恢复先前的提供方对话线程,并且只摘要该提供方处于非活动期间发生的那些运行。当恢复失败、提供方设置不兼容,或累积的交接会导致上下文质量变差时,使用 full_thread_summary。
ProviderTurn
ProviderTurn 是提供方(provider)原生回合(turn)的规范化句柄。
type ProviderTurn = {
id: ProviderTurnId;
providerThreadId: ProviderThreadId;
nodeId: NodeId;
runAttemptId: RunAttemptId | null;
nativeTurnRef: string | null;
ordinal: number;
status: "pending" | "running" | "completed" | "interrupted" | "failed" | "cancelled";
startedAt: string | null;
completedAt: string | null;
};Codex 拥有强原生回合 id。较弱的提供方可能只有序号。二者都会映射到 ProviderTurnId。
RuntimeRequest
请求表示由提供方(provider)发起、需要应用或用户作出响应的回调。
type RuntimeRequest = {
id: RuntimeRequestId;
nodeId: NodeId;
providerTurnId: ProviderTurnId | null;
nativeRequestRef: string | null;
kind:
| "command"
| "file-read"
| "file-change"
| "dynamic_tool_call"
| "user_input"
| "auth_refresh";
status: "pending" | "resolved" | "expired" | "cancelled";
responseCapability:
| { type: "live"; providerSessionId: ProviderSessionId }
| { type: "not_resumable"; reason: string };
createdAt: string;
resolvedAt: string | null;
};权限种类与 V1 ProviderRequestKind 所使用的规范领域值相同。 适配器(adapter)把提供方原生回调名称(例如 Codex 的 item/commandExecution/requestApproval) 以及 Claude 的工具权限名称,映射为这些应用层值。
请求在重启之后仍可保持可见,但只有当其 responseCapability 处于活动状态时才可以响应。
消息
消息属于对话投影(projection),而不属于原始提供方(provider)图。在可能的情况下,它们会链回到运行和节点。
type ConversationMessage = {
id: MessageId;
threadId: ThreadId;
runId: RunId | null;
nodeId: NodeId | null;
role: "user" | "assistant" | "system";
text: string;
attachments: Attachment[];
streaming: boolean;
createdAt: string;
updatedAt: string;
};提供方消息分块通过条目/内容事件(event)收集,并投影为消息。投影可以默认隐藏子级消息和子智能体消息,同时仍把它们保留在图中。
计划、问题与待办列表
V2 把这些内容视为结构化的回合(turn)条目或执行节点。
type PlanArtifact = {
id: PlanId;
threadId: ThreadId;
runId: RunId | null;
nodeId: NodeId;
kind: "proposed_plan" | "todo_list" | "questions";
status: "draft" | "active" | "completed" | "superseded";
markdown?: string;
steps?: Array<{ id: string; text: string; status: "pending" | "running" | "completed" }>;
questions?: UserInputQuestion[];
};Codex 的 turn/plan/updated 映射为 todo_list 制品。计划模式下的最终计划条目映射为 proposed_plan。用户输入问题请求映射为 questions,并附带一个 RuntimeRequest。
Checkpoint
检查点(checkpoint)附着于检查点作用域。根运行检查点是用户可见的对话检查点。子检查点为子智能体、工具或提供方(provider)对话线程(thread)记录嵌套的文件系统状态,且不会推进父运行的计数。
type Checkpoint = {
id: CheckpointId;
threadId: ThreadId;
scopeId: CheckpointScopeId;
runId: RunId | null;
nodeId: NodeId;
parentCheckpointId: CheckpointId | null;
ordinalWithinScope: number;
appRunOrdinal: number | null;
ref: string;
status: "ready" | "missing" | "error" | "stale";
files: CheckpointFileSummary[];
capturedAt: string;
};子级或子智能体的提供方回合(turn)可以创建子检查点。除非它是在已分叉或已提升的对话线程中作为一等应用运行来执行,否则不会创建应用运行检查点。
原始提供方诊断
原始提供方(provider)诊断是只追加的证据,用于调试、支持和生成重放夹具。它们应写入容量有界的轮转日志文件,而不应被当作规范的持久 SQLite 状态。
type RawProviderEvent = {
id: RawEventId;
provider: ProviderKind;
providerSessionId: ProviderSessionId;
sequence: number;
direction: "incoming" | "outgoing";
messageKind: "request" | "response" | "notification" | "error";
method: string | null;
jsonRpcId: string | number | null;
payload: unknown;
observedAt: string;
};在能够获得原始提供方诊断时,领域行为不应依赖于解析历史 UI 事件(event)。生产行为应依赖于规范化的编排事件、实体以及提供方引用。原始提供方帧是证据和重放输入,并不是常规应用状态的事实来源。
规范化的 V2 实体应保留足够的关联元数据,以便解释行为并决定如何路由:
- 提供方种类。
- 提供方会话(session)/对话线程(thread)/回合(turn)引用。
- 在可得时,提供方条目/请求引用。
- 原生 id 强度。
- 在有用时,诊断日志位置或原始帧引用。
重放框架可以把原始提供方通信记录作为输入,而不要求生产环境的 SQLite 保存每一帧原始数据。
投影
V2 应分别暴露以下投影(projection):
- 对话线程(thread)外壳:用于侧边栏的快速列表。
- 对话线程详情:消息、运行、活动、检查点(checkpoint)、计划。
- 执行图:供调试和开发者查看节点与提供方引用的视图。
- 提供方会话(session):活动的运行时/进程状态。
- 待处理请求:可操作的审批与用户输入。
界面可以保持简单,图本身则保持精确。
投影的流式传输应沿用现有的应用快照加游标契约。对话线程详情订阅应返回序号为 N 的投影快照,然后只流式传输 N 之后的事件(event)。前端不应收到已经体现在快照中的事件。重新连接时可以改从一份新快照开始,而不重放本地客户端(client)状态;这份新快照及其 snapshotSequence 成为新的游标边界。
回合条目投影
前端不应自行合并 messages、plans、检查点(checkpoint)、审批和活动来重建显示顺序。V2 为对话线程详情的渲染暴露一份有序的 turnItems 投影(projection)。
turnItems 是投影记录,并不是规范的事实来源。规范图仍然以规范化形式存放在运行、尝试、节点、请求、消息、计划和检查点之中。在需要关联的地方,提供方原生条目引用直接记录在节点和回合条目上。
已分叉的对话线程(thread)还会暴露一份派生的 visibleTurnItems 投影。turnItems 仍然是在该应用对话线程上发出的规范条目。visibleTurnItems 是用户可见的有序读模型(read model):它可以通过分叉源点引用继承而来的源条目,包含诸如「分叉自对话」这样的合成生命周期标记,然后继续接上本地目标对话线程的条目。这样可以让存储保持规范化,同时为对话线程详情视图和测试提供一个由后端拥有、可供断言的表面。
已知的常用工具应当具备结构化的条目变体,使前端能够渲染稳定的自定义组件,而不必解析提供方文本:
type TurnItem =
| UserMessage
| AssistantMessage
| Reasoning
| Plan
| FileChange
| CommandExecution
| FileSearch
| WebSearch
| ApprovalRequest
| Checkpoint
| Compaction
| Handoff
| Fork
| DynamicTool;示例:
type FileChange = {
type: "file_change";
fileName: string;
additions?: number;
deletions?: number;
diffStr?: string;
oldStr?: string;
newStr?: string;
};
type CommandExecution = {
type: "command_execution";
input: string;
status: TurnItemStatus;
output?: string;
exitCode?: number;
};
type DynamicTool = {
type: "dynamic_tool";
toolName: string | null;
input: unknown;
output?: unknown;
};压缩、交接和分叉是编排生命周期条目,而不是动态工具:
compaction:同一个条目可以从标题类似于「正在压缩上下文...」的running,过渡到标题类似于「已压缩上下文」的completed。handoff:记录从一或多个源提供方对话线程/提供方通向目标提供方对话线程/提供方的上下文桥。fork:记录用户或系统基于某次运行、某个节点或某个提供方对话线程创建了新的应用对话线程。
界面可以用确定性组件渲染已知变体,并把 dynamic_tool 渲染为可展开的 JSON 输入/输出。每个回合条目都保留指回 runId、nodeId、providerTurnId 和 nativeItemRef 的引用,使调试视图能够从显示流跳回到图和提供方日志。