feat(gateway): structured stream-event protocol + Telegram draft formatting parity#37250
Merged
Conversation
…atting parity Introduce a typed agent→gateway delivery contract so the gateway (not the agent) decides how each streaming event is rendered per platform. Moves toward smart-agent/smart-gateway separation while reproducing today's behavior exactly in the base class. - gateway/stream_events.py: typed event vocabulary (MessageChunk/Stop, Commentary, ToolCallChunk/Finished, LongToolHint, GatewayNotice). - gateway/stream_dispatch.py: GatewayEventDispatcher routes events through the adapter; adapters can eat events they can't render (e.g. tool chrome on plain-text platforms). - gateway/platforms/base.py: render_message_event + format_tool_event default hooks reproduce the historical emoji/preview tool formatting and consumer delegation 1:1; adapters override for native rendering. - gateway/platforms/telegram.py: send_draft now applies MarkdownV2 (format_message + parse_mode) with a plain-text fallback on BadRequest, fixing the jarring raw-text→formatted shift when the draft finalizes as a real sendMessage. - gateway/config.py: default streaming transport edit → auto. Safe globally: adapters without draft support report supports_draft_streaming()==False and transparently use edit, so only Telegram DMs gain native drafts. Presentation-only contract — nothing rendered here is persisted to conversation history, preserving cache/message-flow invariants.
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-assignment |
3 |
unresolved-attribute |
2 |
unresolved-import |
1 |
First entries
tests/gateway/test_telegram_send_draft_format.py:60: [unresolved-attribute] unresolved-attribute: Attribute `send_message_draft` is not defined on `None` in union `Unknown | None`
tests/gateway/test_telegram_send_draft_format.py:62: [unresolved-attribute] unresolved-attribute: Attribute `MARKDOWN_V2` is not defined on `None` in union `Unknown | None`
tests/gateway/test_telegram_send_draft_format.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/gateway/test_telegram_send_draft_format.py:109: [invalid-assignment] invalid-assignment: Object of type `AsyncMock` is not assignable to attribute `send_message_draft` on type `Unknown | None`
tests/gateway/test_telegram_send_draft_format.py:101: [invalid-assignment] invalid-assignment: Object of type `(c) -> str` is not assignable to attribute `format_message` of type `def format_message(self, content: str) -> str`
tests/gateway/test_telegram_send_draft_format.py:72: [invalid-assignment] invalid-assignment: Object of type `(content) -> str` is not assignable to attribute `format_message` of type `def format_message(self, content: str) -> str`
✅ Fixed issues: none
Unchanged: 4991 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
This was referenced Jun 3, 2026
This was referenced Jun 4, 2026
1 task
unsaltedbutter-ai
added a commit
to unsaltedbutter-ai/hermes-agent
that referenced
this pull request
Jun 6, 2026
The new gateway stream-event protocol added format_tool_event to BasePlatformAdapter in NousResearch#37250. The base implementation renders every ToolCallChunk as an emoji+name+preview string that the gateway then ships through the adapter normal send path. For Telegram that becomes a single edited bubble. Nostr has no in-place message editing — NIP-09 is event deletion, not editing — so every tool progress event would land as its own NIP-17 gift-wrap published to all relays. A multi-tool turn would flood the recipient. Override the hook to return None so the dispatcher eats tool events entirely and only the final agent response reaches the user. Nostr is the first adapter to take advantage of the override; the base default matches today pre-protocol behavior for adapters that have not yet migrated.
JiehoonKwak
added a commit
to JiehoonKwak/hermes-agent
that referenced
this pull request
Jun 9, 2026
Add a dm_only per-platform streaming mode so Telegram can keep native draft streaming for private chats and DM topics while group/forum chats stay on final-send delivery. This preserves existing boolean override behavior and keeps the change config-driven: display.platforms.telegram.streaming can be false, true, or dm_only/private_only. Context: - Hermes was updated to main with PR NousResearch#37250 included. - Group/topic Telegram chats previously hit edit-rate and final-message failure modes when streaming was enabled. - The local runtime needs DM streaming enabled while group delivery remains non-streaming.
JiehoonKwak
added a commit
to JiehoonKwak/hermes-agent
that referenced
this pull request
Jun 9, 2026
Add a dm_only per-platform streaming mode so Telegram can keep native draft streaming for private chats and DM topics while group/forum chats stay on final-send delivery. This preserves existing boolean override behavior and keeps the change config-driven: display.platforms.telegram.streaming can be false, true, or dm_only/private_only. Context: - Hermes was updated to main with PR NousResearch#37250 included. - Group/topic Telegram chats previously hit edit-rate and final-message failure modes when streaming was enabled. - The local runtime needs DM streaming enabled while group delivery remains non-streaming.
unsaltedbutter-ai
added a commit
to unsaltedbutter-ai/hermes-agent
that referenced
this pull request
Jun 10, 2026
The new gateway stream-event protocol added format_tool_event to BasePlatformAdapter in NousResearch#37250. The base implementation renders every ToolCallChunk as an emoji+name+preview string that the gateway then ships through the adapter normal send path. For Telegram that becomes a single edited bubble. Nostr has no in-place message editing — NIP-09 is event deletion, not editing — so every tool progress event would land as its own NIP-17 gift-wrap published to all relays. A multi-tool turn would flood the recipient. Override the hook to return None so the dispatcher eats tool events entirely and only the final agent response reaches the user. Nostr is the first adapter to take advantage of the override; the base default matches today pre-protocol behavior for adapters that have not yet migrated.
changman
pushed a commit
to changman/hermes-agent
that referenced
this pull request
Jun 10, 2026
…atting parity (NousResearch#37250) Introduce a typed agent→gateway delivery contract so the gateway (not the agent) decides how each streaming event is rendered per platform. Moves toward smart-agent/smart-gateway separation while reproducing today's behavior exactly in the base class. - gateway/stream_events.py: typed event vocabulary (MessageChunk/Stop, Commentary, ToolCallChunk/Finished, LongToolHint, GatewayNotice). - gateway/stream_dispatch.py: GatewayEventDispatcher routes events through the adapter; adapters can eat events they can't render (e.g. tool chrome on plain-text platforms). - gateway/platforms/base.py: render_message_event + format_tool_event default hooks reproduce the historical emoji/preview tool formatting and consumer delegation 1:1; adapters override for native rendering. - gateway/platforms/telegram.py: send_draft now applies MarkdownV2 (format_message + parse_mode) with a plain-text fallback on BadRequest, fixing the jarring raw-text→formatted shift when the draft finalizes as a real sendMessage. - gateway/config.py: default streaming transport edit → auto. Safe globally: adapters without draft support report supports_draft_streaming()==False and transparently use edit, so only Telegram DMs gain native drafts. Presentation-only contract — nothing rendered here is persisted to conversation history, preserving cache/message-flow invariants.
unsaltedbutter-ai
added a commit
to unsaltedbutter-ai/hermes-agent
that referenced
this pull request
Jun 12, 2026
The new gateway stream-event protocol added format_tool_event to BasePlatformAdapter in NousResearch#37250. The base implementation renders every ToolCallChunk as an emoji+name+preview string that the gateway then ships through the adapter normal send path. For Telegram that becomes a single edited bubble. Nostr has no in-place message editing — NIP-09 is event deletion, not editing — so every tool progress event would land as its own NIP-17 gift-wrap published to all relays. A multi-tool turn would flood the recipient. Override the hook to return None so the dispatcher eats tool events entirely and only the final agent response reaches the user. Nostr is the first adapter to take advantage of the override; the base default matches today pre-protocol behavior for adapters that have not yet migrated.
JiehoonKwak
added a commit
to JiehoonKwak/hermes-agent
that referenced
this pull request
Jun 12, 2026
Add a dm_only per-platform streaming mode so Telegram can keep native draft streaming for private chats and DM topics while group/forum chats stay on final-send delivery. This preserves existing boolean override behavior and keeps the change config-driven: display.platforms.telegram.streaming can be false, true, or dm_only/private_only. Context: - Hermes was updated to main with PR NousResearch#37250 included. - Group/topic Telegram chats previously hit edit-rate and final-message failure modes when streaming was enabled. - The local runtime needs DM streaming enabled while group delivery remains non-streaming.
JiehoonKwak
added a commit
to JiehoonKwak/hermes-agent
that referenced
this pull request
Jun 12, 2026
Add a dm_only per-platform streaming mode so Telegram can keep native draft streaming for private chats and DM topics while group/forum chats stay on final-send delivery. This preserves existing boolean override behavior and keeps the change config-driven: display.platforms.telegram.streaming can be false, true, or dm_only/private_only. Context: - Hermes was updated to main with PR NousResearch#37250 included. - Group/topic Telegram chats previously hit edit-rate and final-message failure modes when streaming was enabled. - The local runtime needs DM streaming enabled while group delivery remains non-streaming.
unsaltedbutter-ai
added a commit
to unsaltedbutter-ai/hermes-agent
that referenced
this pull request
Jun 13, 2026
The new gateway stream-event protocol added format_tool_event to BasePlatformAdapter in NousResearch#37250. The base implementation renders every ToolCallChunk as an emoji+name+preview string that the gateway then ships through the adapter normal send path. For Telegram that becomes a single edited bubble. Nostr has no in-place message editing — NIP-09 is event deletion, not editing — so every tool progress event would land as its own NIP-17 gift-wrap published to all relays. A multi-tool turn would flood the recipient. Override the hook to return None so the dispatcher eats tool events entirely and only the final agent response reaches the user. Nostr is the first adapter to take advantage of the override; the base default matches today pre-protocol behavior for adapters that have not yet migrated.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The gateway now owns how each streaming event is rendered per platform, instead of the agent baking in formatting that every platform must accept. Adds a typed agent→gateway event contract (the smart-agent/smart-gateway split), and fixes the two Telegram streaming defects users hit: the draft preview rendering raw text then snapping into MarkdownV2 at the end, and edit being the default transport when native draft is strictly better.
Behavior is unchanged out of the box except the two intended fixes — the base class reproduces today's rendering exactly; per-platform native rendering is opt-in for follow-up work.
Changes
gateway/stream_events.py(new): typed event vocabulary —MessageChunk/MessageStop,Commentary,ToolCallChunk/ToolCallFinished,LongToolHint,GatewayNotice.gateway/stream_dispatch.py(new):GatewayEventDispatcherroutes events through the adapter onto the stream-consumer sink + tool-progress queue. Adapters can returnNonefromformat_tool_eventto eat events they can't render (e.g. tool chrome on plain-text platforms like iMessage).gateway/platforms/base.py:render_message_event+format_tool_eventdefault hooks reproduce the historical emoji/preview tool formatting and consumer delegation 1:1.gateway/platforms/telegram.py:send_draftapplies MarkdownV2 (format_message+parse_mode) with a plain-text fallback onBadRequest, so the animated draft renders identically to the final message.gateway/config.py: default streamingtransportedit→auto. Safe globally — adapters without draft support reportsupports_draft_streaming()==Falseand transparently use edit, so only Telegram DMs gain native drafts.Why it's low-risk
The draft transport already produces clean streaming; this formalizes the contract around it and reproduces current behavior in the base class. The event protocol + dispatcher are new standalone modules — nothing in the agent core was re-plumbed. Presentation-only: nothing rendered here is persisted to conversation history, so cache/message-flow invariants are preserved.
Validation
editeverywhereauto(draft on TG DMs, edit elsewhere)Targeted suite: 80 passed (events, send_draft, config, draft consumer) + 196 passed across streaming/telegram/format/batching — no regressions.
Follow-up (not in this PR)
Per-platform native event rendering (Telegram streaming
bashblocks as drafts; iMessage eating tool chrome) — the base class is the parity shim; community/we can overriderender_message_event/format_tool_eventper adapter on top of this contract.Infographic