How the Hermes Agent Messaging Gateway Handles Platform-Specific Adapters for Telegram, Discord, Slack, and WhatsApp
The Hermes Agent messaging gateway abstracts Telegram, Discord, Slack, and WhatsApp behind a unified BasePlatformAdapter interface, allowing the same conversational logic to run across all platforms while respecting each service's unique capabilities and constraints.
The NousResearch Hermes Agent project provides a multi-platform messaging gateway that enables AI agents to communicate seamlessly across different chat services. By implementing platform-specific adapters that inherit from a common base class, the gateway normalizes message formats, media handling, and connection management. This architecture ensures that whether users interact via Telegram bots, Discord servers, Slack workspaces, or WhatsApp chats, the underlying agent logic remains identical.
Base Adapter Interface
All platform adapters inherit from BasePlatformAdapter defined in gateway/platforms/base.py. This abstract class establishes the core contract that every platform must satisfy, ensuring consistent behavior across the messaging gateway.
The base adapter declares the following abstract methods:
connect()– Open a connection to the service and start receiving eventsdisconnect()– Gracefully shut down the connectionsend()– Deliver a text message with optional reply threadingedit_message()– Edit a previously sent message if the platform supports itsend_image()/send_image_file()– Send images by URL or from local filessend_voice()– Send audio or voice filessend_video()– Send video filessend_document()– Send generic file attachmentssend_typing()– Emit a typing indicator while the agent processesget_chat_info()– Query chat metadata including name and typeformat_message()– Convert platform-specific markdowntruncate_message()– Split oversized messages respecting platform limits
The base class also defines common data structures including MessageEvent, MessageType, and SendResult. It provides helper utilities for caching media files locally via cache_image_from_*, cache_audio_from_*, and cache_document_from_* methods. These ensure incoming media payloads are stored in a stable short-lived cache at ~/.hermes/*_cache, allowing vision and transcription tools to access files even after platform URLs expire.
Telegram Adapter
The Telegram implementation in gateway/platforms/telegram.py uses the python-telegram-bot library (v20+) to handle the Bot API.
Connection and Event Loop
The connect() method builds an Application instance using the bot token from self.config.token. It registers three MessageHandler instances for text, commands, and media, then starts polling via updater.start_polling().
Message Normalisation
The adapter converts incoming telegram.Update objects into MessageEvent instances through _handle_text_message, _handle_command, and _handle_media_message. Media types including photos, videos, audio, voice, documents, and stickers are downloaded into local caches using cache_image_from_bytes, cache_audio_from_bytes, and cache_document_from_bytes. Sticker handling includes optional vision analysis with cached descriptions to avoid repeated API calls.
Sending Messages
The send() method formats text using MarkdownV2, truncates content to self.MAX_MESSAGE_LENGTH (4096 characters), and falls back to plain text if the Markdown parser rejects the content. Native media methods including send_image, send_image_file, send_voice, send_video, and send_document use the Telegram Bot API's file upload endpoints. URL-based images are sent directly when possible; otherwise, they are downloaded and uploaded.
Additional Features
The adapter registers a rich set of bot commands (/new, /reset, /model, etc.) during connect() to populate Telegram's built-in command menu. The format_message() method escapes MarkdownV2-specific characters, while extract_images() converts Markdown image syntax into native attachments.
Discord Adapter
The Discord implementation in gateway/platforms/discord.py relies on discord.py to manage the Gateway API and slash commands.
Connection and Event Loop
The connect() method creates a commands.Bot instance with required intents including message content, DMs, guild messages, and members. Slash commands are registered via tree.command and synced on startup using tree.sync().
Message Normalisation
The on_message listener discards bot messages and supports mention-gated replies unless the channel appears in DISCORD_FREE_RESPONSE_CHANNELS. The adapter handles text, commands (starting with /), and attachments. Images, audio, and other files are cached using cache_image_from_url and cache_audio_from_url.
Sending Messages
The send() method resolves the Discord channel, formats text without special escaping, and respects Discord's 2000-character limit by splitting content using truncate_message. Threaded replies use the reference field on the first chunk. Media methods including send_image, send_image_file, send_voice, send_video, and send_document upload files using discord.File. For URLs, the adapter downloads content via aiohttp then uploads as a file because Discord does not render external URLs as inline media.
Approval UI
Dangerous commands are gated by send_exec_approval(), which posts an embed with three buttons: Allow Once, Always Allow, and Deny. The ExecApprovalView verifies the clicking user against DISCORD_ALLOWED_USERS and records permanent approvals using the approval tool.
Additional Features
The adapter automatically resolves usernames provided in DISCORD_ALLOWED_USERS to numeric IDs on startup via _resolve_allowed_usernames.
Slack Adapter
The Slack implementation in gateway/platforms/slack.py uses slack-bolt with Socket Mode to handle real-time messaging without public HTTP endpoints.
Connection and Event Loop
The connect() method creates an AsyncApp with the bot token, fetches the bot's own user ID via auth_test for mention detection, and registers two handlers: handle_message_event for regular messages and handle_hermes_command for the /hermes slash command.
Message Normalisation
The _handle_slack_message method filters out bot messages, edits, and deletions. In channels, the bot only responds when its user ID is mentioned (<@UXXXX>). Attachments are downloaded via _download_slack_file using the bot token for authentication; images and audio are cached locally.
Sending Messages
The send() method posts plain text via chat_postMessage, optionally threading using thread_ts. The send_image and send_image_file methods download images via httpx and upload them with files_upload_v2. If the download fails, the adapter falls back to sending the URL as plain text. The send_voice method uploads audio as a file since Slack has no native voice message type.
Typing Indicator
Slack does not expose a typing API for bots, so send_typing() is a no-op.
Additional Features
The /hermes slash command maps sub-commands (new, reset, status, etc.) to Hermes internal commands, providing a familiar command surface inside Slack.
WhatsApp Adapter
The WhatsApp implementation in gateway/platforms/whatsapp.py follows a bridge pattern because WhatsApp lacks an official bot API for personal accounts.
Architecture Overview
A Node.js process (the "bridge") runs a WhatsApp Web client (using whatsapp-web.js or Baileys) and exposes a tiny HTTP JSON API. The Python adapter communicates with this bridge over localhost:<port>.
Connection and Process Management
The connect() method checks for Node.js, ensures bridge dependencies via npm install, kills any orphaned processes bound to the configured port, then spawns the bridge with subprocess.Popen. The bridge health endpoint (/health) is polled until it reports "status":"connected" (maximum 30 seconds). A background task _poll_messages() repeatedly calls /messages to retrieve inbound WhatsApp messages and converts each into a MessageEvent.
Message Normalisation
The _build_message_event() method interprets bridge payloads, distinguishes media types (PHOTO, VIDEO, VOICE, DOCUMENT), caches remote media using cache_image_from_url and cache_audio_from_url, and builds the appropriate MessageEvent.
Sending Messages
The send() method posts to /send with JSON containing chatId, message, and replyTo. Media methods (send_image, send_image_file, send_video, send_document) delegate to _send_media_to_bridge, which posts to /send-media with the local file path and media type. The bridge handles the actual upload to WhatsApp, including inline playback for videos.
Typing Indicator
Implemented by posting to /typing; the bridge forwards the "typing..." state to the WhatsApp client.
Chat Info
The get_chat_info() method queries /chat/<chatId> on the bridge, returning the chat name, type (group or dm), and participants if available.
Resilience
The adapter aggressively cleans up the bridge process on disconnect(), kills any lingering processes on the same port, and logs bridge output to bridge.log for troubleshooting.
Code Examples
Initialising the Gateway
from gateway.config import load_config
from gateway.platforms.telegram import TelegramAdapter
from gateway.platforms.discord import DiscordAdapter
from gateway.platforms.slack import SlackAdapter
from gateway.platforms.whatsapp import WhatsAppAdapter
async def start_all():
cfg = load_config() # reads ~/.hermes/config.yaml
adapters = [
TelegramAdapter(cfg.platforms.telegram),
DiscordAdapter(cfg.platforms.discord),
SlackAdapter(cfg.platforms.slack),
WhatsAppAdapter(cfg.platforms.whatsapp),
]
# Connect every enabled platform concurrently
await asyncio.gather(*(a.connect() for a in adapters))
# The gateway now routes incoming events to the central AIAgent
# (see hermes_cli/gateway.py for the dispatcher).
# In the real Hermes CLI this is called from hermes_cli/gateway.py
Sending a Message From a Tool
A tool (e.g., a /reply command) can address a platform-agnostic send method:
async def reply_to_user(adapter, chat_id, text):
# `adapter` is any subclass of BasePlatformAdapter
result = await adapter.send(chat_id, text)
if not result.success:
logger.error("Failed to send reply: %s", result.error)
Using the Exec-Approval Flow (Discord)
# In a tool that runs a shell command:
approval_id = await request_approval(
platform_adapter=discord_adapter,
chat_id=channel_id,
command="rm -rf /tmp/*"
)
if approval_id:
# The user clicked "Allow Once" or "Always Allow"
await run_dangerous_command()
The DiscordAdapter.send_exec_approval() automatically builds an embed with three buttons and records permanent approvals when the "Always Allow" button is pressed.
WhatsApp Bridge Startup (Manual)
# Ensure Node.js is installed
node --version
# Install bridge dependencies (run once)
cd $(python -c "import pathlib, hermes_agent; print(pathlib.Path(hermes_agent.__file__).parents[2] / 'scripts' / 'whatsapp-bridge')")
npm install
# Start Hermes; the adapter will launch the bridge automatically:
hermes gateway
The bridge logs (bridge.log) contain QR-code prompts for initial pairing and connection status.
Summary
- The Hermes Agent messaging gateway uses a unified
BasePlatformAdapterinterface defined ingateway/platforms/base.pyto abstract platform differences across Telegram, Discord, Slack, and WhatsApp. - Each concrete adapter implements core lifecycle methods (
connect(),disconnect(),send()) and media handling (send_image(),send_voice(),send_document()) while respecting platform-specific limits and formatting rules. - Telegram uses
python-telegram-botwith MarkdownV2 formatting and native media upload endpoints. - Discord leverages
discord.pywith slash commands, button-based approval UIs, and mention-gating for channel responses. - Slack integrates via
slack-boltusing Socket Mode, with file uploads viafiles_upload_v2and slash command mapping. - WhatsApp employs a bridge pattern with a Node.js subprocess exposing an HTTP API, handling the lack of an official bot API through
whatsapp-web.jsor Baileys.
Frequently Asked Questions
How does the messaging gateway handle different message length limits across platforms?
Each adapter implements the truncate_message() method to split oversized content into chunks that respect platform-specific constraints. For example, the Discord adapter enforces a 2000-character limit per message, while the Telegram adapter supports up to 4096 characters. The base class provides common splitting logic, but individual adapters can override this to handle platform-specific formatting requirements like threaded replies or multi-part message chains.
Can the Hermes Agent send media files like images and voice messages on all platforms?
Yes, all four platform adapters implement the full media suite: send_image(), send_voice(), send_video(), and send_document(). However, implementation details vary by platform. Telegram and Discord support native inline media rendering, while Slack requires file uploads via files_upload_v2. WhatsApp routes media through the Node.js bridge which handles the actual upload to WhatsApp Web. Each adapter also caches remote media locally using cache_image_from_* and cache_audio_from_* helpers to ensure availability after platform URLs expire.
How does the gateway manage authentication and connection lifecycles?
Each adapter manages its own connection state through the standardized connect() and disconnect() methods. Telegram uses polling via python-telegram-bot, Discord uses the Gateway API via discord.py, Slack uses Socket Mode with slack-bolt, and WhatsApp spawns a Node.js bridge process that communicates over localhost HTTP. The base class defines these as abstract methods, ensuring every adapter implements proper lifecycle management including graceful shutdowns and connection health monitoring.
Is there a way to restrict which users can execute dangerous commands on Discord?
Yes, the Discord adapter implements an execution approval system via send_exec_approval(). When a tool attempts to run a dangerous command, the adapter posts an embed with three buttons: Allow Once, Always Allow, and Deny. The ExecApprovalView class verifies the clicking user against the DISCORD_ALLOWED_USERS configuration list. Permanent approvals are recorded using the approval tool, while the adapter automatically resolves usernames to numeric IDs on startup via _resolve_allowed_usernames().
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →