diff --git a/src/agents/tools/message-tool.test.ts b/src/agents/tools/message-tool.test.ts index a289908ac1..5c974e001c 100644 --- a/src/agents/tools/message-tool.test.ts +++ b/src/agents/tools/message-tool.test.ts @@ -162,6 +162,80 @@ describe("message tool description", () => { }); }); +describe("message tool reasoning tag sanitization", () => { + it("strips 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: "internal reasoningHello!", + }); + + const call = mocks.runMessageAction.mock.calls[0]?.[0]; + expect(call?.params?.text).toBe("Hello!"); + }); + + it("strips 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: "reasoning hereReply 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", () => { it("forwards sandboxRoot to runMessageAction", async () => { mocks.runMessageAction.mockClear(); diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index 08309b2efe..44618cbede 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -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 { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.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"; @@ -405,7 +406,17 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool { err.name = "AbortError"; throw err; } - const params = args as Record; + // Shallow-copy so we don't mutate the original event args (used for logging/dedup). + const params = { ...(args as Record) }; + + // Strip reasoning tags from text fields — models may include + // 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 action = readStringParam(params, "action", { required: true,