fix(discord): end-to-end Discord support — toolset loading, tool split, and context injection - #15091
fix(discord): end-to-end Discord support — toolset loading, tool split, and context injection#15091alt-glitch wants to merge 8 commits into
Conversation
|
@BugBot review |
Related Issues & PRsThis 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 resolutionThe core bug —
Bug 2: Stale
|
fd2ef81 to
8abd2cf
Compare
|
@BugBot review |
8abd2cf to
f965c45
Compare
|
@BugBot review |
There was a problem hiding this comment.
✅ 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.
|
@BugBot review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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.
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.
db89678 to
7efd91d
Compare
|
Hey @alt-glitch — Wanted to flag a conflict on section 3 — the
So my ask is: drop the Sections 1, 2, 4–8 are independent of this and stand on their own — pulling |
|
@liujinkun2025 thanks for pointing that out! |
|
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
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. |

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 throughCONFIGURABLE_TOOLSETSto pick which toolset keys are enabled. Platform-specific toolsets likediscord,feishu_doc,feishu_drivearen't inCONFIGURABLE_TOOLSETS(they're not user-toggleable), so the round-trip dropped them on the floor. Even worse: thesearchtoolset (["web_search"]) could leak through on disabled browsers via the recovery pass.Fix: Add a second-pass recovery over all
TOOLSETSafter the configurable reverse-map, using aclaimedset guard to avoid false positives, and aconfigurable_tool_universeguard to prevent overlap toolsets likesearchfrom 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 toolsonce 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_servertool exposed 14 actions includingadd_role,pin_message, andremove_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_threaddiscord_admin(opt-in via_DEFAULT_OFF_TOOLSETS, 11 actions): all the guild/channel/role managementThe dynamic schema rebuild at
model_tools.pyis 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 thehermes-feishucomposite. Agents running on Feishu had zero Feishu-specific tools available.Fix: Add
feishu_docandfeishu_drivetoolsets tohermes-feishu'sincludes.4.
_save_platform_tools— numeric normalization + stale sentinelTwo latent bugs in the save path:
12306:parses asint) causedsorted()type errors downstream.no_mcpsentinel wasn't cleared when the user toggled it off via the TUI, silently disabling MCP servers permanently.Fix: Normalize entries to
stron read; discard staleno_mcpwhen 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
SessionSourceonly exposedthread_idas 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 achannel_id(it does — Discord threads are a channel type). It had no way to reference:search_members)pin_messagein a channel above its thread)Fix: Add
guild_id,parent_chat_id,message_idfields toSessionSource; populate them from the Discord adapter; emit a dedicated IDs block in the session context prompt whenDISCORD_BOT_TOKENis set: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:This pre-dates the
discordtool. With it enabled, the model was being gaslit into refusing valid tool calls.Fix: Gate the disclaimer on
DISCORD_BOT_TOKENbeing unset (matches the tool'scheck_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
SessionSourcefields; mirror the ID block inbuild_session_context_prompt. Can be done per-platform in follow-up PRs.Commits (all cherry-pickable)
fix(tools): recover non-configurable toolsets from composite resolution— the core round-trip fixfeat(discord): split discord_server into discord + discord_admin toolsfeat(feishu): wire feishu doc/drive tools into hermes-feishu compositefix(tools): normalize numeric entries and clear stale no_mcp in _save_platform_toolsfeat(session): add guild_id/parent_chat_id/message_id to SessionSourcefeat(discord): populate guild_id, parent_chat_id, message_id on SessionSourcefix(session): gate stale ""no Discord APIs"" note on DISCORD_BOT_TOKENfeat(session): inject Discord IDs block when discord tool is loadedFixes #5991
Fixes #8616
Fixes #13028
Supersedes #13086, #14101, #14149
Test plan
discordtool loads,discord_admindefault-offdiscord_adminenabled via config: both tools present_save_platform_tools→_get_platform_toolsround-trip preserves platform toolsets after first saveDISCORD_BOT_TOKEN: falls back to the ""no APIs"" disclaimersearchtoolset leak: disabling web+browser doesn't re-enableweb_searchvia recovery pass