diff --git a/docs/providers/discord.md b/docs/providers/discord.md index d0d91c074..1f8dcbcee 100644 --- a/docs/providers/discord.md +++ b/docs/providers/discord.md @@ -33,8 +33,7 @@ Status: ready for DM and guild text channels via the official Discord bot gatewa - Full command list + config: [Slash commands](/tools/slash-commands) 11. Optional guild context history: set `discord.historyLimit` (default 20) to include the last N guild messages as context when replying to a mention. Set `0` to disable. 12. Reactions: the agent can trigger reactions via the `discord` tool (gated by `discord.actions.*`). - - `emoji=""` removes the bot's reaction(s) on the message. - - `remove: true` removes the specific emoji reaction. + - Reaction removal semantics: see [/tools/reactions](/tools/reactions). - The `discord` tool is only exposed when the current provider is Discord. 13. Native commands use isolated session keys (`discord:slash:${userId}`) rather than the shared `main` session. diff --git a/docs/providers/slack.md b/docs/providers/slack.md index 4c7ae1fa2..d8031920d 100644 --- a/docs/providers/slack.md +++ b/docs/providers/slack.md @@ -222,5 +222,5 @@ Slack tool actions can be gated with `slack.actions.*`: ## Notes - Mention gating is controlled via `slack.channels` (set `requireMention` to `true`); `routing.groupChat.mentionPatterns` also count as mentions. - Reaction notifications follow `slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`). -- For the Slack tool, `emoji=""` removes the bot's reaction(s) on the message; `remove: true` removes a specific emoji reaction. +- For the Slack tool, reaction removal semantics are in [/tools/reactions](/tools/reactions). - Attachments are downloaded to the media store when permitted and under the size limit. diff --git a/docs/providers/telegram.md b/docs/providers/telegram.md index 6d18ff123..0999c09bb 100644 --- a/docs/providers/telegram.md +++ b/docs/providers/telegram.md @@ -71,8 +71,7 @@ Controlled by `telegram.replyToMode`: ## Agent tool (reactions) - Tool: `telegram` with `react` action (`chatId`, `messageId`, `emoji`). -- `emoji=""` removes the bot's reaction(s) on the message. -- `remove: true` removes the reaction (Telegram only supports removing your own reaction). +- Reaction removal semantics: see [/tools/reactions](/tools/reactions). - Tool gating: `telegram.actions.reactions` (default: enabled). ## Delivery targets (CLI/cron) diff --git a/docs/providers/whatsapp.md b/docs/providers/whatsapp.md index 53e9f880d..d9d7bb11d 100644 --- a/docs/providers/whatsapp.md +++ b/docs/providers/whatsapp.md @@ -97,8 +97,7 @@ WhatsApp requires a real mobile number for verification. VoIP and virtual number ## Agent tool (reactions) - Tool: `whatsapp` with `react` action (`chatJid`, `messageId`, `emoji`, optional `remove`). - Optional: `participant` (group sender), `fromMe` (reacting to your own message), `accountId` (multi-account). -- `emoji=""` removes the bot's reaction(s) on the message. -- `remove: true` removes the bot's reaction (same effect as empty emoji). +- Reaction removal semantics: see [/tools/reactions](/tools/reactions). - Tool gating: `whatsapp.actions.reactions` (default: enabled). ## Outbound send (text + media) diff --git a/docs/tools/index.md b/docs/tools/index.md index f6f6d2b94..72f0b16c0 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -201,8 +201,7 @@ Notes: - `to` accepts `channel:` or `user:`. - Polls require 2–10 answers and default to 24 hours. - `reactions` returns per-emoji user lists (limited to 100 per reaction). -- `emoji=""` on `react` removes the bot's reaction(s) on the message. -- `remove: true` on `react` removes just that emoji. +- Reaction removal semantics: see [/tools/reactions](/tools/reactions). - `discord.actions.*` gates Discord tool actions; `roles` + `moderation` default to `false`. - `searchMessages` follows the Discord preview spec (limit max 25, channel/author filters accept arrays). - The tool is only exposed when the current provider is Discord. @@ -214,8 +213,7 @@ Core actions: - `react` (`chatJid`, `messageId`, `emoji`, optional `remove`, `participant`, `fromMe`, `accountId`) Notes: -- `emoji=""` removes the bot's reaction(s) on the message. -- `remove: true` removes the bot's reaction (same effect as empty emoji). +- Reaction removal semantics: see [/tools/reactions](/tools/reactions). - `whatsapp.actions.*` gates WhatsApp tool actions. - The tool is only exposed when the current provider is WhatsApp. @@ -226,8 +224,7 @@ Core actions: - `react` (`chatId`, `messageId`, `emoji`, optional `remove`) Notes: -- `emoji=""` removes the bot's reaction(s) on the message. -- `remove: true` removes the reaction (Telegram only supports removing your own reaction). +- Reaction removal semantics: see [/tools/reactions](/tools/reactions). - `telegram.actions.*` gates Telegram tool actions. - The tool is only exposed when the current provider is Telegram. diff --git a/docs/tools/reactions.md b/docs/tools/reactions.md new file mode 100644 index 000000000..ad33976e8 --- /dev/null +++ b/docs/tools/reactions.md @@ -0,0 +1,13 @@ +# Reaction tooling + +Shared reaction semantics across providers: + +- `emoji` is required for reactions. +- `emoji=""` removes the bot's reaction(s) on the message. +- `remove: true` removes the specified emoji when supported. + +Provider notes: + +- **Discord/Slack**: empty `emoji` removes all of the bot's reactions on the message; `remove: true` removes just that emoji. +- **Telegram**: `remove: true` removes your own reaction (Bot API limitation). +- **WhatsApp**: `remove: true` maps to empty emoji (remove bot reaction). diff --git a/src/agents/tools/discord-schema.ts b/src/agents/tools/discord-schema.ts index 05292f375..4e7bb24a0 100644 --- a/src/agents/tools/discord-schema.ts +++ b/src/agents/tools/discord-schema.ts @@ -1,12 +1,14 @@ import { Type } from "@sinclair/typebox"; +import { createReactionSchema } from "./reaction-schema.js"; + export const DiscordToolSchema = Type.Union([ - Type.Object({ - action: Type.Literal("react"), - channelId: Type.String(), - messageId: Type.String(), - emoji: Type.String(), - remove: Type.Optional(Type.Boolean()), + createReactionSchema({ + ids: { + channelId: Type.String(), + messageId: Type.String(), + }, + includeRemove: true, }), Type.Object({ action: Type.Literal("reactions"), diff --git a/src/agents/tools/reaction-schema.ts b/src/agents/tools/reaction-schema.ts new file mode 100644 index 000000000..f33031147 --- /dev/null +++ b/src/agents/tools/reaction-schema.ts @@ -0,0 +1,24 @@ +import { type TSchema, Type } from "@sinclair/typebox"; + +type ReactionSchemaOptions = { + action?: string; + ids: Record; + emoji?: TSchema; + includeRemove?: boolean; + extras?: Record; +}; + +export function createReactionSchema(options: ReactionSchemaOptions) { + const schema: Record = { + action: Type.Literal(options.action ?? "react"), + ...options.ids, + emoji: options.emoji ?? Type.String(), + }; + if (options.includeRemove) { + schema.remove = Type.Optional(Type.Boolean()); + } + if (options.extras) { + Object.assign(schema, options.extras); + } + return Type.Object(schema); +} diff --git a/src/agents/tools/slack-schema.ts b/src/agents/tools/slack-schema.ts index b94ce7c47..ee4682791 100644 --- a/src/agents/tools/slack-schema.ts +++ b/src/agents/tools/slack-schema.ts @@ -1,12 +1,14 @@ import { Type } from "@sinclair/typebox"; +import { createReactionSchema } from "./reaction-schema.js"; + export const SlackToolSchema = Type.Union([ - Type.Object({ - action: Type.Literal("react"), - channelId: Type.String(), - messageId: Type.String(), - emoji: Type.String(), - remove: Type.Optional(Type.Boolean()), + createReactionSchema({ + ids: { + channelId: Type.String(), + messageId: Type.String(), + }, + includeRemove: true, }), Type.Object({ action: Type.Literal("reactions"), diff --git a/src/agents/tools/telegram-schema.ts b/src/agents/tools/telegram-schema.ts index d967649f5..b8d999817 100644 --- a/src/agents/tools/telegram-schema.ts +++ b/src/agents/tools/telegram-schema.ts @@ -1,11 +1,13 @@ import { Type } from "@sinclair/typebox"; +import { createReactionSchema } from "./reaction-schema.js"; + export const TelegramToolSchema = Type.Union([ - Type.Object({ - action: Type.Literal("react"), - chatId: Type.Union([Type.String(), Type.Number()]), - messageId: Type.Union([Type.String(), Type.Number()]), - emoji: Type.String(), - remove: Type.Optional(Type.Boolean()), + createReactionSchema({ + ids: { + chatId: Type.Union([Type.String(), Type.Number()]), + messageId: Type.Union([Type.String(), Type.Number()]), + }, + includeRemove: true, }), ]); diff --git a/src/agents/tools/whatsapp-schema.ts b/src/agents/tools/whatsapp-schema.ts index 95a000cbb..3c8498785 100644 --- a/src/agents/tools/whatsapp-schema.ts +++ b/src/agents/tools/whatsapp-schema.ts @@ -1,14 +1,18 @@ import { Type } from "@sinclair/typebox"; +import { createReactionSchema } from "./reaction-schema.js"; + export const WhatsAppToolSchema = Type.Union([ - Type.Object({ - action: Type.Literal("react"), - chatJid: Type.String(), - messageId: Type.String(), - emoji: Type.String(), - remove: Type.Optional(Type.Boolean()), - participant: Type.Optional(Type.String()), - accountId: Type.Optional(Type.String()), - fromMe: Type.Optional(Type.Boolean()), + createReactionSchema({ + ids: { + chatJid: Type.String(), + messageId: Type.String(), + }, + includeRemove: true, + extras: { + participant: Type.Optional(Type.String()), + accountId: Type.Optional(Type.String()), + fromMe: Type.Optional(Type.Boolean()), + }, }), ]);