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

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

Durable Session:先記 Intent,再做 Effect

用 transactional JSONL 保存接受的工作、model attempt 與 tool intent,讓 crash 後仍可解釋。

約 26 分鐘 6 / 10

一句話掌握本章核心:如果假設網路永遠正常、工具永遠成功、行程絕不崩潰,幾十行就能寫出 Agent。但生產環境的硬核挑戰在於崩潰之後仍能辨認已知與未知。tiny-agent 採用 WAL(Write-Ahead Logging)啟發的 append-only 設計:先記意圖(Intent),再發動副作用(Effect)。

經典問題:被 kill -9 強制關閉後,指令到底執行了沒?

設想一個場景:Agent 剛決定執行 bash deploy.sh。就在系統發出指令的瞬間,作業系統觸發 OOM 或被 kill -9 強制終止。

若採用錯誤的「先做再記」策略崩潰重啟後的致命困境系統被迫面臨的代價
執行完畢才寫磁碟
execute() -> append()
磁碟中可能完全沒有這筆記錄,系統無法判斷 effect 是否已發生。猜錯代價巨大:若重新執行會造成重複部署。若以為沒事則遺漏關鍵變更。
先寫意圖才發動副作用
append(toolStarted) -> execute() -> append(toolResult)
磁碟明確記錄「我打算執行 deploy.sh」,但缺少結束記錄。確定的未知:復原規則會依狀態與 Replay Policy 決定下一步。尚未開始的 effect 可正常啟動,符合 identity/key 的 builtin read 可 replay,執行狀態不明且不可安全 replay 的 effect 才補 interrupted。
WAL 交易順序:先持久化 Intent 再發動 Effect 時序架構圖
圖 5.1:Intent 必須早於 Effect——崩潰後的確定性來自嚴格的 Write-Ahead Logging 交易順序。

Durability 的精確承諾邊界

在計算機系統工程中,切忌使用行銷話術宣稱「永不丟失資料」。tiny-agent 嚴格定義其持久化邊界:

承諾維度是否保證技術原理與限制
Process-Crash Recovery✅ 設計支援在 OS 與儲存仍正常、已完成的 prefix 可讀取的前提下,程式崩潰重啟會丟棄末端殘缺的 Torn Tail,再由 reducer 從通過驗證的完整 prefix 重建狀態。這不是對 filesystem corruption、惡意改寫或硬體故障的保證。
Power-Loss Durability❌ 不保證未在每行寫入時執行極度耗損效能的 fsync() / fdatasync()。若整台伺服器瞬間斷電或硬體故障,未刷入快閃記憶體的 Cache 可能遺失。

單一 JSONL 檔案的兩類核心紀錄

Session 檔案由第一行的全域 Header 與後續的交易行(Transactions)組成。每行是 reducer 與 torn-tail 修復使用的 atomic recovery unit。這不表示任意 OS/filesystem 都保證單次 write 在硬體層永遠全有或全無:

// Line 1: Header(Session 建立時寫入的設定與環境身分)
{"kind":"header","version":2,"id":"019...","createdAt":1787371200000,"cwd":"/workspace","provider":"openrouter","model":"<provider-model-id>","environmentIdentity":"/workspace"}

// Line 2+: recovery transaction line(可以是一筆 Fact,或是多筆綁定的 Fact Array)
[
  {"kind":"entry","seq":1,"id":"019...","entry":{"type":"message","message":{"role":"user","content":"修復 Bug"}}},
  {"kind":"record","seq":2,"id":"019...","record":{"type":"runStarted","operationId":"...","operationKind":"run","inputEntryId":"..."}}
]

三種 Facts 職責劃分

tiny-agent 將存入 WAL 的事件嚴格分為三大類,避免將監控日誌與上下文混淆:

Fact 類型保存內容範例是否會投影給 LLM?主要用途
entryUser/Assistant 訊息、Tool 結果、Compaction 摘要✅ 會(依 Active Context 規則選入)維持模型推論所需的上下文與語意歷史
recordrunStarted, stepAttempt, toolStarted, abortRequested❌ 不會供 Reducer 與 Planner 進行狀態轉移與崩潰復原
usage每次物理 API 請求由 provider 回報並經 adapter 正規化的 Token/Cache 明細❌ 不會獨立的 Usage Ledger。精度與欄位完整性受 provider response 影響

交易不變量 (Transaction Invariants)

  1. 單調遞增 seq:流水號從 1 開始,檔案內每一筆 Fact(包含 Array 內部)嚴格遞增 +1,絕不允許跳號或重複。
  2. 本機 UUIDv7 與外部 ID 分工:tiny-agent 配置的 Fact、Operation、Entry、Step 等 ID 使用 UUIDv7 並受引用不變量約束。provider 傳入的 tool_call_id 是外部 protocol ID,不要求是 UUID。
  3. 先持久化再更新記憶體(WAL 原則):資料必須先以 LF 格式 Append 至磁碟,成功後才允許更新記憶體狀態機。
  4. Torn-Tail 容錯修復:最後一個 \n(LF)之後未完成的殘缺 Bytes 視為 Torn Tail,在開啟 Session 時自動截斷修復。但如果某一行在 LF 之前就存在語法或欄位錯誤,則視為嚴重的 Corruption,系統選擇 Fail-closed 拒絕載入。

深而小的 Storage 介面

SessionStore 是標準的 Deep Module——以極簡的介面隱藏了極其複雜的交易流水號、ID 生成、佇列排程與尾端修復:

// 教學化摘錄。實際 public method 名稱與核心語意如下
interface SessionStore {
    append(input: NewFact | NewFact[]): Promise<CommittedFact[]>; // 驗證後追加一個 recovery unit
    load(): Promise<SessionState>;                                // 取得 reducer 投影的目前狀態
    close(): Promise<void>;                                       // 關閉檔案描述子
}
Torn Tail 尾端截斷修復流程圖
圖 5.2:Torn-Tail 容錯修復——自動偵測最後一個 LF 並丟棄殘缺位元組,確保狀態機基於乾淨歷史重建。

動手驗證 (Hands-on Lab)

先用任意 JSONL 示範 LF framing,再執行真正的 SessionStore contract test(不需消耗 API 額度)。前兩步只展示「最後完整 LF prefix」概念。手工 header/fact 並不符合完整 session schema,不能當成合法 Session fixture:

# 1. 手工建立帶有殘缺尾部的 JSONL 檔案
tmp=$(mktemp)
printf '%s\n' '{"kind":"header","version":2}' > "$tmp"
printf '%s\n' '[{"kind":"record","seq":1}]' >> "$tmp"
printf '%s' '{"torn_tail_corrupted_json":' >> "$tmp"

# 2. 透過 Node.js 執行 lastIndexOf(10) 截斷修復
node -e '
  const b = require("fs").readFileSync(process.argv[1]);
  const lastLF = b.lastIndexOf(10);
  console.log("=== 修復後的純淨 Prefix ===");
  console.log(b.subarray(0, lastLF + 1).toString());
' "$tmp"

# 3. 執行 tiny-agent 官方 SessionStore 測試
cd typescript
node --import tsx --test --test-name-pattern="repairs a torn tail" tests/session.test.ts

透過 WAL 順序與 append-only JSONL,tiny-agent 為正常 runtime 寫入提供可追溯、可驗證內部一致性的 session history。但沒有 MAC、signature、trusted timestamp 或外部 anchor,因此它不是能抵抗有寫檔權限攻擊者的「不可竄改合規 audit log」。當行程重啟並讀入整份日誌後,我們該如何從數百筆時序事件中計算目前狀態並決定下一步?下一章將深入探討 Pure Reducer 與 Planner 的復原狀態機。