跳转至

实体 ID 与关联

原则

Provider ID 是证据。App ID 是身份。

每个持久化实体都使用应用自有 ID 作为主键。Provider 原生 ID 存储为可选引用,仅用于关联、调试、重放和命令路由。

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。

Provider 引用

Provider 引用是运行时实体和事件上的元数据。

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

如果 Provider 回收利用 ID,Provider 适配器必须扩大作用域,直到映射不再有歧义。

映射存储

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 永远保留原始 Provider 帧。

ID 分配规则

  1. 如果 Provider 提供稳定的原生 ID,则复用现有绑定,或分配新的应用 ID 并绑定它。
  2. 如果 Provider 未提供稳定 ID,则按作用域序数分配。
  3. 如果序数不足,则在作用域内添加指纹。
  4. 一旦分配,绝不为该实体更改应用 ID。
  5. 切勿通过字符串匹配 ID 来推断父级/根级完成状态。

作用域序数

弱 Provider 需要确定性序数。

runOrdinalWithinThread
providerTurnOrdinalWithinProviderThread
nodeOrdinalWithinParent
itemOrdinalWithinProviderTurn
requestOrdinalWithinProviderTurn
messageOrdinalWithinNode

序数由规范化器在处理 Provider 帧时分配。对于给定的 Provider 记录,它们必须是可重放确定性的。

指纹

指纹只是关联的回退工具。它们应包含作用域和稳定结构,而不是大量可变文本。

示例:

ProviderTurn fingerprint:
  providerThreadId + runId + providerTurnOrdinalWithinProviderThread

Node / TurnItem fingerprint:
  providerTurnId + itemKind + itemOrdinalWithinProviderTurn + toolName?

RuntimeRequest fingerprint:
  providerTurnId + requestKind + nativeItemRef? + requestOrdinalWithinProviderTurn

不要将完整的助手文本用作主指纹。文本是可变的、可能分块流式传输,并且可能重复。

运行时事件

规范化后的运行时事件应同时携带应用 ID 和 Provider 引用。

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。Provider 引用被保留用于检查与适配器路由。

命令路由

UI 和编排命令以应用 ID 为目标。

approval.respond(RuntimeRequestId)
interrupt.run(RunId)
interrupt.node(NodeId)
fork.fromNode(NodeId)
rollback.toRun(ThreadId, runOrdinal)

Provider 命令层在边缘将应用 ID 解析为 Provider 引用。如果 Provider 引用缺失或不再处于活动状态,命令将失败并返回类型化的能力/状态错误。

示例:

RuntimeRequestId -> nativeRequestRef + providerSessionId
RunId -> root NodeId -> ProviderTurnId -> nativeTurnRef
NodeId -> ProviderThreadId -> nativeThreadRef

重启后的待处理请求

待处理请求记录可能在重启后仍然存在,但 Provider 回调状态通常不会。

V2 应区分历史中看似待处理的请求与可响应的请求:

type ResponseCapability =
  | { type: "live"; providerSessionId: ProviderSessionId }
  | { type: "not_resumable"; reason: string };

UI 可以显示请求已过期,并且用户必须重启或重新运行该轮次。

子代理关联

子代理通过父子执行节点来表示。

对于 Codex:

collabAgentToolCall item
  -> ExecutionNode(kind="subagent" or "tool_call")
receiverThreadIds[]
  -> ProviderThread records
child turn/started and turn/completed
  -> child ProviderTurn and child ExecutionNode

子 ProviderTurn 保留自己的 provider refs。它通过 parentNodeId 链接到父节点,而不是用父节点的 turn id 替换自己的 turn id。

对于弱 provider,适配器可以在当前父节点下按 ordinal 创建子节点。

重放确定性

给定相同的 provider transcript 和相同的初始应用状态,规范化必须产生相同的 id。

要求:

  • 身份绑定以事件或持久化行的形式持久保存。
  • 生成的 id 可以是随机的,只要该绑定在后续投影之前已持久化。
  • Fixture 重放可以使用确定性的 id 生成,以便更容易进行断言。
  • 重新处理同一个 provider frame 时应找到现有绑定,而不是分配新的实体。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 t3/t3code