From 350f956f2c5801f9dc7f0a40a3118194416e45c2 Mon Sep 17 00:00:00 2001 From: L36 Server Date: Tue, 13 Jan 2026 09:20:04 +1030 Subject: [PATCH 1/3] fix(minimax): strip tool invocation XML from assistant text --- CHANGELOG.md | 3 +- src/agents/pi-embedded-utils.test.ts | 162 +++++++++++++++++++++++++++ src/agents/pi-embedded-utils.ts | 21 +++- 3 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 src/agents/pi-embedded-utils.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c0e4f459..cfc082ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,9 @@ - Sandbox: support tool-policy groups in `tools.sandbox.tools` (e.g. `group:memory`, `group:fs`) to reduce config churn. ### Fixes +- Models/MiniMax: strip malformed tool invocation XML (`...` and ``) from assistant text to prevent tool call leaks into user messages. - Tools/Models: MiniMax vision now uses the Coding Plan VLM endpoint (`/v1/coding_plan/vlm`) so the `image` tool works with MiniMax keys (also accepts `@/path/to/file.png`-style inputs). -- Gateway/macOS: reduce noisy loopback WS “closed before connect” logs during tests. +- Gateway/macOS: reduce noisy loopback WS "closed before connect" logs during tests. - Auto-reply: resolve ambiguous `/model` fuzzy matches by picking the best candidate instead of erroring. ## 2026.1.12-1 diff --git a/src/agents/pi-embedded-utils.test.ts b/src/agents/pi-embedded-utils.test.ts new file mode 100644 index 000000000..c865d3f50 --- /dev/null +++ b/src/agents/pi-embedded-utils.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from "vitest"; +import { extractAssistantText } from "./pi-embedded-utils.js"; +import type { AssistantMessage } from "@mariozechner/pi-ai"; + +describe("extractAssistantText", () => { + it("strips Minimax tool invocation XML from text", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: ` +netstat -tlnp | grep 18789 + +`, + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe(""); + }); + + it("strips multiple tool invocations", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: `Let me check that. +/home/admin/test.txt + +`, + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("Let me check that."); + }); + + it("preserves normal text without tool invocations", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: "This is a normal response without any tool calls.", + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("This is a normal response without any tool calls."); + }); + + it("strips tool XML mixed with regular content", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: `I'll help you with that. +ls -la + +Here are the results.`, + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("I'll help you with that.\nHere are the results."); + }); + + it("handles multiple invoke blocks in one message", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: `First check. +file1.txt + +Second check. +pwd + +Done.`, + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("First check.\nSecond check.\nDone."); + }); + + it("handles stray closing tags without opening tags", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: "Some text here.More text.", + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("Some text here.More text."); + }); + + it("returns empty string when message is only tool invocations", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: ` +test + +`, + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe(""); + }); + + it("handles multiple text blocks", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: "First block.", + }, + { + type: "text", + text: ` +ls + +`, + }, + { + type: "text", + text: "Third block.", + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("First block.\nThird block."); + }); +}); diff --git a/src/agents/pi-embedded-utils.ts b/src/agents/pi-embedded-utils.ts index 40c3ec1ce..c3b2d9234 100644 --- a/src/agents/pi-embedded-utils.ts +++ b/src/agents/pi-embedded-utils.ts @@ -1,6 +1,25 @@ import type { AssistantMessage } from "@mariozechner/pi-ai"; import { formatToolDetail, resolveToolDisplay } from "./tool-display.js"; +/** + * Strip malformed Minimax tool invocations that leak into text content. + * Minimax sometimes embeds tool calls as XML in text blocks instead of + * proper structured tool calls. This removes: + * - ... blocks + * - closing tags + */ +function stripMinimaxToolCallXml(text: string): string { + if (!text) return text; + + // Remove ... blocks (non-greedy to handle multiple) + let cleaned = text.replace(/[\s\S]*?<\/invoke>/gi, ""); + + // Remove stray tags + cleaned = cleaned.replace(/<\/minimax:tool_call>/gi, ""); + + return cleaned.trim(); +} + export function extractAssistantText(msg: AssistantMessage): string { const isTextBlock = ( block: unknown, @@ -13,7 +32,7 @@ export function extractAssistantText(msg: AssistantMessage): string { const blocks = Array.isArray(msg.content) ? msg.content .filter(isTextBlock) - .map((c) => c.text.trim()) + .map((c) => stripMinimaxToolCallXml(c.text).trim()) .filter(Boolean) : []; return blocks.join("\n").trim(); From 1eb924739b184217e1dd7c25321f54c55ffe164b Mon Sep 17 00:00:00 2001 From: L36 Server Date: Tue, 13 Jan 2026 10:23:48 +1030 Subject: [PATCH 2/3] style: fix import order in pi-embedded-utils.test.ts --- src/agents/pi-embedded-utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agents/pi-embedded-utils.test.ts b/src/agents/pi-embedded-utils.test.ts index c865d3f50..9d9ce2cc7 100644 --- a/src/agents/pi-embedded-utils.test.ts +++ b/src/agents/pi-embedded-utils.test.ts @@ -1,6 +1,6 @@ +import type { AssistantMessage } from "@mariozechner/pi-ai"; import { describe, expect, it } from "vitest"; import { extractAssistantText } from "./pi-embedded-utils.js"; -import type { AssistantMessage } from "@mariozechner/pi-ai"; describe("extractAssistantText", () => { it("strips Minimax tool invocation XML from text", () => { From e2ea20f86255df6b4a8a28469232fa8c05f88b3a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 13 Jan 2026 00:36:39 +0000 Subject: [PATCH 3/3] fix: gate minimax XML stripping (#809) (thanks @latitudeki5223) --- CHANGELOG.md | 2 +- src/agents/pi-embedded-utils.test.ts | 34 ++++++++++++++++++++++++++++ src/agents/pi-embedded-utils.ts | 11 +++++---- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfc082ccf..5c0501029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ - Sandbox: support tool-policy groups in `tools.sandbox.tools` (e.g. `group:memory`, `group:fs`) to reduce config churn. ### Fixes -- Models/MiniMax: strip malformed tool invocation XML (`...` and ``) from assistant text to prevent tool call leaks into user messages. +- Models/MiniMax: strip malformed tool invocation XML (`...` and ``) from assistant text to prevent tool call leaks into user messages. (#809 — thanks @latitudeki5223) - Tools/Models: MiniMax vision now uses the Coding Plan VLM endpoint (`/v1/coding_plan/vlm`) so the `image` tool works with MiniMax keys (also accepts `@/path/to/file.png`-style inputs). - Gateway/macOS: reduce noisy loopback WS "closed before connect" logs during tests. - Auto-reply: resolve ambiguous `/model` fuzzy matches by picking the best candidate instead of erroring. diff --git a/src/agents/pi-embedded-utils.test.ts b/src/agents/pi-embedded-utils.test.ts index 9d9ce2cc7..2b32c1466 100644 --- a/src/agents/pi-embedded-utils.test.ts +++ b/src/agents/pi-embedded-utils.test.ts @@ -41,6 +41,24 @@ describe("extractAssistantText", () => { expect(result).toBe("Let me check that."); }); + it("keeps invoke snippets without Minimax markers", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: `Example:\n\nls\n`, + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe( + `Example:\n\nls\n`, + ); + }); + it("preserves normal text without tool invocations", () => { const msg: AssistantMessage = { role: "assistant", @@ -57,6 +75,22 @@ describe("extractAssistantText", () => { expect(result).toBe("This is a normal response without any tool calls."); }); + it("strips Minimax tool invocations with extra attributes", () => { + const msg: AssistantMessage = { + role: "assistant", + content: [ + { + type: "text", + text: `Before\nls\n\nAfter`, + }, + ], + timestamp: Date.now(), + }; + + const result = extractAssistantText(msg); + expect(result).toBe("Before\nAfter"); + }); + it("strips tool XML mixed with regular content", () => { const msg: AssistantMessage = { role: "assistant", diff --git a/src/agents/pi-embedded-utils.ts b/src/agents/pi-embedded-utils.ts index c3b2d9234..19e7afd62 100644 --- a/src/agents/pi-embedded-utils.ts +++ b/src/agents/pi-embedded-utils.ts @@ -10,14 +10,15 @@ import { formatToolDetail, resolveToolDisplay } from "./tool-display.js"; */ function stripMinimaxToolCallXml(text: string): string { if (!text) return text; + if (!/minimax:tool_call/i.test(text)) return text; - // Remove ... blocks (non-greedy to handle multiple) - let cleaned = text.replace(/[\s\S]*?<\/invoke>/gi, ""); + // Remove ... blocks (non-greedy to handle multiple). + let cleaned = text.replace(/]*>[\s\S]*?<\/invoke>/gi, ""); - // Remove stray tags - cleaned = cleaned.replace(/<\/minimax:tool_call>/gi, ""); + // Remove stray minimax tool tags. + cleaned = cleaned.replace(/<\/?minimax:tool_call>/gi, ""); - return cleaned.trim(); + return cleaned; } export function extractAssistantText(msg: AssistantMessage): string {