mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-13 04:10:34 +00:00
fix: strip reasoning tags from messaging tool text to prevent <think> leakage (#11053)
Co-authored-by: MEA <mea@MEAdeMac-mini.local>
This commit is contained in:
@@ -162,6 +162,80 @@ describe("message tool description", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("message tool reasoning tag sanitization", () => {
|
||||||
|
it("strips <think> tags from text field before sending", async () => {
|
||||||
|
mocks.runMessageAction.mockClear();
|
||||||
|
mocks.runMessageAction.mockResolvedValue({
|
||||||
|
kind: "send",
|
||||||
|
action: "send",
|
||||||
|
channel: "signal",
|
||||||
|
to: "signal:+15551234567",
|
||||||
|
handledBy: "plugin",
|
||||||
|
payload: {},
|
||||||
|
dryRun: true,
|
||||||
|
} satisfies MessageActionRunResult);
|
||||||
|
|
||||||
|
const tool = createMessageTool({ config: {} as never });
|
||||||
|
|
||||||
|
await tool.execute("1", {
|
||||||
|
action: "send",
|
||||||
|
target: "signal:+15551234567",
|
||||||
|
text: "<think>internal reasoning</think>Hello!",
|
||||||
|
});
|
||||||
|
|
||||||
|
const call = mocks.runMessageAction.mock.calls[0]?.[0];
|
||||||
|
expect(call?.params?.text).toBe("Hello!");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips <think> tags from content field before sending", async () => {
|
||||||
|
mocks.runMessageAction.mockClear();
|
||||||
|
mocks.runMessageAction.mockResolvedValue({
|
||||||
|
kind: "send",
|
||||||
|
action: "send",
|
||||||
|
channel: "discord",
|
||||||
|
to: "discord:123",
|
||||||
|
handledBy: "plugin",
|
||||||
|
payload: {},
|
||||||
|
dryRun: true,
|
||||||
|
} satisfies MessageActionRunResult);
|
||||||
|
|
||||||
|
const tool = createMessageTool({ config: {} as never });
|
||||||
|
|
||||||
|
await tool.execute("1", {
|
||||||
|
action: "send",
|
||||||
|
target: "discord:123",
|
||||||
|
content: "<think>reasoning here</think>Reply text",
|
||||||
|
});
|
||||||
|
|
||||||
|
const call = mocks.runMessageAction.mock.calls[0]?.[0];
|
||||||
|
expect(call?.params?.content).toBe("Reply text");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through text without reasoning tags unchanged", async () => {
|
||||||
|
mocks.runMessageAction.mockClear();
|
||||||
|
mocks.runMessageAction.mockResolvedValue({
|
||||||
|
kind: "send",
|
||||||
|
action: "send",
|
||||||
|
channel: "signal",
|
||||||
|
to: "signal:+15551234567",
|
||||||
|
handledBy: "plugin",
|
||||||
|
payload: {},
|
||||||
|
dryRun: true,
|
||||||
|
} satisfies MessageActionRunResult);
|
||||||
|
|
||||||
|
const tool = createMessageTool({ config: {} as never });
|
||||||
|
|
||||||
|
await tool.execute("1", {
|
||||||
|
action: "send",
|
||||||
|
target: "signal:+15551234567",
|
||||||
|
text: "Normal message without any tags",
|
||||||
|
});
|
||||||
|
|
||||||
|
const call = mocks.runMessageAction.mock.calls[0]?.[0];
|
||||||
|
expect(call?.params?.text).toBe("Normal message without any tags");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("message tool sandbox passthrough", () => {
|
describe("message tool sandbox passthrough", () => {
|
||||||
it("forwards sandboxRoot to runMessageAction", async () => {
|
it("forwards sandboxRoot to runMessageAction", async () => {
|
||||||
mocks.runMessageAction.mockClear();
|
mocks.runMessageAction.mockClear();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "../../gateway/protocol
|
|||||||
import { getToolResult, runMessageAction } from "../../infra/outbound/message-action-runner.js";
|
import { getToolResult, runMessageAction } from "../../infra/outbound/message-action-runner.js";
|
||||||
import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js";
|
import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js";
|
||||||
import { normalizeAccountId } from "../../routing/session-key.js";
|
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||||
|
import { stripReasoningTagsFromText } from "../../shared/text/reasoning-tags.js";
|
||||||
import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||||
import { resolveSessionAgentId } from "../agent-scope.js";
|
import { resolveSessionAgentId } from "../agent-scope.js";
|
||||||
import { listChannelSupportedActions } from "../channel-tools.js";
|
import { listChannelSupportedActions } from "../channel-tools.js";
|
||||||
@@ -405,7 +406,17 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
|||||||
err.name = "AbortError";
|
err.name = "AbortError";
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
const params = args as Record<string, unknown>;
|
// Shallow-copy so we don't mutate the original event args (used for logging/dedup).
|
||||||
|
const params = { ...(args as Record<string, unknown>) };
|
||||||
|
|
||||||
|
// Strip reasoning tags from text fields — models may include <think>…</think>
|
||||||
|
// in tool arguments, and the messaging tool send path has no other tag filtering.
|
||||||
|
for (const field of ["text", "content", "message", "caption"]) {
|
||||||
|
if (typeof params[field] === "string") {
|
||||||
|
params[field] = stripReasoningTagsFromText(params[field] as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cfg = options?.config ?? loadConfig();
|
const cfg = options?.config ?? loadConfig();
|
||||||
const action = readStringParam(params, "action", {
|
const action = readStringParam(params, "action", {
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user