跳转至

功能生命周期

本文档描述 V2 中核心用户功能应如何运作。这些流程描述的是目标行为,而非当前实现的约束。

创建线程

创建应用线程与启动提供方工作是分开的。

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

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

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

如果提供方支持创建空线程,V2 可能会提前创建提供方线程。如果不支持,提供方线程将在首次运行启动时创建。

启动一次运行

当用户发送消息时:

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

当提供方开始执行时:

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

当根提供方轮次完成时:

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

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

引导与排队消息

V2 应支持三类后续输入:

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

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

如果提供方支持当前运行引导,应用会向提供方发送引导命令,并保持同一运行处于活动状态。

如果提供方不支持当前运行引导,应用会中断当前活动的提供方轮次,结束被中断的尝试,并针对同一应用层引导意图启动新的提供方轮次。这可以表示为以下两种方式之一:

  • 相同的 RunId 加上新的根尝试节点,如果 UI 应将引导视为对当前运行的修订;或
  • 通过 supersedesRunId 关联新的 RunId,如果将被中断的尝试保留为已计数的轮次很重要。

优先默认使用相同的 RunId 并附带新的根尝试节点,直到首次尝试产生了对用户有意义的最终助手响应或检查点。

排队会在当前运行之后创建新的运行。

推荐命令结构:

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

策略:

  • 如果提供方支持当前运行引导,则向提供方发送引导命令,并将消息附加到当前运行。
  • 如果不支持,则中断当前活动的提供方轮次,并携带引导消息重启当前运行。
  • 如果用户明确请求新的轮次,即使支持引导也执行排队。
  • 如果没有活动运行,则立即开始。

模型之所以支持这一点,是因为 Run 和 ProviderTurn 是分离的。当首次尝试因引导而中断时,单个应用运行可能包含多次提供方轮次尝试。

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";
};

只有被选中/最终的尝试才能完成运行并创建运行检查点。较早被中断的引导尝试会保留在执行图中,用于审计/调试。

后续轮次

在运行完成后的正常后续会创建新的运行。

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

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

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

在运行之间切换提供方

切换提供方是一等公民级别的上下文交接。它不会分叉应用线程,也不会重写之前的提供方历史。

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

示例:

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

返回某提供方时的默认策略是恢复之前的提供方线程,并仅桥接在该提供方之外发生的增量内容。这既保留了该提供方自身早期工作的原生连续性,又让它了解到其他地方发生的情况。

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

  • 之前的提供方线程无法恢复,
  • 提供方的上下文可能过于陈旧或因反复交接而受到污染,
  • 模型/运行时设置发生了不兼容的更改,
  • 用户明确要求干净的提供方上下文。

回退应在图中明确表示:

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

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

分叉线程

分叉会从某个源点创建一个新的应用线程。它不应要求选择 provider,也不应急切地创建 provider 运行时状态。

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

分叉后的首次运行会解析待处理的 transfer:

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

首选的初次实现应只从稳定的源点进行分叉:已完成的运行、检查点,或具有已知覆盖范围的空闲 provider 线程。从活动运行分叉应使用最新已完成的检查点,或在有意设计活动运行语义之前予以拒绝。

从分叉合并回

合并回(Merge-Back)会将分叉探索带入源线程,而无需将整个分叉转录复制到主 provider 上下文中。

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

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

中断

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

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

终止状态来自 provider 生命周期:

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

应用不应仅因中断请求已返回就将运行标记为终止状态。

如果 provider 不支持中断:

  • 最佳情况:停止 provider 会话,并按策略将运行标记为已中断或已取消。
  • 最坏情况:将中断标记为不受支持,并让运行保持活动状态,直到 provider 退出。

停止按钮发送带有 holdQueue 的 run.interrupt。一次 Stop 还会保持队列挂起,结束该线程的拉取请求监听,并丢弃其委托任务仍然欠下的唤醒通知,因此不会有任何自动机制重启该线程。然后,它会停止该线程下的每个委托任务:一个 delegated-tasks.stop effect 会向每个子线程发送内部 thread.stop 命令,按深度优先顺序执行。这会在提交之后运行,而不是在父命令内部运行,因为一条命令只持有其自身线程的锁。task_cancel 会向其子线程发送相同的 thread.stop。

恢复

恢复有三个相关含义:

  1. 从持久化的投影(projection)在 UI 中恢复应用线程。
  2. 恢复 provider 原生的对话/会话。
  3. 在保持应用和 provider 线程连续性的同时,恢复或重新创建一个活动的 provider 运行时进程。

V2 始终支持从存储的事件/投影恢复应用线程。Provider 线程恢复是 provider 适配器的一个必需原语。每个 provider harness 都必须暴露一个可用于恢复先前 provider 状态的游标/会话/线程句柄。

Provider 运行时恢复也是一个核心原语,而不是仅限测试的钩子。在以下情况下会使用同一条恢复路径:

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

Provider 恢复流程:

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

Provider 运行时恢复流程:

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。测试可以重新创建最外层/运行时,或使用确定性时钟和回放的 provider 传输来驱动同一个 reaper/recovery 服务,但绝不能使用仅限测试的会话重启方法。

如果 provider 恢复失败:

  • 保留历史图。
  • 将 provider 线程标记为未加载或错误。
  • 当用户继续时,从重建的应用上下文创建替代 provider 线程。
  • 将待处理的活动请求标记为不可恢复。

Provider 恢复失败是一种运行时/恢复条件。不应将其建模为“provider 不支持恢复”。

分叉

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

支持的分叉源:

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

稳定的分叉边界:

  • 已完成的运行
  • 带有 provider 快照/检查点的已中断或失败运行
  • 带有 provider 线程的已完成的子代理节点
  • provider 返回的 provider 线程快照
  • 检查点边界

不稳定的边界:

  • 流式消息增量
  • 活动工具调用
  • 待处理的审批
  • 活动子代理轮次,除非明确实现了实时分叉支持

从子代理分叉:

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

如果 provider 无法分叉原生线程,V2 可以通过创建带有投射消息/上下文的新 app 线程来合成一次分叉,但这应通过 capability 标记为较低保真度。

检查点

检查点附加到检查点作用域。根运行和子执行节点都可以被检查点化。

运行前基线:

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

根运行后检查点:

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

子/子代理检查点:

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

中断/失败的运行:

  • 如果文件系统状态有意义且可安全捕获,则捕获并使其状态反映终止状态。
  • 否则,存储缺失/错误的检查点摘要。
  • 允许子代理检查点,但它们不会推进父 app 运行的次数。
  • 根运行检查点应包含或引用子检查点摘要,以便运行具有完整的审计跟踪。

回滚:

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 返回更新后的已加载线程元数据,其中 turns 数组为空;保留的历史必须通过 thread/turns/list 进行回填。其他 provider 可能需要从上下文重新启动合成 provider 线程。

嵌套回滚:

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

如果子检查点变更了与父级相同的工作区,恢复它可能会污染父运行的工作区状态。UI/API 应明确说明这一点。

审批

审批是作用于执行节点的运行时请求。

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

用户响应:

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

规则:

  • 审批 ID 由 app 拥有。
  • provider 请求 ID 是引用(refs)。
  • 如果 provider 暴露实时响应能力,来自子代理的待处理审批仍然可操作。
  • 如果 provider 回调状态已消失,审批响应应失败并返回 not_resumable。

提问 / 用户输入

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

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

回答时:

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

如果 provider 不支持结构化问题,适配器可以投射一条普通的 assistant 消息,并且不产生可响应的请求。

计划与待办列表

V2 区分三个概念:

  • 提议计划(proposed plan):用户可接受/实施的持久化计划工件。
  • 待办列表(todo list):当前运行的实时执行进度。
  • 提问:结构化的用户输入请求。

Codex 示例:

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)

规则:

  • 根运行的待办列表更新主要的计划/进度 UI。
  • 子/子代理待办列表嵌套在其执行节点下。
  • 子计划不会取代根计划,除非被提升或分叉。
  • 实施提议计划时,会根据用户操作创建新的运行或新的线程。

活动

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

示例:

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

活动应保留 nodeId 和 runId,以便 UI 正确地对它们进行分组。

完成屏障

在运行被标记为终止之前,V2 应通过一个完成屏障:

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

这可以防止过早捕获检查点以及不完整的计划状态。

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