← Home

Hermes Telegram Platform architecture study

How Hermes Agent structures its Telegram adapter — the largest platform in their gateway at 6,685 lines. Rich messages, streaming drafts, approval flows, and the patterns worth learning from.
Repo: gateway/platforms/telegram.py. Last studied: 2026-06-14.

Bot API 10.1 Rich Messages

The headline feature. Telegram’s Bot API 10.1 introduced sendRichMessage — bots can now send raw Markdown instead of the notoriously painful MarkdownV2 format. Tables, task lists, collapsible <details> blocks, and math expressions render natively.

How it works

AspectRich pathLegacy path
API methodsendRichMessagesendMessage with MarkdownV2
Content limit32,768 chars4,096 chars
MarkdownRaw agent output (tables, math, details)Escaped MarkdownV2 (fragile)
StreamingsendRichMessageDraft (ephemeral previews)Edit-in-place via editMessageText

Fallback cascade

The rich path is opportunistic. The adapter tries rich first and degrades gracefully:

  1. Pre-check — feature flag on? Content fits 32K? No TDesktop crash shape? Bot supports do_api_request?
  2. Try sendRichMessage — if Telegram returns a permanent error (BadRequest, unsupported endpoint), fall back to legacy MarkdownV2.
  3. Capability latch — if the endpoint itself is unavailable (old python-telegram-bot), latch _rich_send_disabled = True for the adapter’s lifetime. No repeated failed attempts.
  4. Transient errors — do NOT fall back to legacy (risk of duplicate). Surface the error and let the user retry.

This conservative design avoids the worst failure mode: sending the same message twice via different paths.

TDesktop crash workaround

Telegram Desktop 6.9.1 crashes on rich messages containing math inside collapsible <details> blocks (tdesktop#30808). Hermes detects this pattern via regex and skips the rich path entirely for affected content.

Streaming architecture

Agent responses stream in real-time via two mechanisms:

The streaming_overflow_limit() method raises the split threshold to 32K when rich is available, so a reply that fits one rich message isn’t fragmented at the legacy 4K limit.

Status messages edited in place

Status callbacks (“Compressing context…”, “Calling tool…”) route through send_or_update_status(). A {(chat_id, status_key) → message_id} cache ensures the same bubble is edited instead of spawning new ones. If the edit fails (user deleted the message, too old), the cache entry drops and the next emit posts fresh.

Notification volume control

ModeBehavior
important (default)Only final responses, approval prompts, and command confirmations ring. Tool progress and streaming chunks use disable_notification=true.
allEvery outgoing message fires a push notification. Legacy behavior.

This is a smart UX pattern — long agent turns with tool progress bubbles would otherwise spam the user’s phone.

Interactive approval flows

When the agent calls the clarify tool, Telegram renders inline keyboard buttons:

❓ Which framework should I use?
[1. Next.js] [2. Remix] [3. Astro]
[✏️ Other (type answer)]

Exec approval for dangerous commands works the same way — reply “yes”/“no” inline. Configurable timeout (agent.clarify_timeout, default 600s) auto-unblocks if the user doesn’t respond.

Pin-during-turn

When a user message triggers an agent turn, the adapter pins that message for the duration (with disable_notification=true) and unpins it when the response finishes. A lightweight visual indicator that the bot is working, not ignoring you.

Scale

Lines of code6,685 (largest platform adapter)
Base class4,932 lines (base.py)
Network layer259 lines (telegram_network.py)
Librarypython-telegram-bot 22.x
Rich messages addedJune 12, 2026 (with 5 follow-up fixes in 2 days)

Patterns worth adopting