Sistema de plugins
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
plugins/__init__.py—PluginContext, entradas de registro (register_platform,register_provider, etc.)plugin_utils.py— funciones utilitarias comunes a los pluginsmodel-providers/— profiles de providers LLM (ver Providers)platforms/— adaptadores de Telegram/Discord/Slack y otras plataformas (ver Adaptadores de plataforma)context_engine/— extensiones del motor de contexto (context)cron_providers/— backends de programación cronmemory/— backends de memoriabrowser//web/— capacidades de navegador y webimage_gen//video_gen/— generación multimodalobservability//security-guidance/— observabilidad y seguridad
Flujo de datos
- En el arranque,
init_agent(agent/agent_init.py:276) dispara el descubrimiento de plugins. - Cada subdirectorio de plugin se importa; el código a nivel de módulo llama a los métodos de registro de
PluginContext:- model-provider →
register_provider:53 - platform →
platform_registry.register:231 - herramienta (tool) →
registry.register:365(vía política de namespace)
- model-provider →
- 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.
- 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:
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:
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_dirtraga la excepción, suelta un warning y saca el módulo desys.modulespara 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.registerlanzaPermissionErroren lugar de reemplazar; el operator debe ponerplugins.entries.<plugin_id>.allow_tool_override: trueen config.yaml para que se permita. - Context engine no se puede deep-copy: un context engine compartido entre varios agents pasa por
copy.deepcopyal 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.