NousResearch/hermes-agent

Telegram pipe table respects rich_messages config

Cluster 53824 · gateway. Data as of

Nothing open on GitHub. Fix merged in #64423 on 14 Jul 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #64423. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #64423. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and PRs address a bug where pipe table formatting in Telegram bypasses the rich_messages configuration setting. The work focuses on ensuring pipe tables and related prompt hints honor the user's opt-in or opt-out preference for rich messages.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #64423 fix(telegram): respect rich_messages opt-in for pipe tables and prompt hint (#53825, #57132 salvage)

    merged to main

    files: agent/prompt_builder.py, agent/system_prompt.py, plugins/platforms/telegram/adapter.py, tests/agent/test_prompt_builder.py, tests/agent/test_system_prompt.py, tests/gateway/test_telegram_rich_messages.py

    Show the diff
    • agent/prompt_builder.py
      @@ -659,19 +659,7 @@ def format_steer_marker(steer_text: str) -> str:
               "Standard Markdown is automatically converted to Telegram formatting. "
               "Supported: **bold**, *italic*, ~~strikethrough~~, ||spoiler||, "
               "`inline code`, ```code blocks```, [links](url), and ## headers. "
      -        "Telegram now supports rich Markdown, so lean into it: whenever it "
      -        "makes the answer clearer or easier to scan, actively reach for real "
      -        "Markdown tables (pipe `| col | col |` syntax), bullet and numbered "
      -        "lists, task lists (`- [ ]` / `- [x]`), headings, nested blockquotes, "
      -        "collapsible details, footnotes/references, math/formulas (`$...$`, "
      -        "`$$...$$`), underline, subscript/superscript, marked (highlighted) "
      -        "text, and anchors. Default to structured formatting over dense "
      -        "paragraphs for any comparison, set of steps, key/value summary, or "
      -        "tabular data. Prefer real Markdown tables and task lists over "
      -        "hand-built bullet substitutes when presenting structured data; these "
      -        "degrade gracefully (tables become readable bullet groups) when rich "
      -        "rendering is unavailable, but advanced constructs like math and "
      -        "collapsible details may render as plain source text in that case. "
      +        "Prefer bullet lists and labeled key:value pairs for structured data. "
               "You can send media files natively: to deliver a file to the user, "
               "include MEDIA:/absolute/path/to/file in your response. Images "
               "(.png, .jpg, .webp) appear as photos, audio (.ogg) sends as voice "
      @@ -865,6 +853,27 @@ def format_steer_marker(steer_text: str) -> str:
           ),
       }
       
      +# Telegram rich-messages extension — only injected when the user has opted in
      +# to ``platforms.telegram.extra.rich_messages: true``.  The base
      +# PLATFORM_HINTS["telegram"] covers MarkdownV2-compatible constructs; this
      +# extension adds the Bot API 10.1 rich-Markdown guidance (tables, task lists,
      +# collapsible details, math, etc.).
      +TELEGRAM_RICH_MESSAGES_HINT = (
      +    "Telegram now supports rich Markdown, so lean into it: whenever it "
      +    "makes the answer clearer or easier to scan, actively reach for real "
      +    "Markdown tables (pipe `| col | col |` syntax), bullet and numbered "
      +    "lists, task lists (`- [ ]` / `- [x]`), headings, nested blockquotes, "
      +    "collapsible details, footnotes/references, math/formulas (`$...$`, "
      +    "`$$...$$`), underline, subscript/superscript, marked (highlighted) "
      +    "text, and anchors. Default to structured formatting over dense "
      +    "paragraphs for any comparison, set of steps, key/value summary, or "
      +    "tabular data. Prefer real Markdown tables and task lists over "
      +    "hand-built bullet substitutes when presenting structured data; these "
      +    "degrade gracefully (tables become readable bullet groups) when rich "
      +    "rendering is unavailable, but advanced constructs like math and "
      +    "collapsible details may render as plain source text in that case. "
      +)
      +
       # ---------------------------------------------------------------------------
       # Environment hints — execution-environment awareness for the agent.
       # Unlike PLATFORM_HINTS (which describe the messaging channel), these describe
    • agent/system_prompt.py
      @@ -40,6 +40,7 @@
           SKILLS_GUIDANCE,
           STEER_CHANNEL_NOTE,
           TASK_COMPLETION_GUIDANCE,
      +    TELEGRAM_RICH_MESSAGES_HINT,
           TOOL_USE_ENFORCEMENT_GUIDANCE,
           TOOL_USE_ENFORCEMENT_MODELS,
           drain_truncation_warnings,
      @@ -429,6 +430,20 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None)
               except Exception:
                   pass
       
      +    # For Telegram: append the rich-messages extension only when the user has
      +    # opted in to ``platforms.telegram.extra.rich_messages: true``.  The base
      +    # hint covers MarkdownV2-compatible constructs; the extension adds Bot API
      +    # 10.1 guidance (tables, task lists, math, collapsible details, etc.).
      +    if platform_key == "telegram" and _default_hint:
      +        try:
      +            from hermes_cli.config import load_config_readonly
      +            _cfg = load_config_readonly()
      +            _tg_extra = ((_cfg.get("platforms") or {}).get("telegram") or {}).get("extra") or {}
      +            if _tg_extra.get("rich_messages"):
      +                _default_hint = _default_hint.rstrip() + " " + TELEGRAM_RICH_MESSAGES_HINT
      +        except Exception:
      +            pass  # Config read failure — fall back to base hint only
      +
           _effective_hint = _resolve_platform_hint(agent, platform_key, _default_hint)
           if platform_key == "tui" and _effective_hint:
               _effective_hint = _tui_embedded_pane_clarifier(_effective_hint)
    • plugins/platforms/telegram/adapter.py
      @@ -1479,33 +1479,9 @@ def _needs_rich_rendering(self, content: str) -> bool:
                   return True
               return False
       
      -    def _content_is_pipe_table_primary(self, content: str) -> bool:
      -        """True when pipe tables are the only rich construct in *content*.
      -
      -        Tables are auto-routed to ``sendRichMessage`` even when the full
      -        ``rich_messages`` opt-in is off — MarkdownV2 has no table syntax and
      -        the legacy path rewrites them into bullet lists, which reads like a
      -        regression when users enable Telegram Topics and expect native tables.
      -        Task lists, ``<details>``, and block math still require the full opt-in.
      -        """
      -        if not content or not any(
      -            _TABLE_SEPARATOR_RE.match(line) for line in content.splitlines()
      -        ):
      -            return False
      -        if re.search(r"(?m)^\s*[-*]\s+\[[ xX]\]\s+", content):
      -            return False
      -        if re.search(r"(?m)^<details\b|^</details>|^<summary\b|^</summary>", content):
      -            return False
      -        if "$$" in content:
      -            return False
      -        return True
      -
      -    def _rich_delivery_enabled(self, content: str) -> bool:
      -        """Whether rich delivery is allowed for this payload."""
      -        return bool(
      -            getattr(self, "_rich_messages_enabled", True)
      -            or self._content_is_pipe_table_primary(content)
      -        )
      +    def _rich_delivery_enabled(self) -> bool:
      +        """Whether rich delivery is allowed (``rich_messages`` opt-in)."""
      +        return bool(getattr(self, "_rich_messages_enabled", True))
       
           def _rich_eligible(self, content: str) -> bool:
               """Capability/content eligibility for rich, ignoring ``expect_edits``.
      @@ -1517,7 +1493,7 @@ def _rich_eligible(self, content: str) -> bool:
               FINAL edit should still upgrade to rich when the content warrants it.
               """
               return bool(
      -            self._rich_delivery_enabled(content)
      +            self._rich_delivery_enabled()
                   and not getattr(self, "_rich_send_disabled", False)
                   and content
                   and content.strip()
    • tests/agent/test_prompt_builder.py
      @@ -1092,15 +1092,22 @@ def test_telegram_hint_encourages_rich_markdown(self):
               hint = PLATFORM_HINTS["telegram"]
               lowered = hint.lower()
               assert "Telegram has NO table syntax" not in hint
      -        assert "rich markdown" in lowered
      -        assert "table" in lowered
      -        assert "task list" in lowered
      -        assert "math" in lowered
      +        # Base hint covers MarkdownV2-compatible constructs.
      +        assert "MEDIA:" in hint
      +        # Rich-messages extension (TELEGRAM_RICH_MESSAGES_HINT) covers the
      +        # Bot API 10.1 guidance; it is injected conditionally in
      +        # system_prompt.py when rich_messages: true.
      +        from agent.prompt_builder import TELEGRAM_RICH_MESSAGES_HINT
      +        rich_lowered = TELEGRAM_RICH_MESSAGES_HINT.lower()
      +        assert "rich markdown" in rich_lowered
      +        assert "table" in rich_lowered
      +        assert "task list" in rich_lowered
      +        assert "math" in rich_lowered
               # Hint should proactively steer toward structured formatting, not just
               # permit it: bullet + numbered lists for scannable, structured output.
      -        assert "bullet" in lowered
      -        assert "numbered" in lowered
      -        # Local media delivery guidance must remain intact.
      +        assert "bullet" in rich_lowered
      +        assert "numbered" in rich_lowered
      +        # Local media delivery guidance must remain intact in the base hint.
               assert "include MEDIA:" in hint
       
           def test_platform_hints_mattermost(self):
    • tests/agent/test_system_prompt.py
      @@ -99,3 +99,46 @@ def test_absent_without_tools(self, monkeypatch, tmp_path):
               monkeypatch.setenv("TERMINAL_CWD", str(tmp_path))
               agent = _make_agent(valid_tool_names=[], platform="cli")
               assert "coding agent" not in _stable_prompt(agent)
      +
      +
      +class TestTelegramRichMessagesHint:
      +    """Verify that TELEGRAM_RICH_MESSAGES_HINT is conditionally included."""
      +
      +    def test_base_hint_without_rich_messages(self, monkeypatch):
      +        """When rich_messages is False (default), only the base hint is used."""
      +        agent = _make_agent(platform="telegram")
      +        # Mock config to return rich_messages: false (default)
      +        with patch("hermes_cli.config.load_config_readonly") as mock_cfg:
      +            mock_cfg.return_value = {
      +                "platforms": {"telegram": {"extra": {"rich_messages": False}}}
      +            }
      +            stable = _stable_prompt(agent)
      +        # Base hint should be present
      +        assert "Standard Markdown is automatically converted" in stable
      +        # Rich-messages extension should NOT be present
      +        assert "lean into it" not in stable
      +        assert "task lists" not in stable
      +
      +    def test_rich_hint_with_rich_messages_enabled(self, monkeypatch):
      +        """When rich_messages is True, the rich-messages extension is appended."""
      +        agent = _make_agent(platform="telegram")
      +        with patch("hermes_cli.config.load_config_readonly") as mock_cfg:
      +            mock_cfg.return_value = {
      +                "platforms": {"telegram": {"extra": {"rich_messages": True}}}
      +            }
      +            stable = _stable_prompt(agent)
      +        # Base hint should be present
      +        assert "Standard Markdown is automatically converted" in stable
      +        # Rich-messages extension should be present
      +        assert "lean into it" in stable
      +        assert "task lists" in stable
      +        assert "math/formulas" in stable
      +
      +    def test_base_hint_without_config(self, monkeypatch):
      +        """When config has no telegram section, only base hint is used."""
      +        agent = _make_agent(platform="telegram")
      +        with patch("hermes_cli.config.load_config_readonly") as mock_cfg:
      +            mock_cfg.return_value = {}
      +            stable = _stable_prompt(agent)
      +        assert "Standard Markdown is automatically converted" in stable
      +        assert "lean into it" not in stable
    • tests/gateway/test_telegram_rich_messages.py
      @@ -582,21 +582,20 @@ async def test_notification_opt_in_drops_disable_flag():
       
       
       @pytest.mark.asyncio
      -async def test_table_only_uses_rich_when_rich_messages_opt_out():
      -    """Pipe tables auto-route to sendRichMessage even without the full opt-in."""
      +async def test_table_only_uses_legacy_when_rich_messages_opt_out():
      +    """Pipe tables respect the rich_messages: false config and stay on legacy."""
           adapter = _make_adapter(extra={"rich_messages": False})
       
           result = await adapter.send("12345", TABLE_ONLY_CONTENT)
       
           assert result.success is True
      -    api_kwargs = _rich_api_kwargs(adapter)
      -    assert api_kwargs["rich_message"]["markdown"] == TABLE_ONLY_CONTENT
      -    adapter._bot.send_message.assert_not_called()
      +    adapter._bot.do_api_request.assert_not_called()
      +    adapter._bot.send_message.assert_awaited()
       
       
       @pytest.mark.asyncio
      -async def test_table_only_uses_rich_with_default_config():
      -    """Default config keeps task lists on legacy but upgrades bare tables."""
      +async def test_table_only_uses_legacy_with_default_config():
      +    """Default config (rich_messages unset → False) keeps tables on legacy path."""
           config = PlatformConfig(enabled=True, token="[redacted:secret-assignment]")
           adapter = TelegramAdapter(config)
           bot = MagicMock()
      @@ -608,13 +607,13 @@ async def test_table_only_uses_rich_with_default_config():
           result = await adapter.send("12345", TABLE_ONLY_CONTENT)
       
           assert result.success is True
      -    bot.do_api_request.assert_awaited_once()
      -    bot.send_message.assert_not_called()
      +    bot.do_api_request.assert_not_called()
      +    bot.send_message.assert_awaited()
       
       
       @pytest.mark.asyncio
      -async def test_dm_topic_resumed_send_uses_rich_for_table_without_reply_anchor():
      -    """Resumed/synthetic DM-topic sends route tables via direct_messages_topic_id."""
      +async def test_dm_topic_resumed_send_uses_legacy_for_table_when_opt_out():
      +    """Resumed DM-topic sends respect rich_messages: false for tables."""
           adapter = _make_adapter(extra={"rich_messages": False})
       
           result = await adapter.send(
      @@ -628,14 +627,13 @@ async def test_dm_topic_resumed_send_uses_rich_for_table_without_reply_anchor():
           )
       
           assert result.success is True
      -    api_kwargs = _rich_api_kwargs(adapter)
      -    assert api_kwargs["direct_messages_topic_id"] == 20189
      -    assert "reply_parameters" not in api_kwargs
      -    assert api_kwargs["rich_message"]["markdown"] == TABLE_ONLY_CONTENT
      +    adapter._bot.do_api_request.assert_not_called()
      +    adapter._bot.send_message.assert_awaited()
       
       
       @pytest.mark.asyncio
      -async def test_finalize_edit_rich_includes_forum_topic_routing():
      +async def test_finalize_edit_legacy_includes_forum_topic_routing():
      +    """With rich_messages: false, table edits use legacy path with topic routing."""
           adapter = _make_adapter(extra={"rich_messages": False})
       
           result = await adapter.edit_message(
      @@ -647,9 +645,8 @@ async def test_finalize_edit_rich_includes_forum_topic_routing():
           )
       
           assert result.success is True
      -    api_kwargs = _rich_edit_kwargs(adapter)
      -    assert api_kwargs["message_thread_id"] == 5
      -    assert api_kwargs["rich_message"]["markdown"] == TABLE_ONLY_CONTENT
      +    adapter._bot.do_api_request.assert_not_called()
      +    adapter._bot.edit_message_text.assert_awaited()
       
       
       @pytest.mark.asyncio

1 PR closed without merging

From the close comments: merged via another change 1

  • #53825 fix(telegram): respect rich_messages config for pipe table routing (#53824) · merged via another change → #64423
Closed issues · 1
  • Closed#53824fix(telegram): pipe table auto-rich bypasses rich_messages: false config
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#64423
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
14 Jul 2026

For your agent

The same facts, typed.