fix: sanitize assistant session text (#1456) (thanks @zerone0x)
This commit is contained in:
@@ -30,6 +30,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
|
- Message tool: keep path/filePath as-is for send; hydrate buffers only for sendAttachment. (#1444) Thanks @hopyky.
|
||||||
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
- Auto-reply: only report a model switch when session state is available. (#1465) Thanks @robbyczgw-cla.
|
||||||
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
||||||
|
- Agents: sanitize assistant history text to strip tool-call markers. (#1456) Thanks @zerone0x.
|
||||||
- Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.
|
- Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.
|
||||||
- Agents: surface concrete API error details instead of generic AI service errors.
|
- Agents: surface concrete API error details instead of generic AI service errors.
|
||||||
- Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484)
|
- Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484)
|
||||||
|
|||||||
34
src/agents/tools/sessions-helpers.test.ts
Normal file
34
src/agents/tools/sessions-helpers.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { extractAssistantText, sanitizeTextContent } from "./sessions-helpers.js";
|
||||||
|
|
||||||
|
describe("sanitizeTextContent", () => {
|
||||||
|
it("strips minimax tool call XML and downgraded markers", () => {
|
||||||
|
const input =
|
||||||
|
'Hello <invoke name="tool">payload</invoke></minimax:tool_call> ' +
|
||||||
|
"[Tool Call: foo (ID: 1)] world";
|
||||||
|
const result = sanitizeTextContent(input).trim();
|
||||||
|
expect(result).toBe("Hello world");
|
||||||
|
expect(result).not.toContain("invoke");
|
||||||
|
expect(result).not.toContain("Tool Call");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips thinking tags", () => {
|
||||||
|
const input = "Before <think>secret</think> after";
|
||||||
|
const result = sanitizeTextContent(input).trim();
|
||||||
|
expect(result).toBe("Before after");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractAssistantText", () => {
|
||||||
|
it("sanitizes blocks without injecting newlines", () => {
|
||||||
|
const message = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "Hi " },
|
||||||
|
{ type: "text", text: "<think>secret</think>there" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(extractAssistantText(message)).toBe("Hi there");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -111,9 +111,8 @@ export function stripToolMessages(messages: unknown[]): unknown[] {
|
|||||||
* This ensures user-facing text doesn't leak internal tool representations.
|
* This ensures user-facing text doesn't leak internal tool representations.
|
||||||
*/
|
*/
|
||||||
export function sanitizeTextContent(text: string): string {
|
export function sanitizeTextContent(text: string): string {
|
||||||
return stripThinkingTagsFromText(
|
if (!text) return text;
|
||||||
stripDowngradedToolCallText(stripMinimaxToolCallXml(text)),
|
return stripThinkingTagsFromText(stripDowngradedToolCallText(stripMinimaxToolCallXml(text)));
|
||||||
).trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractAssistantText(message: unknown): string | undefined {
|
export function extractAssistantText(message: unknown): string | undefined {
|
||||||
@@ -128,11 +127,11 @@ export function extractAssistantText(message: unknown): string | undefined {
|
|||||||
const text = (block as { text?: unknown }).text;
|
const text = (block as { text?: unknown }).text;
|
||||||
if (typeof text === "string") {
|
if (typeof text === "string") {
|
||||||
const sanitized = sanitizeTextContent(text);
|
const sanitized = sanitizeTextContent(text);
|
||||||
if (sanitized) {
|
if (sanitized.trim()) {
|
||||||
chunks.push(sanitized);
|
chunks.push(sanitized);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const joined = chunks.join("\n").trim();
|
const joined = chunks.join("").trim();
|
||||||
return joined ? sanitizeUserFacingText(joined) : undefined;
|
return joined ? sanitizeUserFacingText(joined) : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/auto-reply/reply/commands-subagents.test.ts
Normal file
23
src/auto-reply/reply/commands-subagents.test.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { extractMessageText } from "./commands-subagents.js";
|
||||||
|
|
||||||
|
describe("extractMessageText", () => {
|
||||||
|
it("preserves user text that looks like tool call markers", () => {
|
||||||
|
const message = {
|
||||||
|
role: "user",
|
||||||
|
content: "Here [Tool Call: foo (ID: 1)] ok",
|
||||||
|
};
|
||||||
|
const result = extractMessageText(message);
|
||||||
|
expect(result?.text).toContain("[Tool Call: foo (ID: 1)]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sanitizes assistant tool call markers", () => {
|
||||||
|
const message = {
|
||||||
|
role: "assistant",
|
||||||
|
content: "Here [Tool Call: foo (ID: 1)] ok",
|
||||||
|
};
|
||||||
|
const result = extractMessageText(message);
|
||||||
|
expect(result?.text).toBe("Here ok");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -107,12 +107,14 @@ function normalizeMessageText(text: string) {
|
|||||||
return text.replace(/\s+/g, " ").trim();
|
return text.replace(/\s+/g, " ").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractMessageText(message: ChatMessage): { role: string; text: string } | null {
|
export function extractMessageText(message: ChatMessage): { role: string; text: string } | null {
|
||||||
const role = typeof message.role === "string" ? message.role : "";
|
const role = typeof message.role === "string" ? message.role : "";
|
||||||
|
const shouldSanitize = role === "assistant";
|
||||||
const content = message.content;
|
const content = message.content;
|
||||||
if (typeof content === "string") {
|
if (typeof content === "string") {
|
||||||
const sanitized = sanitizeTextContent(content);
|
const normalized = normalizeMessageText(
|
||||||
const normalized = normalizeMessageText(sanitized);
|
shouldSanitize ? sanitizeTextContent(content) : content,
|
||||||
|
);
|
||||||
return normalized ? { role, text: normalized } : null;
|
return normalized ? { role, text: normalized } : null;
|
||||||
}
|
}
|
||||||
if (!Array.isArray(content)) return null;
|
if (!Array.isArray(content)) return null;
|
||||||
@@ -122,9 +124,9 @@ function extractMessageText(message: ChatMessage): { role: string; text: string
|
|||||||
if ((block as { type?: unknown }).type !== "text") continue;
|
if ((block as { type?: unknown }).type !== "text") continue;
|
||||||
const text = (block as { text?: unknown }).text;
|
const text = (block as { text?: unknown }).text;
|
||||||
if (typeof text === "string") {
|
if (typeof text === "string") {
|
||||||
const sanitized = sanitizeTextContent(text);
|
const value = shouldSanitize ? sanitizeTextContent(text) : text;
|
||||||
if (sanitized) {
|
if (value.trim()) {
|
||||||
chunks.push(sanitized);
|
chunks.push(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user