Skip to content

Système de plugins

源码版本v2026.7.20

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

Flux de données

  1. Au démarrage, init_agent (agent/agent_init.py:276) déclenche la découverte des plugins.
  2. Chaque sous-répertoire de plugin est importé ; le code au niveau module appelle les méthodes d'enregistrement de PluginContext :
  3. 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.
  4. 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 :

python
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 :

python
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_dir avale l'exception, ne log qu'un warning, et pop le module de sys.modules pour é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.register lève un PermissionError plutôt que de remplacer silencieusement ; l'opérateur doit écrire plugins.entries.<plugin_id>.allow_tool_override: true dans config.yaml pour débloquer.
  • Le moteur de contexte n'est pas profondément copiable : le moteur de contexte partagé entre agents est copy.deepcopy au 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.

Site d'apprentissage communautaire non officiel. Basé sur le code source de NousResearch/hermes-agent (licence MIT).