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
| Arquivo | Para que serve |
|---|---|
gateway/run.py | GatewayRunner — laço principal, comandos de barra, despacho de mensagens (arquivo grande; confira no git a contagem de linhas atual) |
gateway/session.py | SessionStore — persistência de conversa e montagem da chave de sessão |
gateway/delivery.py | Entrega de mensagens de saída para plataformas/canais de destino |
gateway/pairing.py | Fluxo de pareamento por DM para autorizar usuários |
gateway/channel_directory.py | Mapeia IDs de chat para nomes legíveis, para a entrega do cron |
gateway/hooks.py | Descoberta e carga de hooks e despacho de eventos de ciclo de vida |
gateway/mirror.py | Espelhamento de mensagens entre sessões, para o send_message |
gateway/status.py | Gerenciamento 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.py | Registro 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:
- O adaptador de plataforma recebe o evento bruto e o normaliza em um
MessageEvent - 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,/denyou/stop→ passa por cima da guarda (despachado em linha)
- 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
/stope/status - Caso contrário → cria uma instância de
AIAgente roda a conversa
- Resolve a chave de sessão com
- 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:
-
Nível 1 — adaptador base (
gateway/platforms/base.py): verifica_active_sessions. Se a sessão está ativa, enfileira a mensagem em_pending_messagese dispara um evento de interrupção. Isso pega as mensagens antes de elas chegarem ao runner do gateway. -
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 dispararunning_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:
- Flag de liberar todos por plataforma (por exemplo,
TELEGRAM_ALLOW_ALL_USERS) — se estiver definida, todos os usuários daquela plataforma estão autorizados - Lista de permissão da plataforma (por exemplo,
TELEGRAM_ALLOWED_USERS) — IDs de usuário separados por vírgula - Pareamento por DM — usuários autenticados podem parear novos usuários com um código de pareamento
- Liberar todos globalmente (
GATEWAY_ALLOW_ALL_USERS) — se estiver definida, todos os usuários de todas as plataformas estão autorizados - 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:
resolve_command(), dehermes_cli/commands.py, mapeia a entrada para o nome canônico (trata apelidos e casamento por prefixo)- O nome canônico é conferido contra
GATEWAY_KNOWN_COMMANDS - O handler em
_handle_message()despacha com base no nome canônico - Alguns comandos ficam condicionados à configuração (
gateway_config_gatenoCommandDef)
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:
| Fonte | O que fornece |
|---|---|
~/.hermes/.env | Chaves de API, tokens de bot, credenciais de plataforma |
~/.hermes/config.yaml | Configurações de modelo, configuração de ferramentas, opções de exibição |
| Variáveis de ambiente | Sobrescrevem 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 vidasend()— entrega de mensagem de saída- os eventos de entrada são normalizados em um
MessageEvente repassados porhandle_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 CLIhermes sendpara scripts de shell e pelos destinosdeliver: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
| Evento | Quando dispara |
|---|---|
gateway:startup | O processo do gateway inicia |
session:start | Uma nova sessão de conversa começa |
session:end | A sessão termina ou expira |
session:reset | O usuário reinicia a sessão com /new |
agent:start | O agente começa a processar uma mensagem |
agent:step | O agente completa uma iteração de chamada de ferramenta |
agent:end | O 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:
- O gateway cria uma
AIAgentpor mensagem, com o ID da sessão - O
MemoryManagerinicializa o provedor com o contexto da sessão - 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)
- 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:
- As memórias embutidas são descarregadas em disco
- O hook
on_session_end()do provedor de memória dispara - Uma
AIAgenttemporária roda um turno de conversa só de memória - 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 manualsystemctl(Linux) oulaunchctl(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).
Documentos relacionados
- Armazenamento de sessões
- O cron por dentro
- O ACP por dentro
- O laço do agente por dentro
- Gateway de mensagens (guia do usuário)