iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
AI Engineering

AI Agent 上線要想清楚的事:30 天拆解 Harness 的設計取捨系列 第 6 篇

Day 6|按了停止還在改檔:Interrupt、Steer、Cancel 必須分開

  • 分享至 

  • xImage
  •  

🛑 停止畫面、停止工作、擋住舊結果、處理已發生的效果,是四件要分開確認的事。

昨天在 Day 5|訊息先到了,模型還沒讀到:收下和送達要分開記,我們分清楚「使用者的話被收下」和「真的送到模型面前」。模型還在忙,新訊息可以先排隊。

快速回顧一下:Run 是一次任務的執行;Harness 是負責跑 loop 的那一層,讓模型決定下一步、執行工具,再把結果交回模型。

今天接著問:如果使用者按的是停止,也要等模型忙完才處理嗎?

先看同一個事故。使用者請 Agent「把 timeout 調成 30 秒」,這次任務叫 Run A。模型選錯了目錄,Harness 卻已經啟動 shell,準備逐一修改裡面的三個設定檔。使用者發現不對,按下 Stop;畫面停止輸出,磁碟上的檔案卻還在變。

原因並不神祕:如果 Stop 只讓前端停止顯示,或只讓 Harness 不再等待回覆,已經啟動的 shell 不會因此知道自己該停。Harness 還得把停止要求送到負責執行 shell 的程式,再確認它是否結束。

即使 shell 停下來了,也可能已經改完前兩個檔案。取消能要求後續工作停下來,不能讓已完成的寫入自動消失。

今天先分清楚「停一下」或中途補話有哪些處理方式,再看 Codex 和 Claude Code 按下停止各停了什麼、平台與框架的 Interrupt 又是哪一種。接著回到 Run A,把取消拆成四層確認,說明實作時各要接在哪裡,最後再判斷畫面何時才能說「已取消」。

畫面停了,shell 還可能繼續改檔

圖:同一次取消裡,畫面停止、shell 結束、結果送回是不同時間點;最後還要查磁碟上留下了哪些變更。

中途收到新事件,先分清楚要怎麼處理

Run A 還在跑時,使用者可能補話、要求等一下,也可能要求整次取消。這些意圖不同,Harness 不該全部當成「再送一句話給模型」。

下面是我用來比較產品行為的六種語意,不是業界共同標準,也不是互斥、完整的分類法。斜線連接的是相關動作,不表示兩個詞在每套框架都能互換。

我用來比較的語意 放回 Run A,使用者是在要求什麼? 現有工作如何處理?
Queue(排隊) 「先做你的,這句晚點處理。」 先收下輸入,等指定時機再送達;可能是工具結束,也可能是整輪結束。
Steer(調整方向) 「保留可用進度,接下來只看 services/api。」 讓執行中的流程在可接收新輸入的位置改變後續方向;不保證打斷當下的 shell。
Pause / Resume(暫停/恢復) 「先等我確認目錄,之後還要接著做。」 在支援的暫停點保存進度,等回答後恢復。
Cancel / Abort(取消/中止) 「Run A 不要再做下去了。」 結束指定的這次執行;在途工作和已發生的效果還是要另外處理。
Rollback / Restart(回退/重新開始) 「捨棄這次進度,從先前狀態重新來。」 在支援的範圍內回退,再啟動新執行;單純重跑並不包含回退。
Reject(拒收) 使用者送了新要求,但系統回答「目前忙碌,這筆不接受」。 原工作繼續,新要求不進等待清單。

這張表混合了兩個問題:新要求何時處理,以及舊執行要留到什麼程度。 因此它們可以組合。例如,新訊息先排隊,工具結束後拿來調整同一輪,就是先 Queue、再 Steer;中止舊執行後帶著新輸入重跑,也同時包含 Cancel 和 Restart。

Pause 和 Cancel 的差別,則在於系統是否打算保存一個明確的恢復位置。Pause 是「等回答後繼續」;我這裡說的 Cancel 是「這次執行到此為止」。取消後還可以另開工作,讀取之前的紀錄,但那不表示被中止的 shell 會從原本那一行接著跑。

標題裡的 Interrupt 特別容易誤會。它只看名字不足以判斷是暫停、取消,還是中止後立即接新輸入。下面幾個官方實例,剛好用了同一個字,卻有不同效果。

現有產品按停止,各停了什麼?

Codex:Esc 停目前 turn,/stop 處理背景 terminals

先對齊範圍:我用 Run 表示一次任務的執行;Codex 的 turn 是一段回合執行,裡面可以有多次模型與工具呼叫。

一般執行畫面按 Esc,走的是我這裡說的 Cancel / Abort,範圍是目前 turn。 它不是讓 shell 暫停、等一下原地續跑,也不能擴大成整段對話和所有背景工作都取消。

下文的 thread 指同一段持續保留的對話。

以下依 2026-09-20 查到的 Codex main,commit 5c5308f。情境限定為預設按鍵、一般執行畫面,沒有彈窗或編輯模式先接走 Esc。source 的按鍵設定把 Esc 對到中止目前 turn;Core 的操作定義則明寫,這不會終止背景 terminal processes。按鍵設定、操作定義。

操作 目前 source 的處理 它沒有承諾什麼
一般執行畫面按 Esc 送出中止要求,Core 結束目前 turn,回報 TurnAborted。 不因此終止背景 terminal processes,也不復原已修改的檔案。
/ps 列出該 thread 管理的背景 terminals。 列表只是觀察,不會停止工作。
/stop 走獨立的背景 terminal 清理操作,要求終止該 thread 管理的背景 processes。 不是 Esc 的別名,也不是停止機器上或遠端所有工作的總開關。

這裡的背景 terminal,是 Codex 管理、可持續執行 shell 指令的工作,不是使用者另外開的每個終端機視窗。

回到 Run A:如果改檔指令還留在這類 terminal 裡,Esc 結束 turn 後,它還是可能繼續寫檔。 要停這些工作,需另外使用背景 terminal 的停止操作;已寫進去的內容則還要檢查。

這也有實際執行路徑:終端機介面(TUI)收到 /stop,會把要求交給核心執行程式(Core),再由它通知管理中的背景 process 終止。程式本身也提醒,中斷的工具可能已部分執行。TUI 指令分派、Core 處理、process manager、中止後提醒。

Codex 也支援另一條路:執行中接受新輸入,用來調整目前 turn。這才接近我這裡說的 Steer;Core 提供的 StartOrSteer 就是閒置時開始新 turn、有 active turn 時調整它。接受輸入還是不等於模型已經讀到,這點延續 Day 5。如果原本還有待送輸入,中止後也可能開始後續工作,因此 Esc 不能解讀成永久關閉這個 thread。輸入語意、中止與後續工作。

Claude Code:Esc 中斷目前工作,已做的內容保留

Claude Code 的官方互動文件說明:一般執行情境下,Esc 會中斷目前 response 或 tool call,讓使用者重新指示;先前已完成的工作保留。如果有排隊訊息,會接著送出。因此它接近「中止目前執行,再視新輸入繼續」,不能推成整個 session 不再動作。有對話框時,Esc 也可能只是關閉對話框;在權限提示上則是拒絕那個動作。

Run A 已經用 shell 改了兩個檔案,Esc 不會把它們恢復。要回到先前狀態,Claude Code 另有 rewind:輸入框為空時連按兩次 Esc 可開啟選單,也可用 /rewind。而checkpointing 文件特別指出,Bash 指令造成的檔案變更不在其追蹤、復原範圍。

停止和復原,是兩個不同操作。

平台與框架裡的 Interrupt,指的是哪一種?

接下來從操作產品,換成開發者怎麼接流程:先看 LangSmith 如何處理「工作還在跑,又收到新要求」,再看 LangGraph 與 OpenAI Agents SDK 如何做出「動手前先等人」的等待點。先等人核准,和動手後要求停下來,是兩件事。 後兩者是拿來建構 Harness 的框架或程式庫;看的是開發者要接上的控制方式。

LangSmith double texting:同樣叫 Interrupt,還包含接新輸入

LangSmith 是提供 Agent 追蹤、評估與部署的平台;這裡用到的 Deployment/Agent Server,負責把 Agent 流程當成服務執行。 當服務正在處理第一個要求,使用者又送來第二個,就得決定讓誰繼續。官方把這種情況稱為 double texting;下面這組處理策略是部署服務的功能,不能直接當成開源 LangGraph 自帶的同一套機制。平台與框架的分工。

官方文件列出四個選項:

  • Enqueue:預設策略,等前一次完成,再處理新要求。
  • Reject:拒收新要求,原工作繼續。
  • Interrupt:中止目前執行,保留當時進度,再加入新輸入繼續。
  • Rollback:退掉該次執行的進度,連同原始輸入一起移除,再從回退後的狀態處理新輸入。

所以這裡的 Interrupt 不只是「停」,還決定了「接下來用哪些進度繼續」。Rollback 退的是框架管理的狀態;不能據此認定 Run A 已寫入磁碟的檔案也一起復原。外部效果還是要另查。

LangGraph:把「等人確認」做成流程中的一步

LangGraph 是給開發者建立 Agent 流程的框架。 你把工作拆成步驟,寫出各步驟要做的事,以及完成後走向哪一步;它負責依這個流程執行與保存進度。步驟可以是普通程式,也可以呼叫模型。它能用來建構自己的 Harness,並不是安裝後就能直接幫你改程式的聊天產品。官方介紹。

用 Run A 的需求來做,假設我們希望「先讓人確認目錄,再改 timeout」,可以安排成:

  1. 產生方案:找出設定檔,把目錄、原值和預計改成的 30 秒存進流程狀態。此時只提出修改內容,還沒有寫檔。
  2. 等待確認:下一個步驟用 interrupt() 把方案交回應用程式。應用程式顯示確認畫面,流程停在這裡。
  3. 依回答繼續:使用者核准,就走到寫入步驟;拒絕,就走到回報「未修改」的分支。這個分支規則由開發者寫好。

要讓等待後接得回來,需要配置 checkpointer(保存流程進度的元件),再用同一個 thread ID 找回同一份進度。應用程式再把使用者的回答傳回,流程才繼續。因此你可以做出「先產生修改方案,等人稍後確認,再接著執行」的功能;如果要跨 process 等待,進度就要保存到資料庫等持久位置,不能只放記憶體。Interrupts 文件。

有一個會影響程式怎麼拆的細節:恢復時,包含 interrupt 的那個步驟會從函式開頭重跑。 因此上例把產生方案、等待確認、寫入拆開;等待步驟只讀已保存的方案。不要在同一個函式裡先寫檔再等待,否則恢復時,等待之前的寫入可能再做一次。

你從這個例子可以帶走的是:等待人工確認,必須有保存的進度和明確的後續分支。 單純顯示一句「要繼續嗎?」並不會讓正在執行的流程自動停住。

OpenAI Agents SDK:讓指定工具先核准,才真正執行

OpenAI Agents SDK 是建立 Agent 的程式庫。 你提供指示、模型和工具函式,它提供執行迴圈,負責呼叫模型、執行工具,再把結果帶回去。今天要看的是它的 human-in-the-loop(HITL,人工介入)功能:讓需要核准的工具,在動手前先等人的決定。SDK 介紹。

同樣要改 timeout,可以把「修改指定目錄的 timeout」做成一個工具,再標記 needs_approval,要求執行前核准:

  1. 模型提出工具呼叫,例如目錄是 services/api、新值是 30 秒。SDK 遇到需要核准的呼叫,先把待核准資訊交回,這次寫入尚未執行。
  2. 你的應用程式把工具名稱、目錄和新值顯示給使用者,收下核准或拒絕。確認介面要由應用程式接上,不是加了標記就自動長出一個網頁。
  3. 把決定記入這次執行的狀態,再交回 SDK 繼續。核准才執行該工具;拒絕則不執行這次呼叫,將拒絕結果帶回流程。

如果使用者不會立刻回答,可把暫停進度轉成 RunState(可保存、恢復的執行狀態),存好後稍後載入;不必為了等核准一直維持原本那個函式呼叫。HITL 文件。

這樣可以做出「Agent 先準備修改,使用者確認後才寫入」的操作。拒絕的是這次工具呼叫,整個任務還是可能繼續回報或提出其他方案;而已經啟動的 shell,不會因為另有一筆工具等待核准就自動停下來。

兩個例子都屬於 Pause / Resume:LangGraph 讓你安排流程中的等待點與分支;這個 Agents SDK 範例則利用工具的核准設定,在指定呼叫前等待。它們都把確認放在寫入之前。如果 Run A 的 shell 已經開始改檔,就要回到下面的 Cancel 四層處理,不能用「等待核准」代替取消。

把確認放在寫入之前,才有機會阻止錯改

圖:這是兩個範例希望實現的共同操作。核准與拒絕的分支必須接到真正的執行流程;等待期間,圖中的這次寫入尚未開始。

回到 Run A:Cancel 後要分四層確認

現在使用者的意思已經明確:目錄錯了,Run A 不要繼續。以下四層是我用來檢查設計的四個面向;前面的產品不一定用同一個操作涵蓋全部。

第一層:不再派新工作

Run A 已經啟動的 shell 還在跑,但 Harness 應先禁止 Run A 再派出下一個模型請求、工具呼叫或子任務。模型即使剛好回了「接著跑測試」,也不能照做。

後文說的「取消被接受」,是控制程式已確認要取消 A,也使這次執行失去繼續派工和提交結果的資格。前端只回一句「已收到」,還不算這一步完成。

這就是 Day 5 的一般訊息和取消要求要分開的地方。補話可以等工具結束再送到模型;取消要由模型外的控制程式處理,不必等模型讀懂「請停止」。

否則 shell 還要跑三十秒,你也得等三十秒才開始取消。

禁止的是繼續完成原任務;停止的動作、查證結果、清理暫存檔等取消收尾,還是需要執行。

第二層:通知已經在跑的工作停止

不再派工,擋得住下一個工具,擋不住已經啟動的 shell。

Harness 還要通知真正執行指令的那一端停止,再取得結束狀態。

在 Run A 裡,理想情況是 shell 收到要求,停止修改後續檔案。但它也可能來不及停、無法被控制,或已經把工作交給另一台機器。

因此「停止要求送出」和「工作已結束」要分開記。

名稱只能當入口,還要查它停的是什麼。 AutoGen Core 是用訊息串接 Agent 的框架;它的 0.5.5 文件以本地 SingleThreadedAgentRuntime 為例,明寫 stop() 會立即返回,卻不會取消正在執行的 handler(處理訊息的函式)。另一個 stop_when_idle() 則會等到沒有待處理訊息、也沒有 handler 還在執行,才返回;它是在等工作處理完,不是強制中止那些 handler。這個例子提醒我:呼叫 stop 之後,不能只看方法名稱,就認定 Run A 裡正在改檔的工作也停了。AutoGen Core 0.5.5|Agent and Agent Runtime。

第三層:舊 Run 的晚到結果不得生效

假設使用者取消 A,接著送出 Run B:「先只檢查 services/api,不要寫入。」A 的 shell 停下後,過了幾秒才回報「前兩個設定檔已修改,第三個未處理」。這份輸出可能早已產生,只是現在才送回 Harness。

這份回報屬於 A,不能直接拿來宣布 B 已完成,也不能讓 A 依原計畫繼續派出測試。可以保存它,作為查證 A 做過什麼的證據;但不能讓已取消的執行繼續改寫目前任務的進度。

這層必須在取消被接受時就生效,不能等第二層證明 shell 已停才開始擋。

否則等待停止的幾秒鐘,晚到結果就可能先被採用了。

第四層:處理取消前已經發生的 side effects

接著核對 A 的回報。shell 確實已停,磁碟上的第一、第二個檔案已修改,第三個還沒動。這些已對環境造成的變更,就是 side effects(副作用)。

現在要決定的是:保留這兩個變更、恢復原內容,還是先請人確認。這不是前面三層能代替的事。特別是第三層只擋「結果被採用」,不會攔下 shell 直接對磁碟的寫入。

所以前面收到 B 的要求後,如果 A 是否停止還沒確認,就應先讓 B 等待,或讓 B 使用隔離且已確認的檔案版本。否則即使 B 只是讀檔,也可能讀到 A 正在改的一半內容;讓 B 接著寫同一批檔案,更可能互相覆蓋。丟掉 A 的回報,解決不了這種直接寫入的衝突。

四層是四個問題,不是一定要依序等待完成的流水線。

取消被接受時,就要禁止新派工、使舊執行失效,也把停止要求送下去;確認工作結束與清點變更,則可能需要更多時間。

取消 Run A,要分開確認四件事

圖:第一層擋新派工,第二層處理在途工作,第三層擋舊結果,第四層處理已發生的變更。檢查其中一層,不能代替另外三層。

實作時,把這四個問題接到哪裡?

知道各層在解什麼問題後,再看幾個常見做法。以下是實作建議,不是每套 SDK 都要求照抄的欄位或狀態名稱。

派工作和收結果,都先確認這次執行仍然有效

派出前與提交時,都要核對執行資格。這一節同時對應第一層「不再派新工作」與第三層「舊結果不得生效」。

要擋住 A 的回報,Harness 先得知道回報來自誰。每次派出工作時帶上 run_id,回來時核對來源和該 Run 的狀態:A 已取消,就不能再以 A 的名義推進任務。只看「session 還在不在」不夠,因為同一段對話可能已經在處理 B。

「有效」指是否還有權派工、提交結果,不是「哪個 Run 的時間戳最新」。如果系統允許多個 Run 同時執行,就分別核對各自的狀態與資源權限。

同一個 Run 如果會換 worker(跑這個 loop 的 process)接手,光有 run_id 又分不出新舊執行者。要判斷舊 worker 還能不能提交,就得知道哪一版執行權還有效。這和 Day 4 的 Attempt 不同:Attempt 記的是某一步重試了幾次。我把三種識別放在一起比較:

識別 它分辨什麼 什麼時候才需要
run_id 是哪次任務的執行,例如 Run A 或 Run B。 派工與收結果時,用來找對 Run,再核對它是否還有效。
Attempt(Day 4 的步驟嘗試) Run 裡某一步的第幾次嘗試,例如同一個寫檔步驟失敗後重試了幾次。 要看某一步試過幾次、前幾次怎麼失敗時。它記的是步驟重試,無法判斷執行權是否還有效,單靠它擋不住已取消的 Run A 晚到提交。
generation(遞增版本) 目前有效的是哪一版執行權,例如只接受版本 2,版本 1 的回報不能再提交。 同一 Run 換 worker 接手後,需要分辨新舊執行權、拒絕舊 worker 提交時。

Attempt 和 generation 解的是不同問題,不是二選一。

Attempt 留下步驟重試紀錄;generation 這類版本則用來區分新舊執行權。假設 A 在取消前曾換過 worker,已由持有版本 2 的 worker 接手;當時即使 A 還在跑,舊 worker 帶版本 1 回來也不能提交。版本必須隨工作傳遞,再由接收端核對,才會真的擋住舊執行。

如果 A 整個 Run 已取消,接收端核對 Run 狀態就應拒絕它繼續提交;不論 Attempt 或 generation 是多少,都不能讓 A 恢復執行。不必為了取消一律新增名叫 generation 的欄位,但如果要區分同一 Run 內的新舊執行權,就需要版本或等效機制,不能拿 Attempt 的重試次數代替。一步失敗重試還是屬於同一 Run;整件事失敗要從頭重做,則依 Day 4 另開新 Run。

取消與收結果還可能同時發生。

接收端先查「A 還有效」,準備寫入時取消才生效,分開的檢查還是會漏掉。核對資格和更新進度,要與取消操作協調先後。例如用帶有「A 還有效」條件的資料庫更新,讓取消與結果提交共同檢查同一份狀態;不能以為把兩行程式包進 transaction 就一定夠。派工端也要做相同協調:取消先生效,就不能再派;已先派出的工作,則歸第二層追蹤停止。

這延續 Day 5「只有 loop 寫模型對話」的做法:工具把結果交回,由統一入口決定是否寫入;控制程式可以先使 Run 失效,再通知負責執行工具的那一端,不需要等 loop 正在等待的工具先結束。

順序也要誠實:如果結果已先被正式接受,取消才生效,那是取消前已完成的進度,不該事後抹成「沒有發生」。如果整次任務已完成,UI 應回報取消來不及生效。

停止通知要一路傳到 executor,不能只停在 loop

Executor 就是實際啟動工具、管理 process 或呼叫遠端服務的那一端。第二層「通知已經在跑的工作停止」,要通知的就是它。

對能配合停止的工作,可以傳入 cancellation token。它是一份共享的取消通知:工作定期檢查,或在等待時監聽;收到後退出再清理。如果工具根本不檢查,光設 token 不會讓它停止。這是 cooperative cancellation,也就是由執行端配合的取消。Microsoft 的取消模型說明。

Run A 啟動的是外部 shell,所以 executor 還要保留控制該 process 的方法,必要時要求正常結束,再依政策強制終止。這就是 process termination。如果 shell 又生出子 process,也要確認管理範圍包含它們;只結束等待 shell 結果的程式,並不等於 shell 和子 process 都停了。

遠端操作同理:關閉本地連線,只代表不再從那條連線等回覆。要讓遠端工作停下來,得使用對方提供的取消介面,再查它的最終狀態。Codex 把 turn 中止和背景 process 清理拆開,正是這種範圍差異的具體例子。

工具中途取消,模型對話仍要有交代

這是四層之外的對話收尾:準備下一次模型輸入時,還要符合工具呼叫與結果的配對規則。它處理的是對話能否繼續使用,不能代替四層裡的任何一項。

Run A 的模型已提出「修改這三個檔案」的工具呼叫,工具卻在中途被取消。下一次如果繼續使用這段對話,不能只保留呼叫、把結果留白,也不能假裝工具完整成功。

有些模型 API 要求呼叫與結果配對;例如 Anthropic 的工具文件要求對應的工具結果緊接在工具呼叫之後。保留工具呼叫卻漏掉應有的結果,下一次請求就可能因格式不符而被 API 拒絕。沿用這段歷史時,Harness 要依 API 的格式補上中止或未完成的說明,例如:「工作遭中斷;可能已部分改檔,尚未確認全部變更。」

這是 tool-call 收尾,交代這次呼叫怎麼結束;不是准許 A 的晚到成功回報推進 B。 之後查清兩個檔案確實被改過,再由目前負責的流程加入查證結果。原始事件紀錄還是保留,不能為了整理模型輸入而改寫事實。

已經改掉的內容,要有另一套處理方式

先確認改了什麼,再決定保留、復原或補償,這是**第四層「處理已經發生的效果」**要做的事。

對 Run A 的本地檔案,動手前可準備分開的工作目錄、暫存產物或快照,確認後再合併。這樣取消時,較容易辨認與捨棄 A 的變更。

以 Git worktree 為例,官方文件說明:每個 worktree 可以有各自 checkout 出來的檔案;repository 資料則共用,只有 HEAD、index 等每個 worktree 專屬的資料分開。Git|git-worktree。

從取消設計來看,我會再區分:獨立 checkout 還是不等於權限隔離。

worktree 是不同目錄,Git 並沒有替 shell 加上作業系統層的存取限制。如果 shell 原本有權限寫到目錄外,單靠分目錄並不能保護其他檔案;要限制寫入範圍,還得另外設定權限或沙箱。

有保存先前內容,才可能做 rollback,把支援範圍內的狀態退回;而且要避免覆蓋使用者或 B 後來的修改。前面 LangSmith 的 Rollback 也是同一個提醒:回退框架狀態,不會順便還原磁碟或遠端服務。

如果把 Run A 換成「呼叫遠端設定服務更新 timeout」,取消時沒收到回覆,對方可能已更新成功。這時將該操作的結果標成 outcome_unknown:我們不知道成功或失敗。它不是另一種停止指令,也不是所有 Run 都必須有的統一終態。

為了之後查證,送出前先保存本地的操作識別;對方回傳 request ID 或 job ID 後再補上。查得到就查原操作;查不到也不能只因為沒回覆便再送一次。能查詢原操作,不代表直接重送就安全;要重送,還得確認對方能辨認同一筆操作、避免重複執行。Day 18 會接著談這個問題。

已經確認遠端更新成功,卻無法直接回退交易,就可能要另做一次「把設定改回來」的動作。這叫 compensation(補償):它是新的操作,也可能失敗;如果其他人已更新同一設定,更不能盲目覆蓋。補償結果要繼續追蹤,不能把「發出補償要求」當成已恢復。Compensating Transaction。

UI 何時能說 cancelled?

先說清楚按鈕承諾停的是哪個範圍:目前回合、Run A,還是連同它管理的背景工作。同一個 cancelled,範圍不同,所需證據就不同。 下面是我建議的顯示方式,不是框架共同定義的狀態機。

已經確認的事 UI 可以怎麼說
只收到按鈕事件,取消尚未由控制端處理 已收到取消要求。
Run A 已失效,還在等 shell 結束 正在停止;尚有一個 shell 未確認結束。
目前 turn 已中止,產品允許背景工作獨立存在 目前回合已中止;還有背景 terminal 在執行。
Run A 與所屬 shell 已停止,晚到結果不得推進任務;兩個檔案已改過 Run A 已取消;兩個檔案的變更還是保留,尚未復原。
本地 Run A 已停止且舊結果不能提交;遠端更新是否成功、是否還在執行,都還查不到 本地執行已取消;遠端操作結果待確認,可能還在執行。
完成狀態已先確認,取消才到 工作已完成,取消未生效;如需復原,另行處理。

如果產品的 Stop 承諾停止 Run A 與它的 shell,就要在這個範圍內確認不再派工、工作已結束,而且舊執行不能再提交,才顯示「Run A 已取消」。停不下來、還在等回覆,就明確顯示還在停止或尚未確認;不能把轉圈動畫拿掉便宣告完成。

但 cancelled 不等於 rolled back。

Run 已停止,已改過的檔案還是可以保留;遠端操作的結果也可能還在查證。把「執行狀態」和「效果處理狀態」分開顯示,才能同時表達「Run A 不會再繼續」與「有兩個變更尚未復原」。如果產品只取消本地執行、遠端工作還是可能繼續,也要把這個限制直接寫出來。

這也回答了開頭的事故:畫面停了,並不足以判斷取消有沒有完成。要查 Run A 是否還能派工、shell 是否結束、晚到結果是否被擋住,以及那三個設定檔究竟改了幾個。

停止畫面、停止工作、擋住舊結果、處理已發生的效果,是四件要分開確認的事。

可以先從哪裡做起?

假設你已經做到 Day 5:模型讀的對話由 loop 寫入;中途進來的訊息先進收件匣,收下與送達分開記錄。接著從一條會改檔的工具路徑開始,把「按停止之後怎麼收尾」接完整。使用獨立測試目錄,沿用 Run A 逐一修改三個設定檔的情境。

  1. 先處理派工與結果入口。 每次派工帶上 Run 的識別;取消生效後,不再派出 A 的下一步。A 的結果晚到時,留下紀錄,但不得直接推進目前任務。
  2. 接上正在執行的 shell。 保存控制它的方法,取消時通知停止,再分別記下「已要求停止」和「已確認結束」。如果無法確認,就保留這個狀態。
  3. 把收尾結果呈現在畫面上。 核對三個檔案各自是否修改,補上工具未正常完成的說明。確認 A 已不能派工、shell 已結束、晚到結果不得推進任務後,再依實際變更回報,例如「Run A 已取消;兩個檔案的變更還是保留」。尚未確認的項目就直接列出,不提早宣告全部停止。

先做到這三步,就能把停止按鈕接到實際行為,而不是只有一個 cancelled 標籤。之後再依需求加入寫入前核准、檔案隔離或復原;等待核准也不能省掉執行中的取消處理。Day 7 會接著處理 worker 消失後,如何找回進度與工作環境,讓新的 worker 接手;Day 18 再補外部操作結果未知時,如何用穩定的操作識別查證與避免重複執行。

你可以怎麼確認自己讀懂了?

把自己放在 Run A 的事故現場,試著回答下面三題;重點是能說出下一步要查什麼,不是背出 API 名稱。

一、畫面已停,但第二個設定檔還在變。把 Run A 標成 cancelled,有解決問題嗎?

沒有。先確認新的派工已被禁止,再查 shell 是否收到停止要求、是否真的結束。狀態標籤不會替你終止 process;只擋晚到回報,也擋不住 shell 直接寫檔。

二、shell 已停,A 的回報晚到,提到前兩個檔案已修改。這筆回報要全部刪掉嗎?UI 又該怎麼說?

不用刪。它能留下來協助查證 A 做過什麼,但不能直接更新 B 的進度或啟動 A 的下一步。還要確認 A 已不能再派工、舊結果確實不會推進任務,再核對檔案,才能顯示「Run A 已取消;兩個變更還是保留」。執行停止和變更復原,要分開回答。

三、你想讓人確認目錄後才寫入,該把等待放在哪裡?拒絕核准等於取消整個任務嗎?

等待要放在這次寫入之前。LangGraph 可把核准做成獨立步驟,回答後依分支決定是否寫入;Agents SDK 可把該寫入工具設為需要核准,再由應用程式送回決定。拒絕核准,表示不執行這次寫入;整個流程要回報、改提方案或結束,還要有自己的規則。

如果要用實作驗證,就在測試目錄刻意延後 A 的結果回報,再於改到一半時取消。對照派工紀錄、shell 狀態、A/B 的進度與三個檔案,確認四層各自符合預期。只看畫面有沒有停止,測不到後面三件事。

Day 5 讓我們知道輸入何時真的送達;今天則讓取消不必等模型,也界定舊執行何時失效。明天換另一種情況:使用者沒有要求取消,負責執行的 worker 卻突然消失。任務還要繼續,新 worker 得先找回已確認的進度、工作目錄與外部操作,才能判斷接著做什麼。

參考資料

Codex 來源固定於查核時的 commit;以下提供概念對應與 source 入口,不要求讀者逐行追程式才能理解正文。

  1. Codex|按鍵設定、TUI 按鍵處理、互動分支:Esc 的預設用途、執行狀態與介面情境。
  2. Codex|操作定義、Core handler、task 中止與收尾:中止 turn 和停止背景 process 是不同控制操作。
  3. Codex|slash 指令、指令分派、TUI 清理要求、背景 process manager:/ps、/stop 與實際終止範圍。
  4. Codex|Turn input、中止後提醒:StartOrSteer 的接受語意、背景工作及部分執行。
  5. Claude Code|Interactive mode、Checkpointing:Esc、排隊訊息、rewind 與 Bash 變更的限制。
  6. LangSmith Deployment|Double texting:Enqueue、Reject、Interrupt、Rollback。
  7. LangGraph|Overview、Interrupts:框架用途、流程步驟、暫停與恢復時重跑 node。
  8. OpenAI Agents SDK|介紹、HITL:執行迴圈、工具核准、RunState 與恢復。
  9. Anthropic|Handle tool calls:模型對話裡的工具呼叫與結果配對。
  10. Microsoft|Cancellation in Managed Threads、Compensating Transaction:合作式取消與補償的責任和限制。
  11. AutoGen Core 0.5.5|Agent and Agent Runtime:本地 runtime 的 stop() 不取消正在執行的 handler,stop_when_idle() 則等待閒置。
  12. Git|git-worktree:獨立 checkout 與 repository 資料的共用範圍;用來區分工作目錄分開和存取權限隔離。

上一篇
Day 5|訊息先到了,模型還沒讀到:收下和送達要分開記
下一篇
Day 7|Worker 消失後怎麼接手:把進度留在 Process 外
系列文
AI Agent 上線要想清楚的事:30 天拆解 Harness 的設計取捨 共 14 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言