实体 ID 与关联
原则
提供方(provider)id 是证据。应用 id 是身份。
每个持久实体都以应用自己的 id 作为主键。提供方原生 id 作为可选引用保存,只用于关联、调试、重放和命令(command)路由。
ID 族
ThreadId app-visible conversation thread
RunId counted app turn
NodeId execution graph node
ProviderSessionId live/resumable provider runtime session
ProviderThreadId app handle for provider-native conversation
ProviderTurnId app handle for provider-native turn
RuntimeRequestId app handle for provider callback/request
RawEventId optional raw provider diagnostic frame id
CheckpointId app checkpoint id
PlanId app plan/todo/question artifact id提供方(provider)id 绝不取代这些 id。
提供方引用
提供方引用是运行时实体和事件(event)上的元数据。
type ProviderRefs = {
provider: ProviderKind;
nativeSessionRef?: string;
nativeThreadRef?: string;
nativeTurnRef?: string;
nativeItemRef?: string;
nativeRequestRef?: string;
rawEventId?: RawEventId;
method?: string;
};Codex 可以填上其中大部分字段。其他提供方(provider)可能只填一个子集。
关联范围
关联始终带有范围。没有提供方(provider)、提供方会话(session)和对话线程(thread)的范围时,绝不能对原生 id 做全局匹配。
首选的匹配键:
ProviderThread:
provider + providerSessionId + nativeThreadRef
ProviderTurn:
providerThreadId + nativeTurnRef
or providerThreadId + turnOrdinal
Node / TurnItem:
providerTurnId + nativeItemRef
or providerTurnId + itemOrdinal
RuntimeRequest:
providerSessionId + nativeRequestRef
or providerTurnId + requestOrdinal如果提供方会回收 id,提供方适配器(adapter)必须放宽范围,直到映射不再有歧义。
映射存储
V2 需要一张持久的关联表,或等价的事件溯源绑定流。
type IdentityBinding = {
id: IdentityBindingId;
appEntityKind: "provider_thread" | "provider_turn" | "node" | "item" | "request" | "message";
appEntityId: string;
provider: ProviderKind;
providerSessionId: ProviderSessionId;
nativeKind: string | null;
nativeRef: string | null;
scope: {
threadId: ThreadId | null;
runId: RunId | null;
parentNodeId: NodeId | null;
providerThreadId: ProviderThreadId | null;
providerTurnId: ProviderTurnId | null;
};
correlation: "native_exact" | "native_scoped" | "ordinal" | "fingerprint" | "synthetic";
firstRawEventId?: RawEventId;
lastRawEventId?: RawEventId;
createdAt: string;
updatedAt: string;
};这并不是要做成一套庞大的身份子系统。它只是一个最小的持久位置,用来说明「这个原生事物对应这个应用事物」。原始事件引用是可选的诊断指针;绑定本身不得要求生产环境中的 SQLite 永远保留原始提供方帧。
ID 分配规则
- 如果提供方(provider)给出了稳定的原生 id,就复用已有绑定,或者分配一个新的应用 id 并把它绑定上。
- 如果提供方没有给出稳定 id,就按范围序号分配。
- 如果序号不够,就在该范围之内再加一个指纹。
- 一旦分配,就绝不再更改该实体的应用 id。
- 绝不能靠字符串匹配 id,来推断父级或根级已经完成。
范围序号
弱提供方(provider)需要确定性的序号。
runOrdinalWithinThread
providerTurnOrdinalWithinProviderThread
nodeOrdinalWithinParent
itemOrdinalWithinProviderTurn
requestOrdinalWithinProviderTurn
messageOrdinalWithinNode序号由规范化器在处理提供方帧时分配。对给定的一份提供方记录稿,重放结果必须是确定的。
指纹
指纹只是兜底的关联手段。它们应当包含范围和稳定结构,而不是大段可变文本。
示例:
ProviderTurn fingerprint:
providerThreadId + runId + providerTurnOrdinalWithinProviderThread
Node / TurnItem fingerprint:
providerTurnId + itemKind + itemOrdinalWithinProviderTurn + toolName?
RuntimeRequest fingerprint:
providerTurnId + requestKind + nativeItemRef? + requestOrdinalWithinProviderTurn不要把完整的助手文本用作主指纹。文本是可变的,可能分块流入,也可能重复。
运行时事件
规范化后的运行时事件(event)应当同时带上应用 id 和提供方引用。
type RuntimeEvent = {
id: RuntimeEventId;
type: RuntimeEventType;
threadId: ThreadId;
runId: RunId | null;
nodeId: NodeId | null;
parentNodeId: NodeId | null;
providerSessionId: ProviderSessionId | null;
providerThreadId: ProviderThreadId | null;
providerTurnId: ProviderTurnId | null;
nativeItemRef: string | null;
requestId: RuntimeRequestId | null;
providerRefs: ProviderRefs;
payload: unknown;
createdAt: string;
};下游系统应当使用应用 id。提供方引用保留下来,供检查和适配器(adapter)路由使用。
命令路由
界面和编排命令(command)以应用 id 为目标。
approval.respond(RuntimeRequestId)
interrupt.run(RunId)
interrupt.node(NodeId)
fork.fromNode(NodeId)
rollback.toRun(ThreadId, runOrdinal)提供方命令层在边界把应用 id 解析为提供方引用。如果提供方引用缺失,或已经不再有效,该命令会以带类型的能力/状态错误失败。
示例:
RuntimeRequestId -> nativeRequestRef + providerSessionId
RunId -> root NodeId -> ProviderTurnId -> nativeTurnRef
NodeId -> ProviderThreadId -> nativeThreadRef重启之后的待处理请求
待处理请求的记录可以在重启后保留,但提供方的回调状态通常不会。
V2 应当区分历史上看似仍待处理的请求,和现在仍可响应的请求:
type ResponseCapability =
| { type: "live"; providerSessionId: ProviderSessionId }
| { type: "not_resumable"; reason: string };界面可以表明某个请求已经过期,用户必须重新开始,或重新运行这一回合(turn)。
子智能体关联
子智能体通过父子执行节点来表示。
对于 Codex:
collabAgentToolCall item
-> ExecutionNode(kind="subagent" or "tool_call")
receiverThreadIds[]
-> ProviderThread records
child turn/started and turn/completed
-> child ProviderTurn and child ExecutionNode子提供方回合(turn)保留自己的提供方引用。它通过 parentNodeId 链接到父级,而不是用父回合 id 替换自己的回合 id。
对于弱提供方,适配器(adapter)可以在当前活动的父节点下,按序号创建一个子节点。
重放确定性
给定相同的提供方记录稿和相同的初始应用状态,规范化必须产生相同的 id。
要求:
- 身份绑定作为事件(event)或持久行保存。
- 只要绑定在下游投影(projection)之前已经持久化,生成的 id 就可以是随机的。
- 夹具重放可以使用确定性的 id 生成,以便断言更容易。
- 重新处理同一条提供方帧时,应当找到已有绑定,而不是再分配一个实体。