🤖 Agents IA

agent-retry-strategist

Stratégies de retry intelligentes pour sous-agents qui échouent — backoff, fallback, alternatives et recovery.

⚡ Installation & lancement en 1 commande

Copiez-collez dans votre terminal : le skill s'installe dans ~/.claude/skills et Claude Code se lance directement dessus.

macOS / Linux
curl -fsSL https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.sh | sh -s -- agent-retry-strategist --launch
Windows (PowerShell)
iex "& { $(iwr -useb https://raw.githubusercontent.com/khalilbenaz/claude-skills-collection/main/install.ps1) } agent-retry-strategist -Launch"

🚀 Déjà installé ?

claude "/agent-retry-strategist"

Ou tapez /agent-retry-strategist dans une session Claude Code, ou décrivez simplement votre besoin — le skill se déclenche automatiquement via le skill-router.

🔑 Déclencheurs automatiques

Le skill s'active automatiquement quand votre demande contient :

retry agentagent qui échoueagent retryfallback agenterror recovery agentagent resilienceagent failure handlingrelancer agent

📦 Installation manuelle

git clone https://github.com/khalilbenaz/claude-skills-collection.git cp -r claude-skills-collection/skills/agent-retry-strategist ~/.claude/skills/

Payload du plugin : skills/agent-retry-strategist · source éditable : agent-skills/retry-strategist

📖 Manuel

Agent Retry Strategist

Quand utiliser ce skill

Dès qu'un sous-agent peut échouer et que l'échec ne doit pas remonter brutalement. Cas typiques : pipelines de production avec erreurs transitoires (rate limit, timeout, 503), architectures multi-modèles avec quota par provider, traitements longs nécessitant des checkpoints.


Workflow — 10 étapes actionnables

Étape 1 — Classifier l'erreur avant tout retry

ClasseSignauxAction
TRANSIENT429, 503, timeout, connexion resetRetry avec backoff
PERMANENT400 schéma invalide, content policy, token limitFallback ou escalade — jamais de retry
UNKNOWNToute autre exceptionRetry prudent (max 1–2 fois)
TRANSIENT_SIGNALS = ["rate limit", "429", "503", "timeout", "connection", "temporarily"]
PERMANENT_SIGNALS = ["token limit", "content policy", "invalid schema", "401", "403", "400"]

def classify_error(error: Exception) -> str:
    s = str(error).lower()
    if any(x in s for x in TRANSIENT_SIGNALS): return "transient"
    if any(x in s for x in PERMANENT_SIGNALS):  return "permanent"
    return "unknown"

Étape 2 — Choisir la retry policy selon le contexte

StratégieFormule délaiUsage
immediate0 s429 avec Retry-After: 1
fixedbase_delayservice interne fiable
exponentialbase × 2^attemptAPI externe instable
jitteredexponential × random(0.5–1.5)défaut recommandé — évite thundering herd
def compute_delay(attempt: int, base: float = 1.0, jitter: bool = True) -> float:
    delay = base * (2 ** attempt)          # 1s, 2s, 4s, 8s...
    if jitter:
        delay *= 0.5 + random.random()     # ±50% de bruit
    return min(delay, 60.0)               # plafond à 60 s

Étape 3 — Circuit breaker (3 états)

CLOSED ──N failures──► OPEN ──recovery_timeout──► HALF-OPEN
  ▲                                                    │
  └─────────────────── succès ────────────────────────┘
class CircuitBreaker:
    # failure_threshold=5, recovery_timeout=30s
    def can_attempt(self) -> bool:
        if self.state == "OPEN":
            if time.time() - self.last_failure > self.recovery_timeout:
                self.state = "HALF_OPEN"
                return True
            return False   # rejeter sans appel réseau
        return True

Paramètres conseillés par défaut : failure_threshold=5, recovery_timeout=30s.

Étape 4 — Modifier la tâche si l'échec persiste

Un retry identique sur un input identique produit le même échec. Dès la 2e tentative :

def modify_task(args: dict, attempt: int) -> dict:
    m = args.copy()
    if attempt == 1 and "prompt" in m:
        m["prompt"] = m["prompt"][:int(len(m["prompt"]) * 0.6)]
    if attempt == 2:
        m["prompt"] = "Réponse JSON uniquement: " + m["prompt"][:300]
    return m

Étape 5 — Model fallback chain

Définir la chaîne une seule fois dans la config :

MODEL_FALLBACK_CHAIN = [
    "claude-opus-4-5",      # modèle principal
    "claude-sonnet-4-5",    # fallback intermédiaire
    "claude-haiku-3-5",     # fallback léger
]

Basculer vers le suivant à chaque échec consécutif. Journaliser le changement de modèle.

Étape 6 — Tool fallback

TOOL_FALLBACKS = {
    "search_web":    ["fetch_url", "search_academic"],
    "fetch_url":     ["search_web"],
    "db_primary":    ["db_replica"],
    "api_weather_1": ["api_weather_2"],
}

Si l'outil search_web échoue 2 fois → essayer fetch_url. Si le tool de fallback est indisponible aussi → remonter une erreur métier claire.

Étape 7 — Décomposition de tâche sur échec de complexité

Indicateur : erreur de token limit ou réponse tronquée/incohérente.

# Découper une liste de 100 items en chunks de 20
def decompose(items: list, chunk_size: int = 20) -> list[list]:
    return [items[i:i+chunk_size] for i in range(0, len(items), chunk_size)]

# Relancer chaque chunk indépendamment, agréger les résultats
results = await asyncio.gather(*[process_chunk(c) for c in decompose(items)])

Étape 8 — Checkpoints et partial recovery

Persister l'état après chaque étape critique :

# Redis / fichier / base de données
checkpoint = {"step": "step_3", "processed_ids": [1, 2, 3], "ts": time.time()}
redis_client.set(f"checkpoint:{job_id}", json.dumps(checkpoint), ex=3600)

# Au redémarrage : reprendre depuis le checkpoint
ckpt = redis_client.get(f"checkpoint:{job_id}")
if ckpt:
    state = json.loads(ckpt)
    start_from = state["step"]

Étape 9 — Dead Letter Queue (DLQ)

Toute tâche qui épuise ses retries ET ses fallbacks → DLQ. Ne jamais l'abandonner silencieusement.

def send_to_dlq(task: dict, error: Exception, attempts: int):
    record = {
        "task": task,
        "error": str(error),
        "attempts": attempts,
        "ts": datetime.utcnow().isoformat(),
    }
    # SQS: sqs.send_message(QueueUrl=DLQ_URL, MessageBody=json.dumps(record))
    # Redis: redis_client.rpush("dlq:agents", json.dumps(record))
    # Fichier: append to dlq.jsonl
    logger.error("[DLQ] %s", json.dumps(record))

Étape 10 — Analyse des patterns d'échec

from collections import Counter

def failure_report(failure_log: list[dict]) -> dict:
    by_class   = Counter(f["class"]   for f in failure_log)
    by_model   = Counter(f["model"]   for f in failure_log)
    by_tool    = Counter(f["tool"]    for f in failure_log)
    return {"total": len(failure_log), "by_class": dict(by_class),
            "by_model": dict(by_model), "by_tool": dict(by_tool)}

Réviser les politiques si transient/permanent ratio > 3 : 1 → le backoff base est probablement trop court.


Comparaison frameworks

CritèreLangGraphCrewAICustom Python
Retry natifPartiel (node)NonTotal contrôle
Circuit breakerNonNonManuel
Model fallbackNonNonManuel
DLQNonNonManuel
Modification de tâcheNonNonManuel

Recommandation : implémenter RetryStrategist en couche transversale, appelée par l'orchestrateur, indépendamment du framework agent.


Anti-patterns et pièges

Règles de décision rapide

  1. Erreur permanente → pas de retry, fallback ou escalade immédiate.
  2. Erreur transient → jittered exponential backoff, max 3–5 attempts.
  3. 2e tentative → modifier le prompt / réduire le scope.
  4. 3e tentative → changer de modèle ou d'outil.
  5. Toutes tentatives épuisées → DLQ obligatoire, jamais silencieux.
  6. Circuit OPEN → rejeter sans appel réseau, tester une sonde après recovery_timeout.