| import { Type } from "@sinclair/typebox"; |
| import { BLUEBUBBLES_GROUP_ACTIONS } from "../../channels/plugins/bluebubbles-actions.js"; |
| import { listChannelPlugins } from "../../channels/plugins/index.js"; |
| import { |
| listChannelMessageActions, |
| supportsChannelMessageButtons, |
| supportsChannelMessageButtonsForChannel, |
| supportsChannelMessageCards, |
| supportsChannelMessageCardsForChannel, |
| } from "../../channels/plugins/message-actions.js"; |
| import { |
| CHANNEL_MESSAGE_ACTION_NAMES, |
| type ChannelMessageActionName, |
| } from "../../channels/plugins/types.js"; |
| import type { OpenClawConfig } from "../../config/config.js"; |
| import { loadConfig } from "../../config/config.js"; |
| import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../gateway/protocol/client-info.js"; |
| import { getToolResult, runMessageAction } from "../../infra/outbound/message-action-runner.js"; |
| import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js"; |
| import { POLL_CREATION_PARAM_DEFS, POLL_CREATION_PARAM_NAMES } from "../../poll-params.js"; |
| import { normalizeAccountId } from "../../routing/session-key.js"; |
| import { stripReasoningTagsFromText } from "../../shared/text/reasoning-tags.js"; |
| import { normalizeMessageChannel } from "../../utils/message-channel.js"; |
| import { resolveSessionAgentId } from "../agent-scope.js"; |
| import { listChannelSupportedActions } from "../channel-tools.js"; |
| import { channelTargetSchema, channelTargetsSchema, stringEnum } from "../schema/typebox.js"; |
| import type { AnyAgentTool } from "./common.js"; |
| import { jsonResult, readNumberParam, readStringParam } from "./common.js"; |
| import { resolveGatewayOptions } from "./gateway.js"; |
|
|
| const AllMessageActions = CHANNEL_MESSAGE_ACTION_NAMES; |
| const EXPLICIT_TARGET_ACTIONS = new Set<ChannelMessageActionName>([ |
| "send", |
| "sendWithEffect", |
| "sendAttachment", |
| "reply", |
| "thread-reply", |
| "broadcast", |
| ]); |
|
|
| function actionNeedsExplicitTarget(action: ChannelMessageActionName): boolean { |
| return EXPLICIT_TARGET_ACTIONS.has(action); |
| } |
| function buildRoutingSchema() { |
| return { |
| channel: Type.Optional(Type.String()), |
| target: Type.Optional(channelTargetSchema({ description: "Target channel/user id or name." })), |
| targets: Type.Optional(channelTargetsSchema()), |
| accountId: Type.Optional(Type.String()), |
| dryRun: Type.Optional(Type.Boolean()), |
| }; |
| } |
|
|
| const discordComponentEmojiSchema = Type.Object({ |
| name: Type.String(), |
| id: Type.Optional(Type.String()), |
| animated: Type.Optional(Type.Boolean()), |
| }); |
|
|
| const discordComponentOptionSchema = Type.Object({ |
| label: Type.String(), |
| value: Type.String(), |
| description: Type.Optional(Type.String()), |
| emoji: Type.Optional(discordComponentEmojiSchema), |
| default: Type.Optional(Type.Boolean()), |
| }); |
|
|
| const discordComponentButtonSchema = Type.Object({ |
| label: Type.String(), |
| style: Type.Optional(stringEnum(["primary", "secondary", "success", "danger", "link"])), |
| url: Type.Optional(Type.String()), |
| emoji: Type.Optional(discordComponentEmojiSchema), |
| disabled: Type.Optional(Type.Boolean()), |
| allowedUsers: Type.Optional( |
| Type.Array( |
| Type.String({ |
| description: "Discord user ids or names allowed to interact with this button.", |
| }), |
| ), |
| ), |
| }); |
|
|
| const discordComponentSelectSchema = Type.Object({ |
| type: Type.Optional(stringEnum(["string", "user", "role", "mentionable", "channel"])), |
| placeholder: Type.Optional(Type.String()), |
| minValues: Type.Optional(Type.Number()), |
| maxValues: Type.Optional(Type.Number()), |
| options: Type.Optional(Type.Array(discordComponentOptionSchema)), |
| }); |
|
|
| const discordComponentBlockSchema = Type.Object({ |
| type: Type.String(), |
| text: Type.Optional(Type.String()), |
| texts: Type.Optional(Type.Array(Type.String())), |
| accessory: Type.Optional( |
| Type.Object({ |
| type: Type.String(), |
| url: Type.Optional(Type.String()), |
| button: Type.Optional(discordComponentButtonSchema), |
| }), |
| ), |
| spacing: Type.Optional(stringEnum(["small", "large"])), |
| divider: Type.Optional(Type.Boolean()), |
| buttons: Type.Optional(Type.Array(discordComponentButtonSchema)), |
| select: Type.Optional(discordComponentSelectSchema), |
| items: Type.Optional( |
| Type.Array( |
| Type.Object({ |
| url: Type.String(), |
| description: Type.Optional(Type.String()), |
| spoiler: Type.Optional(Type.Boolean()), |
| }), |
| ), |
| ), |
| file: Type.Optional(Type.String()), |
| spoiler: Type.Optional(Type.Boolean()), |
| }); |
|
|
| const discordComponentModalFieldSchema = Type.Object({ |
| type: Type.String(), |
| name: Type.Optional(Type.String()), |
| label: Type.String(), |
| description: Type.Optional(Type.String()), |
| placeholder: Type.Optional(Type.String()), |
| required: Type.Optional(Type.Boolean()), |
| options: Type.Optional(Type.Array(discordComponentOptionSchema)), |
| minValues: Type.Optional(Type.Number()), |
| maxValues: Type.Optional(Type.Number()), |
| minLength: Type.Optional(Type.Number()), |
| maxLength: Type.Optional(Type.Number()), |
| style: Type.Optional(stringEnum(["short", "paragraph"])), |
| }); |
|
|
| const discordComponentModalSchema = Type.Object({ |
| title: Type.String(), |
| triggerLabel: Type.Optional(Type.String()), |
| triggerStyle: Type.Optional(stringEnum(["primary", "secondary", "success", "danger", "link"])), |
| fields: Type.Array(discordComponentModalFieldSchema), |
| }); |
|
|
| const discordComponentMessageSchema = Type.Object( |
| { |
| text: Type.Optional(Type.String()), |
| reusable: Type.Optional( |
| Type.Boolean({ |
| description: "Allow components to be used multiple times until they expire.", |
| }), |
| ), |
| container: Type.Optional( |
| Type.Object({ |
| accentColor: Type.Optional(Type.String()), |
| spoiler: Type.Optional(Type.Boolean()), |
| }), |
| ), |
| blocks: Type.Optional(Type.Array(discordComponentBlockSchema)), |
| modal: Type.Optional(discordComponentModalSchema), |
| }, |
| { |
| description: |
| "Discord components v2 payload. Set reusable=true to keep buttons, selects, and forms active until expiry.", |
| }, |
| ); |
|
|
| function buildSendSchema(options: { |
| includeButtons: boolean; |
| includeCards: boolean; |
| includeComponents: boolean; |
| }) { |
| const props: Record<string, unknown> = { |
| message: Type.Optional(Type.String()), |
| effectId: Type.Optional( |
| Type.String({ |
| description: "Message effect name/id for sendWithEffect (e.g., invisible ink).", |
| }), |
| ), |
| effect: Type.Optional( |
| Type.String({ description: "Alias for effectId (e.g., invisible-ink, balloons)." }), |
| ), |
| media: Type.Optional( |
| Type.String({ |
| description: "Media URL or local path. data: URLs are not supported here, use buffer.", |
| }), |
| ), |
| filename: Type.Optional(Type.String()), |
| buffer: Type.Optional( |
| Type.String({ |
| description: "Base64 payload for attachments (optionally a data: URL).", |
| }), |
| ), |
| contentType: Type.Optional(Type.String()), |
| mimeType: Type.Optional(Type.String()), |
| caption: Type.Optional(Type.String()), |
| path: Type.Optional(Type.String()), |
| filePath: Type.Optional(Type.String()), |
| replyTo: Type.Optional(Type.String()), |
| threadId: Type.Optional(Type.String()), |
| asVoice: Type.Optional(Type.Boolean()), |
| silent: Type.Optional(Type.Boolean()), |
| quoteText: Type.Optional( |
| Type.String({ description: "Quote text for Telegram reply_parameters" }), |
| ), |
| bestEffort: Type.Optional(Type.Boolean()), |
| gifPlayback: Type.Optional(Type.Boolean()), |
| buttons: Type.Optional( |
| Type.Array( |
| Type.Array( |
| Type.Object({ |
| text: Type.String(), |
| callback_data: Type.String(), |
| style: Type.Optional(stringEnum(["danger", "success", "primary"])), |
| }), |
| ), |
| { |
| description: "Telegram inline keyboard buttons (array of button rows)", |
| }, |
| ), |
| ), |
| card: Type.Optional( |
| Type.Object( |
| {}, |
| { |
| additionalProperties: true, |
| description: "Adaptive Card JSON object (when supported by the channel)", |
| }, |
| ), |
| ), |
| components: Type.Optional(discordComponentMessageSchema), |
| }; |
| if (!options.includeButtons) { |
| delete props.buttons; |
| } |
| if (!options.includeCards) { |
| delete props.card; |
| } |
| if (!options.includeComponents) { |
| delete props.components; |
| } |
| return props; |
| } |
|
|
| function buildReactionSchema() { |
| return { |
| messageId: Type.Optional( |
| Type.String({ |
| description: |
| "Target message id for reaction. If omitted, defaults to the current inbound message id when available.", |
| }), |
| ), |
| message_id: Type.Optional( |
| Type.String({ |
| |
| description: |
| "snake_case alias of messageId. If omitted, defaults to the current inbound message id when available.", |
| }), |
| ), |
| emoji: Type.Optional(Type.String()), |
| remove: Type.Optional(Type.Boolean()), |
| targetAuthor: Type.Optional(Type.String()), |
| targetAuthorUuid: Type.Optional(Type.String()), |
| groupId: Type.Optional(Type.String()), |
| }; |
| } |
|
|
| function buildFetchSchema() { |
| return { |
| limit: Type.Optional(Type.Number()), |
| before: Type.Optional(Type.String()), |
| after: Type.Optional(Type.String()), |
| around: Type.Optional(Type.String()), |
| fromMe: Type.Optional(Type.Boolean()), |
| includeArchived: Type.Optional(Type.Boolean()), |
| }; |
| } |
|
|
| function buildPollSchema(options?: { includeTelegramExtras?: boolean }) { |
| const props: Record<string, unknown> = { |
| pollId: Type.Optional(Type.String()), |
| pollOptionId: Type.Optional( |
| Type.String({ |
| description: "Poll answer id to vote for. Use when the channel exposes stable answer ids.", |
| }), |
| ), |
| pollOptionIds: Type.Optional( |
| Type.Array( |
| Type.String({ |
| description: |
| "Poll answer ids to vote for in a multiselect poll. Use when the channel exposes stable answer ids.", |
| }), |
| ), |
| ), |
| pollOptionIndex: Type.Optional( |
| Type.Number({ |
| description: |
| "1-based poll option number to vote for, matching the rendered numbered poll choices.", |
| }), |
| ), |
| pollOptionIndexes: Type.Optional( |
| Type.Array( |
| Type.Number({ |
| description: |
| "1-based poll option numbers to vote for in a multiselect poll, matching the rendered numbered poll choices.", |
| }), |
| ), |
| ), |
| }; |
| for (const name of POLL_CREATION_PARAM_NAMES) { |
| const def = POLL_CREATION_PARAM_DEFS[name]; |
| if (def.telegramOnly && !options?.includeTelegramExtras) { |
| continue; |
| } |
| switch (def.kind) { |
| case "string": |
| props[name] = Type.Optional(Type.String()); |
| break; |
| case "stringArray": |
| props[name] = Type.Optional(Type.Array(Type.String())); |
| break; |
| case "number": |
| props[name] = Type.Optional(Type.Number()); |
| break; |
| case "boolean": |
| props[name] = Type.Optional(Type.Boolean()); |
| break; |
| } |
| } |
| return props; |
| } |
|
|
| function buildChannelTargetSchema() { |
| return { |
| channelId: Type.Optional( |
| Type.String({ description: "Channel id filter (search/thread list/event create)." }), |
| ), |
| channelIds: Type.Optional( |
| Type.Array(Type.String({ description: "Channel id filter (repeatable)." })), |
| ), |
| guildId: Type.Optional(Type.String()), |
| userId: Type.Optional(Type.String()), |
| authorId: Type.Optional(Type.String()), |
| authorIds: Type.Optional(Type.Array(Type.String())), |
| roleId: Type.Optional(Type.String()), |
| roleIds: Type.Optional(Type.Array(Type.String())), |
| participant: Type.Optional(Type.String()), |
| }; |
| } |
|
|
| function buildStickerSchema() { |
| return { |
| emojiName: Type.Optional(Type.String()), |
| stickerId: Type.Optional(Type.Array(Type.String())), |
| stickerName: Type.Optional(Type.String()), |
| stickerDesc: Type.Optional(Type.String()), |
| stickerTags: Type.Optional(Type.String()), |
| }; |
| } |
|
|
| function buildThreadSchema() { |
| return { |
| threadName: Type.Optional(Type.String()), |
| autoArchiveMin: Type.Optional(Type.Number()), |
| appliedTags: Type.Optional(Type.Array(Type.String())), |
| }; |
| } |
|
|
| function buildEventSchema() { |
| return { |
| query: Type.Optional(Type.String()), |
| eventName: Type.Optional(Type.String()), |
| eventType: Type.Optional(Type.String()), |
| startTime: Type.Optional(Type.String()), |
| endTime: Type.Optional(Type.String()), |
| desc: Type.Optional(Type.String()), |
| location: Type.Optional(Type.String()), |
| durationMin: Type.Optional(Type.Number()), |
| until: Type.Optional(Type.String()), |
| }; |
| } |
|
|
| function buildModerationSchema() { |
| return { |
| reason: Type.Optional(Type.String()), |
| deleteDays: Type.Optional(Type.Number()), |
| }; |
| } |
|
|
| function buildGatewaySchema() { |
| return { |
| gatewayUrl: Type.Optional(Type.String()), |
| gatewayToken: Type.Optional(Type.String()), |
| timeoutMs: Type.Optional(Type.Number()), |
| }; |
| } |
|
|
| function buildPresenceSchema() { |
| return { |
| activityType: Type.Optional( |
| Type.String({ |
| description: "Activity type: playing, streaming, listening, watching, competing, custom.", |
| }), |
| ), |
| activityName: Type.Optional( |
| Type.String({ |
| description: "Activity name shown in sidebar (e.g. 'with fire'). Ignored for custom type.", |
| }), |
| ), |
| activityUrl: Type.Optional( |
| Type.String({ |
| description: |
| "Streaming URL (Twitch or YouTube). Only used with streaming type; may not render for bots.", |
| }), |
| ), |
| activityState: Type.Optional( |
| Type.String({ |
| description: |
| "State text. For custom type this is the status text; for others it shows in the flyout.", |
| }), |
| ), |
| status: Type.Optional( |
| Type.String({ description: "Bot status: online, dnd, idle, invisible." }), |
| ), |
| }; |
| } |
|
|
| function buildChannelManagementSchema() { |
| return { |
| name: Type.Optional(Type.String()), |
| type: Type.Optional(Type.Number()), |
| parentId: Type.Optional(Type.String()), |
| topic: Type.Optional(Type.String()), |
| position: Type.Optional(Type.Number()), |
| nsfw: Type.Optional(Type.Boolean()), |
| rateLimitPerUser: Type.Optional(Type.Number()), |
| categoryId: Type.Optional(Type.String()), |
| clearParent: Type.Optional( |
| Type.Boolean({ |
| description: "Clear the parent/category when supported by the provider.", |
| }), |
| ), |
| }; |
| } |
|
|
| function buildMessageToolSchemaProps(options: { |
| includeButtons: boolean; |
| includeCards: boolean; |
| includeComponents: boolean; |
| includeTelegramPollExtras: boolean; |
| }) { |
| return { |
| ...buildRoutingSchema(), |
| ...buildSendSchema(options), |
| ...buildReactionSchema(), |
| ...buildFetchSchema(), |
| ...buildPollSchema({ includeTelegramExtras: options.includeTelegramPollExtras }), |
| ...buildChannelTargetSchema(), |
| ...buildStickerSchema(), |
| ...buildThreadSchema(), |
| ...buildEventSchema(), |
| ...buildModerationSchema(), |
| ...buildGatewaySchema(), |
| ...buildChannelManagementSchema(), |
| ...buildPresenceSchema(), |
| }; |
| } |
|
|
| function buildMessageToolSchemaFromActions( |
| actions: readonly string[], |
| options: { |
| includeButtons: boolean; |
| includeCards: boolean; |
| includeComponents: boolean; |
| includeTelegramPollExtras: boolean; |
| }, |
| ) { |
| const props = buildMessageToolSchemaProps(options); |
| return Type.Object({ |
| action: stringEnum(actions), |
| ...props, |
| }); |
| } |
|
|
| const MessageToolSchema = buildMessageToolSchemaFromActions(AllMessageActions, { |
| includeButtons: true, |
| includeCards: true, |
| includeComponents: true, |
| includeTelegramPollExtras: true, |
| }); |
|
|
| type MessageToolOptions = { |
| agentAccountId?: string; |
| agentSessionKey?: string; |
| config?: OpenClawConfig; |
| currentChannelId?: string; |
| currentChannelProvider?: string; |
| currentThreadTs?: string; |
| currentMessageId?: string | number; |
| replyToMode?: "off" | "first" | "all"; |
| hasRepliedRef?: { value: boolean }; |
| sandboxRoot?: string; |
| requireExplicitTarget?: boolean; |
| requesterSenderId?: string; |
| }; |
|
|
| function resolveMessageToolSchemaActions(params: { |
| cfg: OpenClawConfig; |
| currentChannelProvider?: string; |
| currentChannelId?: string; |
| }): string[] { |
| const currentChannel = normalizeMessageChannel(params.currentChannelProvider); |
| if (currentChannel) { |
| const scopedActions = filterActionsForContext({ |
| actions: listChannelSupportedActions({ |
| cfg: params.cfg, |
| channel: currentChannel, |
| }), |
| channel: currentChannel, |
| currentChannelId: params.currentChannelId, |
| }); |
| const allActions = new Set<string>(["send", ...scopedActions]); |
| |
| |
| for (const plugin of listChannelPlugins()) { |
| if (plugin.id === currentChannel) { |
| continue; |
| } |
| for (const action of listChannelSupportedActions({ cfg: params.cfg, channel: plugin.id })) { |
| allActions.add(action); |
| } |
| } |
| return Array.from(allActions); |
| } |
| const actions = listChannelMessageActions(params.cfg); |
| return actions.length > 0 ? actions : ["send"]; |
| } |
|
|
| function resolveIncludeComponents(params: { |
| cfg: OpenClawConfig; |
| currentChannelProvider?: string; |
| }): boolean { |
| const currentChannel = normalizeMessageChannel(params.currentChannelProvider); |
| if (currentChannel) { |
| return currentChannel === "discord"; |
| } |
| |
| return listChannelSupportedActions({ cfg: params.cfg, channel: "discord" }).length > 0; |
| } |
|
|
| function resolveIncludeTelegramPollExtras(params: { |
| cfg: OpenClawConfig; |
| currentChannelProvider?: string; |
| }): boolean { |
| return listChannelSupportedActions({ |
| cfg: params.cfg, |
| channel: "telegram", |
| }).includes("poll"); |
| } |
|
|
| function buildMessageToolSchema(params: { |
| cfg: OpenClawConfig; |
| currentChannelProvider?: string; |
| currentChannelId?: string; |
| }) { |
| const currentChannel = normalizeMessageChannel(params.currentChannelProvider); |
| const actions = resolveMessageToolSchemaActions(params); |
| const includeButtons = currentChannel |
| ? supportsChannelMessageButtonsForChannel({ cfg: params.cfg, channel: currentChannel }) |
| : supportsChannelMessageButtons(params.cfg); |
| const includeCards = currentChannel |
| ? supportsChannelMessageCardsForChannel({ cfg: params.cfg, channel: currentChannel }) |
| : supportsChannelMessageCards(params.cfg); |
| const includeComponents = resolveIncludeComponents(params); |
| const includeTelegramPollExtras = resolveIncludeTelegramPollExtras(params); |
| return buildMessageToolSchemaFromActions(actions.length > 0 ? actions : ["send"], { |
| includeButtons, |
| includeCards, |
| includeComponents, |
| includeTelegramPollExtras, |
| }); |
| } |
|
|
| function resolveAgentAccountId(value?: string): string | undefined { |
| const trimmed = value?.trim(); |
| if (!trimmed) { |
| return undefined; |
| } |
| return normalizeAccountId(trimmed); |
| } |
|
|
| function filterActionsForContext(params: { |
| actions: ChannelMessageActionName[]; |
| channel?: string; |
| currentChannelId?: string; |
| }): ChannelMessageActionName[] { |
| const channel = normalizeMessageChannel(params.channel); |
| if (!channel || channel !== "bluebubbles") { |
| return params.actions; |
| } |
| const currentChannelId = params.currentChannelId?.trim(); |
| if (!currentChannelId) { |
| return params.actions; |
| } |
| const normalizedTarget = |
| normalizeTargetForProvider(channel, currentChannelId) ?? currentChannelId; |
| const lowered = normalizedTarget.trim().toLowerCase(); |
| const isGroupTarget = |
| lowered.startsWith("chat_guid:") || |
| lowered.startsWith("chat_id:") || |
| lowered.startsWith("chat_identifier:") || |
| lowered.startsWith("group:"); |
| if (isGroupTarget) { |
| return params.actions; |
| } |
| return params.actions.filter((action) => !BLUEBUBBLES_GROUP_ACTIONS.has(action)); |
| } |
|
|
| function buildMessageToolDescription(options?: { |
| config?: OpenClawConfig; |
| currentChannel?: string; |
| currentChannelId?: string; |
| }): string { |
| const baseDescription = "Send, delete, and manage messages via channel plugins."; |
|
|
| |
| if (options?.currentChannel) { |
| const channelActions = filterActionsForContext({ |
| actions: listChannelSupportedActions({ |
| cfg: options.config, |
| channel: options.currentChannel, |
| }), |
| channel: options.currentChannel, |
| currentChannelId: options.currentChannelId, |
| }); |
| if (channelActions.length > 0) { |
| |
| const allActions = new Set(["send", ...channelActions]); |
| const actionList = Array.from(allActions).toSorted().join(", "); |
| let desc = `${baseDescription} Current channel (${options.currentChannel}) supports: ${actionList}.`; |
|
|
| |
| const otherChannels: string[] = []; |
| for (const plugin of listChannelPlugins()) { |
| if (plugin.id === options.currentChannel) { |
| continue; |
| } |
| const actions = listChannelSupportedActions({ cfg: options.config, channel: plugin.id }); |
| if (actions.length > 0) { |
| const all = new Set(["send", ...actions]); |
| otherChannels.push(`${plugin.id} (${Array.from(all).toSorted().join(", ")})`); |
| } |
| } |
| if (otherChannels.length > 0) { |
| desc += ` Other configured channels: ${otherChannels.join(", ")}.`; |
| } |
|
|
| return desc; |
| } |
| } |
|
|
| |
| if (options?.config) { |
| const actions = listChannelMessageActions(options.config); |
| if (actions.length > 0) { |
| return `${baseDescription} Supports actions: ${actions.join(", ")}.`; |
| } |
| } |
|
|
| return `${baseDescription} Supports actions: send, delete, react, poll, pin, threads, and more.`; |
| } |
|
|
| export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { |
| const agentAccountId = resolveAgentAccountId(options?.agentAccountId); |
| const schema = options?.config |
| ? buildMessageToolSchema({ |
| cfg: options.config, |
| currentChannelProvider: options.currentChannelProvider, |
| currentChannelId: options.currentChannelId, |
| }) |
| : MessageToolSchema; |
| const description = buildMessageToolDescription({ |
| config: options?.config, |
| currentChannel: options?.currentChannelProvider, |
| currentChannelId: options?.currentChannelId, |
| }); |
|
|
| return { |
| label: "Message", |
| name: "message", |
| description, |
| parameters: schema, |
| execute: async (_toolCallId, args, signal) => { |
| |
| if (signal?.aborted) { |
| const err = new Error("Message send aborted"); |
| err.name = "AbortError"; |
| throw err; |
| } |
| |
| const params = { ...(args as Record<string, unknown>) }; |
|
|
| |
| |
| for (const field of ["text", "content", "message", "caption"]) { |
| if (typeof params[field] === "string") { |
| params[field] = stripReasoningTagsFromText(params[field]); |
| } |
| } |
|
|
| const cfg = options?.config ?? loadConfig(); |
| const action = readStringParam(params, "action", { |
| required: true, |
| }) as ChannelMessageActionName; |
| const requireExplicitTarget = options?.requireExplicitTarget === true; |
| if (requireExplicitTarget && actionNeedsExplicitTarget(action)) { |
| const explicitTarget = |
| (typeof params.target === "string" && params.target.trim().length > 0) || |
| (typeof params.to === "string" && params.to.trim().length > 0) || |
| (typeof params.channelId === "string" && params.channelId.trim().length > 0) || |
| (Array.isArray(params.targets) && |
| params.targets.some((value) => typeof value === "string" && value.trim().length > 0)); |
| if (!explicitTarget) { |
| throw new Error( |
| "Explicit message target required for this run. Provide target/targets (and channel when needed).", |
| ); |
| } |
| } |
|
|
| const accountId = readStringParam(params, "accountId") ?? agentAccountId; |
| if (accountId) { |
| params.accountId = accountId; |
| } |
|
|
| const gatewayResolved = resolveGatewayOptions({ |
| gatewayUrl: readStringParam(params, "gatewayUrl", { trim: false }), |
| gatewayToken: readStringParam(params, "gatewayToken", { trim: false }), |
| timeoutMs: readNumberParam(params, "timeoutMs"), |
| }); |
| const gateway = { |
| url: gatewayResolved.url, |
| token: gatewayResolved.token, |
| timeoutMs: gatewayResolved.timeoutMs, |
| clientName: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, |
| clientDisplayName: "agent", |
| mode: GATEWAY_CLIENT_MODES.BACKEND, |
| }; |
| const hasCurrentMessageId = |
| typeof options?.currentMessageId === "number" || |
| (typeof options?.currentMessageId === "string" && |
| options.currentMessageId.trim().length > 0); |
|
|
| const toolContext = |
| options?.currentChannelId || |
| options?.currentChannelProvider || |
| options?.currentThreadTs || |
| hasCurrentMessageId || |
| options?.replyToMode || |
| options?.hasRepliedRef |
| ? { |
| currentChannelId: options?.currentChannelId, |
| currentChannelProvider: options?.currentChannelProvider, |
| currentThreadTs: options?.currentThreadTs, |
| currentMessageId: options?.currentMessageId, |
| replyToMode: options?.replyToMode, |
| hasRepliedRef: options?.hasRepliedRef, |
| |
| |
| skipCrossContextDecoration: true, |
| } |
| : undefined; |
|
|
| const result = await runMessageAction({ |
| cfg, |
| action, |
| params, |
| defaultAccountId: accountId ?? undefined, |
| requesterSenderId: options?.requesterSenderId, |
| gateway, |
| toolContext, |
| sessionKey: options?.agentSessionKey, |
| agentId: options?.agentSessionKey |
| ? resolveSessionAgentId({ sessionKey: options.agentSessionKey, config: cfg }) |
| : undefined, |
| sandboxRoot: options?.sandboxRoot, |
| abortSignal: signal, |
| }); |
|
|
| const toolResult = getToolResult(result); |
| if (toolResult) { |
| return toolResult; |
| } |
| return jsonResult(result.payload); |
| }, |
| }; |
| } |
|
|