tiny-agent 從第一性原理打造可靠 Agent

第三部|可靠執行 · 第 7 章

Cancellation 與 Durable Compaction

處理 abort race、合法 transcript,以及 active-context cut 與 durable source partition 的差異。

約 22 分鐘 7 / 8

換個情境:這次不是 process 被強制關掉,是你自己在 bash deploy.sh 跑到一半按下 Esc。跟上一章的 crash 不一樣,這是「使用者主動要求停」,系統理論上有機會好好收尾——但如果收尾的順序錯了,結果一樣糟:如果先取消掉正在跑的 Promise,卻還沒把「使用者要求取消」這件事寫進硬碟,中途再 crash 一次,重開後系統完全不知道使用者曾經要求停止,可能會誤判成「還在跑」而去 retry 或 replay。

所以可靠的 Agent 不能只是「把 Promise 取消掉」。它必須先留下 durable 的 abort intent,再通知目前 phase 停下來,並在退出前補齊合法 transcript 與 operation outcome。

Abort 的正確順序

persist abortRequested
→ signal active model / tool / compaction
→ reconcile open attempt 與 tool calls
→ persist operationFinished(aborted)
→ release resources

順序不能反過來。若先 signal 後 crash,Session 可能完全不知道使用者曾要求 abort;恢復程式可能錯誤地 retry model 或 replay safe tool。

Cancellation 依 Phase 分流

Phase 動作 Durable reconciliation
model 取消 HTTP request 或停止前景等待 open attempt 寫 stepFailed(aborted)
tool signal tool;bash 盡力終止 process tree started call 寫 interrupted;未 started 寫 aborted
compaction 取消 summary request 關閉 attempt,再結束 compaction operation

三條路徑互斥,不是依序執行。Esc 只 abort 目前 operation;Ctrl+C 在必要時 abort 後,還會關閉 MCP clients、Session writer 並恢復 terminal。

Abort Race 的核心規則

Abort 可能與 model response 或 tool completion 同一時間發生。系統必須把競爭序列化成兩種合法歷史:

  1. 成功 settlement 先 durable:abort 看到 operation 已結束,不改寫結果。
  2. abortRequested 先 durable:晚到的成功 response 不能再把 operation 寫成 completed。

這也是為什麼 durable Session 不只靠 AbortController.signal.aborted:memory flag 無法解釋 crash 後的先後順序。

你剛才看到的 abort race,其實是「系統要主動改變狀態,且必須先 durable 才能改」這類問題的一種——這裡改的是 operation 本身進行到哪一步。compaction 是同一類問題的另一種:它自己也是一個獨立的 durable operation(跟 run 一樣有 compactionStarted/operationFinished),但它要多處理一層——active context 該怎麼跟著變。

Compaction 不是刪除歷史

/compact 呼叫目前 model 產生 summary,然後重建 active context。原始 facts 仍留在 append-only Session 中,因此可以 audit 與 recovery。

完整 durable facts
→ reducer materialized active context
→ summary + retained message tail
→ 後續 model requests

兩個容易混淆的邊界

Track entry 1 entry 2 ... entry N-6 ... entry N
Track1:Active-context cut(給模型看) entry 1 → 送摘要 entry 2 → 送摘要 ... → 送摘要 entry N-6 → 送摘要 ... → 原樣保留 entry N → 原樣保留
Track2:Durable source partition(給稽核/recovery 看) entry 1 → compactedEntryIds entry 2 → compactedEntryIds ... → compactedEntryIds entry N-6 → compactedEntryIds ... → retainedEntryIds entry N → retainedEntryIds
圖 6:兩軌共用同一個切點。即使連續進行第二次以上的 compaction,這兩個切點仍然對齊——四語言實作都有一道「找不到 user 邊界就放棄本次 compact」的防護,這道防護同時保證了合成的「上次摘要」訊息不會被算進這次的 retained tail。prior summary 會參與 Track1 的摘要輸入,但不是 Track2 的 source entry。

1. Active-context cut

用於決定本次摘要輸入:保留至少最近 6 則 message,再把 cut 向前移動到 user boundary,避免拆散完整 turn。Repeated compaction 時,prior summary 會參與新的摘要輸入。

2. Durable source partition

用於稽核與 recovery:保存截至 inputThroughEntryId 的所有 durable source message-entry IDs,並分成 compactedEntryIdsretainedEntryIds,再計算 sourceDigest

Prior summary 不是 source message entry,所以不會放進 source partition;但它仍會參與 active-context summarization。兩者解決不同問題。

Compaction 也是正式 Operation

compactionStarted
→ stepAttempt(stepKind=compaction)
→ usage
→ compaction entry
→ operationFinished(completed)

Compaction entry 保存 materialized retained tail 與 source IDs。若 crash 發生在 summary 已經落盤、finish 尚未落盤,recovery 只補 operationFinished,不會再次呼叫 model。若 open attempt 未知,則遵循相同 attempt cap。

Tool Output 也是 Context 控制

TypeScript 與 Rust 會保留最後 2,000 行或 50 KB(先達到者);Go 與 Python 則以約 50 KB 的 byte-oriented tail 為主,不承諾相同的行數語意。四種實作都有約 10 MB 的 capture safety limit,但超限行為不同:有的回傳 capped-output 診斷,有的將它視為 tool error。只有在實作確實捕獲完整輸出時,才會另存 .tiny-agent/tool-output/ 並回傳路徑。

親手驗證

以下練習完全使用 repository 的 deterministic tests,不需要 OpenRouter key。先觀察 cancellation 與 compaction 的 production regression tests,再閱讀固定的 durable facts:

npm --prefix typescript test -- \
  --test-name-pattern="abort|compaction"

grep -R 'abortRequested\|compactionStarted\|sourceDigest' \
  schemas/session/fixtures schemas/session/planner-fixtures | head -40

檢查順序:先找到 abortRequested,再確認對應的 attempt/tool reconciliation 與 operationFinished;compaction 則比對 source partition、sourceDigest 與 materialized retained tail。最後重跑同一個 recovery test,確認第二次 resume 不會再次呼叫模型或重複寫入 outcome。

到此為止,你已經有一個能正確處理失敗的迴圈——下一章要問的是,你怎麼知道它平常做得好不好、做得對不對。