NousResearch/hermes-agent

Telegram rich Markdown hint gating and config

Cluster 57122 · gateway. Data as of

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

The short answer

0open issues
0open PRs
5items grouped
Known?
Yes. 5 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 concern whether Telegram's rich Markdown formatting instructions and prompt hints are shown or applied when the rich_messages config option is disabled. Multiple approaches to gating or defaulting this behavior are represented.

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

3 PRs closed without merging

From the close comments: merged via another change 2 · superseded by another PR 1

  • #57132 fix(agent): gate Telegram rich-Markdown hint on rich_messages config · merged via another change → #64423, #53825
  • #57148 fix(telegram): gate rich markdown prompt hint · merged via another change → #64423, #57132
  • #57210 fix(agent): remove rich Markdown instruction from Telegram platform hint · superseded by another PR → #64423, #57122, #57132
Closed issues · 1
  • Closed#57122Telegram platform hint pushes rich markdown constructs even when rich_messages is disabled (default)
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.40: 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
3 PRs closed without merging
Fix link checked
14 Jul 2026

For your agent

The same facts, typed.