Ce que les dépendances libèrent
Déclarer une dépendance, c'est déclarer une liberté pour tout le reste.
L'épisode précédent a nommé DAGExecutionOrchestrator sans expliquer ce qu'il fait concrètement. La question mérite mieux qu'une ligne. Car la mécanique du DAG n'est pas seulement une façon d'ordonner des passes — c'est une façon de révéler quelles passes n'ont aucune raison d'attendre les autres.
La configuration comme déclaration
Chaque passe de la configuration déclare ses dépendances — les passes dont elle a besoin du résultat avant de pouvoir commencer. Cette déclaration est minimale : une liste d'identifiants de passes. C'est DAGExecutionOrchestrator qui en tire les conséquences.
À partir de ces déclarations, il construit un plan d'exécution par couches successives. Une passe appartient à la couche zéro si elle n'a aucune dépendance — elle peut démarrer immédiatement. Une passe appartient à la couche suivante si toutes ses dépendances appartiennent à des couches précédentes. Le processus se répète jusqu'à ce que toutes les passes soient placées.
Ce graphe se traduit en trois couches d'exécution : A et B ensemble, puis C et D ensemble dès que A et B sont terminées, puis E dès que C est terminée. Le parallélisme n'est pas explicitement configuré — il émerge de l'absence de dépendances entre les passes d'une même couche.
La contrainte d'acyclicité est vérifiée au moment de la construction du plan. Si la configuration déclare une dépendance circulaire — A dépend de B qui dépend de A — le plan ne peut pas être construit. Ce n'est pas un cas d'erreur d'exécution : c'est une configuration invalide, rejetée avant que la première passe ne démarre. J'ai préféré cette rigueur à une détection tardive — mieux vaut un refus net au démarrage qu'un interblocage silencieux découvert en production.
Pour un développeur junior : le tri topologique est l'algorithme qui transforme un graphe de dépendances en couches d'exécution ordonnées. Son nom est plus intimidant que son principe. Il parcourt le graphe en assignant à chaque nœud le niveau le plus tardif de ses dépendances, plus un. Les nœuds sans dépendances reçoivent le niveau zéro. C'est un algorithme classique — mais ce qui importe ici n'est pas l'algorithme lui-même, c'est ce qu'il produit : un plan d'exécution où le parallélisme maximal est garanti par construction, sans qu'aucun développeur n'ait à raisonner explicitement sur quelles passes peuvent tourner ensemble.
Un mécanisme, deux niveaux
DAGExecutionOrchestrator ordonne les passes. PassExecutionOrchestrator ordonne les règles à l'intérieur d'une passe. Ce ne sont pas deux mécanismes différents — c'est le même, appliqué récursivement.
Les règles d'une passe peuvent déclarer des dépendances sur d'autres règles de la même passe, exactement comme les passes déclarent des dépendances entre elles. PassExecutionOrchestrator construit le même plan d'exécution par couches : les règles sans dépendance démarrent immédiatement, les autres attendent que leurs prérequis soient satisfaits. Le parallélisme, là encore, émerge de l'absence de dépendances — il n'est pas imposé, il est révélé.
C'est ici que la décision de scope du sixième épisode révèle sa conséquence directe. AgentService est Transient — une nouvelle instance à chaque injection. Chaque règle, qu'elle soit indépendante ou qu'elle attende le résultat d'une autre, obtient sa propre instance : son propre historique de messages, son propre LoopDetector, son propre StructuredOutputHandler. Elles ne partagent rien — et c'est précisément ce qui leur permet de coexister, et de se succéder sans contamination d'état.
Ce choix de symétrie entre les deux niveaux n'est pas anodin — c'est celui dont je suis le plus satisfait dans cette partie du système. Il dit que la granularité de la dépendance n'est pas fixée par l'architecture — elle est laissée à la configuration. Une passe peut être un monolithe de règles indépendantes, ou un enchaînement fin où chaque règle affine le contexte de la suivante. L'orchestrateur s'adapte ; il ne contraint pas.
Ce qu'une règle qui échoue déclenche
Le parallélisme distribue aussi les défaillances. Or une règle qui échoue — un timeout LLM, un outil qui ne répond plus — ne mérite pas d'être abandonnée à la première tentative. L'architecture de résilience se déploie en trois niveaux, du plus rapide au plus définitif.
Le premier niveau est immédiat. PassExecutionOrchestrator tente une à deux fois de relancer la règle défaillante, sans état persisté, sans délai significatif. C'est la réponse aux erreurs transitoires — la majorité des cas. Si la règle réussit à la deuxième tentative, le reste du graphe n'en a jamais su.
Si les retries immédiats échouent, le second niveau entre en jeu. Le statut de la règle est persisté dans Redis Sentinel — failed_attempt_1, failed_attempt_2, jusqu'au maximum configuré. EvaluationModule publie alors un message de retry sur un exchange RabbitMQ dédié, avec un TTL croissant : trente secondes pour la première tentative différée, deux minutes pour la deuxième, dix minutes pour la troisième. C'est le pattern classique de delay queue RabbitMQ — un exchange mort-lettre qui requeue le message après expiration du TTL. ReEvaluateUseCase reçoit ce message et reprend l'exécution depuis l'état persisté : les règles déjà réussies ne sont pas relancées, seule la règle défaillante est rejouée dans son contexte de dépendances.
Le troisième niveau est l'arrêt. Après N tentatives — trois par défaut, configurable par règle — la règle est marquée permanently_failed. C'est seulement à ce stade que le DAG en tire les conséquences : les règles qui dépendaient de la règle définitivement échouée sont ignorées, les passes concernées publient un résultat partiel. Les branches indépendantes du graphe, elles, n'en sont jamais informées — elles continuent.
La granularité du retry à la règle n'est pas un détail. Elle est rendue possible par le DAG au niveau des règles : puisque le système sait exactement quelles règles ont réussi et lesquelles ont échoué, il peut reprendre avec précision. Retenter une passe entière pour une règle défaillante gaspillerait ce que le graphe sait déjà.
Ce que le DAG ne voit pas
DAGExecutionOrchestrator ordonne, parallélise, isole les défaillances. Il fait tout cela dans le cadre d'une exécution continue — un processus qui tourne sans interruption du premier message à l'événement final.
Or ce cadre peut être rompu. Un pod qui redémarre en pleine exécution. Un message RabbitMQ relivré parce que l'ACK n'est jamais arrivé. Le DAG ne sait pas que l'exécution a été interrompue — il recommencerait depuis le début, sans mémoire de ce qui a déjà été fait.
C'est la question que cet épisode laisse ouverte. Le mécanisme qui garantit qu'une évaluation interrompue ne recommence pas depuis zéro — qui se souvient de ce qui a été terminé, qui sait où reprendre — c'est ce que le prochain épisode raconte.
La prochaine fois : Le DAG ordonne les passes et maximise le parallélisme. Mais que se passe-t-il quand l'exécution s'arrête au milieu — un pod qui redémarre, un crash entre deux passes ? La prochaine fois, on entre dans le mécanisme qui garantit qu'une évaluation ne recommence jamais depuis zéro.