feat(gateway/signal): native formatting, reply quotes, reactions + send_file tool + no-edit streaming#11396
Closed
exiao wants to merge 3 commits into
Closed
Conversation
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.
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 ( Commit 1 ( Thanks for the thorough work — the UTF-16 offset math and the bullet/URL regression fixes were exactly what was needed. |
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
Combined PR that adds three related improvements enabling first-class Signal support:
send_filetool — agents can attach files to outgoing platform messagesThe three are shipped together because #3 depends on the
SUPPORTS_MESSAGE_EDITING/ProcessingOutcomeplumbing 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 uploadsWhy: 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.pyresolves the active adapter from conversation context and callsadapter.send(..., attachments=[path]). Includes path validation, size limits, and MIME sniffing. Auto-discovered bytools/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 APIWhy: 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 = Trueclass attr (adapters that can't edit override toFalse).ProcessingOutcomeenum foron_processing_start/on_processing_completehook return values.StreamConsumerConfig.no_edit_mode: bool = False+ wiring ingateway/run.py(no_edit_mode=not _adapter_supports_edit).stream_consumer.pyno-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
Trueand 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 totest_stream_consumer.py.Risk: Medium — touches the gateway hot path.
Regression matrix
SUPPORTS_MESSAGE_EDITING=True)False)done_safe_limiton no-editCommit 3 —
feat(gateway/signal): native formatting, reply quotes, and reactionsWhy: 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
bodyRangesNew
_markdown_to_signal(text) -> (plain, styles)strips markdown syntax and emits Signal-nativebodyRangesasstart:length:STYLEentries. 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:
* item) no longer misidentified as italics3b. Reply-quote extraction
Parses
dataMessage.quoteand populatesMessageEvent.raw_messagewith 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→ ⏳ andon_processing_complete→ ✅ viasendReactionRPC, usingtargetAuthor/targetTimestampfromraw_message. Uses theProcessingOutcomeenum from commit 2.Sets
SUPPORTS_MESSAGE_EDITING = FalseonSignalAdapterso 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 totest_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 \ -vResult: 275 passed on the combined scope.
Manual Signal verification checklist
Against a real Signal test account:
*renders as bullet, not italic**bold**→ bold range correct across the non-BMP emojiFiles changed
Modified (6):
gateway/platforms/base.pygateway/platforms/signal.pygateway/run.pygateway/stream_consumer.pytests/gateway/test_signal.pytests/gateway/test_stream_consumer.pyNew (4):
tests/gateway/test_signal_format.pytests/gateway/test_stream_consumer_no_edit.pytests/tools/test_send_file_tool.pytools/send_file_tool.pyNotes for reviewers
bodyRangesUTF-16 offset math is the highest-risk area; the dedicated tests (test_utf16_offset_*) are worth focused review.stream_consumer.pyis guarded byself._no_edit_modeand is dead code on edit-capable adapters.