fix(agents): scope message tool schema by channel (#18215)

Co-authored-by: Shadow <shadow@openclaw.ai>
This commit is contained in:
Ayaan Zaidi
2026-02-16 22:04:18 +05:30
committed by GitHub
parent 3a2fffefdb
commit c8a536e30a
4 changed files with 189 additions and 9 deletions

View File

@@ -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",

View File

@@ -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,