假設一個客服 Agent 收到退款申請。
它先檢查訂單,確認符合政策,再呼叫支付服務建立退款。請求送出後,連線逾時。Harness 沒有收到成功回應,也沒有收到明確失敗。
接下來該做什麼?
最危險的答案是:再試一次。
外部支付系統可能根本沒有收到第一個請求,也可能已經完成退款,只是回應在網路途中遺失。若 Harness 把 Timeout 直接解讀成「沒有執行」,第二次呼叫就可能建立重複退款。
Agent 系統的可靠性問題,常常不是模型答錯,而是系統無法回答這幾件事:
- 這次失敗屬於哪一類?
- 前一個操作是否已經產生副作用?
- 這個操作能不能安全重試?
- 下一步應該 Retry、Repair、Fallback、Replan、Compensate,還是 Stop?
- Runtime 還剩多少時間、成本與嘗試次數?
- Worker Crash 後,新的 Worker 要從哪個可信狀態恢復?
可靠性不是多加幾次 Retry。可靠性是讓每一種失敗都有可判斷、可限制、可恢復的路徑。
Failure Class 只回答一半,還要知道副作用確定性
在聊天產品裡,Failure 可能只是沒有答案。Agent 能呼叫 Tool、修改 State、啟動 Background Work 後,同一個「失敗」至少要從三個維度判斷:
- Failure Domain:模型、Schema、Policy、Dependency、State、Capacity,還是 Budget?
- Effect Certainty:確定未執行、可能已執行、部分完成、確定完成,還是仍無法判定?
- Recovery Eligibility:目前 Contract、Deadline、Cost、Idempotency 與 Authority 是否允許下一步?
| Failure domain | 典型案例 | Effect certainty | 常見方向 |
|---|---|---|---|
| Model | Timeout、Provider unavailable、截斷輸出 | 通常沒有外部副作用 | Bounded Retry、Compatible Fallback、Continuation |
| Schema/Validation | Tool Arguments 不符合 Contract | 尚未執行 | Repair、重新產生 Arguments |
| Permission/Policy | 權限不足、Policy Deny | 尚未執行 | Stop、等待 Approval、改走允許路徑 |
| Transient Dependency | Rate Limit、短暫網路故障 | 依 Operation 而定 | Backoff、Jitter、Bounded Retry 或 Reconcile |
| Permanent Business | 餘額不足、訂單不可退款 | 通常已知失敗 | 回報原因,不 Retry |
| State Conflict | Version Conflict、Stale Claim | 本地 Commit 未成立,外部狀態需另查 | Reload Canonical State、Repair、Replan |
| Unknown Outcome | Request 已送出後 Timeout | 可能已產生副作用 | Reconcile,不盲目重送 |
| Partial Outcome | Batch 或 Saga 只完成一部分 | 已有部分副作用 | Forward Repair、Compensate、Human Review |
| Capacity/Overload | Queue 過舊、Sandbox 不足 | 工作尚未開始或停在中途 | Admission Control、Backpressure、Load Shedding |
| Budget Exhaustion | Cost、Time、Step 或 Retry Budget 用盡 | 視目前步驟而定 | Degrade、Checkpoint、Human Takeover 或 Stop |
這個 Taxonomy 不是為 Log 增加漂亮欄位。它決定下一個 State Transition 是否安全。
INVALID_ARGUMENTS 和 RATE_LIMIT 都可能讓 Tool Call 失敗,但原封不動重送第一種不會修好任何事情。PERMISSION_DENIED 也不是等待幾秒就會消失的網路抖動。最需要單獨處理的是 UNKNOWN_OUTCOME:呼叫端沒有收到結果,外部世界卻可能已經改變。

Figure 8-1|Recovery Decision 同時考慮 Failure Domain、Side-effect Certainty、Retry Safety 與剩餘 Budget,不把每種 Error 固定映射成同一個 Action。
Recovery State Machine:Action 也是受控的 State Transition
Recovery 不應散落在每個 Tool Wrapper 的 try/except 裡,也不應把所有 Error 原樣丟回模型臨場決定。
常見 Recovery Action 包括:
- Retry:Fault 可能是暫時性的,Operation 可安全重送,且 Budget 尚未用盡。
- Repair:先修正 Schema、Arguments、Query 或缺少的輸入。
- Fallback:切換到已知相容的 Model、Provider、Tool Implementation 或 Degraded Service Path。
- Replan:目前路徑阻塞,但 Task 仍有其他合法路徑。
- Reconcile:結果未知,先查 External Authority。
- Forward Repair:副作用已成立,不回頭重做,改修後續 State。
- Compensate:執行一個新的校正動作,而不是假裝時間倒流。
- Quarantine:隔離 Poison Message、未知版本或反覆失敗的 Work Unit,等待人工或離線修復。
- Ask Human:商業判斷、Approval 或高風險歧義阻止自動恢復。
- Stop:Policy、永久錯誤、資訊遺失或 Budget 不允許繼續。
recovery_policy:
RATE_LIMIT:
action: retry
owner: dependency_adapter
max_attempts: 3
backoff: exponential_with_jitter
INVALID_TOOL_ARGUMENTS:
action: repair
owner: agent_controller
max_repairs: 2
PERMISSION_DENIED:
action: stop
UNKNOWN_SIDE_EFFECT_OUTCOME:
action: reconcile
blind_resend: false
POISON_MESSAGE:
action: quarantine
BUDGET_EXCEEDED:
action: stop
這份 YAML 只能說明 Recovery Decision 應有 Owner、Budget 與 Outcome。它不能證明 Durable State、Idempotency、External Reconciliation、Concurrency Control 或 Error Mapping 已正確實作。
RunState 至少要保存:
- failure domain 與 effect certainty
- logical attempt 與 transport attempt
- recovery owner
- last delay 與
Retry-After - provider/model/tool 是否改變
- side-effect status 與 reconciliation cursor
- remaining budget
- terminal/quarantine reason
Recovery Action 本身也可能失敗。Compensation、Status Query、Fallback 與 Notification 都需要自己的 Attempt、Idempotency 與 Stop Policy。否則系統只是把無限迴圈從主流程搬到「修復流程」。
Timeout、Retry 與 Backoff 解決的是不同問題
這三個詞常被綁在一起,但責任不同。
Timeout 限制等待時間
Timeout 回答:一個操作最多等多久?
至少要區分:
- Model Call Timeout
- Individual Tool Timeout
- Workflow/Run Deadline
- User Approval Expiry
- Queue Waiting Deadline
Timeout 是呼叫端的觀察結果。它通常只代表「在期限內沒有收到答案」,不代表遠端沒有開始,也不代表遠端一定失敗。
Retry 用另一次嘗試交換成功機率
Retry 適合暫時性錯誤,例如短暫網路故障、Rate Limit 或可恢復的 Provider Error。它會增加 Latency、Cost 與下游負載。
AWS Builders’ Library 特別提醒,Retry 具有 selfish 的特性:呼叫端提高自己的成功機率,同時讓已經過載的下游承受更多工作。多層服務若每層各自 Retry,失敗流量可能被乘法放大。1
因此 Retry Policy 至少要限制:
- Maximum Attempts
- Total Retry Time
- 哪一層擁有 Retry Authority
- 哪些 Error Code 可重試
- 操作是否 Idempotent
- Deadline 是否仍允許下一次嘗試
- 是否遵守 Provider
Retry-After
Backoff 與 Jitter 避免同步重試
Exponential Backoff 讓每次等待時間逐漸增加。Jitter 再加入隨機性,避免大量 Client 在同一時間失敗,又在同一時間醒來,形成一波一波的 Retry Storm。
它們不能把不可重試的錯誤變成可重試,也不能替副作用操作提供 Idempotency。把 Jitter 加在重複付款上,只會讓事故比較有節奏。
Budget 與 Retry Authority 必須跨層可見
Agent 可以重新規劃、重新檢索、重生 Tool Arguments、切換模型並再次驗證。每條恢復路徑都會消耗資源。
常見 Budget 包括:
- Max Steps
- Max Model/Tool Calls
- Logical Retry Count
- Transport Retry Count
- Token/Monetary Budget
- Wall-clock Deadline
- Retrieval Depth
- Context Information-loss Limit
Runtime 必須強制並持久化這些限制。Prompt 裡寫「最多試三次」不是控制。
更麻煩的是隱藏重試:
- SDK 自動 Retry 三次
- Tool Wrapper 再試三次
- Agent Loop 重新呼叫三次
- Queue 在 Consumer Crash 後 Redeliver
每一層單獨看都很保守,組合起來卻可能放大成數十次呼叫。可靠設計應指定 One Retry Owner per Boundary,或至少把 Library Retry 納入同一個 Attempt Ledger。
每個下游呼叫還應傳遞剩餘 Deadline,而不是讓內層擁有比外層 Run 更長的 Timeout。當剩餘時間已不足以完成 Backoff+Execution+Verification 時,下一次 Attempt 不應被排入。
Retry Traffic 也是 Load。AWS Builders’ Library 提醒,Retry 會提高呼叫端成功機率,同時增加可能已過載的下游工作量;多層 Retry 會把 Failure 放大。1
Fallback 要保留 Contract,否則只是換一種失敗
Fallback 的目標是維持可接受服務,不是找到任何能回應的 Provider。
切換前至少要檢查:
- Tool/Structured Output Compatibility
- Context Capacity
- Safety、Policy、Privacy 與 Data Residency
- Required Credential、Region 與 Network Path
- Quality Floor 與 Verification Requirement
- Cost、Deadline 與 Rate Limit
- Current RunState/Checkpoint 是否可被新 Adapter 解讀
- 新路徑是否改變 Side-effect 或 Audit Semantics
例如原模型支援 Tool Calling 與大型 Context,Fallback Model 只支援純文字。API 回應成功,不代表原 Task Contract 還能成立。
Fallback 也可能是服務降級:
- 即時搜尋改用標示 Freshness 的 Cache
- 高精度 Reranker 改用基礎排序
- Browser Automation 改成 Read-only API
- 全量分析改成 Sampling
這些結果需要 degraded、partial 或其他明確狀態,並重新經過 Completion Gate。若 Fallback 降低了 Evidence Quality,舊路徑的 Verification 結論不能自動沿用。
Fallback 本身也要防止 Oscillation。兩個 Provider 若互相被設成備援,沒有 Cooldown 與 Attempt Budget 時,系統可能在兩個故障面之間來回彈跳。
Idempotency:相同 Intent、相同 Scope、相同 Operation Identity
Idempotency 是安全 Retry 的基礎之一,但它不等於「任何時候都可以重送」。
一個高風險 Operation Identity 通常至少綁定:
- Tenant/Actor/Business Intent
- Canonical Target 與 Arguments Digest
- Operation Version
- Idempotency Key
- Provider/Account Scope
- Dedupe Retention Window
- Preconditions 與 Expected Receipt
{
"operation_id": "refund-op-8f31",
"idempotency_key": "order-4821-refund-v1",
"request_hash": "sha256:illustrative-only",
"actor": "support-agent-17",
"tenant_id": "tenant-a",
"action": "create_refund",
"target": "payment_9027",
"amount": 12800,
"currency": "TWD",
"precondition_version": 18,
"status": "intent_committed",
"provider_request_id": null,
"dedupe_valid_until": "2026-07-09T10:00:00Z",
"expected_receipt": "refund_id"
}
這個例子說明 Side Effect 前應先保存 Intent、Canonical Request Hash 與去重邊界。它不能證明 Provider 支援 Idempotency、Clock/Retention 正確,或 Business Preconditions 仍有效。
Stripe 的 Idempotency Key 會保存第一次請求的結果,並拒絕同一 Key 搭配不同參數;官方文件也說明 Key 有保留期限,過期後重用可能被視為新請求。這是 Provider Contract,不是跨系統的永久保證。23
因此「同一 Business Intent 永遠重用同一 Key」仍不夠精確。安全條件應是:
- Canonical Intent 沒有改變
- Request Hash 一致
- Key 仍在 Provider 的 Dedupe Window 內
- Actor/Tenant/Account Scope 一致
- Preconditions 沒有過期
- Retry Policy 明確允許
若其中一項改變,系統需要新的 Operation Version,不能偷偷把新 Intent 偽裝成舊 Retry。
Idempotent 也不必然等於 Retry-safe。 一個 API 最終狀態可能具備 Idempotency,但目前 Approval 已過期、Inventory 已改變、Credential 被撤銷,或 Deadline 已經失效。Retry Safety 是更大的判斷。
Unknown Outcome:先查 Authority,再判斷是否可重送
Unknown Outcome 最容易被「再試一次」放大。

Figure 8-2|Timeout 後先保存 Unknown Outcome,依 Provider Contract 查詢 External Authority;not_found 只有在一致性與 Dedupe 條件成立時,才可能支持 Retry。
Intent committed
→ request submitted
→ external system may execute
→ response lost or deadline exceeded
→ local state = unknown
Harness 應使用 Operation ID、Idempotency Key、Provider Request ID、Business Key 或 Status API 做 Reconciliation,結果可能是:
completednot_started_confirmednot_found_inconclusivepartialfailedunknown
這裡最容易教錯的是把 not_found 直接等同 not_started。Status Index 可能 Eventually Consistent,查詢 API 也可能只看得到部分 Region 或時間範圍。Negative Result 是否足以支持 Retry,必須由 Provider Contract 決定。
安全的下一步通常是:
completed:保存 Receipt,繼續後續 State Transition。not_started_confirmed:在 Budget、Precondition 與 Approval 仍有效時 Retry。not_found_inconclusive:等待 Consistency Window、使用第二種 Lookup,或保持 Unknown。partial:停止後續不可逆步驟,進入 Forward Repair、Compensation 或人工處理。unknown:保持 Unknown,不宣告 Success/Failure。
若 Provider 明確保證同一 Idempotency Key 在有效 Window 內安全去重,系統可能在 Outcome 仍 Unknown 時以同一 Canonical Request 重送。這不是 Blind Retry,而是依 Provider Contract 執行的 Deduplicated Resubmission。若沒有這項保證,就應先 Reconcile 或 Escalate。
Stripe 的 Low-level Error Guidance 也指出,Network Fault 可能出現在 Request Lifecycle 的不同位置,因此需要 Idempotency 與後續 State Inspection。3
Exactly-once 通常只能在局部邊界成立
單一 Database Transaction 內,可以用 Constraint 與 Atomic Commit 控制寫入。跨越 Network、Queue、Worker 與第三方 API 後,端到端 Exactly-once 往往無法由一個元件單獨保證。
更實際的設計組合是:
- At-least-once Delivery
- Stable Operation/Occurrence ID
- Idempotent Consumer
- Inbox/Outbox 或 Durable Operation Record
- Deduplication Window
- Reconciliation
- Compensation/Forward Repair
- Audit Receipt
Queue 可能 Redeliver。Worker 可能完成 External Operation,卻在 Ack 前 Crash。Scheduler 也可能在 Failover 後重送 Trigger。每個 Boundary 都需要回答「這是不是同一個 Intent」,不能靠「我第一次看到這個 Message」判定。
因此,文章中的 Exactly-once 應被理解為某個明確 Scope 內的 Property,例如「同一 Database Unique Key 只 Commit 一次」,而不是一條穿越所有系統的魔法保證。
Compensation 與 Saga:修復動作也可能失敗
Email 已送出、退款進入清算、Deployment 已服務流量、Webhook 已抵達第三方。這些 Effect 無法靠 Database Rollback 回到完全相同的世界。
系統可能選擇:
- Forward Repair
- Reversal Transaction
- Snapshot Restore
- Disable/Quarantine
- Corrective Notification
- Manual Reconciliation

Figure 8-3|每個 Forward Step 與 Compensation 都保存自己的 Receipt、Idempotency 與 Failure State;後續修復不保證能回到原始世界,也不一定永遠採 Reverse Order。
Saga 將長交易拆成 Local Transaction。每個 Step 至少要定義:
- forward action 與 precondition
- durable receipt
- retry owner
- compensation 或 forward-repair action
- compensation idempotency key
- compensation receipt/failure state
- dependency order
- point of no return
- manual escalation path
Compensation 是新的 Side Effect,因此也可能 Timeout、Partial 或 Unknown。它需要自己的 Recovery State Machine,不能在 Audit Log 裡只寫一個 rolled_back: true。
補償順序常常是 Reverse Order,但不是定律。若 Email 已送出,先「撤銷 Email」通常不可能;若退款已完成而本地 Order State 更新失敗,最合理的路徑是 Forward Repair 本地狀態,而不是再做一次反向金流。
Create refund
→ update order state
→ send customer email
- Refund 成功、Order Update 失敗:保存 Refund Receipt,修本地 State。
- Email 失敗:Retry Notification,不重做 Refund。
- Refund 只能部分完成:凍結後續通知,進入 Compensation/Manual Review。
Saga 的價值不是自動倒帶,而是把「整個 Workflow 失敗」拆成可以判斷、追蹤與個別恢復的 Local State。
Circuit Breaker:保護依賴,不替 Retry 做決定
當 Provider 持續失敗時,Retry 會增加 Queue、Latency 與 Cost。Circuit Breaker 在錯誤率或其他健康訊號跨過門檻後,讓新 Call Fail Fast。
常見狀態是:
closed:正常放行open:短時間拒絕新 Callhalf_open:只允許少量 Probe
Circuit Breaker 與 Retry 解決不同問題:Retry 嘗試跨過短暫故障,Breaker 則阻止系統持續呼叫大概率失敗的依賴。Microsoft 的官方 Pattern 也明確區分這兩種責任。4
可靠實作需要:
- 明確 Failure Taxonomy,排除 Business Deny、Validation Error 等非健康故障
- 依 Provider、Operation Class、Region 或 Tenant 選擇合理 Scope
- Rolling Window、Cooldown 與 Probe Budget
- Half-open Probe 的隔離容量
- Shared State 或明確的 Instance-local Semantics
- Fallback、Queue、Reject 或 Degraded Path
- Recovery Evidence
Scope 太粗,一個 Endpoint 的故障可能讓整個 Provider 被封鎖;Scope 太細,又可能讓每個 Worker 都重新撞一次故障。Breaker State、Retry Budget 與 Admission Control 需要協調,否則一層說「停止呼叫」,另一層仍持續排 Background Retry。
Admission Control:不是所有工作都應該進入系統
Agent Workload 可能占用模型、Sandbox、Browser、外部 API 與長時間 Worker。只用一般 API Rate Limit,無法反映一個 Run 可能活十分鐘、開十個 Subagent、呼叫數百次 Tool 的成本。
開始 Run 前可以檢查:
- Tenant Quota
- Global/Per-tenant Concurrency
- Model 與 Tool Capacity
- Sandbox Availability
- Estimated Cost
- Priority
- Deadline
- Required Region/Credential
- Side-effect Risk
若系統明知沒有資源,仍把每個工作都接受進 Queue,只是把即時拒絕換成延遲很久的失敗。
Backpressure、Bulkhead 與 Load Shedding

Figure 8-4|Admission Control 限制入口,Backpressure 讓上游減速,Bulkhead 隔離資源池,Load Shedding 在過載時保留控制與關鍵工作。
Backpressure 讓上游知道下游吃不下
當下游變慢時,可以:
- 使用 Bounded Queue
- 降低 Parallelism
- 暫停新 Subagent
- 延遲低優先工作
- 拒絕已不可能在 Deadline 前完成的 Task
- 回傳 Queue Position 或
Retry-After - 切換相容且有容量的 Provider
Google SRE 將 Overload 視為需要主動處理的可靠性問題,包含早期拒絕、Load Shedding 與 Degraded Response。系統若讓過量工作持續排隊,Latency、Timeout 與 Retry 會互相放大。5
Bulkhead 隔離故障範圍
可以依以下維度分開 Resource Pool:
- Tenant
- Interactive vs Batch
- Read-only vs Side-effect Work
- High-cost vs Low-cost Model
- External Provider
- Evaluation Traffic
- Region
這樣一個租戶的巨大 Batch Job,不會吃掉所有 Interactive Approval 與 Cancellation 能力。
Load Shedding 要保留控制路徑
過載時的優先順序不應只看誰先進 Queue。通常應優先保留:
- Kill Switch 與 Cancellation
- Approval Response
- 已執行高風險副作用的 Reconciliation
- Active Critical Work
- Interactive Request
- Batch 與 Speculative Work
最糟的過載策略,是把取消工作與查詢事故狀態也一起塞在同一條爆滿 Queue 裡。系統不只失控,連煞車線都排隊。
Async Work Lifecycle:Acceptance、Execution、Retention、Delivery 分開
Slow Tool、Indexing Job、Benchmark、Background Agent 與大型 Extraction 都需要 Durable Work Identity。
常見執行狀態包括:
accepted → queued → running → completed
↘ failed
↘ cancelled
↘ expired
↘ unknown
Work Unit 至少保存:
- work ID、type、parent run
- owner/claim generation
- created、queued、started、heartbeat、terminal timestamps
- current state 與 state version
- output/error reference
- retry、idempotency 與 cancellation metadata
- notification/delivery state
- retention policy
Background Acceptance 是原始 Tool Call 的 Terminal Result
{
"work_id": "work-391",
"status": "accepted",
"inspection_uri": "artifact://work-391/status",
"cancellable": true,
"side_effect_may_have_started": false
}
後續 completed、failed 或 cancelled 是 Work Lifecycle Event,不是原始 Tool Request 的第二個 Result。
但 Terminal Work State 也不等於 Result 已交給 Parent。至少要分開:
- Work Terminal State committed
- Result Artifact retained
- Completion Event written to Outbox/Event Log
- Subscriber Delivery attempted
- Subscriber Ack/Parent Acceptance if the protocol supports it
- Retention/Tombstone transition
Result Retention 不應依賴一個模糊的「已通知」
有些 Protocol 沒有可靠 End-to-end Ack。此時系統不能假裝知道使用者已看見結果。它至少應保存 Delivery Record、Attempt、Subscriber Cursor 與 Result Expiry,允許 Pull/Replay。
清理可採分階段:
- 先釋放 Sandbox、Browser、Worker 等大型資源
- 保留 Terminal State 與 Result Artifact
- 透過 Durable Outbox 發送 Completion Event
- 依 Ack/Cursor/Retention Policy 決定何時縮成 Audit Tombstone
notification_sent 只能證明通知服務接受了 Message,不證明 Parent 已處理、使用者已看見,或結果已被接受。
Durable Execution:Replay Workflow,不等於 Replay 外部副作用
Durable Execution 讓 Workflow 在 Process、Worker 或 Infrastructure Failure 後,從 Event History 與已提交進度恢復。Temporal 以 Durable Workflow Execution、Event History 與 Deterministic Replay 為核心模型。6
這個能力仍有明確邊界:
- Workflow Orchestration Code 需要 Determinism/Versioning
- External API、Database 與其他 Non-deterministic Work 應放在 Activity/Tool Boundary
- Activity 仍可能 At-least-once 執行
- Side Effect 仍需要 Idempotency 與 Unknown-outcome Reconciliation
- Workflow Code Upgrade 需要 Versioning/Patching
- Invalid Business Input 不會因 Durable Retry 變有效
Temporal 的官方 Versioning 文件也明確指出,Replay 要求 Workflow Code 維持 Determinism,長時間執行需要 Patching 或 Worker Versioning。7
Durability 保存的是「流程知道自己走到哪裡」。它不會自動替外部世界提供 Exactly-once、Compensation 或正確 Business Decision。
Layered Context Degradation:Context 超限也要有恢復順序
Context Failure 也是 Reliability 問題。
當 Model Context 接近上限,不應直接把整段 Transcript 丟進一次黑箱摘要。比較安全的降級順序是:
- 先持久化大型 Raw Artifact
- 替換可重建的舊 Tool Payload
- 移除低價值中間對話
- 建立符合 Schema 的 Semantic Summary
- 必要時執行 Emergency Compaction
- 已無法保留關鍵狀態時 Safe Stop
裁剪時不能拆開:
- Tool Request 與 Correlated Result
- Approval Request 與 Decision
- Background Acceptance 與 Work ID
- Side-effect Operation 與 Receipt
- Task Claim 與 Ownership Record
Summary 至少要保存:
- Current Objective
- User Constraint
- Decision 與理由
- Modified Resource
- Test/Evidence
- Open Failure
- Active Operation ID
- Remaining Work
- Next Safe Action
若摘要後仍超限,或 Compaction 反覆失敗,Harness 應有 Circuit Breaker。繼續叫模型壓縮模型剛剛壓縮過的摘要,通常不會得到更濃縮的真理,只會得到更薄的證據。
Scheduled Work:每次 Occurrence 都需要獨立 Identity
Scheduled Agent Task 不應在 Timer Callback 裡直接跑完整 Workflow。
Schedule definition
→ occurrence created
→ queue delivery
→ agent consumer executes
→ terminal result retained
→ completion delivered
→ subscriber notified

Figure 8-5|Schedule Definition、Occurrence、Queue Delivery、Work Execution、Result Retention 與 Notification 是不同狀態;Redelivery 不會建立新的 Business Intent。
Job Definition 至少包含:
- job ID、schedule、timezone
- tenant、owner、payload reference
- recurring/one-shot mode
- misfire/catch-up policy
- overlap/concurrency policy
- expiry/deadline
- allowed agent config version
- approval requirement
- schedule version
每次觸發還需要:
- occurrence ID
- scheduled-for timestamp
- dedupe/idempotency key
- definition version
- delivery attempt
- work ID
這樣 Queue Redelivery 或 Scheduler Failover 才能辨認「同一個 Occurrence 再送一次」,而不是建立兩份報告或兩次付款。
Durable Definition 不等於 Durable Execution
- Durable Definition:Schedule 在 Restart 後仍存在。
- Durable Trigger:到期 Occurrence 會被外部 Scheduler/Store 持久化。
- Durable Execution:Worker Crash 後仍能從已提交進度恢復。
- Durable Delivery:Terminal Result 可被 Pull/Replay,不因 Notification 暫時失敗而遺失。
保存 Cron Expression 只證明系統記得「想在什麼時候工作」。
Misfire、Catch-up 與 Overlap 都要明確
系統錯過時間點後可能:
- skip
- fire once now
- replay missed occurrences
- coalesce into one run
前一個 Occurrence 尚未完成時,還要決定:
- skip new occurrence
- buffer one/all
- cancel previous
- allow overlap
Temporal 的 Schedule 文件同樣把 Overlap Policy 與 Catch-up Window 視為不同控制,選擇不當會造成漏跑或恢復後的突發重放。8
Timezone 與 DST 也不能只留給 UI。02:30 在某些日期可能不存在或出現兩次;Occurrence Identity 應保存實際 Instant 與原始 Schedule Semantics。
Failure Testing:驗證 Invariant,不只看最後有沒有成功
Reliability Test 應在精確 Boundary 注入 Fault:
- Intent Commit 前後
- Request Send 前後
- External Side Effect 完成但 Receipt 未保存
- Queue Ack 前 Worker Crash
- Compensation 中途
- Circuit Half-open Probe
- Schedule Trigger Persist 前後
- Notification Outbox Commit 前後
至少覆蓋:
- Model/Tool Timeout
- Rate Limit/Provider Outage
- Duplicate Delivery
- Partial Batch Failure
- Worker Crash/Checkpoint Recovery
- Unknown Outcome/Eventually Consistent Status Query
- Stale Claim Submission
- Queue Backlog/Retry Storm
- Circuit Open/Half-open
- Context Compaction Failure
- Scheduled Misfire/Overlap
- Notification Redelivery
驗收不只問「最後成功了嗎」,還要檢查:
- 是否有 Duplicate Side Effect
- Attempt/Cost/Deadline 是否有界
- RunState 是否可恢復
- Receipt/Unknown State 是否完整
- Queue Load 是否被 Retry 放大
- User-visible State 是否誠實
- Stop/Quarantine 是否生效
- Compensation 是否有自己的 Audit Trail
Chaos Testing 不是隨機 Kill 幾個 Worker 就結束。每個 Test 應有 Hypothesis、Injected Fault、Expected State Transition、Forbidden Outcome 與 Recovery Evidence。若無法說出哪個 Invariant 被驗證,這次 Fault Injection 只是在製造噪音。
用退款案例走一次完整 Recovery
回到開頭的退款任務。
1. 建立 Intent
Harness 先建立 operation_id 與 Idempotency Key,保存訂單、金額、租戶、Actor、Policy Decision 與預期 Receipt。
2. 發送退款請求
Tool Adapter 呼叫支付服務。RunState 記錄 submitted,但尚未標記完成。
3. 發生 Timeout
Recovery Classifier 將錯誤標記為 UNKNOWN_SIDE_EFFECT_OUTCOME,不是普通 TOOL_TIMEOUT。
4. Reconciliation
Harness 使用 Idempotency Key、Provider Request ID 或 Payment ID 查詢退款狀態。
- 若已完成:保存 Refund Receipt,更新訂單狀態。
- 若確認未開始:在 Budget 內以同一 Idempotency Key 重試。
- 若 Partial:凍結後續 Email,交給補償或人工處理。
- 若仍 Unknown:保持
unknown,定時 Poll,不宣告完成。
5. 訂單狀態更新衝突
資料庫回報 Version Conflict。這不是重新退款的理由。Harness Reload Canonical State,重新執行本地 State Transition。
6. 客戶通知失敗
Email Provider Rate Limit。Email Tool 使用自己的 Idempotency/Message ID,按 Retry-After 重試。退款不需要重做。
7. Completion Gate
Verifier 檢查:
- 支付服務的 Refund Receipt
- 訂單的 Canonical Refund State
- Notification State
- Operation Record 與 Audit Trail
只有模型說「退款已處理」不構成完成證據。
這個案例裡,同一個 Run 出現三種失敗:Unknown Outcome、State Conflict 與 Rate Limit。三者都叫 Error,但 Recovery Action 完全不同。
用十五個問題審查 Reliability Harness
- Failure 是否同時描述 Domain 與 Side-effect Certainty?
- 哪個元件持有 Recovery Decision Authority?
- Logical Attempt、SDK Retry、Queue Redelivery 是否進入同一 Ledger?
- Deadline 是否向下傳遞,還是每一層重新開始計時?
- 哪些操作具備明確的 Idempotency Scope、Request Hash 與 Dedupe Window?
- Retry-safe 是否重新檢查 Approval、Precondition、Credential 與 Budget?
- Timeout 後如何查 External Authority?
not_found是否可能只是尚未一致? - Unknown Outcome 是否能保持 Unknown,而不被猜成 Success/Failure?
- Compensation 是否有自己的 Idempotency、Receipt、Failure State 與 Manual Path?
- Fallback 是否保留 Tool、Policy、Region、Evidence Quality 與 Completion Contract?
- Circuit Breaker、Retry Budget 與 Admission Control 是否共享合理 Scope?
- Control、Approval 與 Reconciliation Capacity 是否受到保護?
- Background Completion、Result Retention、Delivery 與 Parent Acceptance 是否分離?
- Scheduled Occurrence 是否有 Stable ID、Overlap、Misfire、Catch-up 與 Expiry Policy?
- 是否在 Commit Boundary 實際測過 Duplicate、Crash、Unknown、Partial 與 Overload?
若主要答案仍是「失敗就再跑一次」「Queue 會處理」「Workflow Engine 應該記得」或「最後看結果」,可靠性仍然建立在希望上。
理論轉實作:Part 08 檢查表
- Failure Model 同時保存 Domain、Effect Certainty 與 Recovery Eligibility。
- Recovery State Machine 為 Retry、Repair、Fallback、Reconcile、Compensate、Quarantine 與 Stop 指定 Authority。
- Retry 有單一 Owner 或共享 Attempt Ledger,並受 Deadline、Cost、Backoff 與 Jitter 限制。
- Fallback 會標示 Degradation,並重新通過 Compatibility 與 Completion Gate。
- High-risk Operation 在執行前保存 Canonical Intent、Request Hash、Operation ID、Idempotency Scope 與 Expected Receipt。
- Retry 檢查 Dedupe Window、Precondition、Approval、Credential 與相同 Canonical Request。
- Unknown Outcome 有獨立狀態;Negative Lookup 不會未經 Provider Contract 就被當成
not_started。 - External Side Effect 具備 Receipt、Missing-receipt/Unknown State 與 Reconciliation Path。
- Saga 支援 Forward Repair;Compensation 有自己的 Idempotency、Receipt、Failure 與 Manual Escalation。
- Circuit Breaker 排除 Business Error,並定義 Scope、Cooldown、Half-open Probe 與 Recovery Evidence。
- Admission、Backpressure、Bulkhead 與 Load Shedding 保留 Cancellation、Approval 與 Reconciliation Capacity。
- Async Work 分開 Acceptance、Execution、Terminal Result、Retention、Delivery 與 Parent Acceptance。
- Durable Execution 沒有被誤認為 External Exactly-once;Workflow Replay 與 Activity Side Effect 分開。
- Context Degradation 先保存 Evidence,且不拆 Request/Result、Approval/Decision 或 Operation/Receipt。
- Scheduled Work 使用 Occurrence ID,並定義 Timezone、DST、Misfire、Catch-up、Overlap、Expiry 與 Config Version。
- Failure Test 在 Durable Boundary 驗證 State Transition、Forbidden Outcome 與 Recovery Evidence。
Part 09 會接著處理另一條獨立邊界:一個操作即使能可靠執行,也不代表它有權執行。下一篇會從 Identity、Permission、Approval、Sandbox、Credential、Tenant Boundary 與 Egress Policy 建立安全鏈。
References
Footnotes
-
Amazon Web Services, Timeouts, retries, and backoff with jitter, accessed 8 July 2026. ↩ ↩2
-
Stripe, Idempotent requests, accessed 8 July 2026. ↩
-
Stripe, Advanced error handling, accessed 8 July 2026. ↩ ↩2
-
Microsoft Azure Architecture Center, Circuit Breaker pattern, accessed 8 July 2026. ↩
-
Google, Handling overload and Addressing cascading failures, accessed 8 July 2026. ↩
-
Temporal, Temporal Platform Documentation and Understanding Temporal, accessed 8 July 2026. ↩
-
Temporal, Safely deploying changes to Workflow code, accessed 8 July 2026. ↩
-
Temporal, Schedule and Troubleshoot missed Schedule Actions, accessed 8 July 2026. ↩