Skip to content

feat(discord): agent-facing add_reaction/remove_reaction (photon parity) - #79074

Open
vel007-ai wants to merge 2 commits into
NousResearch:mainfrom
vel007-ai:feat/discord-agent-reactions
Open

feat(discord): agent-facing add_reaction/remove_reaction (photon parity)#79074
vel007-ai wants to merge 2 commits into
NousResearch:mainfrom
vel007-ai:feat/discord-agent-reactions

Conversation

@vel007-ai

Copy link
Copy Markdown

What does this PR do?

send_message(action="react") resolves reactions by duck-typing public add_reaction/remove_reaction coroutines on the live adapter (tools/send_message_tool.py). Photon ships the pair; the Discord adapter has only the private _add_reaction/_remove_reaction helpers it uses for the 👀/✅ lifecycle ack. So reacting on Discord fails with Platform 'discord' does not support message reactions, even though the adapter is already reacting to messages several times per turn.

This exposes the pair on photon's contract, and fixes the target resolution that made it unusable in practice.

Same shape as #77994 (Matrix), which is the sibling half of this gap.

1. Discord adapter gets the public pair.

  • Same return shapes as photon ({"success", "message_id"} / {"success", "error"}).
  • Deliberately not gated by DISCORD_REACTIONS — that env var exists to mute the automatic per-message ack, not explicit agent intent. Same policy photon documents for PHOTON_REACTIONS.
  • Default target is the message that triggered this turn, recorded per chat in on_processing_start (before the env gate, for the same reason).
  • Auto-threading needs one extra step that photon doesn't: the turn runs with chat_id set to the new thread, while the message that triggered it lives in the parent channel. Both are recorded as keys, and both channels are tried on fetch, so a react from inside an auto-thread lands on the message that started it.
  • remove_reaction retracts only the emoji this process placed, tracked per (chat, message) and bounded. Without that, an unreact would have to guess, and stripping the lifecycle ✅ is the wrong guess. Lost on restart, which matches photon's documented best-effort retraction.

2. react/unreact with no chat now mean "here".

A bare platform target fell through to get_home_channel(). That is right for a send and wrong for a reaction: the agent means this conversation, and the home channel would put the emoji on an unrelated message in a different channel. The current turn's chat (HERMES_SESSION_CHAT_ID) now wins when it is on the same platform, with the home-channel fallback intact for every other case.

This matters more than it looks: the system prompt tells the agent Platform: discord but never its channel id, so without this, the only reliably correct call is one the agent has no way to construct.

Related Issue

Partially addresses #29026 — that request covers both directions (the agent reading reactions as input, and placing them). This is the outbound half only; inbound Discord reaction events are #46855.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

  • plugins/platforms/discord/adapter.py — public add_reaction/remove_reaction, _fetch_reaction_target (thread → parent fallback), _last_inbound_by_chat recorded in on_processing_start, bounded _agent_reactions tracking for retraction.
  • tools/send_message_tool.py_current_turn_chat_id() preferred over the home channel for react/unreact; action schema description updated to say Discord is supported and that a bare platform target means the current chat.
  • tests/gateway/test_discord_reactions.py — 8 tests: explicit id, turn default, thread-parent fallback, env not gating agent intent, both error paths, retract-what-we-added, retract-with-nothing-tracked.
  • tests/tools/test_send_message_react.py — 2 tests: bare target resolves to the current turn; a turn on a different platform does not leak into it (home fallback still wins).

How to Test

  1. pytest tests/gateway/test_discord_reactions.py tests/tools/test_send_message_react.py -q
  2. In a Discord channel, mention the bot so a turn starts, then have it call send_message(action="react", target="discord", emoji="🟢"). The 🟢 lands on the message that mentioned it, alongside the lifecycle ✅ — including when auto-threading moved the turn into a new thread.
  3. send_message(action="unreact", target="discord") retracts the 🟢 and leaves the ✅.

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation — docstrings on the new methods; no user-facing docs describe reaction platform support
  • N/A — no config keys added
  • N/A — no architecture or workflow change
  • I've considered cross-platform impact — pure Python, no OS-specific paths
  • I've updated tool descriptions/schemas (the action description now names Discord and the current-chat default)

V added 2 commits August 4, 2026 21:05
The `send_message(action="react")` tool dispatches to an adapter's public
`add_reaction` / `remove_reaction`, which only photon implemented, so
reacting on Discord silently failed with "platform does not support
message reactions". The Discord adapter already had the mechanics: they
were private and reachable only from the 👀/✅ lifecycle hooks.

Expose them on the photon contract:

- `add_reaction(chat_id, emoji, message_id=None)` and `remove_reaction`,
  returning the same `{"success", "message_id"} / {"success", "error"}`
  shapes, and deliberately NOT gated by DISCORD_REACTIONS (that env var
  mutes the automatic ack, not an explicit agent intent).
- Default the target to the message that triggered this turn, recorded
  per chat at processing start. Auto-threaded turns address the thread
  while their trigger message lives in the parent channel, so both are
  recorded as keys and both channels are tried on fetch.
- Track the emoji we added per (chat, message) so an unreact retracts it
  rather than stripping the lifecycle ✅. Bounded, and lost on restart,
  which matches photon's best-effort retraction.
A bare 'platform' target fell through to the home channel, which is right
for a send and wrong for a reaction: the agent means "this conversation",
and the home channel would put the emoji on an unrelated message. Prefer
the chat the turn is running in when it is on the same platform, keeping
the home-channel fallback for every other case.
@vel007-ai

Copy link
Copy Markdown
Author

Cross-linking: while using this in anger I hit the limitation that this PR alone doesn't solve the agent-facing case — send_message is deliberately not agent-callable (toolsets.py: outbound platform messaging is handled outside the agent loop), so no model can reach action="react" regardless of which adapters implement the pair.

This PR still stands on its own for photon parity and the non-agent callers. #80817 covers the agent-facing half by adding add_reaction/remove_reaction to the discord REST tool, which models can call. The two are complementary, not alternatives.

@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins platform/discord Discord bot adapter P3 Low — cosmetic, nice to have labels Aug 7, 2026
@eyeonall

Copy link
Copy Markdown

Cross-linking the convergence work: #89405 has been updated to incorporate the agent-facing adapter reaction helper goals from this PR while keeping the final surface plugin-owned through hooks, manifests, shared request builders, and adapter delivery helpers.

Attribution for this contribution is included in #89405: #79074 by @vel007-ai.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/discord Discord bot adapter type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants