Seu plugin parou de carregar? Os caminhos de import antigos morreram em 2026-09-14


Você escreveu um plugin pequeno, ele funcionou por um mês e hoje simplesmente parou. Depois de um hermes update e de um restart do gateway, o bot fica quieto, hermes plugins list diz que seu plugin não está carregado, e o log contém um único ImportError. Você não tocou em uma linha de código.

Isso não é um sistema de plugins instável. É uma limpeza programada: a janela de compatibilidade fechou. Veja por que a camada existia, o que exatamente foi removido e como trazer seu plugin de volta.

1. A decomposição de setembro moveu caminhos internos

Em setembro de 2026 o Hermes dividiu seus módulos grandes em arquivos irmãos <stem>_<topic> (PR #102117). Os usuários quase não notam. Plugins que importam de caminhos internos notam na hora: os nomes em que eles confiavam não estão mais definidos onde costumavam estar.

O projeto sempre foi explícito sobre isso: caminhos de import internos nunca fizeram parte do contrato de plugins. O Native plugin compatibility contract documentado protege comportamento — plugin.yaml, register(ctx), payloads de hook, interfaces de provider — não a localização de um módulo Python.

Mas a decomposição teria quebrado plugins existentes da noite para o dia, então uma camada de compatibilidade temporária foi enviada junto, com data de expiração publicada: 2026-09-14. E ela fez bem mais do que o nome sugere:

  • 328 blocos PLUGIN-COMPAT anexados aos módulos afetados;
  • 1.148 nomes públicos movidos reexpostos através de tabelas de ponteiros __getattr__ preguiçosas (lazy, para evitar ciclos de import);
  • 592 nomes de terceiros ou da stdlib restaurados, porque o módulo antigo costumava expô-los por acidente;
  • 290 definições públicas restauradas ao pé da letra que tinham sido deletadas como não usadas;
  • 41 helpers privados restaurados e 17 imports restaurados, puramente para dar suporte às definições acima;
  • 3 módulos inteiros deletados transformados em stubs para reexportar do seu substituto;
  • 34 nomes que não puderam ser restaurados de forma alguma (variáveis de laço vazadas e coisas do tipo), listados apenas por completude.

Os autores de plugins tinham um verificador dedicado: hermes plugins compat <caminho-do-seu-plugin> imprimia cada file:line, caminho antigo para caminho novo, e saía com 1 enquanto restasse algo. Em runtime, a primeira resolução através de um shim levantava um HermesPluginCompatWarning (um FutureWarning), uma vez por nome por processo. Usuários que não podiam esperar por um autor tinham uma saída de emergência: plugins.allow_deprecated_imports: true.

2. O que foi removido

O PR #126164 deletou a camada em 2026-09-28: 360 arquivos, +38 / −27.749 linhas. Foram embora com ela:

  • todos os 328 blocos PLUGIN-COMPAT, mais os três módulos stub de reexportação;
  • as ferramentas: COMPAT_MANIFEST.md, compat_manifest.json, scripts/check_compat_pointers.py e seu passo de lint no CI;
  • todas as superfícies de report: o aviso em banner na CLI, hermes plugins compat, a seção do hermes doctor, o aviso pós-update, o diálogo único do Desktop e o skip pré-import do loader;
  • a própria saída de emergência — plugins.allow_deprecated_imports não existe mais, e defini-la não faz nada.

Três stubs inertes foram mantidos de propósito. O raciocínio é prático: um update que já estava rodando código pré-remoção pode importar esses módulos de forma lazy depois da troca de checkout, e deletá-los de vez quebraria o updater. Então hermes_cli/plugin_compat.py mantém três funções silenciosas (compat_report retorna um dict vazio, removal_in_effect retorna true, summary_lines retorna uma lista vazia) puramente para sobreviver a essa ordem.

3. O que acontece com o seu plugin

Versão curta: um plugin que ainda importa um caminho antigo não carrega. Ele falha com um ImportError, e hermes plugins list mostra o motivo. Ele não está “desabilitado” — ele nunca iniciou, e é por isso que o bot simplesmente fica mudo.

Duas formas de corrigir, em ordem de preferência:

  1. Use ctx e as ABCs documentadas. A maioria das necessidades do tipo “preciso importar uma função interna” já é coberta por métodos de PluginContext ou por uma classe base abstrata de provider. Código escrito assim sobrevive a um remexer de arquivos.
  2. Se você precisa importar, importe do módulo que define o nome hoje. O mapa completo de módulo antigo para nova localização ainda existe — é o manifesto do último commit que trouxe a camada: COMPAT_MANIFEST.md (commit 5912ed81ed9). Atualize o import, reinicie o gateway, confira o status de carregamento.

Uma fronteira que vale conhecer: nomes privados nunca foram restaurados. Se seu plugin importava algo como _foo, ou fazia monkeypatch em uma costura interna, isso nunca foi coberto pela camada — mesmo que você nunca tenha visto um aviso, já pode estar quebrado. Esses casos têm de migrar para o equivalente público ou para o novo módulo.

4. Encontrando a linha exata que quebrou

hermes plugins list            # status de carregamento e motivo, por plugin
hermes plugins show <name>     # detalhes de um plugin (incluindo emits / listens)
hermes plugins doctor <path>   # valida o próprio diretório do plugin

O teste mais direto ignora o Hermes e importa o plugin no mesmo ambiente Python:

python -c "import your_plugin"

O ModuleNotFoundError: No module named 'agent.tools' (ou qualquer nome que apareça) é a linha a mudar. Reinicie o gateway depois — plugins carregam no início do processo.

5. O que essa mudança não tocou

Se seu plugin só usa superfícies documentadas, a remoção não é problema para você:

  • plugin.yaml e register(ctx): métodos documentados de PluginContext não são removidos nem renomeados; parâmetros novos são opcionais, têm valores padrão e devem ser keyword-only; campos de retorno existentes não são removidos nem silenciosamente alterados de tipo.
  • Payloads de hook: dados novos chegam apenas como campos de keyword, nunca mudando o significado ou a posição de um campo existente. Um callback legado recebe os campos que declara; um callback com **kwargs recebe o payload completo — por isso plugins novos devem sempre declarar **kwargs.
  • Manifestos: campos desconhecidos em plugin.yaml são ignorados, então um Hermes mais antigo consegue carregar um plugin com metadados de uma release mais nova.
  • Interfaces de provider: métodos novos têm implementação padrão, e contexto novo de callback só é encaminhado quando a inspeção de assinatura mostra que o provider o aceita.
  • A política de deprecação continua com quatro condições duras: uma substituição mais instruções de migração, um aviso no máximo uma vez por processo nomeando a release de remoção mais próxima, suporte ao comportamento antigo por pelo menos mais duas releases minor, e cobertura de compatibilidade baseada em comportamento para os dois caminhos ao longo dessa janela.

Versão curta: o contrato de plugins é comportamento, não caminhos de arquivo. Escreva seguindo a documentação e o próximo refactor não vai te tocar.

Se você está começando agora, comece pelo nosso guia do catálogo de plugins; para rodar um backend de memória, o texto sobre a migração dos providers de memória conta a outra metade da história de setembro. Detalhes comando por comando estão na referência de hermes plugins, e para religar plugins em runtime veja o guia de rewire ao vivo.