Skip to content

Commit 7b7c6f0

Browse files
committed
fix: handle primitives error in StreamingToolCallProcessor
1 parent 6ede6b9 commit 7b7c6f0

File tree

2 files changed

+104
-15
lines changed

2 files changed

+104
-15
lines changed

src/core/task/__tests__/tool-call-helper.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,64 @@ describe("handleOpenaiToolCallStreaming", () => {
310310
expect(xml).toContain("<echo>")
311311
expect(xml).toContain("<msg>hi</msg>")
312312
})
313+
314+
it("should delegate to processor.processChunk apply_diff", () => {
315+
const processor = new StreamingToolCallProcessor()
316+
const chunk = [
317+
{
318+
index: 0,
319+
id: "1",
320+
function: {
321+
name: "apply_diff",
322+
arguments:
323+
'{"args":{"file":[{"diff":[{"replace":"catch (Exception e) {if (true) {}throw e;}","search":"catch (Exception e) {throw e;}","start_line":252}],"path":"Test.java"}]}}',
324+
},
325+
},
326+
]
327+
const xml = handleOpenaiToolCallStreaming(processor, chunk, "openai").chunkContent
328+
expect(xml).toContain("<search>")
329+
})
330+
331+
it("should delegate to processor.processChunk apply_diff2", () => {
332+
const processor = new StreamingToolCallProcessor()
333+
const chunk1 = [
334+
{
335+
index: 0,
336+
id: "1",
337+
function: {
338+
name: "apply_diff",
339+
arguments:
340+
'{"args":{"file":[{"diff":[{"replace":"catch (Exception e) {if (1==1) {}throw e;}","test":tr',
341+
},
342+
},
343+
]
344+
const chunk2 = [
345+
{
346+
index: 0,
347+
id: "",
348+
function: {
349+
name: "",
350+
arguments: 'ue,"search":"catch (Exception e) {throw e;}","start_line":25',
351+
},
352+
},
353+
]
354+
const chunk3 = [
355+
{
356+
index: 0,
357+
id: "",
358+
function: {
359+
name: "",
360+
arguments: '2}],"path":"Test.java"}]}}',
361+
},
362+
},
363+
]
364+
let xml = handleOpenaiToolCallStreaming(processor, chunk1, "openai").chunkContent
365+
expect(xml).not.toContain("<search>")
366+
expect(xml).not.toContain("true")
367+
xml += handleOpenaiToolCallStreaming(processor, chunk2, "openai").chunkContent
368+
expect(xml).toContain("<search>")
369+
expect(xml).toContain("true")
370+
xml += handleOpenaiToolCallStreaming(processor, chunk3, "openai").chunkContent
371+
expect(xml).toContain("252")
372+
})
313373
})

src/core/task/tool-call-helper.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ToolCallProviderType } from "../../shared/tools"
1212
* Defines the possible states of the JSON parser.
1313
*/
1414
enum ParserState {
15+
EXPECT_ROOT, // Expecting root object or array
1516
EXPECT_VALUE,
1617
EXPECT_KEY,
1718
EXPECT_COLON,
@@ -41,7 +42,7 @@ class ToolCallProcessingState {
4142
cursor = 0
4243

4344
// The current state of the parser FSM (Finite State Machine).
44-
parserState = ParserState.EXPECT_VALUE
45+
parserState = ParserState.EXPECT_ROOT
4546

4647
// Flags for handling string parsing.
4748
inString = false
@@ -54,6 +55,8 @@ class ToolCallProcessingState {
5455
xmlTagStack: string[] = []
5556
// Buffer for the current string literal (key or value) being parsed.
5657
currentString = ""
58+
// Buffer for accumulating primitive values across chunks
59+
primitiveBuffer = ""
5760
// Flag to track if we're at the start of an array to prevent duplicate tags.
5861
justOpenedArray = false
5962
}
@@ -315,24 +318,46 @@ export class StreamingToolCallProcessor {
315318
continue
316319
}
317320

318-
// Handle primitives (numbers, booleans, null)
321+
// Handle primitives - accumulate characters until we hit a delimiter
319322
if (state.parserState === ParserState.EXPECT_VALUE) {
320-
const primitiveMatch = args.substring(state.cursor).match(/^-?\d+(\.\d+)?|true|false|null/)
321-
if (primitiveMatch) {
322-
const value = primitiveMatch[0]
323-
const tag = state.xmlTagStack.pop()!
324-
if (tag) {
325-
xml += `${value}${this.onCloseTag(tag, toolName)}`
326-
}
327-
state.parserState = ParserState.EXPECT_COMMA_OR_CLOSING
328-
state.cursor += value.length
323+
// Check if this character could be part of a primitive value
324+
if (
325+
(char >= "0" && char <= "9") ||
326+
char === "-" ||
327+
char === "." ||
328+
(char >= "a" && char <= "z") ||
329+
(char >= "A" && char <= "Z")
330+
) {
331+
// Accumulate the character
332+
state.primitiveBuffer += char
333+
state.cursor++
329334
continue
335+
} else if (state.primitiveBuffer.length > 0) {
336+
// We've hit a delimiter, check if we have a complete primitive
337+
const value = state.primitiveBuffer.trim()
338+
if (value === "true" || value === "false" || value === "null" || /^-?\d+(\.\d+)?$/.test(value)) {
339+
// We have a valid primitive
340+
const tag = state.xmlTagStack.pop()!
341+
if (tag) {
342+
xml += `${value}${this.onCloseTag(tag, toolName)}`
343+
}
344+
state.parserState = ParserState.EXPECT_COMMA_OR_CLOSING
345+
state.primitiveBuffer = ""
346+
// Don't increment cursor - let the delimiter be processed in the switch
347+
continue
348+
} else {
349+
// Invalid primitive, reset buffer and continue
350+
state.primitiveBuffer = ""
351+
}
330352
}
331353
}
332354

333355
switch (char) {
334356
case "{":
335-
if (state.parserState === ParserState.EXPECT_VALUE) {
357+
if (
358+
state.parserState === ParserState.EXPECT_VALUE ||
359+
state.parserState === ParserState.EXPECT_ROOT
360+
) {
336361
const parent = state.bracketStack[state.bracketStack.length - 1]
337362
if (parent === "[") {
338363
// For an object inside an array, we might need to add the repeating tag.
@@ -381,7 +406,10 @@ export class StreamingToolCallProcessor {
381406
}
382407
break
383408
case "[":
384-
if (state.parserState === ParserState.EXPECT_VALUE) {
409+
if (
410+
state.parserState === ParserState.EXPECT_VALUE ||
411+
state.parserState === ParserState.EXPECT_ROOT
412+
) {
385413
state.bracketStack.push("[")
386414
state.parserState = ParserState.EXPECT_VALUE // An array contains values
387415
state.justOpenedArray = true
@@ -401,11 +429,12 @@ export class StreamingToolCallProcessor {
401429
if (state.parserState === ParserState.EXPECT_VALUE) {
402430
// We've encountered the start of a string that is a JSON value.
403431
state.isStreamingStringValue = true
404-
} else {
432+
state.inString = true
433+
} else if (state.parserState === ParserState.EXPECT_KEY) {
405434
// This is the start of a string that is a JSON key.
406435
state.isStreamingStringValue = false
436+
state.inString = true
407437
}
408-
state.inString = true
409438
break
410439
case ":":
411440
if (state.parserState === ParserState.EXPECT_COLON) {

0 commit comments

Comments
 (0)