mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: prevent "Attempting to finalize unknown tool call" warning for unstarted tool calls
- Add hasStarted check in processFinishReason() to only emit tool_call_end events for tool calls that have been properly started (received a name) - Add diagnostic warning when skipping unstarted tool calls - Add comprehensive tests for the hasStarted behavior with started, unstarted, and mixed scenarios - Fixes issue #11071 where GLM models send tool call IDs without names, causing synchronization issues between rawChunkTracker and streamingToolCalls This prevents the warning "Attempting to finalize unknown tool call" that appears when GLM models send incomplete tool call data (ID without name).
This commit is contained in:
parent
fd46e31134
commit
b92cd76e6d
2 changed files with 133 additions and 4 deletions
|
|
@ -166,16 +166,27 @@ export class NativeToolCallParser {
|
|||
/**
|
||||
* Process stream finish reason.
|
||||
* Emits end events when finish_reason is 'tool_calls'.
|
||||
* Only emits events for tool calls that have been properly started (have a name).
|
||||
*/
|
||||
public static processFinishReason(finishReason: string | null | undefined): ToolCallStreamEvent[] {
|
||||
const events: ToolCallStreamEvent[] = []
|
||||
|
||||
if (finishReason === "tool_calls" && this.rawChunkTracker.size > 0) {
|
||||
for (const [, tracked] of this.rawChunkTracker.entries()) {
|
||||
events.push({
|
||||
type: "tool_call_end",
|
||||
id: tracked.id,
|
||||
})
|
||||
// Only emit end event if the tool call was properly started
|
||||
// (i.e., we received a name and emitted tool_call_start)
|
||||
if (tracked.hasStarted) {
|
||||
events.push({
|
||||
type: "tool_call_end",
|
||||
id: tracked.id,
|
||||
})
|
||||
} else {
|
||||
// Log diagnostic warning for unstarted tool calls
|
||||
console.warn(
|
||||
`[NativeToolCallParser] Skipping tool_call_end for unstarted tool call: ${tracked.id} ` +
|
||||
`(no name received, hasStarted=false)`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -343,4 +343,122 @@ describe("NativeToolCallParser", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("processFinishReason", () => {
|
||||
describe("hasStarted check", () => {
|
||||
it("should emit tool_call_end for started tool calls", () => {
|
||||
// Simulate a tool call that has been properly started
|
||||
NativeToolCallParser.processRawChunk({
|
||||
index: 0,
|
||||
id: "call_started",
|
||||
name: "read_file",
|
||||
arguments: '{"path":"test.ts"}',
|
||||
})
|
||||
|
||||
const events = NativeToolCallParser.processFinishReason("tool_calls")
|
||||
|
||||
expect(events).toHaveLength(1)
|
||||
expect(events[0]).toEqual({
|
||||
type: "tool_call_end",
|
||||
id: "call_started",
|
||||
})
|
||||
})
|
||||
|
||||
it("should NOT emit tool_call_end for unstarted tool calls", () => {
|
||||
// Simulate a tool call that received an ID but no name (not started)
|
||||
NativeToolCallParser.processRawChunk({
|
||||
index: 0,
|
||||
id: "call_unstarted",
|
||||
// No name provided - tool call is tracked but not started
|
||||
})
|
||||
|
||||
const events = NativeToolCallParser.processFinishReason("tool_calls")
|
||||
|
||||
// Should not emit any events because the tool call wasn't started
|
||||
expect(events).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle mixed started and unstarted tool calls", () => {
|
||||
// First tool call: properly started
|
||||
NativeToolCallParser.processRawChunk({
|
||||
index: 0,
|
||||
id: "call_started_1",
|
||||
name: "read_file",
|
||||
arguments: '{"path":"test1.ts"}',
|
||||
})
|
||||
|
||||
// Second tool call: tracked but not started (no name)
|
||||
NativeToolCallParser.processRawChunk({
|
||||
index: 1,
|
||||
id: "call_unstarted",
|
||||
// No name - won't be started
|
||||
})
|
||||
|
||||
// Third tool call: properly started
|
||||
NativeToolCallParser.processRawChunk({
|
||||
index: 2,
|
||||
id: "call_started_2",
|
||||
name: "write_to_file",
|
||||
arguments: '{"path":"output.ts"}',
|
||||
})
|
||||
|
||||
const events = NativeToolCallParser.processFinishReason("tool_calls")
|
||||
|
||||
// Should only emit end events for the two started tool calls
|
||||
expect(events).toHaveLength(2)
|
||||
expect(events[0]).toEqual({
|
||||
type: "tool_call_end",
|
||||
id: "call_started_1",
|
||||
})
|
||||
expect(events[1]).toEqual({
|
||||
type: "tool_call_end",
|
||||
id: "call_started_2",
|
||||
})
|
||||
})
|
||||
|
||||
it("should not emit events when finish_reason is not tool_calls", () => {
|
||||
// Set up a started tool call
|
||||
NativeToolCallParser.processRawChunk({
|
||||
index: 0,
|
||||
id: "call_started",
|
||||
name: "read_file",
|
||||
arguments: '{"path":"test.ts"}',
|
||||
})
|
||||
|
||||
// Process with different finish reason
|
||||
const events = NativeToolCallParser.processFinishReason("stop")
|
||||
|
||||
expect(events).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle tool call that receives name in a separate chunk", () => {
|
||||
// First chunk: ID only
|
||||
NativeToolCallParser.processRawChunk({
|
||||
index: 0,
|
||||
id: "call_delayed_name",
|
||||
})
|
||||
|
||||
// At this point, tool call is tracked but not started
|
||||
let events = NativeToolCallParser.processFinishReason("tool_calls")
|
||||
expect(events).toHaveLength(0)
|
||||
|
||||
// Clear state and try again with name
|
||||
NativeToolCallParser.clearRawChunkState()
|
||||
|
||||
// Simulate proper sequence with name
|
||||
NativeToolCallParser.processRawChunk({
|
||||
index: 0,
|
||||
id: "call_delayed_name",
|
||||
name: "read_file",
|
||||
})
|
||||
|
||||
events = NativeToolCallParser.processFinishReason("tool_calls")
|
||||
expect(events).toHaveLength(1)
|
||||
expect(events[0]).toEqual({
|
||||
type: "tool_call_end",
|
||||
id: "call_delayed_name",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue