Skip to content

feat(discord): interactive components — buttons, select menus, REST + WebSocket paths#19413

Open
TerminalSausage wants to merge 3 commits into
NousResearch:mainfrom
TerminalSausage:feat/discord-interactive-components
Open

feat(discord): interactive components — buttons, select menus, REST + WebSocket paths#19413
TerminalSausage wants to merge 3 commits into
NousResearch:mainfrom
TerminalSausage:feat/discord-interactive-components

Conversation

@TerminalSausage

@TerminalSausage TerminalSausage commented May 3, 2026

Copy link
Copy Markdown

What does this PR do?

Add Discord Message Component v1 support to the send_message tool, enabling agents to send messages with clickable buttons and string-select menus. Component interactions route back to the agent as MessageEvent instances.

Two send paths are supported:

  • WebSocketdiscord.py attaches a discord.ui.View to channel.send(), interactions handled automatically via AgentComponentView
  • REST_build_discord_components() builds raw JSON payloads; an on_interaction handler acknowledges and routes orphaned interactions via ComponentStore

Related Issue

None — feature addition.

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)
  • ✅ Tests (adding or improving test coverage)

Changes Made

  • gateway/platforms/discord_components.py (NEW) — ComponentStore, AgentComponentView, build_view_from_spec(), helpers (_truncate_custom_id, _format_interaction_text)
  • gateway/platforms/discord.py_normalize_component_spec() format adapter, on_interaction REST handler, View attachment in send()
  • tools/send_message_tool.pycomponents JSON schema, _build_discord_components() REST builder, _prefix_custom_id(), ComponentStore registration after REST send
  • tests/gateway/test_discord_components.py (NEW) — 62 tests across 10 classes covering all public functions and edge cases

How to Test

  1. Run the test suite: pytest tests/gateway/test_discord_components.py -v (62 tests)
  2. Start the gateway and use send_message with components parameter to send button/select messages to a Discord channel
  3. Click buttons and select menu options — verify interactions route back to the agent as MessageEvent instances
  4. Test edge cases: disabled buttons, link buttons (no custom_id), >5 row truncation, label/custom_id length clamping, empty component specs

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(discord):, style(discord):)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (62 tests across 10 classes)
  • I've tested on my platform: Ubuntu 24.04 (Linux)

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — N/A (no user-facing docs changes needed; tool schema is self-documenting)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no new config keys)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A (additive change, no architecture changes)
  • I've considered cross-platform impact (Windows, macOS) — N/A (Discord API is platform-agnostic)
  • I've updated tool descriptions/schemas if I changed tool behavior (components schema added to send_message)

🔧 Implementation Details

Features

  • Buttons: primary, secondary, success, danger, link (with url field, no custom_id)
  • String Select Menus: up to 25 options, configurable min_values/max_values, placeholder text
  • Multi-row layouts: up to 5 action rows per message
  • Disabled state: supported on both buttons and select menus
  • Custom IDs: auto-prefixed with hermes: namespace to avoid collisions
  • Label/custom_id truncation: gracefully handles Discord's 80/100 char limits

Test Coverage (62 tests)

Class Area Tests
TestTruncateCustomId Helper 4
TestFormatInteractionText Helper 4
TestComponentStore CRUD + cleanup 7
TestIsAgentComponent Prefix check 4
TestBuildViewFromSpec Factory 5
TestAgentComponentViewBuildFromSpec Spec parsing 3
TestAgentComponentViewMakeButton Button builder 11
TestAgentComponentViewMakeSelect Select builder 6
TestBuildDiscordComponents REST builder 9
TestNormalizeComponentSpec Format normalizer 9

send_message Schema Addition

{
  "components": [
    {
      "components": [
        { "type": "button", "style": "primary", "label": "Approve", "custom_id": "approve" },
        { "type": "button", "style": "link", "label": "Docs", "url": "https://..." }
      ]
    },
    {
      "components": [
        {
          "type": "string_select",
          "custom_id": "priority",
          "label": "Priority",
          "placeholder": "Choose...",
          "options": [
            { "label": "High", "value": "high" },
            { "label": "Low", "value": "low" }
          ]
        }
      ]
    }
  ]
}

Out of Scope (Future Work)

  • Modal support (text input components)
  • User/Role/Channel select menus
  • Interaction routing unit tests (on_interaction async handler — requires complex Discord mock setup)
  • Ephemeral response support

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have platform/discord Discord bot adapter comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets labels May 3, 2026
@TerminalSausage TerminalSausage force-pushed the feat/discord-interactive-components branch from f8cf169 to ca26919 Compare May 4, 2026 00:28
@teknium1 teknium1 closed this May 11, 2026
@teknium1 teknium1 reopened this May 11, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for this — solid feature with good test coverage. Before we salvage onto current main, two changes we'd like you to make:

1. Gate the components schema behind Discord being configured

Right now the components JSON schema lands in the send_message tool description for every user, on every API call, even if they never touch Discord. That's a permanent token tax for a Discord-only feature. Please make the schema entry conditional on Discord being an active platform (check pconfig/loaded platforms at tool-schema build time) so non-Discord users don't pay for it.

2. Wrap the _view_store._views access in on_interaction

adapter_self._client._connection._view_store._views is a discord.py internal — not part of the public API. A discord.py minor-version change could rename or restructure that dict and silently break component routing. Wrap the lookup in try/except (AttributeError, KeyError) and treat a failed lookup as "no registered View, fall through to ComponentStore."

We'll handle the Discord docs page update ourselves during salvage. The deferred items in your "Out of Scope" list (modals, user/role/channel selects, ephemeral) are fine to leave for follow-ups.

Once those two changes are pushed we'll cherry-pick onto current main and merge — no need to rebase the branch yourself.

… WebSocket paths

Add Discord interactive UI components (Message Component v1) support to
the send_message tool, enabling agents to send messages with clickable
buttons and string-select menus. Component interactions route back to the
agent as MessageEvent instances.

Two send paths:
- WebSocket (discord.py View): interactions handled automatically
- REST (aiohttp): new on_interaction handler acknowledges and routes
  orphaned interactions via ComponentStore

Changes:
- gateway/platforms/discord_components.py (NEW): ComponentStore,
  AgentComponentView, build_view_from_spec(), helpers
- gateway/platforms/discord.py: _normalize_component_spec(), on_interaction
  handler for REST-sent components, View attachment on channel.send()
- tools/send_message_tool.py: components schema, _build_discord_components()
  REST builder, ComponentStore registration after REST send
- tests/gateway/test_discord_components.py (NEW): 62 tests across 10
  classes covering all public functions and edge cases
…iew_store access

- Strip 'components' property from send_message schema when Discord is
  not a connected platform, avoiding permanent token tax for non-Discord
  users (follows browser_navigate web-tools stripping pattern)
- Wrap _view_store._views access in on_interaction with try/except
  (AttributeError, KeyError) so discord.py internal API changes don't
  silently break component routing
@TerminalSausage TerminalSausage force-pushed the feat/discord-interactive-components branch from b775560 to 2c15df5 Compare May 11, 2026 21:15
… gating

- Deep-copy schema before stripping components to avoid mutating the
  registry's base schema (components would vanish for the entire
  process lifetime after first call with Discord disconnected)
- Include Discord connectivity status in cache key so toggling Discord
  correctly invalidates cached tool definitions
- Extract helpers (_discord_components_schema_enabled,
  _without_send_message_components_schema) for testability
- Add 3 tests covering mutation safety, cache invalidation, and
  schema restoration on Discord reconnect

Based on Codex review feedback for PR NousResearch#19413.
Co-authored-by: Codex <codex@openai.com>
@TerminalSausage

TerminalSausage commented May 11, 2026

Copy link
Copy Markdown
Author

Hey, thanks for reviewing this. Pushed the changes from your feedback.

In addition, Fixed a bug where the components schema was getting popped off the registry base via shallow reference. Now deep-copies before stripping. Also added Discord connectivity to the cache key so toggling it doesn't serve stale tool definitions. Added a few tests for both.

Let me know if you spot anything else.

0xarkstar added a commit to 0xarkstar/hermes-agent that referenced this pull request May 18, 2026
…active components)

PR NousResearch#19413 (TerminalSausage, "interactive components — buttons, select
menus, REST + WebSocket paths") is being salvaged by maintainer onto
current main. That PR owns Discord button emission + interaction
dispatch via the existing send_message tool extended with a `components`
schema. To avoid an on_interaction handler conflict (discord.py allows
only one registered handler per event) and reduce review surface, this
PR's button-specific wiring is removed.

Removed:
- tools/discord_button_tool.py (outbound discord_send_button_message tool)
- tests/tools/test_discord_button_message.py
- SkillButtonView class in gateway/platforms/discord_interactions.py
- handle_skill_button_interaction method
- _dispatch_synthetic method (button-only)
- _build_button_payload method (button-only)
- on_interaction event registration in gateway/platforms/discord.py
- Button-specific tests in tests/gateway/test_discord_interactions.py
- Button outbound sections in docs/migration/triggers-v1.md
- Button send/receive sections in CONTRIBUTING.md
- discord_button_tool entry in tests/tools/test_registry.py

Retained as framework extension points:
- metadata.hermes.triggers.button schema (agent/skill_utils.py)
- button event matcher in gateway/skill_resolver.py
- make_skill_custom_id / is_skill_custom_id / SKILL_CUSTOM_ID_PREFIX
  helpers in gateway/platforms/discord_interactions.py
- button trigger example in CONTRIBUTING.md (now annotated as bridge target)

Rationale: a follow-up PR can plug PR NousResearch#19413's component dispatch into
resolve_event_skills('button', payload, skills) via the retained helpers,
completing the round-trip from skill declaration → component emission →
click → skill dispatch.

This PR now covers: skill_resolver + frontmatter parser + Discord inbound
reaction routing + Discord mention routing + outbound reaction tools
(discord_add_reaction / discord_remove_reaction) + Feishu BC integration.

Tests: 132 pass across the trigger framework + reaction tools +
event counters + cache-first + mention routing suites.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@TerminalSausage

TerminalSausage commented Jun 6, 2026

Copy link
Copy Markdown
Author

I have decided to consider a different approach using a tool to enforce the interactive component format to guide the model. This specific PR relies on the model to understand the discord syntax and capabilities. You can see the new work here: #39911

@teknium1

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

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets 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