¿Tu plugin ha dejado de cargar? Las rutas de import antiguas murieron el 2026-09-14


Escribiste un plugin pequeño, funcionó durante un mes y hoy simplemente ha dejado de funcionar. Después de hermes update y un reinicio del gateway, el bot se queda callado, hermes plugins list dice que tu plugin no está cargado y el log contiene un único ImportError. No has tocado una línea de código.

Esto no es un sistema de plugins inestable. Es una limpieza planificada: la ventana de compatibilidad se cerró. Aquí va por qué existía esa capa, qué se eliminó exactamente y cómo recuperar tu plugin.

1. La descomposición de septiembre movió rutas internas

En septiembre de 2026, Hermes partió sus módulos grandes en archivos hermanos <stem>_<topic> (PR #102117). Los usuarios apenas lo notan. Los plugins que importan desde rutas internas lo notan de inmediato: los nombres en los que confiaban ya no están definidos donde solían estar.

El proyecto siempre ha sido explícito al respecto: las rutas de import internas nunca formaron parte del contrato de plugins. El documentado Native plugin compatibility contract protege comportamiento —plugin.yaml, register(ctx), payloads de hooks, interfaces de provider—, no la ubicación de un módulo de Python.

Pero la descomposición habría roto los plugins existentes de la noche a la mañana, así que se publicó junto a ella una capa de compatibilidad temporal con fecha de caducidad anunciada: 2026-09-14. Hizo bastante más de lo que parece:

  • 328 bloques PLUGIN-COMPAT añadidos a los módulos afectados;
  • 1.148 nombres públicos movidos y reexpuestos mediante tablas de punteros __getattr__ perezosos (perezosos, para evitar ciclos de import);
  • 592 nombres de terceros o de la stdlib restaurados, porque el módulo antiguo los exponía de forma incidental;
  • 290 definiciones públicas restauradas tal cual que habían sido borradas por no usarse;
  • 41 helpers privados restaurados y 17 imports restaurados, solo para sostener las definiciones anteriores;
  • 3 módulos completos eliminados, dejados como stubs que reexportan desde su reemplazo;
  • 34 nombres que no se pudieron restaurar en absoluto (variables de bucle filtradas y similares), listados solo por completitud.

Quien escribía plugins tenía un comprobador dedicado: hermes plugins compat <path-to-your-plugin> imprimía cada file:line, la ruta antigua y la nueva, y salía con 1 mientras quedara algo pendiente. En tiempo de ejecución, la primera resolución a través de un shim lanzaba un HermesPluginCompatWarning (un FutureWarning), una vez por nombre y por proceso. Quien no podía esperar a un autor tenía una vía de escape: plugins.allow_deprecated_imports: true.

2. Qué se eliminó

El PR #126164 borró la capa el 2026-09-28: 360 archivos, +38 / −27,749 líneas. Se fue con ella:

  • los 328 bloques PLUGIN-COMPAT, más los tres módulos stub de reexportación;
  • el tooling: COMPAT_MANIFEST.md, compat_manifest.json, scripts/check_compat_pointers.py y su paso de lint en CI;
  • todas las superficies que avisaban: el aviso del banner del CLI, hermes plugins compat, la sección de hermes doctor, el aviso posterior a la actualización, el diálogo de una sola vez del Desktop y el pre-import skip del loader;
  • la vía de escape misma: plugins.allow_deprecated_imports ya no existe, y configurarla no hace nada.

Se mantuvieron a propósito tres stubs inertes. El razonamiento es práctico: una actualización que ya estaba ejecutando código anterior a la eliminación puede importar esos módulos de forma perezosa después del cambio de checkout, y borrarlos directamente haría caer el actualizador. Por eso hermes_cli/plugin_compat.py conserva tres funciones silenciosas (compat_report devuelve un dict vacío, removal_in_effect devuelve true, summary_lines devuelve una lista vacía) solo para sobrevivir a ese orden.

3. Qué le pasa a tu plugin

Versión corta: un plugin que todavía importa una ruta antigua no puede cargarse. Falla con un ImportError y hermes plugins list muestra el motivo. No está «deshabilitado»: nunca llegó a arrancar, y por eso el bot simplemente se queda callado.

Dos formas de arreglarlo, por orden de preferencia:

  1. Usa ctx y las ABC documentadas. La mayoría de las necesidades del tipo «tengo que importar una función interna» ya están cubiertas por métodos de PluginContext o por una clase base abstracta de provider. El código escrito así sobrevive a una reorganización de archivos.
  2. Si tienes que importar, importa del módulo que define ese nombre hoy. El mapa completo de módulo antiguo a ubicación nueva sigue existiendo; es el manifiesto del último commit que incluía la capa: COMPAT_MANIFEST.md (commit 5912ed81ed9). Actualiza el import, reinicia el gateway y comprueba el estado de carga.

Un límite que conviene conocer: los nombres privados nunca se restauraron. Si tu plugin importaba algo como _foo, o hacía monkeypatch en una costura interna, nunca estuvo cubierto por la capa; aunque nunca vieras un aviso, puede que ya estuviera roto. Esos casos tienen que pasar al equivalente público o al módulo nuevo.

4. Encontrar la línea exacta que se rompió

hermes plugins list            # estado de carga y motivo, por plugin
hermes plugins show <name>     # detalles de un plugin (incluye emits / listens)
hermes plugins doctor <path>   # valida el directorio del plugin

La prueba más directa se salta Hermes e importa el plugin en el mismo entorno de Python:

python -c "import your_plugin"

Ese ModuleNotFoundError: No module named 'agent.tools' (o el nombre que aparezca) es la línea que hay que cambiar. Reinicia el gateway después: los plugins se cargan al arrancar el proceso.

5. Qué no tocó este cambio

Si tu plugin solo usa superficies documentadas, la eliminación te es indiferente:

  • plugin.yaml y register(ctx): los métodos documentados de PluginContext no se eliminan ni se renombran; los parámetros nuevos son opcionales, tienen valores por defecto y están pensados para ser solo de palabra clave (keyword-only); los campos de retorno existentes no se eliminan ni se cambian de tipo en silencio.
  • Payloads de hooks: los datos nuevos llegan solo como campos de palabra clave, nunca cambiando el significado ni la posición de uno existente. Un callback heredado recibe los campos que declara; un callback con **kwargs recibe el payload completo, y por eso los plugins nuevos deberían declarar siempre **kwargs.
  • Manifiestos: los campos desconocidos de plugin.yaml se ignoran, así que un Hermes más antiguo puede cargar un plugin que lleve metadatos de una release más nueva.
  • Interfaces de provider: los métodos nuevos tienen implementaciones por defecto, y el contexto nuevo de callback solo se pasa cuando la inspección de la firma muestra que el provider lo acepta.
  • La política de deprecación sigue teniendo cuatro condiciones duras: un reemplazo más instrucciones de migración, un aviso como máximo una vez por proceso indicando la release más temprana en la que se elimina, soporte del comportamiento antiguo durante al menos dos releases menores más, y cobertura de compatibilidad basada en comportamiento para ambas rutas durante toda esa ventana.

La versión corta: el contrato de plugins es comportamiento, no rutas de archivo. Escribe según la documentación y la próxima refactorización no te tocará.

Si acabas de empezar, empieza por nuestra guía del catálogo de plugins; para montar un backend de memoria, el artículo sobre la migración de memory providers cuenta la otra mitad de la historia de septiembre. Los detalles comando a comando están en la referencia de hermes plugins, y para reconectar plugins en tiempo de ejecución, consulta la guía de reconexión en vivo.