Votre plugin ne se charge plus ? Les anciens chemins d'import sont morts le 2026-09-14


Vous avez écrit un petit plugin, il a fonctionné pendant un mois, et aujourd’hui il s’est arrêté. Après une hermes update et un redémarrage du gateway, le bot devient muet, hermes plugins list indique que votre plugin n’est pas chargé, et le journal contient un unique ImportError. Vous n’avez pas touché une seule ligne de code.

Ce n’est pas un système de plugins capricieux. C’est un nettoyage planifié : la fenêtre de compatibilité s’est refermée. Voici pourquoi la couche a existé, ce qui a été retiré exactement, et comment récupérer votre plugin.

1. La décomposition de septembre a déplacé les chemins internes

En septembre 2026, Hermes a découpé ses gros modules en fichiers frères <stem>_<topic> (PR #102117). Les utilisateurs s’en aperçoivent à peine. Les plugins qui importent depuis des chemins internes, eux, le remarquent immédiatement : les noms sur lesquels ils comptaient ne sont plus définis là où ils étaient.

Le projet a toujours été clair là-dessus : les chemins d’import internes n’ont jamais fait partie du contrat de plugin. Le Native plugin compatibility contract documenté protège le comportement — plugin.yaml, register(ctx), les payloads de hook, les interfaces de provider — pas l’emplacement d’un module Python.

Mais la décomposition aurait cassé du jour au lendemain les plugins existants, donc une couche de compatibilité temporaire a été livrée en même temps, avec une date d’expiration annoncée : le 2026-09-14. Elle en faisait plus que son nom ne le suggère :

  • 328 blocs PLUGIN-COMPAT ajoutés aux modules concernés ;
  • 1 148 noms publics déplacés réexposés via des tables de pointeurs __getattr__ paresseuses (paresseuses, pour éviter les cycles d’import) ;
  • 592 noms tiers ou de la stdlib restaurés, parce que l’ancien module les exposait incidemment ;
  • 290 définitions publiques restaurées mot pour mot, qui avaient été supprimées comme inutilisées ;
  • 41 helpers privés restaurés et 17 imports restaurés, uniquement pour soutenir les définitions ci-dessus ;
  • 3 modules entièrement supprimés remplacés par des stubs qui réexportent depuis leur remplaçant ;
  • 34 noms qui n’ont pas pu être restaurés du tout (variables de boucle fuitées et autres du même genre), listés pour mémoire.

Les auteurs de plugins disposaient d’un vérificateur dédié : hermes plugins compat <path-to-your-plugin> affichait chaque file:line, ancien chemin vers nouveau chemin, et sortait avec le code 1 tant qu’il restait quelque chose. À l’exécution, la première résolution passant par un shim levait un HermesPluginCompatWarning (un FutureWarning), une fois par nom et par processus. Les utilisateurs qui ne pouvaient pas attendre l’auteur avaient une porte de sortie : plugins.allow_deprecated_imports: true.

2. Ce qui a été retiré

La PR #126164 a supprimé la couche le 2026-09-28 : 360 fichiers, +38 / −27 749 lignes. Ont disparu avec elle :

  • les 328 blocs PLUGIN-COMPAT, plus les trois modules stub de réexport ;
  • l’outillage : COMPAT_MANIFEST.md, compat_manifest.json, scripts/check_compat_pointers.py et son étape de lint en CI ;
  • toutes les surfaces de signalement : la bannière du CLI, hermes plugins compat, la section de hermes doctor, l’avis après mise à jour, la boîte de dialogue unique du Desktop et le saut de pré-import du loader ;
  • la porte de sortie elle-même — plugins.allow_deprecated_imports n’existe plus, et la définir ne fait rien.

Trois stubs inertes ont été délibérément conservés. Le raisonnement est pratique : une mise à jour qui exécutait déjà du code d’avant le retrait peut importer paresseusement ces modules après le basculement du checkout, et les supprimer purement et simplement ferait planter l’updater. hermes_cli/plugin_compat.py conserve donc trois fonctions silencieuses (compat_report renvoie un dict vide, removal_in_effect renvoie true, summary_lines renvoie une liste vide) uniquement pour survivre à cet ordre d’exécution.

3. Ce qui arrive à votre plugin

Version courte : un plugin qui importe encore un ancien chemin ne peut pas se charger. Il échoue avec un ImportError, et hermes plugins list en affiche la raison. Il n’est pas « désactivé » — il n’a jamais démarré, c’est pourquoi le bot devient simplement muet.

Deux façons de le corriger, par ordre de préférence :

  1. Utilisez ctx et les ABC documentées. La plupart des besoins du type « j’ai besoin d’importer une fonction interne » sont déjà couverts par les méthodes de PluginContext ou par une classe de base abstraite de provider. Du code écrit ainsi survit à un remaniement de fichiers.
  2. S’il vous faut vraiment importer, importez depuis le module qui définit ce nom aujourd’hui. La table complète ancien module vers nouvel emplacement existe toujours — c’est le manifeste du dernier commit qui a livré la couche : COMPAT_MANIFEST.md (commit 5912ed81ed9). Mettez l’import à jour, redémarrez le gateway, vérifiez l’état de chargement.

Une limite à connaître : les noms privés n’ont jamais été restaurés. Si votre plugin importait quelque chose comme _foo, ou monkeypatchait une couture interne, il n’a jamais été couvert par la couche — même si vous n’avez jamais vu d’avertissement, il est peut-être déjà cassé. Ces cas doivent migrer vers l’équivalent public ou vers le nouveau module.

4. Trouver la ligne exacte qui a cassé

hermes plugins list            # état de chargement et raison, par plugin
hermes plugins show <name>     # détails d'un plugin (y compris emits / listens)
hermes plugins doctor <path>   # valider le répertoire du plugin lui-même

Le test le plus direct court-circuite Hermes et importe le plugin dans le même environnement Python :

python -c "import your_plugin"

Le ModuleNotFoundError: No module named 'agent.tools' (ou le nom qui apparaît) est la ligne à changer. Redémarrez le gateway ensuite — les plugins se chargent au démarrage du processus.

5. Ce que ce changement n’a pas touché

Si votre plugin n’utilise que des surfaces documentées, ce retrait ne vous concerne pas :

  • plugin.yaml et register(ctx) : les méthodes documentées de PluginContext ne sont ni retirées ni renommées ; les nouveaux paramètres sont facultatifs, ont des valeurs par défaut et sont destinés à être keyword-only ; les champs de retour existants ne sont ni retirés ni retypés en silence.
  • Payloads de hook : les nouvelles données arrivent uniquement sous forme de champs nommés, jamais en changeant le sens ou la position d’un champ existant. Un callback legacy reçoit les champs qu’il déclare ; un callback avec **kwargs reçoit le payload complet — c’est pourquoi les nouveaux plugins devraient toujours déclarer **kwargs.
  • Manifestes : les champs plugin.yaml inconnus sont ignorés, donc un Hermes plus ancien peut charger un plugin portant des métadonnées d’une version plus récente.
  • Interfaces de provider : les nouvelles méthodes ont des implémentations par défaut, et le nouveau contexte de callback n’est transmis que si l’inspection de signature montre que le provider l’accepte.
  • La politique de dépréciation conserve quatre conditions strictes : un remplaçant accompagné d’instructions de migration, un avertissement au plus une fois par processus nommant la première release de retrait, le support de l’ancien comportement pendant au moins deux releases mineures supplémentaires, et une couverture de compatibilité fondée sur le comportement pour les deux chemins pendant toute cette fenêtre.

Version courte : le contrat de plugin, c’est le comportement, pas les chemins de fichiers. Écrivez en vous appuyant sur la documentation et le prochain refactor ne vous touchera pas.

Si vous débutez, commencez par notre guide du catalogue de plugins ; pour faire tourner un backend mémoire, le bilan de la migration des memory providers raconte l’autre moitié de l’histoire de septembre. Les détails commande par commande sont dans la référence hermes plugins, et pour recâbler des plugins à l’exécution, voyez le guide du rewire à chaud.