核心图与数据模型¶
概述¶
V2 将编排建模为一个图,其中包含少量持久化实体类型:
Project
AppThread
Run
ExecutionNode tree
ProviderThread handles
Context transfers and handoffs
Checkpoints
核心划分如下:
AppThread:T3 Code 中用户可见的对话。Run:应用线程上经过计数的用户可见轮次。ExecutionNode:一次 run 内部 provider/运行时工作的单元。ProviderThread:provider 原生的对话句柄。ProviderSession:一个活跃或可恢复的 provider 进程/运行时。ContextTransfer:一种 provider 中立的关系统一/源点记录,用于分叉、provider 切换、合并回写和子代理。ContextHandoff:一种物化的可移植上下文工件,当原生传输不可用或不充分时使用。
provider 特定的生命周期保留在 provider 引用和诊断性原始 provider 日志中。应用行为由应用拥有的 ID、V2 原生编排事件和图关系驱动。
V2 事件是持久的,因为它们是应用状态转换日志。原始 provider 帧不是持久的应用状态。它们可以通过可选的关联 ID(如 rawEventId)附加,但规范的持久化行是标准化后的 V2 事件。
每个已提交的 V2 事件都有一个由存储分配的单调递增的 sequence。投影快照和 WebSocket 流使用该序列作为游标边界:
该序列属于存储的事件信封,而不是 provider 事件,也不是领域负载。provider 原生的顺序通过 provider 引用和诊断日志单独保留。客户端应将快照的 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
status
AppThread¶
AppThread 是稳定的面向用户的对话。它拥有消息、run、检查点和 UI 状态。它不必永远与 provider 原生线程一一对应。
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 指向当前支撑应用线程的 provider 原生对话。根据操作的不同,分叉、provider 迁移或恢复可能会创建新的 provider 线程,同时保留同一个应用线程。
defaultProvider 只是当前为未来运行选择的默认 provider。历史运行保留它们自己的 provider 和 provider 线程绑定。
lineage 有意保持为轻量级的浏览元数据。操作源点、惰性原生分叉解析、可移植上下文交接、合并回写增量以及子代理结果传输都存放在 ContextTransfer 行中,因为一个应用线程可能随时间参与多次传输。
type AppThreadLineage = {
parentThreadId: ThreadId | null;
relationshipToParent: "fork" | "subagent" | null;
rootThreadId: ThreadId;
};
Run¶
Run 是经过计数的用户可见轮次。这取代了使用 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 的 run 才会计入用户可见轮次数和检查点数。
providerThreadId 是本次运行使用的 provider 原生对话。这使混合 provider 的应用线程变得明确:run 1 可能是 Codex,run 2 可能是 Claude,run 3 可能回到原始的 Codex provider 线程。
contextHandoffId 指向本次运行消费的物化交接工件。一个 run 也可能通过传输的 target/source 字段与更广泛的 ContextTransfer 关联。交接是负载;传输是持久的关系和策略记录。
RunAttempt¶
RunAttempt 表示一次应用运行中的一次 provider 执行尝试。大多数运行只有一次尝试。引导(steering)、重试、provider 恢复或 provider 切换恢复可能会产生多次尝试。
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;
};
运行尝试使得应用级引导(app-level steering)即使在无法引导活动中的原生轮次的 provider 上也能工作。应用可以中断活动中的 provider 轮次,并在同一个 RunId 下创建替代尝试。
只有一次尝试是运行完成和检查点(checkpointing)的最终选定尝试。被取代/被中断的尝试会保留在执行图中,用于审计/调试。
ExecutionNode¶
ExecutionNode 是运行时工作的通用单元。它是 provider 事件与应用行为之间的桥梁。
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;
};
运行中的根节点(root node)是唯一允许完成该运行的节点。子代理节点、工具节点、审批节点和计划节点可以独立完成。
CheckpointScope¶
CheckpointScope 描述了一个可以设置检查点的文件系统状态单元。根运行和子执行节点都可以拥有检查点作用域。
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 运行时进程/会话,例如 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;
};
如果 provider 支持,一个 provider 会话可以托管一个或多个 provider 线程。如果 provider 每个进程只支持一个活动对话,V2 仍将其建模为一个挂接到该会话上的 provider 线程。
会话实体是用于实时或可恢复运行时的持久元数据,而不是运行时句柄本身。内存中的进程/客户端句柄可能会因服务器重启、空闲回收器(idle reaper)释放它或 provider 进程崩溃而消失。在所有情况下,应用都会将 ProviderSession、ProviderThread 和 native 恢复引用保存在持久状态中,然后通过正常的 provider 会话管理器路径重建实时运行时。
Provider 运行时 ID 和序号不得依赖适配器进程内存。如果某个值被持久化或必须在恢复后继续存在,则应通过 orchestrator/correlation/id 服务分配,或从持久化的 provider 引用推导得出。
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;
};
当 provider 线程支撑一个一等应用线程时,会设置 appThreadId。当 provider 线程嵌套在执行节点(如子代理)下时,会设置 ownerNodeId。provider 线程之后可以被 fork 或提升为一等应用线程。
一个 provider 线程的原生运行覆盖范围可能存在缺口。例如:运行 1-5 使用 Codex provider 线程 A,运行 6-8 使用 Claude provider 线程 B,运行 9 带着涵盖运行 6-8 的交接摘要返回到 Codex 线程 A。在这种情况下,Codex 线程 A 仍然是同一个 provider 线程,但它会在运行 9 之前有一个显式的 ContextHandoff。
ContextTransfer¶
ContextTransfer 记录源/目标关系以及上下文应如何在它们之间移动。它是用户 fork、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 };
创建转移(transfer)的成本应该很低。昂贵的上下文交接会在目标运行启动、且所选 provider 已知时才被惰性物化。
ContextHandoff¶
ContextHandoff 是在需要桥接可移植 provider 上下文时创建的一等工件。这种情况最常见于在两次运行之间切换 provider 时,但也适用于 provider 恢复失败、必须从应用历史中为新 provider 线程播种的情况。
在更广泛的转移模型中,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;
};
交接不仅仅是提示文本。它是图谱的一部分,可以被检查、重新生成、取代或审计。
首选的返回 provider 策略是 delta_since_target_last_seen:恢复之前的 provider 线程,仅汇总该 provider 不活跃期间发生的运行。当恢复失败、provider 设置不兼容、或累积的交接会导致上下文质量不佳时,使用 full_thread_summary。
ProviderTurn¶
ProviderTurn 是 provider 原生回合的标准化句柄。
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。功能较弱的 provider 可能只有序号。两者都会映射到 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 使用的规范化领域值相同。适配器将 provider 原生的回调名称(如 Codex 的 item/commandExecution/requestApproval 和 Claude 的工具权限名称)映射为这些应用级值。
请求在重启后仍可能保持可见,但仅当它们的 responseCapability 为 live 时才可响应。
消息¶
消息是会话投影的一部分,而非原始 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;
};
Provider 消息块通过 items/content 事件收集,并投影为消息。默认情况下,投影可能隐藏子/子代理(subagent)消息,同时在图谱中保留它们。
计划、问题与待办列表¶
V2 将这些视为结构化回合项或执行节点。
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。
检查点¶
检查点挂接到检查点作用域。根运行检查点是用户可见的会话检查点。子检查点记录子代理、工具或 provider 线程的嵌套文件系统状态,但不会增加父运行的计数。
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;
};
子/子代理 provider 回合可以创建子检查点。除非它作为一等应用运行在分叉/提升的线程中执行,否则不会创建应用运行检查点。
原始 Provider 诊断信息¶
原始 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 事件。生产行为应依赖规范化后的编排事件/实体和提供方引用。原始提供方帧是证据和重放输入,而不是正常应用状态的事实来源。
规范化后的 V2 实体应保留足够的关联元数据,以解释和路由行为:
- 提供方类型。
- 提供方会话/线程/回合引用。
- 提供方项/请求引用(如果可用)。
- 原生 ID 强度。
- 诊断日志位置或原始帧引用(如有用)。
重放框架可以将原始提供方记录作为输入,而无需生产环境 SQLite 存储每一个原始帧。
投影¶
V2 应暴露独立的投影:
- 线程外壳:侧边栏快速列表。
- 线程详情:消息、运行、活动、检查点、计划。
- 执行图:节点和提供方引用的调试/开发者视图。
- 提供方会话:实时运行时/进程状态。
- 待处理请求:可操作的审批/用户输入。
UI 可以保持简洁,同时图保持精确。
投影流应使用现有的应用快照加游标(snapshot-plus-cursor)契约。线程详情订阅应在序列 N 处返回投影快照,然后只流式传输 N 之后的事件。前端不应接收快照中已反映的事件。重新连接时可以从新快照重置,而不是重放本地客户端状态;新快照及其 snapshotSequence 成为新的游标边界。
回合项投影¶
前端不应通过自行合并 messages、plans、检查点、审批和活动来重建显示顺序。V2 为线程详情渲染提供了有序的 turnItems 投影。
turnItems 是投影记录,而不是规范事实来源。规范图仍然在运行、尝试、节点、请求、消息、计划和检查点中保持规范化。在需要关联的地方,提供方原生项引用直接位于节点和回合项上。
分叉线程还暴露派生的 visibleTurnItems 投影。turnItems 仍然是该应用线程上发出的规范项。visibleTurnItems 是用户可见的有序读模型:它可以通过分叉源点引用继承的源项,包含诸如“从对话分叉”之类的合成生命周期标记,然后继续使用本地目标线程项。这既保持了存储的规范化,又为线程详情视图和测试提供了一个由后端持有的断言面。
常见工具应具有结构化的项变体,以便前端无需解析提供方文本即可渲染稳定的自定义组件:
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)、交接(handoff)和分叉(fork)是编排生命周期项,而不是动态工具:
compaction:一个项可以从标题为“正在压缩上下文...”的running状态转换为标题为“已压缩上下文”的completed状态。handoff:记录从一个或多个源提供方线程/提供方到目标提供方线程/提供方的上下文桥接。fork:记录用户或系统从运行、节点或提供方线程创建了新应用线程。
UI 可以使用确定性组件渲染已知变体,并将 dynamic_tool 渲染为可展开的 JSON 输入/输出。每个回合项都保留回 runId、nodeId、providerTurnId 和 nativeItemRef 的引用,以便调试视图可以从显示流跳回图和提供方日志。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 t3/t3code