Skip to content

Sistema de plugins

源码版本v2026.7.20

Responsabilidad

Los plugins son el esqueleto de extensión de Hermes: provider profiles, adaptadores (adapter) de plataforma, context engine, proveedores (provider) de cron, backends de memoria, navegador, generación de imágenes/vídeo… todos se integran por el mecanismo de plugins. plugins/ ofrece un PluginContext (entrada de registro (registry)) + funciones utilitarias; cada subdirectorio de plugin es autónomo por dominio. Coordina con ToolRegistry/PlatformRegistry: el plugin solo «declara y registra»; el registro se encarga de «consultar y despachar».

Motivo de diseño

¿Por qué «convención de directorio + autorregistro en la importación» en lugar de un plugins.yaml explícito? Lo que se busca es bajar la fricción de añadir una capacidad: sueltas un subdirectorio en $HERMES_HOME/plugins/model-providers/<name>/, el __init__.py se descubre y se importa solo, y el código a nivel de módulo llama a register_provider() — sin tocar config. El coste es que el orden y los overrides se resuelven con la política del registro (namespace, opt-in explícito de override); el premio es hot-plug y entrada de terceros sin fricción.

Archivos clave

Flujo de datos

  1. En el arranque, init_agent (agent/agent_init.py:276) dispara el descubrimiento de plugins.
  2. Cada subdirectorio de plugin se importa; el código a nivel de módulo llama a los métodos de registro de PluginContext:
  3. Los registros (singletons) se convierten en la fuente de verdad; el bucle principal y el gateway solo leen los registros, sin depender directamente de un plugin concreto.
  4. Los plugins son hot-pluggable: descargarlos es solo unregister; el registro gestiona prioridades y overrides.

Código clave

Descubrimiento de plugins por escaneo de directorio

_discover_providers es la entrada de descubrimiento de model-providers. Escanea dos directorios (bundled en el repo + $HERMES_HOME de usuario) y, para cada subdirectorio, llama a _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")

El «usuario pisa al bundled» viene de este orden: ambos pasos llaman a register_provider y el segundo sobrescribe al primero. Los subdirectorios que empiezan por _ o . se ignoran; eso deja una salida limpia para __pycache__ y similares.

Autorregistro: importar el módulo es registrarlo

_import_plugin_dir carga el directorio como módulo con importlib.util; al ejecutarse el código de nivel de módulo, la llamada a register_provider(profile) empuja el profile al registro 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 y user usan namespaces de módulo distintos, para que dos profiles con el mismo nombre en dos HERMES_HOME no se hagan alias mutuamente en sys.modules.

Registro: impedir que se pise silenciosamente una herramienta interna

registry.register rechaza por defecto el override de un mismo nombre entre toolsets distintos. Entre MCP se permite (el refresco es legítimo), pero un plugin que quiera reemplazar una herramienta interna tiene que pedir override=True explícito y, encima, el operator debe tener allow_tool_override: true en config — si no, PermissionError en lugar de reemplazo silencioso. La autoría se decide por handler.__globals__["__name__"], atado en la definición; un plugin no puede lavar un override con una lambda anidada y disfrazarlo de «módulo interno». Ver registry.register:365-436.

Límites y fallos

  • Import del plugin falla: _import_plugin_dir traga la excepción, suelta un warning y saca el módulo de sys.modules para evitar estado semi-cargado. Un plugin roto no mata al agent, pero el provider/plataforma correspondiente no aparece — hay que mirar el log para notarlo.
  • Pisar una herramienta interna sin opt-in explícito: registry.register lanza PermissionError en lugar de reemplazar; el operator debe poner plugins.entries.<plugin_id>.allow_tool_override: true en config.yaml para que se permita.
  • Context engine no se puede deep-copy: un context engine compartido entre varios agents pasa por copy.deepcopy al arrancar el child agent, para aislar el estado de budget. Si el plugin retiene un lock, una conexión a DB u otro objeto no deep-copyable, la copia falla y se cae al compresor interno.

Resumen

Plugin = «subdirectorio + registro». Todas las capacidades transversales (provider, plataforma, herramienta, memoria, cron, multimodal) pasan por el mismo esqueleto de registro, manteniendo estable el bucle principal. Añadir una capacidad equivale a crear un subdirectorio de plugin y registrarlo, sin tocar el tronco del agent.

Sitio de aprendizaje comunitario no oficial. Basado en el código fuente de NousResearch/hermes-agent (licencia MIT).