fix(agents): strip leaked tool call text from assistant messages
When replaying conversation history to Gemini, tool calls without thought_signature are downgraded to text blocks like [Tool Call: ...]. This leaked internal technical info into user-facing chat messages. Added stripDowngradedToolCallText filter alongside existing Minimax filter to remove these text representations before extraction.
This commit is contained in:
@@ -257,4 +257,105 @@ describe("extractAssistantText", () => {
|
|||||||
const result = extractAssistantText(msg);
|
const result = extractAssistantText(msg);
|
||||||
expect(result).toBe("First block.\nThird block.");
|
expect(result).toBe("First block.\nThird block.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("strips downgraded Gemini tool call text representations", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[Tool Call: exec (ID: toolu_vrtx_014w1P6B6w4V92v4VzG7Qk12)]
|
||||||
|
Arguments: { "command": "git status", "timeout": 120000 }`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips multiple downgraded tool calls", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[Tool Call: read (ID: toolu_1)]
|
||||||
|
Arguments: { "path": "/some/file.txt" }
|
||||||
|
[Tool Call: exec (ID: toolu_2)]
|
||||||
|
Arguments: { "command": "ls -la" }`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips tool results for downgraded calls", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[Tool Result for ID toolu_123]
|
||||||
|
{"status": "ok", "data": "some result"}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves text around downgraded tool calls", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Let me check that for you.
|
||||||
|
[Tool Call: browser (ID: toolu_abc)]
|
||||||
|
Arguments: { "action": "act", "request": "click button" }`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("Let me check that for you.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles multiple text blocks with tool calls and results", () => {
|
||||||
|
const msg: AssistantMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Here's what I found:",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[Tool Call: read (ID: toolu_1)]
|
||||||
|
Arguments: { "path": "/test.txt" }`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `[Tool Result for ID toolu_1]
|
||||||
|
File contents here`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "Done checking.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = extractAssistantText(msg);
|
||||||
|
expect(result).toBe("Here's what I found:\nDone checking.");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,32 @@ function stripMinimaxToolCallXml(text: string): string {
|
|||||||
return cleaned;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip downgraded tool call text representations that leak into text content.
|
||||||
|
* When replaying history to Gemini, tool calls without `thought_signature` are
|
||||||
|
* downgraded to text blocks like `[Tool Call: name (ID: ...)]`. These should
|
||||||
|
* not be shown to users.
|
||||||
|
*/
|
||||||
|
function stripDowngradedToolCallText(text: string): string {
|
||||||
|
if (!text) return text;
|
||||||
|
if (!/\[Tool (?:Call|Result)/i.test(text)) return text;
|
||||||
|
|
||||||
|
// Remove [Tool Call: name (ID: ...)] blocks and their Arguments.
|
||||||
|
// Match until the next [Tool marker or end of string.
|
||||||
|
let cleaned = text.replace(
|
||||||
|
/\[Tool Call:[^\]]*\]\n?(?:Arguments:[\s\S]*?)?(?=\n*\[Tool |\n*$)/gi,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove [Tool Result for ID ...] blocks and their content.
|
||||||
|
cleaned = cleaned.replace(
|
||||||
|
/\[Tool Result for ID[^\]]*\]\n?[\s\S]*?(?=\n*\[Tool |\n*$)/gi,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
return cleaned.trim();
|
||||||
|
}
|
||||||
|
|
||||||
export function extractAssistantText(msg: AssistantMessage): string {
|
export function extractAssistantText(msg: AssistantMessage): string {
|
||||||
const isTextBlock = (block: unknown): block is { type: "text"; text: string } => {
|
const isTextBlock = (block: unknown): block is { type: "text"; text: string } => {
|
||||||
if (!block || typeof block !== "object") return false;
|
if (!block || typeof block !== "object") return false;
|
||||||
@@ -31,7 +57,9 @@ export function extractAssistantText(msg: AssistantMessage): string {
|
|||||||
const blocks = Array.isArray(msg.content)
|
const blocks = Array.isArray(msg.content)
|
||||||
? msg.content
|
? msg.content
|
||||||
.filter(isTextBlock)
|
.filter(isTextBlock)
|
||||||
.map((c) => stripMinimaxToolCallXml(c.text).trim())
|
.map((c) =>
|
||||||
|
stripDowngradedToolCallText(stripMinimaxToolCallXml(c.text)).trim(),
|
||||||
|
)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
return blocks.join("\n").trim();
|
return blocks.join("\n").trim();
|
||||||
|
|||||||
Reference in New Issue
Block a user