Le problème
La demande paraît simple : vérifier le statut de paiement et donner une réponse courte au client.
Dans les traces, c'est autre chose : en 11 minutes, un run a fait 33 appels,
10 ont renvoyé 200, 14 ont renvoyé timeout, et 9 ont renvoyé 502/503.
Pour une tâche de cette classe, cela peut être autour de ~$1.90 au lieu des ~$0.14 habituels.
Le service est formellement "vivant" : une partie des appels passe, il n'y a pas de panne totale. Mais la queue des runs grossit, la latency saute, et les utilisateurs reçoivent un résultat instable.
Le système ne tombe pas.
Il se bloque simplement, lentement, entre succès rares et échecs répétés.
Analogie : imagine une caisse où le terminal accepte parfois la carte puis se fige. Le magasin n'est pas fermé, mais la file grandit chaque minute. Une panne partielle dans les systèmes d'agents fonctionne pareil : l'infrastructure semble disponible, mais il n'existe plus de chemin stable vers la réponse.
Pourquoi ça arrive
En production, cela se passe généralement ainsi :
- une dépendance devient instable (
timeout,5xx, parfois200) ; - des retries démarrent dans plusieurs couches en même temps ;
- le run retient les workers plus longtemps, la queue grossit ;
- d'autres workflows ralentissent aussi à cause de ressources partagées ;
- sans fail-fast ni safe-mode, le système multiplie les coûts au lieu d'isoler l'échec.
Dans le trace, cela apparaît comme un pattern mixte : tool_2xx_rate tient encore,
mais timeout_rate, retry_attempts_per_run et queue_backlog montent en même temps.
Le problème n'est pas un seul timeout.
Runtime ne bascule pas la dépendance instable en degraded mode, tant que la panne reste locale.
Pannes les plus fréquentes
En production, quatre patterns de partial outage reviennent le plus souvent.
Piège du succès intermittent (Intermittent success trap)
Le tool renvoie parfois 200, ce qui masque la dégradation.
L'agent continue d'insister sur le même canal, au lieu d'un basculement contrôlé.
Cause typique : pas de seuil de "santé" de dépendance au niveau run.
Retry amplification entre couches
Le client HTTP, le gateway et le runtime font chacun leurs retries. Même une petite hausse d'erreurs devient vite une vague d'appels inutiles.
Cause typique : retry policy non centralisée.
Queue bloquée par des runs "bruyants" (Queue starvation)
Les runs problématiques restent longtemps bloqués, occupent le worker pool et évincent les tâches saines.
Cause typique : absence de limites de durée de run et de budget-gates.
Attente d'une réponse "parfaite" sans degrade path
Le système essaie d'attendre un résultat "idéal", alors que la dépendance est clairement dégradée.
Cause typique : pas de contrat partial/fallback côté utilisateur.
Comment détecter ces problèmes
Une panne partielle est visible via la combinaison des métriques health, runtime et queue.
| Métrique | Signal de partial outage | Action |
|---|---|---|
degraded_dependency_rate | une dépendance renvoie souvent timeout/5xx | activer degraded mode et réduire le fan-out |
tool_2xx_with_high_timeout_rate | 200 et forte part de timeout apparaissent ensemble | ajouter un seuil de health, ne pas se baser sur 200 seul |
retry_attempts_per_run | trop de répétitions pour un run | centraliser les retries et limiter le retry budget |
run_duration_p95 | runs longs qui "pendent" | ajouter fail-fast timeout et stop reasons |
queue_backlog | la queue grossit avec trafic normal | isoler le chemin dégradé et activer fallback |
Comment distinguer partial outage de full outage
Toute dégradation n'est pas une panne totale. Question clé : y a-t-il un chemin d'exécution stable, ou seulement des "succès" aléatoires.
Normal pour full outage si :
- presque tous les appels échouent de façon uniforme (
5xxou indisponibilité totale) ; - le système passe rapidement en fail-fast ;
- il n'y a pas l'illusion du "ça marche parfois".
Dangereux pour partial outage si :
- dans le même run, on mélange
200,timeoutet5xx; - l'agent répète les appels car il voit de rares succès ;
- queue/latency montent sans incident global explicite.
Comment stopper ces pannes
En pratique :
- figer un health snapshot des dépendances au début du run ;
- dès que le seuil est dépassé, basculer le workflow en degraded mode ;
- garder les retries dans un seul tool gateway avec budget strict ;
- renvoyer partial/fallback avec stop reason explicite, au lieu d'attendre "indéfiniment".
Guard minimal pour partial outage :
from dataclasses import dataclass
import time
RETRYABLE = {408, 429, 500, 502, 503, 504}
@dataclass(frozen=True)
class OutageLimits:
max_retry_per_call: int = 2
max_retry_total: int = 6
max_run_seconds: int = 45
max_tool_calls: int = 14
degraded_error_threshold: float = 0.35
min_sample_size: int = 5
class PartialOutageGuard:
def __init__(self, limits: OutageLimits = OutageLimits()):
self.limits = limits
self.started_at = time.time()
self.tool_calls = 0
self.retry_count = 0
self.total_calls = 0
self.error_calls = 0
def before_tool_call(self) -> str | None:
self.tool_calls += 1
if self.tool_calls > self.limits.max_tool_calls:
return "partial_outage:tool_call_budget"
if (time.time() - self.started_at) > self.limits.max_run_seconds:
return "partial_outage:run_timeout"
return None
def on_tool_result(self, status_code: int, attempt: int) -> str | None:
self.total_calls += 1
if status_code in RETRYABLE:
self.error_calls += 1
error_rate = self.error_calls / max(1, self.total_calls)
if (
self.total_calls >= self.limits.min_sample_size
and error_rate >= self.limits.degraded_error_threshold
):
return "partial_outage:degraded_mode"
self.retry_count += 1
if self.retry_count > self.limits.max_retry_total:
return "partial_outage:retry_budget"
if attempt >= self.limits.max_retry_per_call:
return "partial_outage:retry_exhausted"
return "partial_outage:retry_allowed"
error_rate = self.error_calls / max(1, self.total_calls)
if (
self.total_calls >= self.limits.min_sample_size
and error_rate >= self.limits.degraded_error_threshold
):
return "partial_outage:degraded_mode"
return None
Dans cette version, retry_count compte toutes les réponses retryable dans le run,
et attempt est le nombre de répétitions pour un appel précis.
C'est un guard de base.
En production, on l'étend en général avec des per-tool health probes,
un circuit breaker et un chemin safe-mode dédié pour les runs dégradés.
before_tool_call(...) et on_tool_result(...) sont appelés dans le tool gateway,
pour que la décision de dégradation soit centralisée, et non dupliquée dans chaque couche.
Où c'est implémenté dans l'architecture
En production, le contrôle du partial outage est presque toujours réparti entre trois couches du système.
Tool Execution Layer fournit les health signals :
error rate, timeout patterns, retry budget et circuit breaker.
C'est là qu'on voit qu'une dépendance est déjà instable, même si une partie des appels retourne encore 200.
Agent Runtime prend les décisions au niveau run : bascule en degraded mode, stop reasons et fin contrôlée avec fallback. Sans cette couche, le système continue d'attendre "encore un appel réussi".
Orchestration Topologies définit comment isoler un workflow dégradé du reste du système (bulkheads, queues, priorités). Cela évite qu'une dégradation locale devienne un incident partagé.
Auto-vérification
Vérification rapide avant release. Coche les points et regarde le statut ci-dessous.
C'est un sanity-check court, pas un audit formel.
Progression: 0/8
⚠ Il y a des signaux de risque
Il manque des contrôles de base. Fermez les points clés de la checklist avant release.
FAQ
Q : Pourquoi partial outage est souvent pire que full outage ?
R : Parce qu'il est masqué par "ça marche parfois".
Le système ne s'arrête pas et continue de brûler du temps, des tokens et du worker pool.
Q : Faut-il désactiver immédiatement le tool dégradé ?
R : Pas toujours. En général, on active degraded mode : limiter retries, réduire fan-out, et passer sur un chemin partial/fallback.
Q : Où prendre les décisions sur retries et dégradation ?
R : Dans un seul tool gateway. Sinon chaque couche fait ses propres retries et partial outage s'amplifie vite.
Q : Que montrer à l'utilisateur quand la dépendance dégrade ?
R : Un stop reason explicite, ce qui a échoué, et une prochaine étape contrôlée : réponse partielle ou nouvelle tentative après rétablissement.
Partial outage ressemble rarement à un crash bruyant. C'est une dégradation silencieuse : le système avance encore, mais ne tient plus la qualité ni le rythme. C'est pourquoi les agents de production ont besoin non seulement de retries, mais aussi d'un mode de dégradation strict et d'une isolation des dépendances.
Pages liées
Si ce problème apparaît en production, ces pages sont aussi utiles :
- Pourquoi les agents IA échouent — carte générale des pannes en production.
- Tool failure — comment une erreur locale d'outil devient incident.
- Deadlocks — comment les états d'attente grandissent pendant la dégradation des dépendances.
- Cascading failures — comment partial outage se propage vers d'autres workflows.
- Agent Runtime — où piloter degraded mode, stop reasons et fallback.
- Tool Execution Layer — où maintenir retries, health signals et circuit breaker.