How Hermes Formats Telegram Messages So Well

A deep dive into Hermes Agent's dual-path rendering pipeline — Bot API 10.1 Rich Messages versus MarkdownV2 fallback — and the engineering decisions that keep agent replies readable on every Telegram client.

1. The Problem

Telegram bots communicate through the Bot API, which historically only supported MarkdownV2 and HTML as parse modes. Standard Markdown — the lingua franca of LLM outputs (tables, task lists, math blocks, collapsible sections) — has no native representation in MarkdownV2. This creates a gap:

What LLMs outputWhat MarkdownV2 supports
Pipe tables (| col | col |)❌ No table syntax — | is just an escaped literal
Task lists (- [x] done)❌ No checkbox syntax
Collapsible <details> blocks❌ Not supported
Block math ($$...$$)❌ Not supported
Standard links, bold, italic✅ Supported with different syntax

Without careful conversion, agent replies to Telegram users would look broken: tables rendered as noisy backslash-pipe text, special characters triggering parse errors, and formatting silently dropped.

2. The Architecture: Two-Path Rendering

Hermes does not force the agent to "think in Telegram syntax." Instead, the agent outputs standard Markdown, and the Telegram gateway adapter routes it through one of two paths:

Agent Output (Standard Markdown) _needs_rich_rendering()? YES NO Rich Path sendRichMessage MarkdownV2 Fallback editMessageText

Path A: Bot API 10.1 Rich Messages (Default)

When the content contains constructs that MarkdownV2 degrades, Hermes sends via Telegram's native sendRichMessage endpoint using the agent's raw, unmodified markdown.

FeatureRich Message Handling
Pipe tablesNative Telegram table rendering
Task lists (- [ ] / - [x])Native checkboxes
Collapsible details (<details>)Native expand/collapse blocks
Block math ($$...$$)Native math rendering
Streaming previewssendRichMessageDraft in private chats

Delivery mechanics:

Limits & safeguards:

Path B: MarkdownV2 Fallback

For ordinary replies or when rich messages are unavailable, Hermes runs the markdown through a 10-step conversion pipeline:

3. The MarkdownV2 Conversion Pipeline

All logic lives in TelegramAdapter.format_message() in gateway/platforms/telegram.py.

Step 0: Table Rewrite

Before any other conversion, GFM pipe tables are detected and rewritten into Telegram-friendly row groups via _wrap_markdown_tables():

Input:
| Name  | Status | Uptime |
|-------|--------|--------|
| poller| on     | 99.9%  |
| dash  | on     | 99.8%  |

Output:
**poller**
• Name: poller
• Status: on
• Uptime: 99.9%

**dash**
• Name: dash
• Status: on
• Uptime: 99.8%

This is necessary because MarkdownV2's \| is just a literal escaped pipe — tables would otherwise render as unreadable backslash noise.

Step 1–2: Protect Code Regions

Fenced code blocks and inline code spans are extracted into placeholder tokens so their contents are never touched by the escaping steps that follow. Inside code:

Step 3: Convert Links

Standard markdown links are converted with proper escaping:

Input:  Click [here](https://example.com/path_(foo))
Output: Click [here](https://example.com/path_\(foo\))

Step 4–7: Convert Formatting Syntax

Standard MarkdownMarkdownV2Implementation
## Title*Title* (bold)re.sub(r'^#{1,6}\s+(.+)$', ...)
**bold***bold*re.sub(r'\*\*(.+?)\*\*', ...)
*italic*_italic_re.sub(r'\*([^*\n]+)\*', ...)
~~strike~~~strike~re.sub(r'~~(.+?)~~', ...)
||spoiler||||spoiler||Protected from \| escaping

Step 8: Escape All Remaining Special Characters

Any character outside a protected code region that is special in MarkdownV2 gets a preceding backslash:

_MDV2_ESCAPE_RE = re.compile(r'([_*\[\]()~`>#\+\-=|{}.!\\])')

def _escape_mdv2(text: str) -> str:
    return _MDV2_ESCAPE_RE.sub(r'\\\1', text)

Characters escaped: _ * [ ] ( ) ~ ` > # + - = | { } . ! \

Step 9: Restore Protected Regions

The placeholder tokens are swapped back with their protected code block/inline code content, now properly escaped for MarkdownV2.

4. Streaming & Delivery

PhasePrivate ChatsGroups / Supergroups
StreamingsendRichMessageDraft for real-time token previewFalls back to editMessageText (drafts are DM-only)
FinalizationeditMessageText with rich_message edits the preview in-placeSends final message directly
FallbackAny draft failure switches to edit-based streaming for that turnAlready on edit path

5. Key Engineering Decisions

Agent writes standard Markdown

The agent should not be burdened with platform-specific syntax. Separation of concerns: agent generates content, gateway adapts delivery.

Two paths instead of one

MarkdownV2 is the safe, universal baseline. Rich messages are used only when they materially improve output. This avoids client compatibility issues.

Table → bullet groups

More readable on mobile than fenced code blocks for small tables. Larger tables fall back to code blocks.

Placeholders for code regions

A clean pattern that prevents the escaping regex from corrupting code contents, while still applying MarkdownV2's required internal escapes.

In-place rich edit

Deleting the preview and sending a new message causes flicker. Editing the existing message's rich_message field is seamless.

Transparent fallback

If rich delivery fails for any reason, the user still gets a perfectly readable MarkdownV2 message. No error bubbles, no lost messages.

Character limit gate

The 32,768 char pre-check avoids a round-trip to Telegram's servers for oversized content.

6. Configuration

Operators can tune behavior in ~/.hermes/config.yaml:

gateway:
  streaming:
    enabled: true
    transport: auto    # auto | draft | edit | off
  platforms:
    telegram:
      extra:
        rich_messages: true        # Bot API 10.1 rich messages
        disable_link_previews: false

7. Results

The pipeline successfully bridges the gap between "what LLMs naturally output" and "what Telegram can render." Users see:

8. Files Referenced

FilePurpose
gateway/platforms/telegram.pyMain adapter: format_message, rich send logic, streaming
gateway/platforms/base.pyBasePlatformAdapter with REQUIRES_EDIT_FINALIZE contract
gateway/stream_consumer.pyStreaming edit/draft orchestration
gateway/rich_sent_store.pyIndex of rich messages for continuation tracking
tests/gateway/test_telegram_format.pyUnit tests for escaping, table wrapping, edge cases
website/docs/user-guide/messaging/telegram.mdOperator-facing documentation

Case study compiled from live source inspection of the Hermes Agent codebase. Hermes is a personal AI agent platform by Nous Research.

References

  1. Telegram. (2025). Bot API Documentation — Sending Rich Messages. core.telegram.org/api/richtext — 32,768 UTF-8 character limit for rich message payloads.
  2. Nous Research. (2025). Hermes Agent. GitHub Repository. github.com/NousResearch/hermes-agent — Source code for gateway/platforms/telegram.py and related modules cited in this case study.