Skip to content

feat(gateway/signal): native formatting, reply quotes, reactions + send_file tool + no-edit streaming#11396

Closed
exiao wants to merge 3 commits into
NousResearch:mainfrom
exiao:feat/signal-native-formatting-and-send-file
Closed

feat(gateway/signal): native formatting, reply quotes, reactions + send_file tool + no-edit streaming#11396
exiao wants to merge 3 commits into
NousResearch:mainfrom
exiao:feat/signal-native-formatting-and-send-file

Conversation

@exiao

@exiao exiao commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Combined PR that adds three related improvements enabling first-class Signal support:

  1. send_file tool — agents can attach files to outgoing platform messages
  2. No-edit-mode streaming — gateway streaming path for platforms without a message-edit API
  3. Signal adapter native formatting, reply quotes, and reactions — consumes the no-edit path

The three are shipped together because #3 depends on the SUPPORTS_MESSAGE_EDITING / ProcessingOutcome plumbing introduced in #2, and #1 is the natural companion for attachment-capable platforms like Signal.


Commit 1 — feat(tools): send_file tool for platform attachment uploads

Why: There was no first-class way for an agent to attach a file (image, PDF, audio) to an outgoing platform message — you had to hack the path into the prompt.

What: New tools/send_file_tool.py resolves the active adapter from conversation context and calls adapter.send(..., attachments=[path]). Includes path validation, size limits, and MIME sniffing. Auto-discovered by tools/registry.py.

Tests: 28 new tests in tests/tools/test_send_file_tool.py.

Risk: Low — pure addition.


Commit 2 — feat(gateway): no-edit-mode streaming for platforms without message edit API

Why: Signal (and some other transports) have no outbound message-edit API, so the existing streaming loop that repeatedly edits a placeholder message fails silently and leaves a trailing cursor visible in clients.

What:

  • BasePlatformAdapter.SUPPORTS_MESSAGE_EDITING: bool = True class attr (adapters that can't edit override to False).
  • ProcessingOutcome enum for on_processing_start/on_processing_complete hook return values.
  • StreamConsumerConfig.no_edit_mode: bool = False + wiring in gateway/run.py (no_edit_mode=not _adapter_supports_edit).
  • stream_consumer.py no-edit branch flushes on paragraph breaks (\n\n) above a minimum buffer plus segment/done/commentary events, instead of time-interval edits.

All edit-mode platforms (Telegram, Matrix, Discord) default True and take the original code path unchanged — the new branch is effectively dead code for them.

Tests: 23 new tests in tests/gateway/test_stream_consumer_no_edit.py, plus regression extensions to test_stream_consumer.py.

Risk: Medium — touches the gateway hot path.

Regression matrix

Scenario Expected
Telegram (SUPPORTS_MESSAGE_EDITING=True) progressive edit, cursor visible — unchanged
Signal (False) paragraph-boundary splits or single final message, no cursor
Short response (<min buffer) on no-edit platform one flush at done
Long response crossing _safe_limit on no-edit split at paragraph breaks

Commit 3 — feat(gateway/signal): native formatting, reply quotes, and reactions

Why: Signal messages from Hermes currently show literal **bold**, `code`, # heading. Inbound reply-quote context is ignored. Reactions (✅ on completion) never fire.

What — three sub-features on the same adapter:

3a. Markdown → Signal bodyRanges

New _markdown_to_signal(text) -> (plain, styles) strips markdown syntax and emits Signal-native bodyRanges as start:length:STYLE entries. Offsets are computed in UTF-16 code units so non-BMP emoji stay aligned. Supports BOLD, ITALIC, STRIKE, MONO, headings → BOLD, fenced/inline code, link unwrapping.

Includes two previously-reported regression fixes:

  • Bullet lists (* item) no longer misidentified as italics
  • URLs containing underscores no longer italicized around the dot

3b. Reply-quote extraction

Parses dataMessage.quote and populates MessageEvent.raw_message with sender + timestamp_ms. Lets the gateway's existing [Replying to: "..."] injector work on Signal, matching Telegram/Matrix behavior.

3c. Processing reactions

Overrides on_processing_start → ⏳ and on_processing_complete → ✅ via sendReaction RPC, using targetAuthor/targetTimestamp from raw_message. Uses the ProcessingOutcome enum from commit 2.

Sets SUPPORTS_MESSAGE_EDITING = False on SignalAdapter so the no-edit streaming path activates.

Tests: 40+ new tests in tests/gateway/test_signal_format.py (markdown conversion, UTF-16 offset correctness with non-BMP emoji, bullet-list and URL false-positive regressions, reply-quote extraction, reaction payload shape) + regression extensions to test_signal.py.

Risk: Medium-High — biggest patch in this PR. Feature is opt-in in the sense that only Signal users experience it.


Test plan

Automated (all green locally)

source venv/bin/activate
python -m pytest \
  tests/tools/test_send_file_tool.py \
  tests/gateway/test_stream_consumer.py \
  tests/gateway/test_stream_consumer_no_edit.py \
  tests/gateway/test_signal.py \
  tests/gateway/test_signal_format.py \
  -v

Result: 275 passed on the combined scope.

Manual Signal verification checklist

Against a real Signal test account:

  • Code block rendering — ask "write me a python hello world in a code block" → monospace render, no literal backticks
  • Bullet list, no italic false positive — ask "give me a bulleted list of 3 kitchen utensils" → * renders as bullet, not italic
  • URL preservation — ask "summarize https://example.com/foo_bar_baz" → URL intact, no italicization around underscores or dot
  • Reply-quote context — quote-reply one of Hermes' messages with "what did you mean?" → response references the quoted text
  • Reaction lifecycle — send message, observe ⏳ on start and ✅ on completion, then the reply
  • Emoji + formatting UTF-16 offsets — message with 👋 and **bold** → bold range correct across the non-BMP emoji

Files changed

Modified (6):

  • gateway/platforms/base.py
  • gateway/platforms/signal.py
  • gateway/run.py
  • gateway/stream_consumer.py
  • tests/gateway/test_signal.py
  • tests/gateway/test_stream_consumer.py

New (4):

  • tests/gateway/test_signal_format.py
  • tests/gateway/test_stream_consumer_no_edit.py
  • tests/tools/test_send_file_tool.py
  • tools/send_file_tool.py

Notes for reviewers

  • Happy to split into three separate PRs if preferred — each commit stands on its own.
  • Signal bodyRanges UTF-16 offset math is the highest-risk area; the dedicated tests (test_utf16_offset_*) are worth focused review.
  • The no-edit-mode branch in stream_consumer.py is guarded by self._no_edit_mode and is dead code on edit-capable adapters.

exiao added 3 commits April 17, 2026 00:19
Adds a first-class send_file tool so agents can attach files (images,
PDFs, audio, etc.) to outgoing platform messages without hacking paths
into prompts. Complements the existing send_message tool.

The tool resolves the active platform adapter from conversation
context and calls adapter.send(..., attachments=[path]). Includes
path validation, size limits, and MIME sniffing.

Tool is auto-discovered by tools/registry.py on startup.

Tests: 28 new tests in tests/tools/test_send_file_tool.py covering
path resolution, size limits, invalid inputs, and adapter dispatch.
…dit API

Some platforms (notably Signal) have no outbound message-edit API, so
our existing streaming loop that repeatedly edits a placeholder message
fails silently and leaves a trailing cursor visible in clients.

Changes:
- BasePlatformAdapter gains SUPPORTS_MESSAGE_EDITING: bool = True class
  attribute. Adapters that cannot edit override to False.
- Introduces ProcessingOutcome enum for on_processing_start/complete
  hook return values (consumed by downstream reaction logic).
- StreamConsumerConfig gains no_edit_mode: bool = False.
- gateway/run.py wires adapter capability into the config:
    no_edit_mode=not _adapter_supports_edit
- stream_consumer.py adds a no-edit branch that flushes on paragraph
  breaks (\n\n) above a minimum buffer, plus segment/done/commentary
  events, instead of time-interval edits. Platforms that support editing
  are unchanged.

Tests: 23 new tests in tests/gateway/test_stream_consumer_no_edit.py,
plus regression extensions to tests/gateway/test_stream_consumer.py.
All edit-mode platforms (Telegram, Matrix, Discord) default True and
take the original code path unchanged.
Three Signal adapter improvements that depend on the no-edit-mode
plumbing from the previous commit.

1. Native formatting (markdown -> Signal bodyRanges)
   Signal renders markdown as literal characters (**bold**, `code`, #
   heading), which looks broken. Added _markdown_to_signal(text) that
   strips markdown syntax and emits Signal-native bodyRanges as
   start:length:STYLE entries. Offsets are computed in UTF-16 code
   units so non-BMP emoji stay aligned. Supports BOLD, ITALIC, STRIKE,
   MONO, and headings mapped to BOLD. Fenced code and inline code are
   handled; link syntax is unwrapped to visible text + URL.

   Includes edge-case fixes reported previously:
   - Bullet lists ("* item") no longer misidentified as italics
   - URLs containing underscores no longer italicized around the dot

2. Reply-quote context
   Parses dataMessage.quote on inbound messages and populates
   MessageEvent.raw_message with sender + timestamp_ms. This lets the
   gateway's existing [Replying to: "..."] injector (gateway/run.py)
   work on Signal, matching Telegram/Matrix behavior.

3. Processing reactions
   Overrides on_processing_start -> hourglass and on_processing_complete
   -> checkmark via the sendReaction JSON-RPC using targetAuthor and
   targetTimestamp pulled from raw_message. Uses the ProcessingOutcome
   enum introduced in the previous commit.

Also sets SUPPORTS_MESSAGE_EDITING = False on SignalAdapter so the
no-edit streaming path activates.

Tests: 40+ new tests in tests/gateway/test_signal_format.py covering
markdown conversion, UTF-16 offset correctness with non-BMP emoji,
bullet-list and URL false-positive regressions, reply-quote extraction,
and reaction payload shape. Regression extensions to test_signal.py.
@exiao exiao closed this Apr 17, 2026
@exiao exiao deleted the feat/signal-native-formatting-and-send-file branch April 17, 2026 10:59
@teknium1

Copy link
Copy Markdown
Contributor

FYI — commit 3 from this PR (Signal native formatting + reply quotes + reactions) has been salvaged onto current main in #17417 with your authorship preserved (exiao <2093036+exiao@users.noreply.github.com>).

Commit 1 (send_file tool) was dropped and can be its own discussion. Commit 2's no-edit-mode plumbing (SUPPORTS_MESSAGE_EDITING, ProcessingOutcome, __no_edit__ stream sentinel) had already landed independently from other PRs between when this was opened and today, so only the SignalAdapter.SUPPORTS_MESSAGE_EDITING = False flag flip from that commit shipped here.

Thanks for the thorough work — the UTF-16 offset math and the bullet/URL regression fixes were exactly what was needed.

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.

2 participants