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
- 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
Open PRs0
Fixes on record
-
#64423 fix(telegram): respect rich_messages opt-in for pipe tables and prompt hint (#53825, #57132 salvage)
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.pyShow 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.00means 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 LabAt a glance
- Subsystem
- gateway
- Merged PRs
- #64423
- Closed issues
- 1
- Attempts
- 3 PRs closed without merging
- Fix link checked
- 14 Jul 2026