按下取消之後,各層怎麼收手
工具失敗回給模型,用戶按 Esc 才停 turn。取消令牌從任務傳到採樣再傳到工具。已完成的結果留在歷史裏,100 毫秒之後的硬拆不可逆。
- 協議入口是 Interrupt,不殺後台 terminalprotocol.rs L546
- 任務令牌先 cancel,這是信號還不是硬拆tasks/mod.rs L887
- 採樣用子令牌盯着流,or_cancel 變成 TurnAbortedturn.rs L2273
- 工具派發再 child 一次,父令牌取消則一起取消stream_events_utils.rs L319
- handler 已走完就保留真實結果,沒走完才 abortparallel.rs L182
- 等 100 毫秒,沒收完就 task.handle.aborttasks/mod.rs L913
- 追加 turn_aborted 片段,立刻 flush_rollouttasks/mod.rs L927
- 最後才發 EventMsg::TurnAbortedtasks/mod.rs L955
task.handle.abort 沒有回切。已經完成的工具回執也不會被改寫成 aborted。後台 terminal 繼續跑,要殺它走另一條操作。你讓 Codex 改一個測試檔案。模型先跑 cargo test,編譯器吐了兩屏 rustc 報錯,退出碼是 1。下一秒對話停了,介面彈出 turn aborted。很多人第一次寫 agent 會這麼幹:工具返回 Err,整輪跟着死。模型還沒看見 stderr,會話已經結束。
分診發生在工具回到對話的那道門上,一共三層門檻。
最淺一層:進程已經跑過。退出碼非零、命令超時、沙箱拒絕,都收成工具回執。success 寫成 true,意思是 handler 跑完了,回執可以餵給模型。命令成不成功寫在正文裏。
出處:codex-rs/core/src/tools/context.rs 第 344 至 353 行
中間一層:調用沒做成。參數壞了、進程沒拉起來、apply_patch 上下文對不上,走 RespondToModel。回執 success 才是 false。模型讀到文案,自己改再試。
最深一層:payload 對不上、任務 join 失敗,才寫 Fatal,升成 CodexErr,停 turn。
工具層自己的枚舉只有兩檔。RespondToModel 把字串餵回模型。Fatal 才升成引擎錯誤。預設把非 Fatal 折成 Ok。想停對話,得寫出 Fatal 或 CodexErr。
use thiserror::Error;
/// Error returned while executing a model-visible tool invocation.
#[derive(Debug, Error, PartialEq)]
pub enum FunctionCallError {
#[error("{0}")]
RespondToModel(String),
#[error("Fatal error: {0}")]
Fatal(String),
}
openai/codex,核對文件 codex-rs/tools/src/function_call_error.rs,commit 4f39251a01,核對日期 2026-08-22。這段枚舉本身就是分診合同:兩檔,沒有第三檔警告或重試。cargo test 退出碼 1 停在最淺一層,連錯誤枚舉的門檻都沒邁過。沙箱拒絕同樣走 Ok。代理攔請求時給命令進程回 HTTP 403,也停在最淺一層。模型 API 的 403 才是引擎錯誤。兩處 403 不要併成一檔。
出處:codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs 第 384 至 411 行;codex-rs/network-proxy/src/responses.rs 第 76 至 83 行
handler 裏一個 IO 錯誤如果直接冒到 turn 循環,整輪對話跟着死。預設回灌,想停必須顯式寫出。這條綫換語言重寫也用得上。自己做內部 agent,先抄這一檔:工具失敗回灌,引擎失敗停循環。
用戶按 Esc,要停的不只是當前那次採樣。採樣可能還在讀流,工具可能正在寫檔案,後台 terminal 可能已經拉起來了。只殺採樣,工具會繼續改磁盤。一刀切殺掉所有進程,unified exec 的後台任務也會被誤殺。
任務啓動時現造一張取消令牌。採樣請求用 child_token(),工具派發再 child 一次。父令牌一取消,子令牌一起取消。子令牌自己取消,不影響父令牌。這就是取消樹。
出處:codex-rs/core/src/session/turn.rs 第 1399 行;codex-rs/core/src/stream_events_utils.rs 第 319 至 324 行
or_cancel 把誰先到寫成固定形狀。任意 future 配一張令牌。取消先到,返回 CancelErr,一律變成 TurnAborted。
出處:codex-rs/async-utils/src/lib.rs 第 4 至 31 行;codex-rs/protocol/src/error.rs 第 269 至 273 行
用戶按 Esc,協議入口是 Op::Interrupt。合同寫死:中止當前任務,不殺後台 terminal。handle_task_abort 先 cancel(),再等 100 毫秒。超時就 task.handle.abort()。令牌先發信號,任務有機會自己收尾。收不完,再硬拆。硬拆這一步不可逆。
出處:codex-rs/protocol/src/protocol.rs 第 546 至 548 行;codex-rs/core/src/tasks/mod.rs 第 66 行、第 880 至 913 行
一份用戶意圖,多層各自收尾。合作式取消加硬期限,是併發系統的通用形狀。Python 裏用 asyncio.Event 就能做最小版。不必抄 37 個變體的 CodexErr。
取消發生在工具已經改了檔案之後,歷史裏怎麼記。如果把已經完成的輸出改寫成 aborted by user,模型下一輪會以為寫入沒做成,可能再打一遍補丁。
工具 future 同時等派發結果和令牌。令牌先到,還要看 handler 是否已經走到終態。已經走完,就保留真實結果。沒走完,才 abort,再造一條 AbortedToolOutput,正文是 aborted by user after Xs。
出處:codex-rs/core/src/tools/parallel.rs 第 177 至 206 行、第 243 至 260 行
然後追加一段模型可見標記,包在 <turn_aborted> 裏。文案承認兩件事:unified exec 可能還在後台跑,被中止的工具可能已經執行了一部分。標記寫進歷史之後立刻 flush_rollout()。有的客戶端收到 TurnAborted 會同步重讀 rollout,標記必須先落盤。
出處:codex-rs/core/src/context/turn_aborted.rs 第 1 至 35 行;codex-rs/core/src/tasks/mod.rs 第 920 至 962 行
歷史只追加、不改寫。中止只往後面加片段,已落盤的工具輸出不動。下一輪模型能同時看見完成的輸出、被中止工具的回執、以及中止標記。上下文治理的第一條就是這個形狀。
出處:AGENTS.md 第 91 至 100 行
DSH:throw 折成 isError,循環 throw 才停 turn
DSH 每輪新建一個 AbortController。工具 body 的 throw 不會穿過循環。執行器把它收成 isError: true 回執,輪次繼續。bash 的註釋寫成產品合同:Non-zero exits are reported, not errored。循環自己的失敗才停 turn。用戶取消寫成 turn/end aborted。
DSH 省掉兩檔枚舉,因為執行器 catch 已經分了模型能看見和引擎必須終止。代價是約定:漏過 dispatchToolBody 的 throw 仍會變成 turn/end error。Codex 用類型把這條路收窄。
Grok:整份 ToolError 回模型,引擎停靠 SamplingError
Grok 的 ToolError 模組頭把 detail 寫成必須回給模型的説明。Cancelled、Timeout、Execution 都是同一種回灌。工具層沒有 Fatal 檔。會話層把執行失敗收成 tool_result,turn 繼續。
引擎停靠另一套類型。SamplingError 不可重試才停採樣。Actor 上的 CancellationToken 觸發是關機,單次工具取消走 CancelRegistry。令牌不承擔 Codex 那種工具分診。
Esc 落在兩種時刻,歷史裏各留下什麼
apply_patch 已經寫入檔案,成功回執還在飛。這時用戶按 Esc。下一輪模型會在歷史裏看見什麼:成功回執、aborted by user、<turn_aborted> 片段,呢三樣各會唔會出現?後台 terminal 仲喺唔喺度?
再把時刻改成 handler 還在跑,重答一遍。哪一步是不可逆的,為什麼已經落盤的檔案不會跟着中止事件一起消失。