Skip to main content

durable-execution

Implements durable execution patterns for fault-tolerant AI agent workflows including checkpoint-based persistence, exponential backoff retry, idempotency keys, and crash recovery strategies across LangGraph, Temporal, and generic async frameworks.

来源信息

仓库
paulpas/agent-skill-router
最近来源活动
2026年6月4日 23:31
检测到的 SKILL.md 语言
英语
星标
7
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
durable-execution
description
Implements durable execution patterns for fault-tolerant AI agent workflows including checkpoint-based persistence, exponential backoff retry, idempotency keys, and crash recovery strategies across LangGraph, Temporal, and generic async frameworks.
license
MIT
compatibility
opencode
metadata
{"version":"1.0.0","domain":"agent","triggers":"durable execution, checkpointing, crash recovery, idempotency, retry strategy, state persistence, fault tolerance, temporal","role":"implementation","scope":"implementation","output-format":"code","content-types":["code","guidance","do-dont","examples"],"related-skills":"langgraph-state-machine, tool-use-function-calling, multi-agent-orchestration, agent-reliability-engineering","archetypes":["tactical","enforcement"],"anti_triggers":["brainstorming","vague ideation"],"response_profile":{"verbosity":"low","directive_strength":"high","abstraction_level":"operational"}}
# Durable Execution for AI Agent Workflows Implements durable execution patterns that ensure AI agent workflows can survive crashes, network failures, and API rate limits without losing progress. This skill covers checkpoint-based state persistence, idempotent operation design, exponential backoff retry strategies, replay detection, and the saga pattern for multi-step compensation — essential primitives for production-grade agent systems as of 2025-2026. ## TL;DR Checklist - [ ] Wrap every workflow step in a try/except with structured error classification - [ ] Assign idempotency keys to all side-effect-producing operations (API calls, DB writes) - [ ] Implement exponential backoff with jitter for retryable errors - [ ] Persist state checkpoints after each successful node completion - [ ] Detect and handle replay scenarios during recovery (Temporals' `replay_started` pattern) - [ ] Design compensation logic for every irreversible action in a multi-step workflow --- ## When to Use Use this skill when: - Building AI agent workflows that must survive process crashes, restarts, or OOM kills - Your agents call external APIs (LLM providers, web services, databases) that can fail transiently - You need deterministic replay of completed steps after a failure (not re-execution from scratch) - Multi-step workflows require compensation — undoing previous actions when a later step fails - Your system operates in production with SLA requirements that demand fault tolerance ## When NOT to Use Avoid this skill for: - Purely stateless, idempotent single-shot LLM calls (no workflow state to persist) - Prototypes and one-off experiments where crash recovery adds unnecessary complexity - Real-time inference pipelines where checkpoint overhead is unacceptable (< 10ms p99 latency) --- ## Core Workflow ### 1. Classify Errors for Retry Strategy Not all errors are equal. Classify every failure into retryable, terminal, or recoverable — this determines whether you retry, give up, or attempt recovery. ```python import enum import time import random from dataclasses import dataclass class ErrorCategory(str, enum.Enum): """Classifies workflow errors by their retry behavior.""" RETRYABLE = "retryable" # Transient: network timeout, rate limit, API error 429/503 TERMINAL = "terminal" # Permanent: invalid input, schema mismatch, auth failure RECOVERABLE = "recoverable" # Needs intervention: human approval required, external dependency down @dataclass(frozen=True) class WorkflowError: """Structured error that carries classification and retry metadata.""" category: ErrorCategory message: str original_exception: Exception | None = None retry_after_seconds: float = 0.0 @classmethod def from_http_error(cls, status_code: int, body: str) -> "WorkflowError": """Classify HTTP errors by status code.""" if 400 <= status_code < 500: return cls( category=ErrorCategory.TERMINAL, message=f"Client error {status_code}: {body}", ) elif status_code == 429: return cls( category=ErrorCategory.RETRYABLE, message=f"Rate limited (429). Retry after headers.", retry_after_seconds=2.0, ) elif status_code >= 500: return cls( category=ErrorCategory.RETRYABLE, message=f"Server error {status_code}: {body}", retry_after_seconds=5.0, ) else: return cls( category=ErrorCategory.TERMINAL, message=f"Unexpected HTTP error {status_code}: {body}", ) def is_retryable(self) -> bool: return self.category in (ErrorCategory.RETRYABLE, ErrorCategory.RECOVERABLE) ``` **Checkpoint:** Every LLM API call, tool invocation, and external service call must catch exceptions and convert them to `WorkflowError` objects. Never propagate raw third-party exceptions up the workflow stack — they carry framework-specific details that break recovery logic. ### 2. Implement Exponential Backoff with Jitter When retrying transient failures, use exponential backoff with random jitter to prevent thundering herd problems when multiple agents restart simultaneously. ```python import asyncio import logging from typing import Callable, TypeVar T = TypeVar("T") logger = logging.getLogger(__name__) def calculate_backoff( attempt: int, base_delay: float = 1.0, max_delay: float = 60.0, jitter_factor: float = 0.5, ) -> float: """Calculate delay for exponential backoff with random jitter. Args: attempt: Current retry attempt number (0-based). base_delay: Initial delay in seconds. Doubles each attempt. max_delay: Maximum delay cap in seconds. jitter_factor: Random jitter multiplier (0.0 = no jitter, 0.5 = ±50%). Returns: Delay in seconds before the next retry. Formula: min(base_delay * 2^attempt * random(1 - jitter, 1 + jitter), max_delay) """ exponential_delay = base_delay * (2 ** attempt) jitter_range = exponential_delay * jitter_factor jitter = random.uniform(-jitter_range, jitter_range) return min(exponential_delay + jitter, max_delay) async def retry_with_backoff( operation: Callable[[], T], max_retries: int = 3, base_delay: float = 1.0, error_types: tuple[type[Exception], ...] | None = None, ) -> T: """Execute an operation with exponential backoff retry. Args: operation: Async callable to execute. max_retries: Maximum number of retries before giving up. base_delay: Base delay in seconds (doubles each attempt). error_types: Only retry these exception types. None = retry all. Returns: The return value of the successful operation. Raises: Last exception if all retries are exhausted. """ last_exception: Exception | None = None for attempt in range(max_retries + 1): try: if asyncio.iscoroutinefunction(operation): return await operation() else: return operation() except Exception as exc: last_exception = exc # Only retry if error type is in the allowed set (or any type if None) if error_types and not isinstance(exc, error_types): raise if attempt >= max_retries: break delay = calculate_backoff(attempt, base_delay) logger.warning( "Attempt %d/%d failed. Retrying in %.1fs: %s", attempt + 1, max_retries, delay, exc, ) await asyncio.sleep(delay) raise RuntimeError( f"Operation failed after {max_retries + 1} attempts. Last error: {last_exception}" ) from last_exception # --- Usage Examples --- async def call_llm_with_retry(prompt: str) -> str: """Call an LLM API with automatic retry on transient failures.""" import httpx # noqa: F811 async def _llm_call() -> str: async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( "https://api.openai.com/v1/chat/completions", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={ "model": "gpt-4", "messages": [{"role": "user", "content": prompt}], "max_tokens": 1024, }, ) response.raise_for_status() return response.json()["choices"][0]["message"]["content"] # Only retry on network errors and server errors (5xx) return await retry_with_backoff( operation=_llm_call, max_retries=3, base_delay=2.0, error_types=(httpx.HTTPError, httpx.RemoteProtocolError), ) async def call_with_workflow_error(prompt: str) -> str: """Call LLM with WorkflowError classification.""" import httpx # noqa: F811 last_error: Exception | None = None for attempt in range(4): try: async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( "https://api.openai.com/v1/chat/completions", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={"model": "gpt-4", "messages": [{"role": "user", "content": prompt}], "max_tokens": 1024}, ) response.raise_for_status() return response.json()["choices"][0]["message"]["content"] except httpx.HTTPStatusError as exc: workflow_error = WorkflowError.from_http_error(exc.response.status_code, exc.response.text) if workflow_error.is_retryable(): delay = calculate_backoff(attempt, base_delay=2.0) logger.info("Retryable error %s. Sleeping %.1fs", workflow_error.message, delay) await asyncio.sleep(delay + workflow_error.retry_after_seconds) last_error = exc else: raise RuntimeError(workflow_error.message) from exc except httpx.HTTPError as exc: # Network errors are always retryable delay = calculate_backoff(attempt, base_delay=2.0) logger.info("Network error %s. Retrying in %.1fs", exc, delay) await asyncio.sleep(delay) last_error = exc raise RuntimeError(f"LLM call failed after 4 attempts") from last_error ``` ### 3. Design Idempotent Operations Every side-effect-producing operation must be idempotent — producing the same result when called multiple times with the same input. This is essential for crash recovery: after a restart, you cannot know whether the original call succeeded or failed, so re-executing must be safe. ```python import hashlib import json import uuid from datetime import datetime, timezone class IdempotencyStore: """Thread-safe idempotency key store backed by an in-memory dict. In production, replace with Redis (SET NX EX) or a relational DB with a unique constraint on the idempotency_key column. """ def __init__(self) -> None: self._cache: dict[str, dict] = {} self._lock: object = object() # Placeholder — use threading.Lock in prod @staticmethod def generate_key(operation_name: str, input_hash: str) -> str: """Generate a deterministic idempotency key from operation name and inputs.""" raw = f"{operation_name}:{input_hash}" return hashlib.sha256(raw.encode()).hexdigest()[:32] @staticmethod def hash_input(data: dict) -> str: """Create a stable hash of input data for idempotency key generation.""" canonical = json.dumps(data, sort_keys=True, default=str) return hashlib.sha256(canonical.encode()).hexdigest()[:16] def check_and_set(self, key: str, result: dict, ttl_seconds: int = 3600) -> bool: """Atomically check if a result exists for this idempotency key. Args: key: The idempotency key to check/set. result: The operation result to cache if key is new. ttl_seconds: How long to keep the cached result. Returns: True if the key was newly set (first call). False if the key already existed (duplicate call — return cached result). """ # In production: use Redis SET NX EX or DB transaction if key in self._cache: return False # Duplicate — caller should return cached result self._cache[key] = { "result": result, "created_at": datetime.now(timezone.utc).isoformat(), "ttl_seconds": ttl_seconds, } return True # New call — proceed with operation def get_result(self, key: str) -> dict | None: """Retrieve cached result for an idempotency key.""" entry = self._cache.get(key) if entry is None: return None created_at = datetime.fromisoformat(entry["created_at"]) age_seconds = (datetime.now(timezone.utc) - created_at).total_seconds() if age_seconds > entry["ttl_seconds"]: del self._cache[key] # Expired return None return entry["result"] class IdempotentOperation: """Mixin class providing idempotent operation execution.""" def __init__(self) -> None: self.idempotency_store = IdempotencyStore() async def execute_idempotently( self, operation_name: str, input_data: dict, operation_fn: Callable[..., dict], ttl_seconds: int = 3600, ) -> dict: """Execute an operation with idempotency guarantee. If the same operation with the same inputs was previously executed (and cached), returns the cached result without re-executing. Args: operation_name: Logical name of the operation (e.g., "web_search", "code_exec"). input_data: Input parameters to hash for deduplication. operation_fn: The actual operation to execute. ttl_seconds: How long to cache results. Returns: Operation result — either newly computed or from cache. """ input_hash = self.idempotency_store.hash_input(input_data) key = self.idempotency_store.generate_key(operation_name, input_hash) # Check for existing cached result (idempotent read) cached_result = self.idempotency_store.get_result(key) if cached_result is not None: logger.info("Idempotent hit for %s (%s)", operation_name, key[:12]) return cached_result # Execute the actual operation result = operation_fn(input_data) # Cache the result (idempotent write) self.idempotency_store.check_and_set(key, result, ttl_seconds) logger.info("Idempotent miss for %s (%s) — cached result", operation_name, key[:12]) return result # --- Usage Example --- def search_web(input_data: dict) -> dict: """Example tool that is NOT naturally idempotent (web content changes).""" # In production: make this call an external API return { "query": input_data["query"], "results": [ {"title": f"Result 1 for {input_data['query']}", "url": f"https://example.com/1"}, {"title": f"Result 2 for {input_data['query']}", "url": f"https://example.com/2"}, ], } async def idempotent_search(operation: IdempotentOperation, query: str) -> dict: """Search with idempotency — same query returns cached results.""" return await operation.execute_idempotently( operation_name="web_search", input_data={"query": query}, operation_fn=search_web, ttl_seconds=300, # Cache for 5 minutes (web results are relatively stable) ) ``` ### 4. Implement Checkpoint-Based State Persistence Persist workflow state after each step completes. On recovery, reload the last checkpoint and resume from where you left off. ```python import json import os from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from pathlib import Path from typing import Any @dataclass class Checkpoint: """Serialized workflow state at a specific point in execution.""" workflow_id: str step_index: int step_name: str state: dict[str, Any] timestamp: str = "" metadata: dict[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: if not self.timestamp: self.timestamp = datetime.now(timezone.utc).isoformat() class CheckpointStore: """File-based checkpoint store for workflow state persistence. In production, replace with a database (PostgreSQL) or Redis for concurrent access and atomic writes. """
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看