mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-09 15:35:17 +00:00
fix(agents): scope message tool schema by channel (#18215)
Co-authored-by: Shadow <shadow@openclaw.ai>
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Infra/Fetch: ensure foreign abort-signal listener cleanup never masks original fetch successes/failures, while still preventing detached-finally unhandled rejection noise in `wrapFetchWithAbortSignal`. Thanks @Jackten.
|
||||
- Gateway/Config: prevent `config.patch` object-array merges from falling back to full-array replacement when some patch entries lack `id`, so partial `agents.list` updates no longer drop unrelated agents. (#17989) Thanks @stakeswky.
|
||||
- Agents/Models: probe the primary model when its auth-profile cooldown is near expiry (with per-provider throttling), so runs recover from temporary rate limits without staying on fallback models until restart. (#17478) Thanks @PlayerGhost.
|
||||
- Agents/Tools: scope the `message` tool schema to the active channel so Telegram uses `buttons` and Discord uses `components`. (#18215) Thanks @obviyus.
|
||||
- Telegram: keep draft-stream preview replies attached to the user message for `replyToMode: "all"` in groups and DMs, preserving threaded reply context from preview through finalization. (#17880) Thanks @yinghaosang.
|
||||
- Telegram: disable block streaming when `channels.telegram.streamMode` is `off`, preventing newline/content-block replies from splitting into multiple messages. (#17679) Thanks @saivarunk.
|
||||
- Telegram: route non-abort slash commands on the normal chat/topic sequential lane while keeping true abort requests (`/stop`, `stop`) on the control lane, preventing command/reply race conditions from control-lane bypass. (#17899) Thanks @obviyus.
|
||||
@@ -70,6 +71,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone.
|
||||
- Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.
|
||||
- Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.
|
||||
- Agents/Tools: scope the `message` tool schema to the active channel so Telegram uses `buttons` and Discord uses `components`. (#18215) Thanks @obviyus.
|
||||
- Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx.
|
||||
- Telegram: replace inbound `<media:audio>` placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.
|
||||
- Telegram: retry inbound media `getFile` calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ChannelPlugin } from "../../channels/plugins/types.js";
|
||||
import type { MessageActionRunResult } from "../../infra/outbound/message-action-runner.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
@@ -93,6 +93,97 @@ describe("message tool path passthrough", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("message tool schema scoping", () => {
|
||||
const telegramPlugin: ChannelPlugin = {
|
||||
id: "telegram",
|
||||
meta: {
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
selectionLabel: "Telegram",
|
||||
docsPath: "/channels/telegram",
|
||||
blurb: "Telegram test plugin.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct", "group"], media: true },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send", "react"] as const,
|
||||
supportsButtons: () => true,
|
||||
},
|
||||
};
|
||||
|
||||
const discordPlugin: ChannelPlugin = {
|
||||
id: "discord",
|
||||
meta: {
|
||||
id: "discord",
|
||||
label: "Discord",
|
||||
selectionLabel: "Discord",
|
||||
docsPath: "/channels/discord",
|
||||
blurb: "Discord test plugin.",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct", "group"], media: true },
|
||||
config: {
|
||||
listAccountIds: () => ["default"],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
actions: {
|
||||
listActions: () => ["send", "poll"] as const,
|
||||
},
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
setActivePluginRegistry(createTestRegistry([]));
|
||||
});
|
||||
|
||||
it("hides discord components when scoped to telegram", () => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{ pluginId: "telegram", source: "test", plugin: telegramPlugin },
|
||||
{ pluginId: "discord", source: "test", plugin: discordPlugin },
|
||||
]),
|
||||
);
|
||||
|
||||
const tool = createMessageTool({
|
||||
config: {} as never,
|
||||
currentChannelProvider: "telegram",
|
||||
});
|
||||
const properties =
|
||||
(tool.parameters as { properties?: Record<string, unknown> }).properties ?? {};
|
||||
const actionEnum = (properties.action as { enum?: string[] } | undefined)?.enum ?? [];
|
||||
|
||||
expect(properties.components).toBeUndefined();
|
||||
expect(properties.buttons).toBeDefined();
|
||||
expect(actionEnum).toContain("send");
|
||||
expect(actionEnum).toContain("react");
|
||||
expect(actionEnum).not.toContain("poll");
|
||||
});
|
||||
|
||||
it("shows discord components when scoped to discord", () => {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{ pluginId: "telegram", source: "test", plugin: telegramPlugin },
|
||||
{ pluginId: "discord", source: "test", plugin: discordPlugin },
|
||||
]),
|
||||
);
|
||||
|
||||
const tool = createMessageTool({
|
||||
config: {} as never,
|
||||
currentChannelProvider: "discord",
|
||||
});
|
||||
const properties =
|
||||
(tool.parameters as { properties?: Record<string, unknown> }).properties ?? {};
|
||||
const actionEnum = (properties.action as { enum?: string[] } | undefined)?.enum ?? [];
|
||||
|
||||
expect(properties.components).toBeDefined();
|
||||
expect(properties.buttons).toBeUndefined();
|
||||
expect(actionEnum).toContain("send");
|
||||
expect(actionEnum).toContain("poll");
|
||||
expect(actionEnum).not.toContain("react");
|
||||
});
|
||||
});
|
||||
|
||||
describe("message tool description", () => {
|
||||
const bluebubblesPlugin: ChannelPlugin = {
|
||||
id: "bluebubbles",
|
||||
|
||||
@@ -5,7 +5,9 @@ import { BLUEBUBBLES_GROUP_ACTIONS } from "../../channels/plugins/bluebubbles-ac
|
||||
import {
|
||||
listChannelMessageActions,
|
||||
supportsChannelMessageButtons,
|
||||
supportsChannelMessageButtonsForChannel,
|
||||
supportsChannelMessageCards,
|
||||
supportsChannelMessageCardsForChannel,
|
||||
} from "../../channels/plugins/message-actions.js";
|
||||
import {
|
||||
CHANNEL_MESSAGE_ACTION_NAMES,
|
||||
@@ -139,7 +141,11 @@ const discordComponentMessageSchema = Type.Object({
|
||||
modal: Type.Optional(discordComponentModalSchema),
|
||||
});
|
||||
|
||||
function buildSendSchema(options: { includeButtons: boolean; includeCards: boolean }) {
|
||||
function buildSendSchema(options: {
|
||||
includeButtons: boolean;
|
||||
includeCards: boolean;
|
||||
includeComponents: boolean;
|
||||
}) {
|
||||
const props: Record<string, unknown> = {
|
||||
message: Type.Optional(Type.String()),
|
||||
effectId: Type.Optional(
|
||||
@@ -205,6 +211,9 @@ function buildSendSchema(options: { includeButtons: boolean; includeCards: boole
|
||||
if (!options.includeCards) {
|
||||
delete props.card;
|
||||
}
|
||||
if (!options.includeComponents) {
|
||||
delete props.components;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
|
||||
@@ -351,7 +360,11 @@ function buildChannelManagementSchema() {
|
||||
};
|
||||
}
|
||||
|
||||
function buildMessageToolSchemaProps(options: { includeButtons: boolean; includeCards: boolean }) {
|
||||
function buildMessageToolSchemaProps(options: {
|
||||
includeButtons: boolean;
|
||||
includeCards: boolean;
|
||||
includeComponents: boolean;
|
||||
}) {
|
||||
return {
|
||||
...buildRoutingSchema(),
|
||||
...buildSendSchema(options),
|
||||
@@ -371,7 +384,7 @@ function buildMessageToolSchemaProps(options: { includeButtons: boolean; include
|
||||
|
||||
function buildMessageToolSchemaFromActions(
|
||||
actions: readonly string[],
|
||||
options: { includeButtons: boolean; includeCards: boolean },
|
||||
options: { includeButtons: boolean; includeCards: boolean; includeComponents: boolean },
|
||||
) {
|
||||
const props = buildMessageToolSchemaProps(options);
|
||||
return Type.Object({
|
||||
@@ -383,6 +396,7 @@ function buildMessageToolSchemaFromActions(
|
||||
const MessageToolSchema = buildMessageToolSchemaFromActions(AllMessageActions, {
|
||||
includeButtons: true,
|
||||
includeCards: true,
|
||||
includeComponents: true,
|
||||
});
|
||||
|
||||
type MessageToolOptions = {
|
||||
@@ -398,13 +412,58 @@ type MessageToolOptions = {
|
||||
requireExplicitTarget?: boolean;
|
||||
};
|
||||
|
||||
function buildMessageToolSchema(cfg: OpenClawConfig) {
|
||||
const actions = listChannelMessageActions(cfg);
|
||||
const includeButtons = supportsChannelMessageButtons(cfg);
|
||||
const includeCards = supportsChannelMessageCards(cfg);
|
||||
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 withSend = new Set<string>(["send", ...scopedActions]);
|
||||
return Array.from(withSend);
|
||||
}
|
||||
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";
|
||||
}
|
||||
// Components are currently Discord-specific.
|
||||
return listChannelSupportedActions({ cfg: params.cfg, channel: "discord" }).length > 0;
|
||||
}
|
||||
|
||||
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);
|
||||
return buildMessageToolSchemaFromActions(actions.length > 0 ? actions : ["send"], {
|
||||
includeButtons,
|
||||
includeCards,
|
||||
includeComponents,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -481,7 +540,13 @@ function buildMessageToolDescription(options?: {
|
||||
|
||||
export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
const agentAccountId = resolveAgentAccountId(options?.agentAccountId);
|
||||
const schema = options?.config ? buildMessageToolSchema(options.config) : MessageToolSchema;
|
||||
const schema = options?.config
|
||||
? buildMessageToolSchema({
|
||||
cfg: options.config,
|
||||
currentChannelProvider: options.currentChannelProvider,
|
||||
currentChannelId: options.currentChannelId,
|
||||
})
|
||||
: MessageToolSchema;
|
||||
const description = buildMessageToolDescription({
|
||||
config: options?.config,
|
||||
currentChannel: options?.currentChannelProvider,
|
||||
|
||||
@@ -26,6 +26,17 @@ export function supportsChannelMessageButtons(cfg: OpenClawConfig): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function supportsChannelMessageButtonsForChannel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel?: string;
|
||||
}): boolean {
|
||||
if (!params.channel) {
|
||||
return false;
|
||||
}
|
||||
const plugin = getChannelPlugin(params.channel as Parameters<typeof getChannelPlugin>[0]);
|
||||
return plugin?.actions?.supportsButtons?.({ cfg: params.cfg }) === true;
|
||||
}
|
||||
|
||||
export function supportsChannelMessageCards(cfg: OpenClawConfig): boolean {
|
||||
for (const plugin of listChannelPlugins()) {
|
||||
if (plugin.actions?.supportsCards?.({ cfg })) {
|
||||
@@ -35,6 +46,17 @@ export function supportsChannelMessageCards(cfg: OpenClawConfig): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function supportsChannelMessageCardsForChannel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel?: string;
|
||||
}): boolean {
|
||||
if (!params.channel) {
|
||||
return false;
|
||||
}
|
||||
const plugin = getChannelPlugin(params.channel as Parameters<typeof getChannelPlugin>[0]);
|
||||
return plugin?.actions?.supportsCards?.({ cfg: params.cfg }) === true;
|
||||
}
|
||||
|
||||
export async function dispatchChannelMessageAction(
|
||||
ctx: ChannelMessageActionContext,
|
||||
): Promise<AgentToolResult<unknown> | null> {
|
||||
|
||||
Reference in New Issue
Block a user