Pular para o conteúdo principal

Documentação oficial desta página · v0.20.6

O gateway por dentro

O gateway por dentro

O gateway de mensagens é o processo de longa duração que conecta o Hermes a mais de 20 plataformas externas de mensagem, por uma arquitetura unificada.

Arquivos principais​

ArquivoPara que serve
gateway/run.pyGatewayRunner — laço principal, comandos de barra, despacho de mensagens (arquivo grande; confira no git a contagem de linhas atual)
gateway/session.pySessionStore — persistência de conversa e montagem da chave de sessão
gateway/delivery.pyEntrega de mensagens de saída para plataformas/canais de destino
gateway/pairing.pyFluxo de pareamento por DM para autorizar usuários
gateway/channel_directory.pyMapeia IDs de chat para nomes legíveis, para a entrega do cron
gateway/hooks.pyDescoberta e carga de hooks e despacho de eventos de ciclo de vida
gateway/mirror.pyEspelhamento de mensagens entre sessões, para o send_message
gateway/status.pyGerenciamento de trava de token para instâncias de gateway com escopo de perfil
gateway/builtin_hooks/Ponto de extensão para hooks sempre registrados (nenhum vem de fábrica)
gateway/platform_registry.pyRegistro de adaptadores, fábricas e carregadores adiados (preguiçosos) dos plugins de plataforma que vêm de fábrica
plugins/platforms/<name>/Adaptadores de mensagem que vêm de fábrica (na maioria das plataformas: adapter.py + plugin.yaml)
gateway/platforms/O base.py compartilhado, mais adaptadores legados/diretos (Signal, servidor de API, webhooks, …)

Visão geral da arquitetura​

┌─────────────────────────────────────────────────┐
│ GatewayRunner │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Telegram │ │ Discord │ │ Slack │ │
│ │ Adaptador│ │ Adaptador│ │ Adaptador│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ ▼ │
│ _handle_message() │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ Despacho de Criação Sessões de │
│ comando de da fila/segundo │
│ barra AIAgent plano │
│ │ │
│ ▼ │
│ SessionStore │
│ (persistência SQLite) │
└───────┴─────────────┴─────────────┴─────────────┘

Fluxo da mensagem​

Quando uma mensagem chega de qualquer plataforma:

  1. O adaptador de plataforma recebe o evento bruto e o normaliza em um MessageEvent
  2. O adaptador base verifica a guarda de sessão ativa:
    • Se o agente está rodando para essa sessão → enfileira a mensagem e dispara o evento de interrupção
    • Se for /approve, /deny ou /stop → passa por cima da guarda (despachado em linha)
  3. O GatewayRunner._handle_message() recebe o evento:
    • Resolve a chave de sessão com _session_key_for_source() (formato: agent:main:{platform}:{chat_type}:{chat_id})
    • Verifica a autorização (veja Autorização, abaixo)
    • Verifica se é um comando de barra → despacha para o handler de comandos
    • Verifica se o agente já está rodando → intercepta comandos como /stop e /status
    • Caso contrário → cria uma instância de AIAgent e roda a conversa
  4. A resposta volta pelo adaptador de plataforma

Formato da chave de sessão​

As chaves de sessão codificam todo o contexto de roteamento:

agent:main:{platform}:{chat_type}:{chat_id}

Por exemplo: agent:main:telegram:private:123456789

Plataformas com noção de thread (tópicos de fórum do Telegram, threads do Discord, threads do Slack) podem incluir IDs de thread na parte do chat_id. Nunca monte chaves de sessão na mão — use sempre build_session_key(), de gateway/session.py.

A guarda de mensagens em dois níveis​

Quando um agente está rodando, as mensagens que chegam passam por duas guardas em sequência:

  1. Nível 1 — adaptador base (gateway/platforms/base.py): verifica _active_sessions. Se a sessão está ativa, enfileira a mensagem em _pending_messages e dispara um evento de interrupção. Isso pega as mensagens antes de elas chegarem ao runner do gateway.

  2. Nível 2 — runner do gateway (gateway/run.py): verifica _running_agents. Intercepta comandos específicos (/stop, /new, /queue, /status, /approve, /deny) e os roteia como convém. Todo o resto dispara running_agent.interrupt().

Comandos que precisam chegar ao runner enquanto o agente está bloqueado (como /approve) são despachados em linha, via await self._message_handler(event) — eles pulam o sistema de tarefas em segundo plano para evitar condições de corrida.

Autorização​

O gateway usa uma verificação de autorização em várias camadas, avaliadas nesta ordem:

  1. Flag de liberar todos por plataforma (por exemplo, TELEGRAM_ALLOW_ALL_USERS) — se estiver definida, todos os usuários daquela plataforma estão autorizados
  2. Lista de permissão da plataforma (por exemplo, TELEGRAM_ALLOWED_USERS) — IDs de usuário separados por vírgula
  3. Pareamento por DM — usuários autenticados podem parear novos usuários com um código de pareamento
  4. Liberar todos globalmente (GATEWAY_ALLOW_ALL_USERS) — se estiver definida, todos os usuários de todas as plataformas estão autorizados
  5. Padrão: negar — usuários não autorizados são rejeitados

Fluxo de pareamento por DM​

Admin: /pair
Gateway: "Código de pareamento: ABC123. Compartilhe com o usuário."
Usuário novo: ABC123
Gateway: "Pareado! Agora você está autorizado."

O estado do pareamento é persistido em gateway/pairing.py e sobrevive a reinícios.

Despacho de comandos de barra​

Todos os comandos de barra no gateway passam pelo mesmo pipeline de resolução:

  1. resolve_command(), de hermes_cli/commands.py, mapeia a entrada para o nome canônico (trata apelidos e casamento por prefixo)
  2. O nome canônico é conferido contra GATEWAY_KNOWN_COMMANDS
  3. O handler em _handle_message() despacha com base no nome canônico
  4. Alguns comandos ficam condicionados à configuração (gateway_config_gate no CommandDef)

A guarda de agente em execução​

Comandos que NÃO podem rodar enquanto o agente está processando são rejeitados logo de cara:

if _quick_key in self._running_agents:
if canonical == "model":
return "⏳ Agent is running — wait for it to finish or /stop first."

Os comandos que passam por cima da guarda (/stop, /new, /approve, /deny, /queue, /status) têm tratamento especial.

Fontes de configuração​

O gateway lê a configuração de várias fontes:

FonteO que fornece
~/.hermes/.envChaves de API, tokens de bot, credenciais de plataforma
~/.hermes/config.yamlConfigurações de modelo, configuração de ferramentas, opções de exibição
Variáveis de ambienteSobrescrevem qualquer um dos itens acima

Diferente da CLI (que usa load_cli_config() com padrões embutidos no código), o gateway lê o config.yaml direto pelo carregador de YAML. Isso significa que chaves de configuração que existem no dict de padrões da CLI, mas não no arquivo de configuração do usuário, podem se comportar de forma diferente entre a CLI e o gateway.

Adaptadores de plataforma​

A maioria das plataformas de mensagem vem como adaptadores de plugin em plugins/platforms/<name>/adapter.py; alguns adaptadores legados ainda ficam diretamente em gateway/platforms/. Todos estendem BasePlatformAdapter, de gateway/platforms/base.py:

plugins/platforms/ # adaptadores empacotados como plugin (um diretório cada)
├── telegram/adapter.py # API de Bot do Telegram (long polling ou webhook)
├── discord/adapter.py # bot do Discord via discord.py
├── slack/adapter.py # Socket Mode do Slack
├── whatsapp/adapter.py # API Cloud do WhatsApp Business
├── matrix/adapter.py # Matrix via mautrix (E2EE opcional)
├── mattermost/adapter.py # API WebSocket do Mattermost
├── email/adapter.py # E-mail via IMAP/SMTP
├── sms/adapter.py # SMS via Twilio
├── dingtalk/adapter.py # WebSocket do DingTalk
├── feishu/adapter.py # WebSocket ou webhook do Feishu/Lark
├── wecom/adapter.py # callback do WeCom (WeChat Work)
├── line/adapter.py # API de Mensagens do LINE
├── teams/adapter.py # Microsoft Teams
├── irc/adapter.py # IRC (exemplo canônico de trava com escopo)
├── homeassistant/adapter.py # integração de conversa do Home Assistant
└── … # google_chat, ntfy, photon, raft, simplex, …

gateway/platforms/ # base do núcleo + adaptadores diretos legados
├── base.py # BasePlatformAdapter — lógica compartilhada por todas as plataformas
├── signal.py # Signal via API REST do signal-cli
├── weixin.py # Weixin (WeChat pessoal) via API do iLink Bot
├── bluebubbles.py # Apple iMessage via servidor BlueBubbles no macOS
├── qqbot/ # QQ Bot (Tencent QQ) via API Oficial v2 (subpacote)
├── yuanbao.py # adaptador de DM/grupo do Yuanbao (Tencent)
├── msgraph_webhook.py # webhook de notificação de mudança do Microsoft Graph (Teams, Outlook etc.)
├── webhook.py # adaptador de webhook de entrada/saída
└── api_server.py # adaptador de servidor de API REST

Carga adiada: os plugins kind: platform que vêm de fábrica registram carregadores baratos com register_deferred em gateway/platform_registry.py (via hermes_cli/plugins.py), de modo que os SDKs de plataforma só são importados quando o gateway inicia, entrega, ou roda configuração/status — e não num simples hermes chat. A resolução carrega um adaptador na consulta; a enumeração completa só roda os carregadores pendentes nos caminhos que precisam de todas as plataformas.

Plataformas experimentais apoiadas em conector usam o adaptador de relay genérico em gateway/relay/, em vez de um módulo de plataforma direto. Quando GATEWAY_RELAY_URL ou gateway.relay_url está configurado, o gateway registra a plataforma relay, disca para o conector por um WebSocket de saída e recebe quadros descriptor, inbound e interrupt_inbound nesse mesmo socket. O conector anuncia um CapabilityDescriptor; o Hermes pode mandar de volta pelo relay respostas normais de saída, operações follow_up sem token e quadros de interrupção. O contrato de protocolo, com base no código-fonte, está em docs/relay-connector-contract.md.

Os adaptadores implementam uma interface comum:

  • connect() / disconnect() — gerenciamento de ciclo de vida
  • send() — entrega de mensagem de saída
  • os eventos de entrada são normalizados em um MessageEvent e repassados por handle_message()

Travas de token​

Adaptadores que se conectam com credenciais únicas chamam acquire_scoped_lock() no connect() e release_scoped_lock() no disconnect(). Isso impede que dois perfis usem o mesmo token de bot ao mesmo tempo.

Caminho de entrega​

As entregas de saída (gateway/delivery.py) cuidam de:

  • Resposta direta — devolve a resposta ao chat de origem
  • Entrega no canal padrão — roteia as saídas de tarefas de cron e os resultados de segundo plano para um canal padrão configurado
  • Entrega em destino explícito — o motor de envio especificando telegram:-1001234567890, exposto pela CLI hermes send para scripts de shell e pelos destinos deliver: do cron
  • Entrega entre plataformas — entrega em uma plataforma diferente da mensagem de origem

As entregas de tarefas de cron NÃO são espelhadas no histórico de sessão do gateway — elas vivem só na sessão de cron delas. Essa é uma escolha deliberada de projeto, para evitar violações de alternância de mensagens.

Hooks​

Hooks de gateway são módulos Python que respondem a eventos de ciclo de vida:

Eventos de hook do gateway​

EventoQuando dispara
gateway:startupO processo do gateway inicia
session:startUma nova sessão de conversa começa
session:endA sessão termina ou expira
session:resetO usuário reinicia a sessão com /new
agent:startO agente começa a processar uma mensagem
agent:stepO agente completa uma iteração de chamada de ferramenta
agent:endO agente termina e devolve a resposta
command:*Qualquer comando de barra é executado

Os hooks são descobertos em gateway/builtin_hooks/ (um ponto de extensão — hoje vazio na distribuição que vem de fábrica; _register_builtin_hooks() é um esboço que não faz nada) e em ~/.hermes/hooks/ (instalados pelo usuário). Cada hook é um diretório com um manifesto HOOK.yaml e um handler.py.

Integração com provedor de memória​

Quando um plugin de provedor de memória (por exemplo, o Honcho) está habilitado:

  1. O gateway cria uma AIAgent por mensagem, com o ID da sessão
  2. O MemoryManager inicializa o provedor com o contexto da sessão
  3. As ferramentas do provedor (por exemplo, honcho_profile, viking_search) são roteadas assim:
AIAgent._invoke_tool()
→ self._memory_manager.handle_tool_call(name, args)
→ provider.handle_tool_call(name, args)
  1. No fim ou na reinicialização da sessão, on_session_end() dispara para limpeza e descarga final dos dados

Ciclo de vida da descarga de memória​

Quando uma sessão é reiniciada, retomada ou expira:

  1. As memórias embutidas são descarregadas em disco
  2. O hook on_session_end() do provedor de memória dispara
  3. Uma AIAgent temporária roda um turno de conversa só de memória
  4. O contexto é então descartado ou arquivado

Manutenção em segundo plano​

O gateway roda manutenção periódica junto com o tratamento de mensagens:

  • Tique do cron — verifica os agendamentos das tarefas e dispara as que estão na hora
  • Expiração de sessão — limpa sessões abandonadas depois do tempo limite
  • Descarga de memória — descarrega a memória de forma proativa antes de a sessão expirar
  • Atualização de cache — atualiza as listas de modelos e o status dos provedores

Gerenciamento do processo​

O gateway roda como um processo de vida longa, gerenciado por:

  • hermes gateway start / hermes gateway stop — controle manual
  • systemctl (Linux) ou launchctl (macOS) — gerenciamento de serviço
  • Arquivo de PID em ~/.hermes/gateway.pid — rastreio de processo com escopo de perfil

Escopo de perfil x global: start_gateway() usa arquivos de PID com escopo de perfil. O hermes gateway stop para só o gateway do perfil atual. O hermes gateway stop --all usa uma varredura global com ps aux para matar todos os processos de gateway (usado durante atualizações).