Skip to content

feat(gateway): structured stream-event protocol + Telegram draft formatting parity#37250

Merged
teknium1 merged 1 commit into
mainfrom
hermes/hermes-123fc42b
Jun 2, 2026
Merged

feat(gateway): structured stream-event protocol + Telegram draft formatting parity#37250
teknium1 merged 1 commit into
mainfrom
hermes/hermes-123fc42b

Conversation

@teknium1

@teknium1 teknium1 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

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): GatewayEventDispatcher routes events through the adapter onto the stream-consumer sink + tool-progress queue. Adapters can return None from format_tool_event to 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_event default hooks reproduce the historical emoji/preview tool formatting and consumer delegation 1:1.
  • gateway/platforms/telegram.py: send_draft applies MarkdownV2 (format_message + parse_mode) with a plain-text fallback on BadRequest, so the animated draft renders identically to the final message.
  • gateway/config.py: default streaming transport editauto. Safe globally — adapters without draft support report supports_draft_streaming()==False and 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

Before After
Telegram draft formatting raw text → snaps to MarkdownV2 at finalize MarkdownV2 throughout, plain-text fallback on parse error
Default streaming transport edit everywhere auto (draft on TG DMs, edit elsewhere)
Tool-chrome rendering decision agent-side, all platforms identical adapter decides; can eat unrenderable chrome
New tests 19 (events/dispatcher) + 3 (send_draft)

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 bash blocks as drafts; iMessage eating tool chrome) — the base class is the parity shim; community/we can override render_message_event/format_tool_event per adapter on top of this contract.

Infographic

Structured stream-event protocol

…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.
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-123fc42b vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9647 on HEAD, 9637 on base (🆕 +10)

🆕 New issues (6):

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.

@teknium1 teknium1 merged commit 787936d into main Jun 2, 2026
23 checks passed
@teknium1 teknium1 deleted the hermes/hermes-123fc42b branch June 2, 2026 07:33
This was referenced Jun 2, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant