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

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

Cancellation 與 Durable Compaction

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

約 22 分鐘7 / 8

Cancellation不是「把Promise遺失掉」。可靠Agent必須先留下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

兩個容易混淆的邊界

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 行或 50KB(先達到者);Go 與 Python 則以約 50KB 的 byte-oriented tail 為主,不承諾相同的行數語意。四種實作都有約 10MB 的 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。

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