Skip to content

Commit 42e0023

Browse files
committed
feat(sendblue): expose tapback action to the agent via send_message
The adapter's outbound send_reaction() (patch NousResearch#3) was reachable from gateway code but invisible to the agent — no tool surface, and the inbound message_handle (already on MessageEvent / SessionSource) was never injected into the model-visible session-context block. - gateway/session.py: Platform.SENDBLUE branch with iMessage notes plus a triggering-message-handle IDs block (current turn only). - tools/send_message_tool.py: send_message gains action='react' with message_handle / reaction params, _handle_react validation arms, and a _react_sendblue dispatcher that mirrors _send_sendblue's throwaway- adapter pattern. _REACTION_PLATFORMS gates platform support. - agent/prompt_builder.py: sharpen the sendblue blurb to point at the now-real send_message(action='react', ...) mechanism and the handle source (the original tapback line from patch NousResearch#4 was aspirational). - tests/tools/test_sendblue_reaction.py: 15 tests covering dispatch, validation arms, schema, and reaction normalization. - LOCAL_PATCHES.md: new patch NousResearch#11 entry + file-index updates.
1 parent 716021b commit 42e0023

5 files changed

Lines changed: 512 additions & 11 deletions

File tree

LOCAL_PATCHES.md

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Last updated: 2026-05-02
1010
Pinned upstream base: `73bf3ab1b22314ed9dfecbb59242c03742fe72af`
1111
(release v0.12.0, 2026-04-30).
1212

13+
Patches: #1#11 (see numbered sections below).
14+
1315
---
1416

1517
## 1. Cron waker hook for external supervisors
@@ -316,6 +318,63 @@ and PDF — attachments land inline on iMessage.
316318

317319
---
318320

321+
## 11. Sendblue tapback action exposed to the agent
322+
323+
**Problem:** Patch #3's adapter ships with an outbound `send_reaction()`
324+
method (the only adapter on the fork that has one), but nothing in the
325+
agent-facing surface lets the model actually call it. The inbound
326+
`message_handle` was already plumbed through to
327+
`MessageEvent.message_id` and `SessionSource.message_id` (the
328+
`SessionSource` field even carries the comment "for pin/reply/react"),
329+
but it was never injected into the model-visible session-context block,
330+
so the model had no handle to react to. As a result the SendBlue
331+
tapback API was reachable from gateway code but invisible to the agent.
332+
333+
**Solution:** Two small additions, no new files, no base-class changes.
334+
- `gateway/session.py` gains a `Platform.SENDBLUE` branch (mirrors the
335+
BLUEBUBBLES iMessage notes verbatim) that, when the current turn has a
336+
`message_id`, appends a `**iMessage IDs (for the `react` action of
337+
`send_message`):**` block surfacing the triggering-message handle plus
338+
a one-line invocation example. Only the current turn's handle is
339+
exposed — historical handles are deliberately not embedded in the
340+
transcript (would require cross-cutting prompt changes for marginal
341+
agent benefit).
342+
- `tools/send_message_tool.py` grows an `action="react"` arm of the
343+
existing `send_message` tool (rather than a new tool) so target
344+
resolution and platform gating reuse the existing surface. New
345+
`message_handle` and `reaction` schema params (with the closed
346+
Sendblue reaction enum). New `_handle_react()` validator dispatches
347+
to `_react_sendblue()`, which mirrors `_send_sendblue`'s throwaway-
348+
adapter pattern and calls `adapter.send_reaction()`. A
349+
`_REACTION_PLATFORMS = frozenset({"sendblue"})` gate makes the
350+
one-platform restriction explicit and easy to grow.
351+
352+
**Commits:**
353+
- _(pending — to be filled in on next snapshot build)_
354+
355+
**Files modified:**
356+
- `gateway/session.py` — Platform.SENDBLUE branch + iMessage IDs block
357+
- `tools/send_message_tool.py` — schema, dispatcher arm, `_handle_react`,
358+
`_react_sendblue`
359+
- `agent/prompt_builder.py` — sharpen the sendblue platform blurb to
360+
reference the now-real `send_message(action='react', ...)` mechanism
361+
and the session-context handle source (the original aspirational
362+
tapback line from patch #4 was vague — the agent had no way to act on
363+
it).
364+
365+
**Files added:**
366+
- `tests/tools/test_sendblue_reaction.py` — 15 tests covering
367+
`_react_sendblue` adapter dispatch, `_handle_react` validation arms
368+
(missing target/handle/reaction, unsupported platform, missing chat_id,
369+
unconfigured platform), reaction-name lower-casing, the `send_message`
370+
dispatcher routing for `action='react'`, and schema advertisement of
371+
the new action/params. Placed in upstream-owned `tests/tools/` per the
372+
same new-file-OK exception documented in patch #5.
373+
374+
**Drop when:** Same as #3.
375+
376+
---
377+
319378
## File Index
320379

321380
All files touched by local patches, grouped by area:
@@ -328,6 +387,7 @@ All files touched by local patches, grouped by area:
328387
- `gateway/config.py`#4
329388
- `gateway/platforms/bluebubbles.py`#2 (webhook env guard)
330389
- `gateway/platforms/sendblue.py`#3 (adapter), #7 (auth probe)
390+
- `gateway/session.py`#11 (sendblue tapback IDs block)
331391

332392
### CLI
333393
- `hermes_cli/platforms.py`#4
@@ -339,13 +399,14 @@ All files touched by local patches, grouped by area:
339399
- `cron/waker_hook.py`#1 (added), #4 (modified)
340400

341401
### Tools / agent prompts
342-
- `agent/prompt_builder.py`#4
343-
- `tools/send_message_tool.py`#4, #10
402+
- `agent/prompt_builder.py`#4, #11
403+
- `tools/send_message_tool.py`#4, #10, #11
344404
- `toolsets.py`#4
345405

346406
### Tests
347407
- `tests/gateway/test_sendblue.py`#5 (added; new file in upstream dir)
348408
- `tests/tools/test_sendblue_media.py`#10 (added; new file in upstream dir)
409+
- `tests/tools/test_sendblue_reaction.py`#11 (added; new file in upstream dir)
349410
- `tests/local_patches/__init__.py`#8 (added)
350411
- `tests/local_patches/test_thinking_reasoning_pad.py`#8 (added)
351412

agent/prompt_builder.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,13 @@ def _strip_yaml_frontmatter(content: str) -> str:
386386
"appear as text messages. You can send media files natively: include "
387387
"MEDIA:/absolute/path/to/file in your response. Images (.jpg, .png, "
388388
".heic) appear as photos and other files arrive as attachments. You "
389-
"can also send tapback reactions (love, like, dislike, laugh, "
390-
"emphasize, question) when reacting to a recent message is more "
391-
"appropriate than sending a reply."
389+
"can also send a tapback reaction to the user's current message — "
390+
"call send_message(action='react', target='sendblue:<chat_id>', "
391+
"message_handle=<handle>, reaction=<love|like|dislike|laugh|emphasize|question>). "
392+
"The message_handle is surfaced in the session-context block as "
393+
"'Triggering message handle' and only refers to the message you are "
394+
"currently answering; you cannot react to earlier messages. Prefer a "
395+
"tapback over a one-word text reply when a quick acknowledgement fits."
392396
),
393397
"mattermost": (
394398
"You are in a Mattermost workspace communicating with your user. "

gateway/session.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,31 @@ def build_session_context_prompt(
365365
"If the user needs a detailed answer, give the short version first "
366366
"and offer to elaborate."
367367
)
368+
elif context.source.platform == Platform.SENDBLUE:
369+
lines.append("")
370+
lines.append(
371+
"**Platform notes:** You are responding via iMessage (Sendblue). "
372+
"Keep responses short and conversational — think texts, not essays. "
373+
"Structure longer replies as separate short thoughts, each separated "
374+
"by a blank line (double newline). Each block between blank lines "
375+
"will be delivered as its own iMessage bubble, so write accordingly: "
376+
"one idea per bubble, 1–3 sentences each. "
377+
"If the user needs a detailed answer, give the short version first "
378+
"and offer to elaborate."
379+
)
380+
# Triggering message handle — required input for the `react` action
381+
# of `send_message`, which sends an iMessage tapback. Only the current
382+
# turn's handle is exposed; historical handles are not in the transcript,
383+
# so the agent can only react to the message it's currently answering.
384+
if context.source.message_id:
385+
lines.append("")
386+
lines.append("**iMessage IDs (for the `react` action of `send_message`):**")
387+
lines.append(f" - Triggering message handle: `{context.source.message_id}`")
388+
lines.append(
389+
" - To tapback the user's message, call "
390+
"`send_message(action='react', target='sendblue:<chat_id>', "
391+
"message_handle='<handle above>', reaction='love'|'like'|'dislike'|'laugh'|'emphasize'|'question')`."
392+
)
368393
elif context.source.platform == Platform.YUANBAO:
369394
lines.append("")
370395
lines.append(

0 commit comments

Comments
 (0)