Skip to content

fix(discord): end-to-end Discord support — toolset loading, tool split, and context injection - #15091

Closed
alt-glitch wants to merge 8 commits into
mainfrom
sid/fix-platform-tool-loading
Closed

fix(discord): end-to-end Discord support — toolset loading, tool split, and context injection#15091
alt-glitch wants to merge 8 commits into
mainfrom
sid/fix-platform-tool-loading

Conversation

@alt-glitch

@alt-glitch alt-glitch commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Getting Hermes working end-to-end on Discord exposed four classes of bugs, each stacked on the next. This PR fixes all of them as independent, cherry-pickable commits.


1. Toolset loading — platform tools were silently dropped

_get_platform_tools() forward-resolves composites (e.g. hermes-discord) into tool names, then reverse-maps back through CONFIGURABLE_TOOLSETS to pick which toolset keys are enabled. Platform-specific toolsets like discord, feishu_doc, feishu_drive aren't in CONFIGURABLE_TOOLSETS (they're not user-toggleable), so the round-trip dropped them on the floor. Even worse: the search toolset (["web_search"]) could leak through on disabled browsers via the recovery pass.

Fix: Add a second-pass recovery over all TOOLSETS after the configurable reverse-map, using a claimed set guard to avoid false positives, and a configurable_tool_universe guard to prevent overlap toolsets like search from re-enabling disabled tools.

The recovery pass runs in both branches of the read path — fresh composite resolution and explicit-config reads. Before this fix, saving via hermes tools once would silently delete Discord/Feishu-specific toolsets because they can't appear in the TUI checklist.

2. Discord tool split — unsafe server management surface

The single discord_server tool exposed 14 actions including add_role, pin_message, and remove_role. Agents that are chatty in Discord should be able to read and reply, but server management is a foot-gun that shouldn't be on by default.

Split into two tools:

  • discord (default, 3 actions): fetch_messages, search_members, create_thread
  • discord_admin (opt-in via _DEFAULT_OFF_TOOLSETS, 11 actions): all the guild/channel/role management

The dynamic schema rebuild at model_tools.py is loop-driven so both tools get their intents-aware descriptions independently.

3. Feishu wiring — tools existed but weren't in any composite

feishu_doc_read, feishu_drive_* (4 tools) were registered but not included in the hermes-feishu composite. Agents running on Feishu had zero Feishu-specific tools available.

Fix: Add feishu_doc and feishu_drive toolsets to hermes-feishu's includes.

4. _save_platform_tools — numeric normalization + stale sentinel

Two latent bugs in the save path:

  • YAML-parsed numeric toolset entries (e.g. bare 12306: parses as int) caused sorted() type errors downstream.
  • The no_mcp sentinel wasn't cleared when the user toggled it off via the TUI, silently disabling MCP servers permanently.

Fix: Normalize entries to str on read; discard stale no_mcp when it's not in the new selection.


What it actually takes to have a working Discord agent

Fixing (1)–(4) made the tool load. Using it revealed two more layers:

5. Session context injection — model didn't know the IDs

SessionSource only exposed thread_id as a raw identifier. When the agent got ""fetch the last 5 messages from this channel"" in a Discord thread, it had to guess that the thread ID doubles as a channel_id (it does — Discord threads are a channel type). It had no way to reference:

  • The guild (for search_members)
  • The parent channel (for pin_message in a channel above its thread)
  • The triggering message (for pin/reply/react)

Fix: Add guild_id, parent_chat_id, message_id fields to SessionSource; populate them from the Discord adapter; emit a dedicated IDs block in the session context prompt when DISCORD_BOT_TOKEN is set:

**Discord IDs (for the `discord` / `discord_admin` tools):**
  - Guild: `602496214750986260`
  - Parent channel: `602496214750986262`
  - Thread: `1497290677208350760` (use as `channel_id` for fetch_messages etc.)
  - Triggering message: `1497290687610360001`

The block adapts to context: threads show guild/parent/thread/message, regular channels show guild/channel/message, DMs show only the message ID.

6. Stale ""you don't have Discord APIs"" platform note

build_session_context_prompt() had a hardcoded note telling the model:

You do NOT have access to Discord-specific APIs — you cannot search channel history, pin messages, manage roles, or list server members. Do not promise to perform these actions.

This pre-dates the discord tool. With it enabled, the model was being gaslit into refusing valid tool calls.

Fix: Gate the disclaimer on DISCORD_BOT_TOKEN being unset (matches the tool's check_fn). With a token the note disappears; without one it remains accurate.


Follow-up work (not in this PR)

Other platforms with server/message APIs (Slack, Matrix) likely benefit from the same context-injection treatment — raw guild/channel/message IDs in the system prompt when their respective platform tools are loaded. Same pattern as Discord: extend the adapter to populate new SessionSource fields; mirror the ID block in build_session_context_prompt. Can be done per-platform in follow-up PRs.


Commits (all cherry-pickable)

  1. fix(tools): recover non-configurable toolsets from composite resolution — the core round-trip fix
  2. feat(discord): split discord_server into discord + discord_admin tools
  3. feat(feishu): wire feishu doc/drive tools into hermes-feishu composite
  4. fix(tools): normalize numeric entries and clear stale no_mcp in _save_platform_tools
  5. feat(session): add guild_id/parent_chat_id/message_id to SessionSource
  6. feat(discord): populate guild_id, parent_chat_id, message_id on SessionSource
  7. fix(session): gate stale ""no Discord APIs"" note on DISCORD_BOT_TOKEN
  8. feat(session): inject Discord IDs block when discord tool is loaded

Fixes #5991
Fixes #8616
Fixes #13028
Supersedes #13086, #14101, #14149

Test plan

  • Discord platform: discord tool loads, discord_admin default-off
  • Discord with discord_admin enabled via config: both tools present
  • Feishu platform: all 5 feishu tools reach the model
  • Non-platform composites (telegram, cli): unchanged, no leakage
  • _save_platform_tools_get_platform_tools round-trip preserves platform toolsets after first save
  • Discord agent in a thread: system prompt includes guild, parent channel, thread, and message IDs
  • Discord agent without DISCORD_BOT_TOKEN: falls back to the ""no APIs"" disclaimer
  • search toolset leak: disabling web+browser doesn't re-enable web_search via recovery pass

@alt-glitch alt-glitch changed the title Sid/fix platform tool loading fix(tools): recover platform-specific toolsets from composite resolution Apr 24, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator Author

@BugBot review

@alt-glitch

Copy link
Copy Markdown
Collaborator Author

Related Issues & PRs

This PR addresses a family of bugs around toolset resolution that have been reported repeatedly. Here's the full map:

Bug 1: Platform toolsets silently dropped from composite resolution

The core bug — _get_platform_tools() reverse-maps through CONFIGURABLE_TOOLSETS only, silently dropping platform-specific toolsets like discord and feishu_doc that aren't in that catalog.

Bug 2: Stale no_mcp sentinel persists after re-enabling MCP

Bug 3: YAML numeric toolset names cause TypeError in sorted()

Bug 4: Discord tool split (security/utility separation)

The discord_server monolith (14 actions) exposed server management operations by default. This PR splits it into discord (core: fetch_messages, search_members, create_thread) and discord_admin (opt-in: 11 management actions).

Bug 5: Feishu tools unreachable via composite

feishu_doc_read and four feishu_drive tools were registered but never wired into the hermes-feishu composite, making them unreachable on the Feishu platform.


Closing tags summary

Fixes: #5991, #8616, #13028
Supersedes: #13086, #14101, #14149 (all three are open PRs fixing subsets of what this PR covers)

Comment thread hermes_cli/tools_config.py
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/tools Tool registry, model_tools, toolsets platform/discord Discord bot adapter platform/feishu Feishu / Lark adapter labels Apr 24, 2026
@alt-glitch
alt-glitch force-pushed the sid/fix-platform-tool-loading branch from fd2ef81 to 8abd2cf Compare April 24, 2026 11:35
@alt-glitch

Copy link
Copy Markdown
Collaborator Author

@BugBot review

Comment thread hermes_cli/tools_config.py
@alt-glitch
alt-glitch force-pushed the sid/fix-platform-tool-loading branch from 8abd2cf to f965c45 Compare April 24, 2026 11:46
@alt-glitch

Copy link
Copy Markdown
Collaborator Author

@BugBot review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f965c45. Configure here.

@alt-glitch
alt-glitch marked this pull request as ready for review April 24, 2026 13:48
@alt-glitch alt-glitch changed the title fix(tools): recover platform-specific toolsets from composite resolution fix(discord): end-to-end Discord support — toolset loading, tool split, and context injection Apr 24, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator Author

@BugBot review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit db89678. Configure here.

Comment thread hermes_cli/tools_config.py
Comment thread tools/discord_tool.py
The reverse-mapping loop in _get_platform_tools only checked
CONFIGURABLE_TOOLSETS, silently dropping platform-specific toolsets
like discord and feishu_doc whose tools were in the composite but
had no configurable key. Add a second pass over TOOLSETS that picks
up unclaimed toolsets whose tools are present in the resolved
composite.
Split the monolithic discord_server tool (14 actions) into two:

- discord: core actions (fetch_messages, search_members, create_thread)
  that are useful for the agent's normal operation. Auto-enabled on
  the discord platform via the pipeline fix.

- discord_admin: server management actions (list channels/roles, pins,
  role assignment) that require explicit opt-in via hermes tools.
  Added to CONFIGURABLE_TOOLSETS and _DEFAULT_OFF_TOOLSETS.
The feishu_doc and feishu_drive tools were registered in the tool
registry but never added to the hermes-feishu composite toolset.
The pipeline fix from the prior commit now recovers them automatically
once they are in the composite.
…_platform_tools

YAML parses bare numeric toolset names (e.g. 12306:) as int, causing
TypeError in sorted() since the read path normalizes to str but the
save path did not.

The no_mcp sentinel was preserved in existing entries even when the
user re-enabled MCP servers, causing MCP to stay silently disabled.
Groundwork for injecting raw platform identifiers into the agent's
system prompt.  Currently only `thread_id` is exposed as a raw ID —
callers in a Discord thread had to guess `channel_id == thread_id`
(which happens to work because threads are channels in Discord's REST
API) and had no way to reference the parent channel, guild, or the
triggering message.

Adds three optional fields:

- `guild_id` — Discord guild / Slack workspace / Matrix server scope
- `parent_chat_id` — parent channel when chat_id refers to a thread
- `message_id` — ID of the triggering message (pin/reply/react)

Extends `BasePlatformAdapter.build_source()` to accept + forward them
and teaches `to_dict`/`from_dict` to serialize them.  Behaviourally a
no-op: nothing reads the fields yet and they default to None.
…onSource

Discord knows all four identifiers for every inbound message — guild,
channel (or thread), parent channel when in a thread, and the
triggering message.  Pass them into ``SessionSource`` via the new
``build_source()`` kwargs so downstream code (context-prompt builder,
delivery, logging) can use them without re-resolving from discord.py
objects.

For auto-threaded messages, remember the original channel as the
parent before swapping ``chat_id`` to the freshly created thread.

Behavioural: still a no-op — nothing consumes these fields yet.
The Discord platform note in the session context prompt claimed the
agent has no server-management APIs — pre-dating the discord tool.
With a bot token configured the agent actually has fetch_messages,
search_members, create_thread, and optionally the discord_admin tool;
telling the model otherwise causes it to refuse or apologise for
calls it is fully able to make.

Gate the disclaimer on DISCORD_BOT_TOKEN being unset, matching the
tool's own ``check_fn``.  Without a token the note still appears and
remains accurate; with a token the model is no longer gaslit into
refusing valid tool calls.
When DISCORD_BOT_TOKEN is set — meaning the discord tool actually
loads — emit a dedicated IDs block in the session context prompt so
the agent can call ``fetch_messages``, ``pin_message``, etc. with
real identifiers instead of probing.

Currently only ``thread_id`` was exposed as a raw ID (via the
``description`` string).  The agent in a Discord thread had to guess
that the thread ID doubles as a channel ID for the REST API (it
does), and it had no way to reference the parent channel, the guild,
or the triggering message at all.

The block adapts to context:

  - Thread:     guild / parent channel / thread / message
  - Channel:    guild / channel / message
  - (DM has no guild/channel IDs worth listing; only message)

Discord isn't in _PII_SAFE_PLATFORMS, so IDs ship unredacted.
@liujinkun2025

Copy link
Copy Markdown
Contributor

Hey @alt-glitch_get_platform_tools() 's round-trip bug (section 1) is a great catch. The recovery pass dropping non-configurable toolsets on the floor is a real issue independent of feishu_drive, and the claimed-set guard looks right.

Wanted to flag a conflict on section 3 — the feishu_drive half of "wire feishu doc/drive into hermes-feishu composite" collides with #14427, which deletes those four feishu_drive_* tools entirely. I'd like to keep going in the delete direction rather than wire them up. The architectural reason, in case it didn't make it here from #14427:

feishu_drive_* tools aren't dead just because the loader silently drops them. They're dead by design:

  • The comment handler's own system prompt already tells the model: "Do NOT call feishu_drive_add_comment or feishu_drive_reply_comment yourself." Replies are posted by the handler's own code path, not via a tool call. Even with sections 1 + 3 landed, the prompt forbids invoking them.
  • feishu_drive_list_comments / feishu_drive_list_comment_replies have zero callers in any prompt — pure dead surface area.

So my ask is: drop the feishu_drive half of section 3 and let #14427 handle it. feishu_doc is fine to wire in — that one is genuinely used (agents read documents). Wiring feishu_drive would re-expose global tools I originally registered by mistake (#11898 was my bad), and _DEFAULT_OFF doesn't help here because the architectural concern is the prompt-level "don't call this", not the default-on/off bit.

Sections 1, 2, 4–8 are independent of this and stand on their own — pulling feishu_drive out of section 3 doesn't block the rest of your PR.

@alt-glitch

Copy link
Copy Markdown
Collaborator Author

@liujinkun2025 thanks for pointing that out!

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for this, Siddharth — it was a thorough, well-structured end-to-end pass at Discord support, and the clean cherry-pickable commit split made it easy to verify.

Closing as redundant: every piece of this PR has since landed on main independently over the intervening commits:

  • discord/discord_admin split + _DEFAULT_OFF_TOOLSETS + intents-aware dynamic schema → tools/discord_tool.py
  • Toolset-recovery pass for non-configurable platform toolsets (the claimed + configurable_tool_universe guards) → hermes_cli/tools_config.py
  • Feishu doc/drive wired into the hermes-feishu composite → toolsets.py
  • _save_platform_tools normalization — numeric entries coerced to str, stale no_mcp sentinel cleared
  • SessionSource ID fields (guild_id/parent_chat_id/message_id) + the Discord IDs context block + the DISCORD_BOT_TOKEN-gated disclaimer → gateway/session.py (note: guild_id has since been refactored to scope_id)

The current implementation matches your design closely. Appreciate you mapping out all six layers it takes to get a working Discord agent — that breakdown held up.

@teknium1 teknium1 closed this Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P1 High — major feature broken, no workaround platform/discord Discord bot adapter platform/feishu Feishu / Lark adapter type/bug Something isn't working

Projects

None yet

3 participants