功能生命周期¶
本文档描述 V2 中核心用户功能应如何运作。这些流程描述的是目标行为,而非当前实现的约束。
创建线程¶
创建应用线程与启动提供方工作是分开的。
thread.create
-> AppThread created
-> no provider session required yet
-> no provider thread required yet unless eager provider creation is enabled
提供方线程的创建可以是惰性的:
如果提供方支持创建空线程,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 应支持三类后续输入:
引导是一种应用层意图,用于改变当前运行的指令。提供方原生的引导只是其中一种实现策略。
如果提供方支持当前运行引导,应用会向提供方发送引导命令,并保持同一运行处于活动状态。
如果提供方不支持当前运行引导,应用会中断当前活动的提供方轮次,结束被中断的尝试,并针对同一应用层引导意图启动新的提供方轮次。这可以表示为以下两种方式之一:
- 相同的
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。
恢复¶
恢复有三个相关含义:
- 从持久化的投影(projection)在 UI 中恢复应用线程。
- 恢复 provider 原生的对话/会话。
- 在保持应用和 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 标记为较低保真度。
检查点¶
检查点附加到检查点作用域。根运行和子执行节点都可以被检查点化。
运行前基线:
根运行后检查点:
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