Système de plugins
Responsabilité
Les plugins (plugins) sont le squelette d'extension d'Hermes : profils de provider, adaptateurs (adapters) de plateforme, moteur de contexte (context), backend cron, backend de mémoire, navigateur, génération image/vidéo… tous sont intégrés via le mécanisme de plugins. plugins/ fournit un PluginContext (entrée d'enregistrement) + des fonctions utilitaires ; chaque sous-répertoire de plugin est autonome par domaine. Il travaille de pair avec ToolRegistry / PlatformRegistry : le plugin se contente de « déclarer et enregistrer », le registre (registry) gère « requête et dispatch ».
Mot de conception
Pourquoi « convention de répertoire + auto-enregistrement à l'import du module » plutôt qu'un plugins.yaml explicite ? L'objectif est de baisser le frottement pour ajouter une capacité : glisser un sous-répertoire dans $HERMES_HOME/plugins/model-providers/<name>/, le __init__.py est découvert et importé automatiquement, le code au niveau module appelle register_provider() pour achever l'enregistrement — pas besoin de toucher à un fichier de config. En contrepartie, le tri et la surcharge reposent sur la stratégie du registre (espace de noms, opt-in explicite pour override), mais en échange on gagne le hot-plugging et l'intégration des plugins tiers à frottement zéro.
Fichiers clés
plugins/__init__.py—PluginContext, entrées d'enregistrement (register_platform,register_provider, etc.)plugin_utils.py— fonctions utilitaires partagées par les pluginsmodel-providers/— profils de provider LLM (voir Providers)platforms/— adaptateurs Telegram/Discord/Slack etc. (voir Adaptateurs de plateforme)context_engine/— extension du moteur de contextecron_providers/— backends de planification cronmemory/— backends de mémoirebrowser//web/— navigateur et capacités Webimage_gen//video_gen/— génération multimodaleobservability//security-guidance/— observabilité et sécurité
Flux de données
- Au démarrage,
init_agent(agent/agent_init.py:276) déclenche la découverte des plugins. - Chaque sous-répertoire de plugin est importé ; le code au niveau module appelle les méthodes d'enregistrement de
PluginContext:- model-provider →
register_provider:53 - platform →
platform_registry.register:231 - outil →
registry.register:365(via la stratégie d'espace de noms)
- model-provider →
- Le registre (singleton) devient la source de vérité des requêtes ; la boucle principale et la passerelle (gateway) se contentent de lire le registre, sans dépendre directement des plugins concrets.
- Les plugins sont hot-pluggables : le déchargement se fait par
unregister, le registre gère les priorités et les surcharges.
Code clé
Découverte des plugins par scan de répertoire
_discover_providers est l'entrée de découverte des model-providers. Elle scanne deux répertoires (le bundled du dépôt + le répertoire utilisateur $HERMES_HOME), et pour chaque sous-répertoire appelle _import_plugin_dir :
def _discover_providers() -> None:
"""1. Bundled at <repo>/plugins/model-providers/<name>/
2. User at $HERMES_HOME/plugins/model-providers/<name>/
Later steps win on name collision."""
global _discovered
if _discovered: return
_discovered = True
if _BUNDLED_PLUGINS_DIR.is_dir():
for child in sorted(_BUNDLED_PLUGINS_DIR.iterdir()):
if child.is_dir() and not child.name.startswith(("_", ".")):
_import_plugin_dir(child, "bundled")
user_dir = _user_plugins_dir()
if user_dir is not None:
for child in sorted(user_dir.iterdir()):
if child.is_dir() and not child.name.startswith(("_", ".")):
_import_plugin_dir(child, "user")Le « répertoire utilisateur surcharge le bundled » vient de cet ordre : les deux étapes appellent register_provider, le second écrase le premier. Les répertoires préfixés par _ ou . sont ignorés, ce qui laisse une porte de sortie pour __pycache__ et similaires.
Auto-enregistrement : importer le module = enregistrer
_import_plugin_dir utilise importlib.util pour charger le répertoire comme un module ; le code au niveau module, exécuté à l'import, appelle register_provider(profile) pour pousser le profil dans le registre global :
module_name = (f"plugins.model_providers.{safe_name}" if source == "bundled"
else f"_hermes_user_provider_{safe_name}")
spec = importlib.util.spec_from_file_location(
module_name, init_file, submodule_search_locations=[str(plugin_dir)])
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)Bundled et user utilisent des espaces de noms de module distincts, pour éviter que deux profils homonymes sous deux HERMES_HOME différents ne s'aliasent dans sys.modules.
Registre : empêcher la surcharge silencieuse des outils intégrés
registry.register refuse par défaut la surcharge homonyme entre toolsets. La surcharge entre MCP est autorisée (le scénario de refresh est légitime), mais un plugin qui veut remplacer un outil (tool) intégré doit explicitement passer override=True, et l'opérateur doit d'abord mettre allow_tool_override: true dans la config pour débloquer — sinon c'est un PermissionError, pas une substitution silencieuse. La détermination du owner se base sur handler.__globals__["__name__"], lié à la définition, donc un plugin ne peut pas blanchir une surcharge en la passant par un lambda imbriqué pour la faire passer pour « issue d'un module intégré ». Voir registry.register:365-436.
Limites et échecs
- Échec d'import d'un plugin :
_import_plugin_diravale l'exception, ne log qu'un warning, et pop le module desys.modulespour éviter un état à moitié chargé. Un plugin cassé ne fait pas planter tout l'agent, mais le provider/plateforme correspondant est absent — il faut lire les logs pour s'en rendre compte. - Surcharge d'un outil intégré sans opt-in explicite :
registry.registerlève unPermissionErrorplutôt que de remplacer silencieusement ; l'opérateur doit écrireplugins.entries.<plugin_id>.allow_tool_override: truedansconfig.yamlpour débloquer. - Le moteur de contexte n'est pas profondément copiable : le moteur de contexte partagé entre agents est
copy.deepcopyau démarrage du child agent pour isoler l'état du budget (budget) ; si un plugin détient des locks, des connexions DB ou d'autres objets non profondément copiables, le deepcopy échoue et retombe sur le compresseur intégré.
Résumé
Plugin = « sous-répertoire + enregistrement ». Toutes les capacités transverses (provider, plateforme, outil, mémoire, cron, multimodal) passent par le même squelette d'enregistrement, ce qui maintient la boucle principale stable. Ajouter une capacité = créer un sous-répertoire de plugin et l'enregistrer, sans toucher au tronc principal de l'agent.