Repository navigation
feat(telegram): single-message streaming + Bot API 10.3 extras (stop button, effects, ephemerals), all opt-in (#110564) - #110611
Conversation
Hardening pass update — docs + live-API verification & activity overlaySince the first revision we exercised this feature directly against the real Telegram Bot API (current docs, 10.3) and the live bot, and folded the findings back in. Pushed commits: Verified against Bot API docs + live probes (macOS host, system proxy):
Hardening fixes in this revision:
New: transient activity overlay (opt-in, inside the same message):
Tests: 74→(now) green in Follow-ups noted (not in this PR): draft |
4096-split policy switch (deferred vs eager pagination)Added
Tests: deferred keeps |
|
Update: Telegram platform features (Bot API 10.3), each behind its own opt-in switch — commit
Also in this push: stateful multi-line expandable-blockquote conversion (the activity-overlay framing), and a Telegram adapter-extras test file plus busy-ack ephemeral tests. Docs: Verification: 12-suite gateway regression 307 passed / 0 failed (format, adapter extras, consumer, display-config, busy family, polling-progress). |
a225bb5 to
7d4c4e7
Compare
|
Refresh: rebased onto current Local verification: every test file this PR touches is green (stream consumer, tool-progress, telegram adapter/format/rich-messages, display config, busy ack — 250 tests). Unchanged context: this ships alongside #111631 (mirroring local-surface turns) — both fully opt-in behind independent switches, no code dependency between the two, so either can land first. Happy to adjust scope or split further if that helps review. |
…gate + docs (NousResearch#110564) Keeps one editable streaming preview alive for a whole agent turn — text on both sides of tool calls keeps updating the same message instead of sealing a new one at every boundary. Opt-in via display.platforms.telegram.streaming_single_message; Telegram-only; active while text tool-progress is quiet (off/log). Approach prototyped in NousResearch#110571 (@KoNit-K) — same config key and boundary skip — extended with: - quiet-progress gate also accepts tool_progress: log (no chat bubbles either; matches the gateway's own {"off", "log"} quiet definition); - mid-turn commentary no longer resets the evolving preview; - docs (messaging guide + cli-config.yaml.example) and boundary tests (2 tool boundaries, commentary continuity, quiet-mode gating). Tests: scripts/run_tests.sh tests/gateway/test_stream_consumer.py base: 3 failed → with change: 74 passed, 1 skipped; adjacent stream/display suites: 98 passed, 0 failed.
…vity overlay, docs Follow-up hardening on the single-message streaming opt-in (NousResearch#110564), found and fixed by exercising the real Telegram Bot API (10.3 docs + live probes) against the feature: - Gate on the EFFECTIVE tool-progress mode via the shared `resolve_tool_progress()` resolver — the same winning source the display path runs with (YAML wins, null inherits to the env bridge, tier defaults last) — so the single-message gate can never disagree with the mode the turn actually runs with. Previously the env bridge could leave tool bubbles ON while the gate treated progress as quiet. Red → green test included. - Transient activity overlay (new, opt-in): while the single preview runs, tool starts and thinking snippets render under a `---` rule inside the same message and are replaced the moment real text arrives — the final edit never carries them. Switches: display.platforms.<p>.streaming_single_message_activity (default true; quiet tool_progress would otherwise show nothing during tool runs) and streaming_single_message_thinking (opt-in). Works on both Telegram transports: the ephemeral draft preview (sendMessageDraft) and the edit path; tool lines reuse the consumer's existing native-overlay machinery. - Wire the runner: tool.started events and `_thinking` (scratch/reasoning) events route into the overlay even though progress bubbles are quiet — before the progress-queue guard, since single mode runs without a queue. Docs: cli-config.yaml.example + messaging guide updated; verified against the Bot API reference (edit window: unlimited for bot-sent messages; drafts: 30s ephemeral preview, private chats only; 4096-char cap enforced server-side with codepoint counting — the consumer's utf16 accounting stays conservative).
Per the requested rule for the single-message streaming mode: - `streaming_single_message_4096_split: true` — over-limit content is cut into several ≤4096 messages as the preview fills (classic eager sealing). - Default `false` — deferred pagination: the preview is NOT cut while the turn runs; an over-limit turn-final is paged by the adapter's overflow split instead, so the live phase stays one message and pages only appear when the text genuinely overflows (rich-eligible content first rides the platform's 32,768-character cap). Nothing is ever lost — pagination just arrives later. Consumer gate `_eager_overflow_split()`; config plumbing in run_turn + display_config; docs in cli-config.yaml.example and the messaging guide. Tests: deferred keeps `sends == 1` with an over-limit final edit; eager seals into multiple messages; both replayed for zero lost/duplicated characters; config resolution incl. master-gate collapse. 101 passed in the two suites.
…sts, message effects, ephemeral scope - stop_button extra: DM draft previews carry can_stop via the raw API; a stopped_message_generation update re-dispatches as a synthetic /stop into the standard interrupt path; disabled or unauthorized presses are ignored - checklist_emoji extra + sender scope → native sendChecklist (business accounts only; MarkdownV2 fallback elsewhere) - message_effects switch: validated effect ids (👍👎🔥🎉🎊, live-verified; ❤ requires paid sends and is excluded), numeric pass-through, private chats only; attach on turn-final sends after message_effect_min_seconds (60s) - ephemeral_messages extra: group busy-acks whisper to the sender via extra_metadata; no group-wide leak on failure - formatter: stateful multi-line expandable-quote conversion (style A)\n\nTests: telegram format/adapter-extras/consumer/busy-ack suites green; 12-suite gateway regression 307 passed. .docs: messaging/index.md + cli-config extras section
- _try_send_rich attaches message_effect_id from metadata under the same gate as the legacy path (turn-final + DM-only + verified id); a completed reply rendered via sendRichMessage no longer silently drops the effect - tests: rich send carries the field when requested, omits it otherwise
7d4c4e7 to
c2758f6
Compare
Why this matters
One smooth message instead of a fragmented stream — plus four Bot API 10.3 capabilities, each behind its own opt-in switch.
Long Telegram answers that cross tool calls currently arrive as a chain of separate messages (a new send at every boundary). This PR keeps the entire turn in one editable message with a compact live activity overlay, and adds the modern Bot API 10.3 extras — everything opt-in, defaults off:
streaming_single_message) — one message per turn; thinking/tool activity as an expandable overlay; per-switch controls for activity, thinking, and 4096-split behavior; composes with quiet tool-progress modes (off/log).stop_button) — Bot API 10.3 dismissible stop control on draft previews; a press routes to/stopwith the standard semantics.message_effects) — a completion effect (🎉 …) on the turn-final reply (private chats), with a minimum-turn-age gate.ephemeral_messages) — busy-ack whispered to the sender in groups; never falls back to a group-wide send.sendChecklistwhere the account type supports it (the business-account limitation is documented).Companion PR — a two-part messaging upgrade
This is the first half of a coherent messaging/Telegram upgrade; both PRs are designed to land and be used together:
They are split into two PRs only for compatibility / independent reviewability — neither depends on the other's code. The mirror PR reuses this PR's long-text parameter semantics (
streaming_single_message_4096_split) by design, so the two behave consistently when enabled together.What does this PR do?
Adds an opt-in Telegram streaming mode that keeps one editable preview for the entire turn — text emitted on both sides of tool calls keeps updating the same message instead of sealing a new one at every tool boundary.
Verified behavior (local suites + a mock-adapter scenario bench):
Relation to #110571
Builds on the approach prototyped in #110571 by @KoNit-K — same config key (
display.platforms.telegram.streaming_single_message) and the same one-line boundary skip — so maintainers can prefer either PR or fold them together without config churn. Extensions in this PR:offandlog—tool_progress: logemits no chat bubbles either (the gateway's owntool_progress_enabledtreats{"off", "log"}as quiet), so both quiet modes can host the single message.website/docs/user-guide/messaging/index.md+cli-config.yaml.example.Related Issue
Implements #110564 (companion to #110571).
Type of Change
Changes Made
gateway/display_config.py— optionstreaming_single_message(default false) + normalizer.gateway/run_turn.py— resolves the opt-in (Telegram-only, quiet tool-progressoff/log) into the consumer config.gateway/stream_consumer.py—single_message_per_turn: skip the segment reset at tool boundaries and after commentary; docstrings updated.tests/gateway/test_stream_consumer.py— 3 new tests.website/docs/user-guide/messaging/index.md,cli-config.yaml.example— docs.How to Test
scripts/run_tests.sh tests/gateway/test_stream_consumer.py→ base3 failed, 71 passed→ with the change74 passed, 1 skipped, 0 failed.Full
pytest tests/ -qnot run locally (suite too large); scoped suites above + CI for the rest. Tested on macOS (Darwin 27), Python 3.11.Checklist
Code
pytest tests/ -q— ran the scoped suites instead (see How to Test); CI covers the full matrixDocumentation & Housekeeping
cli-config.yaml.example)CONTRIBUTING.md/AGENTS.md— N/A