Skip to content

Commit d7d8dce

Browse files
committed
fix: avoid XML false positives for <editor>/<editable> tags
1 parent 6495ef3 commit d7d8dce

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed

src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,54 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
242242
expect(toolResult.is_error).toBe(true)
243243
expect(toolResult.content).toContain("due to user rejecting a previous tool")
244244
})
245+
246+
it("should not treat <editor> as XML tool markup", async () => {
247+
mockTask.assistantMessageContent = [
248+
{
249+
type: "text",
250+
content: "Please open the <editor> panel and continue.",
251+
partial: false,
252+
},
253+
]
254+
255+
await presentAssistantMessage(mockTask)
256+
257+
expect(mockTask.say).toHaveBeenCalledWith(
258+
"text",
259+
"Please open the <editor> panel and continue.",
260+
undefined,
261+
false,
262+
)
263+
expect(mockTask.say).not.toHaveBeenCalledWith(
264+
"error",
265+
expect.stringContaining("XML tool calls are no longer supported"),
266+
)
267+
expect(mockTask.didAlreadyUseTool).toBe(false)
268+
expect(mockTask.consecutiveMistakeCount).toBe(0)
269+
})
270+
271+
it("should not treat <editable> as XML tool markup", async () => {
272+
mockTask.assistantMessageContent = [
273+
{
274+
type: "text",
275+
content: "Use an <editable> region in the docs example.",
276+
partial: false,
277+
},
278+
]
279+
280+
await presentAssistantMessage(mockTask)
281+
282+
expect(mockTask.say).toHaveBeenCalledWith(
283+
"text",
284+
"Use an <editable> region in the docs example.",
285+
undefined,
286+
false,
287+
)
288+
expect(mockTask.say).not.toHaveBeenCalledWith(
289+
"error",
290+
expect.stringContaining("XML tool calls are no longer supported"),
291+
)
292+
expect(mockTask.didAlreadyUseTool).toBe(false)
293+
expect(mockTask.consecutiveMistakeCount).toBe(0)
294+
})
245295
})

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,25 @@ function containsXmlToolMarkup(text: string): boolean {
10481048
// Avoid regex so we don't keep legacy XML parsing artifacts around.
10491049
// Note: This is a best-effort safeguard; tool_use blocks without an id are rejected elsewhere.
10501050

1051+
const isTagBoundary = (char: string | undefined): boolean => {
1052+
if (char === undefined) {
1053+
return true
1054+
}
1055+
return char === ">" || char === "/" || char === " " || char === "\n" || char === "\r" || char === "\t"
1056+
}
1057+
1058+
const hasTagReference = (haystack: string, prefix: string): boolean => {
1059+
let index = haystack.indexOf(prefix)
1060+
while (index !== -1) {
1061+
const nextChar = haystack[index + prefix.length]
1062+
if (isTagBoundary(nextChar)) {
1063+
return true
1064+
}
1065+
index = haystack.indexOf(prefix, index + 1)
1066+
}
1067+
return false
1068+
}
1069+
10511070
// First, strip out content inside markdown code fences to avoid false positives
10521071
// when users paste documentation or examples containing tool tag references.
10531072
// This handles both fenced code blocks (```) and inline code (`).
@@ -1085,5 +1104,5 @@ function containsXmlToolMarkup(text: string): boolean {
10851104
"write_to_file",
10861105
] as const
10871106

1088-
return toolNames.some((name) => lower.includes(`<${name}`) || lower.includes(`</${name}`))
1107+
return toolNames.some((name) => hasTagReference(lower, `<${name}`) || hasTagReference(lower, `</${name}`))
10891108
}

0 commit comments

Comments
 (0)