Ce qui entoure la boucle
Ce qu'on nomme mal, on le redécoupe deux fois.
La boucle agentique ne sait pas. C'est ce que l'épisode précédent a établi — et chaque ignorance, en creux, désigne une responsabilité distincte qui l'attend de l'autre côté de la frontière. Nommer ces responsabilités, c'est tracer les modules. C'est aussi, dans une certaine mesure, rendre irréversible une décision : un module nommé est un module qu'on ne refond plus sans conséquences.
Ce que chaque ignorance révèle
La boucle ne sait pas quel LLM appeler. Quelque part dans le système, une responsabilité l'attend : instancier le bon client selon le provider configuré, résoudre les clés API, exposer une interface uniforme quelle que soit la cible. Je l'ai appelée LLMModule.
La boucle ne sait pas quels tools sont disponibles. Une autre responsabilité tient ce registre — quels outils existent, comment les exécuter, quels paramètres ils acceptent. Elle s'appelle ToolsModule.
La boucle ne sait pas comment assembler le prompt qu'elle va envoyer au LLM. Une autre encore compile les templates, injecte le contexte cross-pass, les schémas de sortie, la documentation des tools. Elle s'appelle PromptModule.
La boucle ne sait pas qu'elle est en train de tourner en rond. Une dernière responsabilité surveille les patterns de tool calls, détecte les répétitions, gère la chaîne de fallback pour l'extraction de la sortie structurée. Elle s'appelle SafetyModule.
Quatre ignorances. Quatre modules. La liste est courte — non par manque d'imagination, mais parce que les responsabilités n'étaient vraiment que quatre. Ces quatre modules ne sont pas indépendants dans l'écosystème d'AgenticLayer : ils vivent à l'intérieur d'un assemblage qui les contient et les expose comme une unité cohérente. Cet assemblage s'appelle AgentModule.
Il reste une ignorance : la boucle ne sait pas où chercher le code qu'un tool lui retourne. Or cette responsabilité-là ne peut pas vivre à l'intérieur d'AgentModule — elle sert plus que la boucle. Elle sert l'indexation, la recherche sémantique, le cache partagé entre tous les agents et toutes les instances. Elle s'appelle VectorStoreModule, et elle vit à côté d'AgentModule, non à l'intérieur.
La règle des dépendances
Cinq modules principaux structurent AgenticLayer. Nommer les modules ne suffit pas — il faut aussi décider qui connaît qui, et dans quel sens.
La règle est simple à énoncer, moins simple à respecter : les dépendances vont du général vers le spécifique, jamais en sens inverse. EvaluationModule sait ce qu'est une évaluation — un cycle de passes, de règles, de verdicts. AgentModule sait ce qu'est une boucle. Le premier a besoin du second pour évaluer chaque règle. Le second n'a aucune raison de savoir ce qu'est une évaluation. Cette règle, formulée ainsi, paraît évidente. Elle l'est — une fois qu'on l'a enfreinte.
EvaluationModule et CompetencyModule sont au même niveau d'usage — deux domaines distincts qui consomment AgentModule indépendamment, sans se connaître l'un l'autre. L'un orchestre des évaluations de code, l'autre pilote la progression de compétences dans le temps. Même moteur, intentions différentes. Ni l'un ni l'autre n'a de raison d'importer le domaine de l'autre — ils communiquent à travers ArangoDB, pas à travers des interfaces NestJS.
AgentModule dépend de VectorStoreModule pour l'accès aux fichiers via le cache — mais ni VectorStoreModule ni DocumentProcessingModule ne savent qu'AgentModule existe.
La direction des flèches n'est pas cosmétique. Elle dit qui peut changer sans affecter qui. VectorStoreModule peut évoluer — changer son implémentation Qdrant, modifier sa stratégie de cache — sans qu'AgentModule en soit informé, tant que les interfaces restent stables. C'est l'isolement que procure la direction des dépendances.
Pour un développeur junior : en NestJS, un module déclare ce qu'il exporte — les providers qu'il rend accessibles aux modules qui l'importent. Un provider non exporté est invisible de l'extérieur, même si le module est importé. Cette encapsulation n'est pas une convention : elle est vérifiée par le framework au démarrage. Tenter d'injecter un provider d'un module qui ne l'exporte pas lève une erreur immédiate. C'est ce qui rend les règles de dépendance non seulement souhaitables, mais contraignantes.
L'assemblage dans l'assemblage
AgentModule est lui-même un assemblage. Il ne contient pas un seul service — il agrège quatre sous-modules aux responsabilités distinctes, avec leurs propres règles de dépendance internes.
LLMModule et SafetyModule n'ont aucune dépendance interne — ils sont les feuilles du graphe. ToolsModule importe VectorStoreModule : les tools qui lisent du code passent par l'interface IVectorStoreCache avant d'atteindre Qdrant. PromptModule importe ToolsModule — non pas pour exécuter des tools, mais pour générer leur documentation dans le prompt système : l'agent doit savoir quels outils il peut appeler, et cette description est compilée au moment de l'assemblage du prompt.
Cette dernière dépendance est la moins évidente. Elle dit quelque chose d'important : la description d'un outil est inséparable de l'outil lui-même. PromptModule ne peut pas inventer la documentation de search_code ou de get_file — il la demande à ToolsModule, qui la produit à partir de l'enregistrement réel des outils. Le prompt et les tools sont couplés par la réalité, pas par une convention.
Ce qui meurt entre deux règles
La structure des modules est posée. Elle soulève immédiatement une question que NestJS rend impossible d'ignorer : combien de temps vit chaque service ?
AgentService porte l'historique des messages d'une boucle, le résultat partiel en cours de construction, l'état courant de l'exécution. Si deux évaluations partagent la même instance, l'historique de l'une contamine la suivante. AgentService doit donc mourir et renaître à chaque évaluation de règle. En NestJS, cela s'appelle un scope Transient.
ToolsRegistry, lui, est un registre statique — une map de noms d'outils vers leurs implémentations. Il n'a pas d'état mutable entre deux règles. Le recréer à chaque appel serait simplement du gaspillage, et surtout, trompeur : cela laisserait croire que le registre a un état par exécution, alors qu'il n'en a pas. ToolsRegistry est Singleton.
LoopDetector mémorise les patterns de tool calls pour détecter les boucles. Sa mémoire est scoped à une exécution : un détecteur qui se souvient des appels de la règle précédente voit du bruit là où il devrait voir un pattern. LoopDetector est Transient — comme AgentService, il meurt entre deux règles.
Ce que NestJS appelle "scope" n'est pas un paramètre de configuration parmi d'autres. C'est un contrat sur le cycle de vie d'un service, explicite dans le code, vérifié par le framework. Un scope mal choisi ne produit pas toujours une erreur immédiate — parfois juste un comportement difficile à reproduire, visible uniquement sous charge, quand deux évaluations tournent en parallèle sur la même instance. Il y a beaucoup à dire sur ces contrats. Ce sera l'objet du prochain épisode.
Ce que la structure ferme
Un graphe de dépendances bien orienté ne se contente pas d'organiser le code. Il rend certaines choses impossibles.
Il est impossible d'injecter accidentellement un provider d'AgentModule dans VectorStoreModule — ce dernier ne connaît pas AgentModule. Il est impossible de créer une dépendance circulaire entre deux modules sans que NestJS le détecte au démarrage. Il est impossible d'ignorer le cycle de vie d'un service : le scope est déclaré, visible, et ses conséquences sont vérifiées.
Ces impossibilités sont le produit du nommage. Un module nommé avec précision a une frontière claire — et une frontière claire génère des contraintes naturelles sur ce qui peut la traverser. EvaluationModule sait qu'il dépend d'AgentModule. Il ne sait pas ce qu'AgentModule contient. C'est exactement ce qu'il doit savoir.
La structure est posée. Ce qu'elle ne dit pas encore, c'est comment chaque service à l'intérieur d'AgentModule se comporte à l'exécution — quand plusieurs règles tournent en parallèle, quand un LLM est indisponible, quand la boucle détecte qu'elle tourne en rond. C'est là qu'entrent les scopes, les contrats d'interface, et les stratégies de fallback.
La prochaine fois : Les modules sont nommés, les frontières tracées. Mais AgentModule cache encore quelque chose — une question sur le cycle de vie de ses services que la structure seule ne résout pas. La prochaine fois, on entre dans ses scopes, ses contrats, et dans ce que signifie concrètement "une instance par règle".