功能生命週期

這是目標架構的設計文件

這一頁描述的是 orchestration 的目標設計,不一定等於目前程式碼的實際行為。原文刻意不討論遷移與向後相容。

這份文件說明使用者會直接用到的核心功能,在 V2 裡應該怎麼運作。這些流程描述的是目標行為,不是目前實作的限制。

建立 Thread#

建立 app thread 和開始 Provider 的工作,是分開的兩件事。

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

Provider thread 可以延後到需要時才建立:

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

如果 Provider 支援建立空的 thread,V2 可以提早建立 Provider thread。如果不支援,Provider thread 會在第一個 run 開始時才建立。

開始一個 run#

使用者送出訊息時:

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

Provider 開始執行時:

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

根節點的 Provider turn 完成時:

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

只有根節點(root node)可以讓 run 完成。

Steer 與排隊訊息#

V2 應該支援三類後續輸入:

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

Steer 是 app 層級的一種意圖:想要改變進行中的 run 所依循的指示。Provider 原生的 steering 只是其中一種實作策略。

如果 Provider 支援對進行中的 run 做 steering,app 就送一個 steer command 給 Provider,並讓同一個 run 繼續進行。

如果 Provider 不支援對進行中的 run 做 steering,app 會中斷進行中的 Provider turn、把被中斷的那次嘗試(attempt)收尾,然後為同一個 app 層級的 steering 意圖啟動一個替代的 Provider turn。這可以用以下兩種方式之一來表示:

  • 沿用同一個 RunId,加上一個新的根嘗試節點(root attempt node):適用於 UI 應該把 steering 視為「修改進行中的 run」的情況;或者
  • 建立一個新的 RunId,用 supersedesRunId 連結起來:適用於「把被中斷的那次嘗試保留為一個算數的 turn」很重要的情況。

建議的預設做法是沿用同一個 RunId 並加上新的根嘗試節點,直到第一次嘗試已經產生了對使用者有意義的最終助理回覆或 checkpoint 為止。

排隊(queueing)則是在進行中的 run 之後建立一個新的 run。

建議的 command 形狀:

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

政策:

  • 如果 Provider 支援對進行中的 run 做 steering,就把 steer 送給 Provider,並把訊息附加到進行中的 run 上。
  • 如果不支援,就中斷進行中的 Provider turn,並把 steering 訊息一併帶入,重新啟動進行中的 run。
  • 如果使用者明確要求開一個新的 turn,即使可以 steering 也改為排隊。
  • 如果沒有進行中的 run,就立刻開始。

這個模型之所以能支援這些做法,是因為 Run 和 ProviderTurn 是分開的。如果第一次嘗試為了 steering 而被中斷,一個 app run 就可能有多次 Provider turn 的嘗試。

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

只有被選定的/最後的那次嘗試會讓 run 完成,並建立這個 run 的 checkpoint。先前因為 steering 而被中斷的嘗試會留在執行圖(execution graph)裡,供稽核與除錯使用。

後續的 turn#

一個 run 完成之後,一般的後續訊息會建立一個新的 run。

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

如果先前的 Provider thread 因為 runtime 錯誤、缺少原生狀態,或 Provider 設定不相容而無法恢復,這個 Provider thread 會被標記為無法使用,app 再用重建出來的 context 建立一個替代的 Provider thread。

這條替代路徑是錯誤復原流程,不是依 Provider 能力而分出來的分支。

在 run 之間更換 Provider#

更換 Provider 是一等公民的 context handoff。它不會 fork app thread,也不會改寫先前的 Provider 歷史。

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

回到某個 Provider 時,預設政策是恢復先前的 Provider thread,並且只補上「離開這個 Provider 期間」的差異(delta)。這樣可以保留該 Provider 對自己先前工作的原生連續性,同時讓它知道別處發生了什麼。

備援政策:遇到以下情況時,建立一個全新的 Provider thread,並附上整個 app thread 的摘要:

  • 先前的 Provider thread 無法恢復,
  • 這個 Provider 的 context 很可能已經太舊,或被反覆的 handoff 弄亂了,
  • 模型/runtime 設定的變更彼此不相容,
  • 使用者明確要求一個乾淨的 Provider context。

這個備援做法在圖裡應該明確呈現:

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

更換 Provider 時,絕對不應該在沒有記錄 handoff 的情況下,把隱藏的摘要串接到任意的 prompt 裡。Handoff 的文字是 run 輸入合約的一部分,必須可以稽核。

Fork Thread#

Fork 是從某個來源點建立一個新的 app thread。它不應該要求先選 Provider,也不應該急著建立 Provider 的 runtime 狀態。

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

Fork 上的第一個 run 會解析這個待處理的 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

建議第一版實作只從穩定的來源點 fork:已完成的 run、checkpoint,或是涵蓋範圍已知的閒置 Provider thread。從進行中的 run fork 時,應該使用最近一個已完成的 checkpoint,或者直接拒絕,直到「進行中 run」的語意經過審慎設計為止。

從 fork 合併回來(merge-back)#

Merge-back 把在 fork 上探索的成果帶回來源 thread,但不會把整份 fork 的對話紀錄複製到主要的 Provider context 裡。

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

這份差異(delta)應該描述 fork 點之後改變了什麼:做了哪些決定、改了哪些檔案、執行了哪些指令/測試、得到什麼結論,以及還沒解決的問題。除非有需要,它不應該包含 Provider 協定的雜訊,也不應該包含完整的對話紀錄內容。

中斷#

中斷的對象是一個 run 或一個節點。

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

App 不應該只因為中斷請求已經回傳,就把 run 標記為已終止。

如果 Provider 不支援中斷:

  • 最好的情況:停止 Provider session,並依政策把 run 標記為 interrupted 或 cancelled。
  • 最壞的情況:標記為不支援中斷,讓 run 維持進行中,直到 Provider 結束為止。

恢復#

「恢復」有三個相關的意思:

  1. 在 UI 裡,從已持久化的 projection 恢復一個 app thread。
  2. 恢復一段 Provider 原生的對話/session。
  3. 復原或重新建立一個活著的 Provider runtime 行程,同時保持 app 和 Provider thread 的連續性。

V2 一律支援從已儲存的 event/projection 恢復 app thread。恢復 Provider thread 則是 Provider adapter 必須具備的基本操作(primitive)。每一個 Provider harness 都必須提供一個 cursor/session/thread 代號(handle),可以用來恢復先前的 Provider 狀態。

Provider runtime 的復原也是核心的基本操作,不是只給測試用的 hook。以下情況都使用同一條復原路徑:

  • 使用者重新啟動 app/server,所有 Provider 行程都不在了,
  • 閒置 session 的回收器(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 runtime 復原流程:

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。測試可以重新建立最外層的 layer/runtime,或是用可決定的(deterministic)時鐘和重播的 Provider transport 去驅動同一個回收器/復原服務,但不可以使用只給測試用的 session 重啟方法。

如果 Provider 恢復失敗:

  • 保留歷史的圖。
  • 把 Provider thread 標記為未載入或錯誤。
  • 使用者繼續對話時,用重建出來的 app context 建立替代的 Provider thread。
  • 把待處理的即時請求標記為無法恢復。

Provider 恢復失敗是一種 runtime/復原狀況。不應該把它建模成「Provider 不支援恢復」。

Fork#

Fork 是從一個穩定的來源,建立一個新的一等公民 AppThread。

支援的 fork 來源:

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

穩定的 fork 邊界:

  • 已完成的 run
  • 被中斷或失敗、但有 Provider snapshot/checkpoint 的 run
  • 已完成、且帶有 Provider thread 的 subagent 節點
  • Provider 回傳的 Provider thread snapshot
  • checkpoint 邊界

不穩定的邊界:

  • 串流中的訊息 delta
  • 進行中的工具呼叫
  • 待處理的核准請求
  • 進行中的 subagent turn,除非明確實作了即時 fork 的支援

從 subagent fork:

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

如果 Provider 無法 fork 原生的 thread,V2 可以用 projection 出來的訊息/context 建立一個新的 app thread,藉此合成出一個 fork,但應該透過能力(capability)把它標記為保真度較低。

Checkpoint 的擷取#

Checkpoint 掛在 checkpoint scope 上。根 run 和子執行節點都可以做 checkpoint。

Run 開始之前的基準(baseline):

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

根 run 結束之後的 checkpoint:

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 的 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

被中斷/失敗的 run:

  • 如果檔案系統的狀態有意義,而且可以安全地擷取,就擷取下來,並讓狀態反映它的終止狀態。
  • 如果不是,就儲存一筆「缺少/錯誤」的 checkpoint 摘要。
  • 允許子節點/subagent 的 checkpoint,但它們不會讓上層 app 的 run 計數往前推進。
  • 根 run 的 checkpoint 應該包含或參照子 checkpoint 的摘要,讓這個 run 有完整的稽核軌跡。

Rollback:

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/rollback 會回傳一份具權威性的 Provider thread snapshot。其他 Provider 可能需要從 context 合成,重新啟動一個 Provider thread。

巢狀的 rollback:

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

如果子 checkpoint 改動的是和上層相同的 workspace,還原它可能會弄髒上層 run 的 workspace 狀態。UI/API 應該把這一點明確呈現出來。

核准請求#

核准請求是以執行節點為範圍的 runtime request。

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 的 request id 只是參照(ref)。
  • 來自 subagent 的待處理核准請求,只要 Provider 提供即時回應的能力,就仍然可以處理。
  • 如果 Provider 的 callback 狀態已經不在了,回應核准請求時應該以 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 不支援結構化的提問,adapter 可以只投影出一則一般的助理訊息,而不產生可以回應的請求。

計畫與待辦清單#

V2 把三個概念分開:

  • 提議的計畫(proposed plan):一份持久的計畫 artifact,使用者可以接受/付諸實作。
  • 待辦清單(todo list):目前這個 run 的即時執行進度。
  • 提問(ask questions):結構化的使用者輸入請求。

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)

規則:

  • 根 run 的待辦清單會更新主要的計畫/進度 UI。
  • 子節點/subagent 的待辦清單巢狀掛在它們自己的執行節點底下。
  • 子節點的計畫不會取代根計畫,除非被提升(promote)或 fork 出去。
  • 實作提議的計畫時,會依使用者的操作建立一個新的 run 或一個新的 thread。

Activity#

Activity 是執行圖 event 的一種 projection。

例子:

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

Activity 應該保留 nodeId 和 runId,讓 UI 可以正確地把它們分組。

完成前的關卡#

在 run 被標記為已終止之前,V2 應該先通過一道收尾關卡(finalization barrier):

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,也避免計畫狀態不完整。

本頁譯自 docs/orchestration-v2/feature-lifecycles.md(英文原文,版本 164455b)。標示「本站補充」的區塊不在原文裡。

非官方翻譯,與 T3 Code 的維護者無關。內容由 AI 翻譯並補充,未經逐頁人工校對,可能有錯誤或已經過時,請以英文原文為準。

原文 © T3 Tools Inc.,以 MIT 授權釋出;本站為其官方 repository 中 docs/ 的翻譯。本站原始碼與問題回報