T3 Code 非官方中文翻译不是官方产品,也不是 T3 Tools 官方文档。源提交 d2c25950b4d9英文原文GitHub 源文件
Skip to content

功能生命周期 ​

本文说明 V2 中面向用户的核心功能应当如何工作。这些流程描述的是目标行为,而不是当前实现的约束。

创建对话线程 ​

创建应用对话线程(thread)与启动提供方(provider)工作是分开的。

text
thread.create
  -> AppThread created
  -> no provider session required yet
  -> no provider thread required yet unless eager provider creation is enabled

提供方对话线程的创建可以是惰性的:

text
first run starts
  -> ensure ProviderSession
  -> ensure ProviderThread
  -> send provider turn

如果提供方支持创建空对话线程,V2 可以提前创建提供方对话线程。否则,提供方对话线程会在首次运行开始时创建。

启动一次运行 ​

用户发送消息时:

text
thread.message.send
  -> ConversationMessage(role=user)
  -> Run(status=queued, ordinal=N)
  -> root ExecutionNode(kind=root_turn, countsForRun=true)
  -> enqueue provider command

提供方(provider)执行开始时:

text
provider turn accepted/started
  -> ProviderTurn allocated or correlated
  -> root node status=running
  -> run status=running

根提供方回合(turn)完成时:

text
root provider turn/completed
  -> finalize assistant streams
  -> finalize plans/todos/questions
  -> root node terminal
  -> capture checkpoint if applicable
  -> run terminal

只有根节点才能完成该运行。

引导与消息排队 ​

V2 应支持三类后续输入:

text
steer active run in place
steer active run by restart
queue next run

引导(steering)是应用层意图,用于改变活动运行的指令。提供方(provider)原生的引导只是其中一种实现策略。

如果提供方支持对活动运行进行引导,应用会向提供方发送引导命令(command),并让同一次运行保持活动。

如果提供方不支持对活动运行进行引导,应用会中断活动的提供方回合(turn),完结被中断的尝试,并为同一个应用层引导意图启动一个替代的提供方回合。这可以表示为以下两种形式之一:

  • 同一个 RunId 配上新的根尝试节点,如果界面应把引导视为对活动运行的修订;或者
  • 一个新的 RunId,由 supersedesRunId 关联,如果需要把被中断的尝试保留为一次计入的回合。

在首次尝试产生对用户有意义的终态助手回复或检查点(checkpoint)之前,首选默认是同一个 RunId 配上新的根尝试节点。

排队会在活动运行之后创建一次新的运行。

建议的命令形式:

ts
type MessageDispatchMode =
  | { type: "steer_active"; targetRunId: RunId }
  | { type: "restart_active"; targetRunId: RunId }
  | { type: "queue_after_active" }
  | { type: "start_immediately" };

策略:

  • 如果提供方支持对活动运行进行引导,就向提供方发送引导,并把该消息附到活动运行上。
  • 否则,中断活动的提供方回合,并在纳入引导消息后重新启动活动运行。
  • 如果用户明确要求一个新回合,即使引导可用,也要排队。
  • 如果没有活动运行,就立即开始。

该模型支持这种做法,是因为 Run 与 ProviderTurn 是分开的。当首次尝试因引导而被中断时,单次应用运行可以有多次提供方回合尝试。

ts
type RunAttempt = {
  id: RunAttemptId;
  runId: RunId;
  attemptOrdinal: number;
  rootNodeId: NodeId;
  providerThreadId: ProviderThreadId;
  providerTurnId: ProviderTurnId | null;
  reason: "initial" | "steering_restart" | "retry" | "provider_recovery";
  status: "running" | "completed" | "interrupted" | "failed" | "cancelled";
};

只有被选中的最终尝试才会完成该运行并创建运行检查点。更早被中断的引导尝试仍保留在执行图中,供审计和调试使用。

后续回合 ​

在已完成的运行之后,普通的后续回合会创建一次新运行。

text
previous Run completed
user sends follow-up
  -> new Run ordinal=N+1
  -> new root node
  -> same AppThread
  -> same ProviderThread if resumable

如果先前的提供方对话线程(thread)因运行时错误、缺少原生状态或提供方(provider)设置不兼容而无法恢复,该提供方对话线程会被标记为不可用,应用会根据重建的上下文创建一条替代的提供方对话线程。

替代路径是错误恢复流程,而不是提供方能力分支。

在运行之间更换提供方 ​

更换提供方(provider)是一种一等上下文交接。它不会分叉应用对话线程(thread),也不会改写先前的提供方历史。

text
user selects a different provider for next run
  -> create Run with target provider
  -> resolve or create target ProviderThread
  -> create ContextHandoff if target provider thread lacks current app context
  -> inject handoff summary into provider context
  -> send user message as the new root turn

示例:

text
runs 1-5: Codex provider thread C1
switch to Claude
  -> summarize runs 1-5 for Claude
  -> create Claude provider thread L1
runs 6-8: Claude provider thread L1
switch back to Codex
  -> resume Codex provider thread C1 if possible
  -> summarize runs 6-8 for Codex
  -> send run 9 to C1 with the delta handoff

回到某个提供方时,默认策略是恢复先前的提供方对话线程,并且只桥接离开该提供方期间的增量。这样既保留该提供方自身先前工作的原生连续性,也让它了解别处发生了什么。

回退策略:在以下情况下,创建一条带有完整应用对话线程摘要的新提供方对话线程:

  • 先前的提供方对话线程无法恢复,
  • 该提供方的上下文很可能已经过时,或因反复交接而受到污染,
  • 模型或运行时设置发生了不兼容的变更,
  • 用户明确要求一个干净的提供方上下文。

回退应在图中显式体现:

text
new ProviderThread
  forked/reconstructed from AppThread summary
  ContextHandoff(strategy=full_thread_summary)

切换提供方时,绝不能在不记录交接的情况下,把隐藏摘要拼进任意提示词。交接文本是运行输入契约的一部分,必须可审计。

分叉对话线程 ​

分叉会从某个源点创建一条新的应用对话线程(thread)。它不应要求选择提供方(provider),也不应预先创建提供方运行时状态。

text
thread.fork
  -> create target AppThread
  -> record AppThread lineage
  -> create ContextTransfer(type=fork, status=pending)
  -> no ProviderSession required
  -> no ProviderThread required
  -> no portable context handoff required

分叉上的首次运行会解决待处理的转移:

text
first message on fork selects provider P
  -> if source provider is P and native fork is available:
       resolve by provider-native fork
     else:
       materialize portable context
  -> create/resume target ProviderThread
  -> send resolved context + user message

首选的初始实现应只从稳定的源点分叉:已完成的运行、检查点(checkpoint),或覆盖范围已知的空闲提供方对话线程。从活动运行分叉时,应使用最近一次已完成的检查点,或者在活动运行的语义被有意设计之前予以拒绝。

从分叉合并回来 ​

合并回来会把分叉中的探索带回源对话线程(thread),同时不会把整份分叉对话记录复制进主提供方(provider)上下文。

text
source thread forked at point S
fork thread explores through point F
user sends next source-thread message with merge-back intent
  -> create ContextTransfer(type=merge_back, basePoint=S, sourcePoint=F)
  -> build delta context lazily
  -> inject delta with the next source-thread user message

增量应描述自分叉点以来发生的变化:决策、变更的文件、运行过的命令与测试、结论,以及未解决的问题。除非必要,否则不应包含提供方协议噪声或完整的对话记录内容。

中断 ​

中断针对一次运行或一个节点。

text
interrupt.run(RunId)
  -> resolve root node
  -> resolve provider turn/thread refs
  -> provider interrupt command if supported
  -> mark interrupt requested

终态来自提供方(provider)生命周期:

text
provider interrupt request returns
  -> command acknowledged only
provider emits root turn/completed status=interrupted
  -> root node interrupted
  -> run interrupted
  -> checkpoint policy runs

应用不应仅仅因为中断请求已经返回,就把该运行标记为终态。

如果提供方不支持中断:

  • 最好的情况:停止提供方会话(session),并按策略把运行标记为已中断或已取消。
  • 最坏的情况:把中断标记为不受支持,并让运行保持活动,直到提供方退出。

「停止」按钮发送 run.interrupt,并附带 holdQueue。一次停止还会挂起队列,结束该对话线程(thread)的拉取请求(pull request)监视,并丢弃其委托任务仍然欠下的唤醒,因此不会有任何自动机制重新启动该对话线程。接着,它会停止该对话线程下的每一个委托任务:一个 delegated-tasks.stop 效应按深度优先,向每个子对话线程发送内部的 thread.stop 命令(command)。这一步在提交之后执行,而不是在父级命令内部执行,因为一条命令只持有自己对话线程的锁。task_cancel 会向其子级发送同一个 thread.stop。

恢复 ​

恢复有三个相关含义:

  1. 在界面中根据已持久化的投影恢复应用对话线程(thread)。
  2. 恢复提供方(provider)原生的对话或提供方会话(session)。
  3. 在保持应用与提供方对话线程连续性的同时,恢复或重建活动的提供方运行时进程。

V2 始终支持根据已存储的事件(event)与投影恢复应用对话线程。提供方对话线程的恢复是提供方适配器(adapter)必须提供的原语。每个提供方的智能体驾驭层(agent harness)都必须暴露一个游标、提供方会话或对话线程句柄,用来恢复先前的提供方状态。

提供方运行时恢复同样是核心原语,而不是仅供测试的钩子。以下情况使用同一条恢复路径:

  • 用户重启应用或服务器后,所有提供方进程都已消失,
  • 空闲提供方会话回收器有意释放提供方进程,
  • 提供方进程崩溃或失去网络,且策略允许重试。
text
open app thread
  -> load AppThread, Runs, Messages, ExecutionGraph summary
  -> if provider work must continue, resume the relevant ProviderThread

提供方恢复流程:

text
resolve activeProviderThreadId
resolve nativeThreadRef / nativeConversationHeadRef resume cursor
start ProviderSession
provider resume
bind new ProviderSessionId to ProviderThread

提供方运行时恢复流程:

text
provider runtime unavailable
  -> mark ProviderSession stopped/error with reason
  -> remove live runtime handle from ProviderSessionManager
  -> keep ProviderThread and nativeThreadRef durable
  -> on next work, open a new provider runtime
  -> resume the active ProviderThread before starting/retrying work

空闲清理与崩溃恢复必须使用生产环境的生命周期 API。测试可以重建最外层或运行时,也可以用确定性时钟和重放的提供方传输来驱动同一个回收器与恢复服务,但不得使用仅供测试的提供方会话重启方法。

如果提供方恢复失败:

  • 保留历史图。
  • 把提供方对话线程标记为未加载或错误。
  • 当用户继续时,根据重建的应用上下文创建替代的提供方对话线程。
  • 把待处理的实时请求标记为不可恢复。

提供方恢复失败是运行时或恢复方面的状况。不应把它建模为「提供方不支持恢复」。

分叉 ​

分叉会从稳定源创建一个新的一等 AppThread。

支持的分叉来源:

ts
type ForkSource =
  | { type: "run"; threadId: ThreadId; runId: RunId }
  | { type: "node"; nodeId: NodeId }
  | {
      type: "provider_thread";
      providerThreadId: ProviderThreadId;
      providerTurnId?: ProviderTurnId;
    };

稳定的分叉边界:

  • 已完成的运行
  • 带有提供方(provider)快照或检查点(checkpoint)的已中断或失败运行
  • 带有提供方对话线程(thread)的已完成子智能体(subagent)节点
  • 提供方返回的提供方对话线程快照
  • 检查点边界

不稳定的边界:

  • 流式消息增量
  • 活动中的工具调用
  • 待处理的审批
  • 活动中的子智能体回合(turn),除非明确实现了实时分叉支持

从子智能体分叉:

text
subagent node selected
  -> resolve ProviderThread owned by node
  -> provider fork/resume if supported
  -> create new AppThread
  -> attach forked ProviderThread
  -> copy/link checkpoint baseline

如果提供方无法分叉原生对话线程,V2 可以通过创建一条带有投影消息与上下文的新应用对话线程来合成一次分叉,但应按能力将其标记为较低保真度。

建立检查点 ​

检查点(checkpoint)附着在检查点作用域上。根运行与子执行节点都可以建立检查点。

运行前基线:

text
before root execution starts
  -> ensure root checkpoint for current app run ordinal - 1

根运行结束后的检查点:

text
root node terminal
  -> drain/finalize streams
  -> capture root checkpoint for app run ordinal
  -> compute diff from prior root checkpoint
  -> mark run terminal with checkpoint

子级或子智能体(subagent)检查点:

text
child checkpointable node starts
  -> create child CheckpointScope under parent scope
child node terminal
  -> drain/finalize child streams/items
  -> capture child checkpoint for that scope
  -> compute diff from parent/previous child checkpoint
  -> mark child node checkpointed

已中断或失败的运行:

  • 如果文件系统状态有意义且可以安全捕获,则进行捕获,并让状态反映终态。
  • 否则,存储一份缺失或错误的检查点摘要。
  • 允许子级或子智能体检查点,但它们不会推进父级应用运行的计数。
  • 为根运行建立检查点时,应包含或引用子检查点摘要,使该运行拥有完整的审计轨迹。

回滚:

text
rollback.toRun(threadId, targetRunOrdinal)
  -> restore filesystem checkpoint targetRunOrdinal
  -> compute provider turns to roll back if provider supports rollback
  -> call provider rollback
  -> reconcile returned provider snapshot if available
  -> mark later runs rolled_back
  -> delete or mark stale later checkpoint refs

Codex 的 thread/revert 会返回更新后的已加载对话线程(thread)元数据,其中 turns 数组为空;保留的历史必须通过 thread/turns/list 补全。其他提供方(provider)可能需要根据上下文,对提供方对话线程做一次合成重启。

嵌套回滚:

text
rollback.node(nodeId, checkpointId)
  -> restore child checkpoint if scope cwd/ref is available
  -> roll back provider thread for that child scope if supported
  -> mark descendant child checkpoints stale/rolled_back
  -> preserve parent run checkpoint history unless parent filesystem state changed

如果子检查点改动的工作区与父级相同,恢复它可能会让父级运行的工作区状态变脏。界面与 API 应明确标出这一点。

审批 ​

审批是作用域限定在执行节点上的运行时请求。

text
provider requestApproval
  -> RuntimeRequest(kind=...)
  -> approval ExecutionNode
  -> thread pending request projection
  -> optional thread status waiting

用户响应:

text
approval.respond(RuntimeRequestId)
  -> resolve live provider callback
  -> send provider response
  -> RuntimeRequest resolved
  -> approval node completed/cancelled

规则:

  • 审批标识归应用所有。
  • 提供方(provider)请求标识是引用。
  • 如果提供方暴露了实时响应能力,来自子智能体(subagent)的待处理审批仍然可以处理。
  • 如果提供方回调状态已经消失,审批响应应以 not_resumable 失败。

提问 / 用户输入 ​

提问是一种用户输入请求,其本身并不是计划。

text
provider requests user input
  -> RuntimeRequest(kind=user_input)
  -> ExecutionNode(kind=user_input_request)
  -> Question artifact
  -> run status=waiting

回答时:

text
user-input.respond(RuntimeRequestId, answers)
  -> provider callback
  -> request resolved
  -> node completed
  -> run resumes/runs

如果提供方(provider)不支持结构化提问,适配器(adapter)可以投影出一条普通的助手消息,而不产生可响应的请求。

计划与待办列表 ​

V2 区分三个概念:

  • 提议的计划:一份持久的计划制品,用户可以接受或实施。
  • 待办列表:当前运行的实时执行进度。
  • 提问:结构化的用户输入请求。

Codex 示例:

text
turn/plan/updated
  -> PlanArtifact(kind=todo_list)
  -> activity/projection update

item/plan/delta or plan item completed
  -> PlanArtifact(kind=proposed_plan)

item/tool/requestUserInput
  -> PlanArtifact(kind=questions)
  -> RuntimeRequest(kind=user_input)

规则:

  • 根运行的待办列表会更新主计划与进度界面。
  • 子级或子智能体(subagent)的待办列表嵌套在各自的执行节点之下。
  • 除非被提升或分叉,子计划不会取代根计划。
  • 实施提议的计划时,会根据用户的操作创建一次新运行或一条新的对话线程(thread)。

活动 ​

活动是执行图事件(event)的投影。

示例:

text
tool.started
approval.requested
approval.resolved
plan.updated
checkpoint.captured
subagent.started
subagent.completed
runtime.warning

活动应保留 nodeId 与 runId,以便界面能正确分组。

完成屏障 ​

在一次运行被标记为终态之前,V2 应通过一道收尾屏障:

text
root provider turn terminal
  -> flush assistant text buffers
  -> close active assistant messages
  -> finalize plan/todo artifacts
  -> close open non-live child nodes if provider marked them done
  -> capture checkpoint
  -> publish run terminal

这样可以防止过早捕获检查点(checkpoint),以及计划状态不完整。

非官方译文 · 源提交 d2c25950b4d9 · 不代表 T3 Tools