durable execution을 사용하면 AI agent가 process crash, timeout, deployment, 긴 대기 이후에도 이미 완료된 부작용을 반복하지 않고 여러 단계의 작업을 재개할 수 있습니다. 이를 위해 진행 상황을 영구 저장하고, 쓰기를 안전하게 반복할 수 있어야 하며, 결과를 알 수 없는 호출을 복구하는 규칙이 필요합니다.
retry loop만으로는 durable execution이 되지 않습니다. 응답이 사라진 뒤 email 전송이나 payment를 재시도하면 시스템이 복구하려던 바로 그 실패가 발생할 수 있습니다.
외부 영향을 중심으로 workflow를 모델링하세요
invoice를 생성하고 전송한 뒤 task를 완료로 표시하는 workflow를 생각해 보세요. 전송은 성공했지만 worker가 성공을 기록하기 전에 crash하면 다음 실행에서는 성공과 실패 어느 쪽도 안전하게 가정할 수 없습니다.
sequenceDiagram
participant W as Workflow
participant L as Operation ledger
participant E as Email provider
W->>L: Reserve send_invoice:task_442
W->>E: Send with operation key
E-->>W: Response lost
Note over W: Worker restarts
W->>L: Read operation status
W->>E: Reconcile by provider ID or key
E-->>W: Already sent
W->>L: Mark completed
W->>W: Continue from checkpoint
workflow record에는 시스템이 의도한 작업이 담깁니다. provider나 기준이 되는 backend에는 실제로 일어난 일이 담깁니다. 복구 과정에서는 호출을 반복할지 결정하기 전에 두 정보를 대조합니다.
workflow와 activity를 분리하세요
workflow는 step, wait, branch, retry를 조율합니다. activity는 API 호출이나 file 쓰기 같은 외부 영향을 수행합니다. replay 기반 engine을 사용할 때 workflow 결정은 deterministic하게 유지하고, network call, clock, random value, provider SDK는 activity 안에 격리하세요.
Temporal의 durable execution 모델은 event history를 기록하고 deterministic replay로 실패 후 workflow state를 복원합니다. 이 engine을 도입할 수도 있고 operations table과 queue를 사용해 더 작은 state machine을 구현할 수도 있습니다. 요구 사항은 같습니다. 저장된 history가 resume 지점을 결정해야 합니다.
재시도 가능한 모든 쓰기를 idempotent하게 만드세요
논리적 영향 하나에 key 하나를 할당한 뒤 client, worker, provider의 재시도 전체에서 같은 key를 사용하세요. 첫 terminal result를 저장하고 다른 parameter로 같은 key를 재사용하면 거부합니다.
Stripe의 idempotent request contract는 실패를 포함해 한 key로 들어온 첫 요청의 status code와 body를 저장합니다. retention 동작은 Stripe 고유의 것이지만 설계 원칙은 다른 곳에도 적용됩니다. 반복 요청이 같은 operation인지는 model이 아니라 server가 판단해야 합니다.
provider가 idempotency를 지원하지 않으면 adapter 주위에 operation ledger를 두세요. 자체 재시도에서 발생하는 중복은 줄일 수 있지만 timeout이 난 provider call이 실패했다고 증명할 수는 없습니다. reconciliation lookup을 추가하거나 해결되지 않은 영향이 큰 결과를 사람이 검토하게 하세요.
검증된 사실을 checkpoint에 기록하세요
유용한 checkpoint는 완료된 operation ID와 다음에 허용되는 transition을 기록합니다. 영향을 확인한 뒤 기록하고, 가능하다면 자체 database write와 state transition을 atomic하게 처리하세요.
model message만 checkpoint로 삼지 마세요. conversation history의 “The email was sent”는 provider result가 아니라 model의 주장일 수 있습니다. provider message ID나 operation record reference를 저장하세요.
persistent state 가이드는 task progress를 memory 및 business data와 구분합니다.
각 retry를 어느 계층이 맡는지 정하세요
Model SDK, HTTP client, queue, workflow engine, tool server, provider가 모두 재시도할 수 있습니다. 이 동작들이 곱해지면 세 번으로 설정한 시도가 수십 번의 호출로 늘어날 수 있습니다.
각 operation에 다음 내용을 문서화하세요.
- 어떤 실패를 재시도할 수 있는가
- 어느 계층이 재시도를 수행하는가
- 최대 시도 횟수와 총 경과 시간
- backoff와 provider rate-limit signal
- 시도마다 재사용하는 idempotency key
- 시도 소진 후 terminal state
validation과 authorization 실패는 중단해야 합니다. rate limit에는 기다릴 수 있습니다. 쓰기 작업의 network timeout은 재시도 전에 결과를 조정해야 합니다. model이 error string만 보고 이런 규칙을 즉석에서 정하게 해서는 안 됩니다.
대기를 저장된 state로 취급하세요
human approval, scheduled time, webhook, long-running deployment는 잠든 process가 아니라 workflow state입니다. wait condition과 wake-up signal을 저장하세요. 중복 signal을 안전하게 처리하고, 지연된 승인을 재개할 때 해당 사용자가 여전히 권한을 갖고 있는지 확인하세요.
cancellation을 state transition으로 제공하세요. 취소는 이후 단계를 멈춰야 하지만 이미 완료된 부작용을 지울 수는 없습니다. invoice 무효화 같은 compensation은 cancellation과 별도로 정의하세요.
결과를 알 수 없는 구간을 테스트하세요
성공 경로 테스트는 가장 어려운 상태를 놓칩니다. server가 요청을 완료했지만 client가 응답을 받지 못했을 수 있는 상태입니다. 외부 호출 직후, 완료 record를 쓰기 전에 실패를 주입하세요. 다른 worker에서 다시 시작해 결과가 한 번만 조정되는지 확인합니다.
approval 중, backoff 중, cancellation 후에도 중단해 보세요. 동일한 workflow ID와 operation ID로 원래 시도와 재개된 실행 전체에 trace 하나를 기록하세요. 실패 복구 가이드는 이 사례를 전체 runbook으로 바꿔 줍니다.
아래 프롬프트는 production을 건드리지 않고 첫 중단 테스트를 설계합니다. 예상 결과가 “the agent will figure it out”이라면 아직 workflow에 복구 contract가 없는 것입니다.