Skip to content

Commit d6bbb08

Browse files
committed
Fix the V2 version as well
1 parent b743231 commit d6bbb08

File tree

3 files changed

+60
-25
lines changed

3 files changed

+60
-25
lines changed

src/core/assistant-message/__tests__/parseAssistantMessage.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,8 @@ const isEmptyTextContent = (block: AssistantMessageContent) =>
361361
const invocation = ["<read_file>", "<path>demo.txt</path>", "</read_file>"].join("\n")
362362
const result = parser(invocation)
363363
expect(result).toHaveLength(1)
364-
const tool = result[0]
365-
if (tool.type !== "tool_use") throw new Error("Expected tool_use block")
364+
const tool = result[0] as ToolUse
365+
expect(tool.type).toBe("tool_use")
366366
expect(tool.name).toBe("read_file")
367367
expect(tool.params.path).toBe("demo.txt")
368368
})

src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,13 @@ const runBenchmark = () => {
8181
const namePadding = maxNameLength + 2
8282

8383
console.log(
84-
`| ${"Test Case".padEnd(namePadding)} | V1 Time (ms) | V2 Time (ms) | V1/V2 Ratio | V1 Heap (bytes) | V2 Heap (bytes) |`,
85-
)
86-
console.log(
87-
`| ${"-".repeat(namePadding)} | ------------ | ------------ | ----------- | ---------------- | ---------------- |`,
84+
`| ${"Test Case".padEnd(namePadding)} | V1 Time (ms) | V2 Time (ms) | V1 Heap (bytes) | V2 Heap (bytes) |`,
8885
)
86+
console.log(`| ${"-".repeat(namePadding)} | ------------ | ------------ | ---------------- | ---------------- |`)
8987

9088
for (const testCase of testCases) {
9189
const v1Time = measureExecutionTime(parseAssistantMessageV1, testCase.input)
9290
const v2Time = measureExecutionTime(parseAssistantMessageV2, testCase.input)
93-
const timeRatio = v1Time / v2Time
9491

9592
const v1Memory = measureMemoryUsage(parseAssistantMessageV1, testCase.input)
9693
const v2Memory = measureMemoryUsage(parseAssistantMessageV2, testCase.input)
@@ -99,7 +96,6 @@ const runBenchmark = () => {
9996
`| ${testCase.name.padEnd(namePadding)} | ` +
10097
`${v1Time.toFixed(4).padStart(12)} | ` +
10198
`${v2Time.toFixed(4).padStart(12)} | ` +
102-
`${timeRatio.toFixed(2).padStart(11)} | ` +
10399
`${formatNumber(Math.round(v1Memory.heapUsed)).padStart(16)} | ` +
104100
`${formatNumber(Math.round(v2Memory.heapUsed)).padStart(16)} |`,
105101
)

src/core/assistant-message/parseAssistantMessageV2.ts

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess
4646
let currentParamValueStart = 0 // Index *after* the opening tag of the current param.
4747
let currentParamName: ToolParamName | undefined = undefined
4848

49+
// Track whether we are inside markdown code blocks or inline code to avoid treating textual mentions
50+
// of tool tags (e.g. <read_file>) as actual tool invocations.
51+
let insideCodeBlock = false // ``` fenced code block
52+
let insideInlineCode = false // `inline code`
53+
54+
// Helper to decide if we should parse for tool-related tags at the current position
55+
const shouldParseToolTags = () => !insideCodeBlock && !insideInlineCode
56+
4957
// Precompute tags for faster lookups.
5058
const toolUseOpenTags = new Map<string, ToolName>()
5159
const toolParamOpenTags = new Map<string, ToolParamName>()
@@ -63,16 +71,39 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess
6371
for (let i = 0; i < len; i++) {
6472
const currentCharIndex = i
6573

74+
// Detect fenced code block (```).
75+
if (!insideInlineCode && i + 2 < len && assistantMessage.slice(i, i + 3) === "```") {
76+
insideCodeBlock = !insideCodeBlock
77+
i += 2 // Skip the two extra backticks.
78+
continue
79+
}
80+
81+
// Detect inline code (`) when not inside a fenced code block and not
82+
// part of triple backticks.
83+
if (!insideCodeBlock && assistantMessage[i] === "`") {
84+
insideInlineCode = !insideInlineCode
85+
}
86+
87+
// If we are in any kind of code context, treat everything as plain text.
88+
if (!shouldParseToolTags()) {
89+
// If we're not already tracking text content, start now.
90+
if (!currentTextContent) {
91+
currentTextContentStart = currentCharIndex
92+
currentTextContent = { type: "text", content: "", partial: true }
93+
}
94+
95+
continue
96+
}
97+
6698
// Parsing a tool parameter
6799
if (currentToolUse && currentParamName) {
68100
const closeTag = `</${currentParamName}>`
69-
// Check if the string *ending* at index `i` matches the closing tag
101+
102+
// Check if the string *ending* at index `i` matches the closing tag.
70103
if (
71104
currentCharIndex >= closeTag.length - 1 &&
72-
assistantMessage.startsWith(
73-
closeTag,
74-
currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag.
75-
)
105+
// Start checking from potential start of tag.
106+
assistantMessage.startsWith(closeTag, currentCharIndex - closeTag.length + 1)
76107
) {
77108
// Found the closing tag for the parameter.
78109
const value = assistantMessage
@@ -175,6 +206,23 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess
175206
currentCharIndex >= tag.length - 1 &&
176207
assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)
177208
) {
209+
// Check that this is likely an actual tool invocation and not
210+
// an inline textual reference.
211+
// We consider it an invocation only if the next non-whitespace
212+
// character is a newline (\n or \r) or an opening angle bracket
213+
// '<' (which would start the first parameter tag).
214+
let j = currentCharIndex + 1 // Position after the closing '>' of the opening tag.
215+
216+
while (j < len && assistantMessage[j] === " ") {
217+
j++
218+
}
219+
220+
const nextChar = assistantMessage[j] ?? ""
221+
222+
if (nextChar && nextChar !== "<" && nextChar !== "\n" && nextChar !== "\r") {
223+
// Treat as plain text, not a tool invocation.
224+
continue
225+
}
178226
// End current text block if one was active.
179227
if (currentTextContent) {
180228
currentTextContent.content = assistantMessage
@@ -201,22 +249,13 @@ export function parseAssistantMessageV2(assistantMessage: string): AssistantMess
201249
.trim()
202250

203251
if (potentialText.length > 0) {
204-
contentBlocks.push({
205-
type: "text",
206-
content: potentialText,
207-
partial: false,
208-
})
252+
contentBlocks.push({ type: "text", content: potentialText, partial: false })
209253
}
210254
}
211255

212256
// Start the new tool use.
213-
currentToolUse = {
214-
type: "tool_use",
215-
name: toolName,
216-
params: {},
217-
partial: true, // Assume partial until closing tag is found.
218-
}
219-
257+
// Assume partial until closing tag is found.
258+
currentToolUse = { type: "tool_use", name: toolName, params: {}, partial: true }
220259
currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag.
221260
startedNewTool = true
222261

0 commit comments

Comments
 (0)