Skip to content

Commit 4a7393c

Browse files
committed
more tool call merging tests and cases
1 parent dac9009 commit 4a7393c

File tree

2 files changed

+144
-2
lines changed

2 files changed

+144
-2
lines changed

gui/src/util/toolCallState.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,129 @@ describe("addToolCallDeltaToState", () => {
5757
expect(result.toolCall.function.name).toBe("searchFiles");
5858
});
5959

60+
it("should handle name streaming in full progressive chunks", () => {
61+
// Test case where model streams the name progressively but includes full prefix each time
62+
// e.g. "readFi" -> "readFil" -> "readFile"
63+
const currentState: ToolCallState = {
64+
status: "generating",
65+
toolCall: {
66+
id: "call123",
67+
type: "function",
68+
function: {
69+
name: "readFi",
70+
arguments: "{}",
71+
},
72+
},
73+
toolCallId: "call123",
74+
parsedArgs: {},
75+
};
76+
77+
const delta: ToolCallDelta = {
78+
function: {
79+
name: "readFil",
80+
},
81+
};
82+
83+
const result = addToolCallDeltaToState(delta, currentState);
84+
expect(result.toolCall.function.name).toBe("readFil");
85+
86+
// Continue the streaming
87+
const nextDelta: ToolCallDelta = {
88+
function: {
89+
name: "readFile",
90+
},
91+
};
92+
93+
const finalResult = addToolCallDeltaToState(nextDelta, result);
94+
expect(finalResult.toolCall.function.name).toBe("readFile");
95+
});
96+
97+
it("should keep original name when receiving duplicate name chunks", () => {
98+
// Test case where model streams the complete name multiple times
99+
// e.g. "readFile" -> "readFile" -> "readFile"
100+
const currentState: ToolCallState = {
101+
status: "generating",
102+
toolCall: {
103+
id: "call123",
104+
type: "function",
105+
function: {
106+
name: "readFile",
107+
arguments: "{}",
108+
},
109+
},
110+
toolCallId: "call123",
111+
parsedArgs: {},
112+
};
113+
114+
const delta: ToolCallDelta = {
115+
function: {
116+
name: "readFile",
117+
},
118+
};
119+
120+
const result = addToolCallDeltaToState(delta, currentState);
121+
expect(result.toolCall.function.name).toBe("readFile");
122+
});
123+
124+
it("should handle partial name streaming", () => {
125+
// Test case where model streams the name in parts
126+
// e.g. "read" -> "File"
127+
const currentState: ToolCallState = {
128+
status: "generating",
129+
toolCall: {
130+
id: "call123",
131+
type: "function",
132+
function: {
133+
name: "read",
134+
arguments: "{}",
135+
},
136+
},
137+
toolCallId: "call123",
138+
parsedArgs: {},
139+
};
140+
141+
const delta: ToolCallDelta = {
142+
function: {
143+
name: "File",
144+
},
145+
};
146+
147+
const result = addToolCallDeltaToState(delta, currentState);
148+
expect(result.toolCall.function.name).toBe("readFile");
149+
});
150+
151+
it("should ignore new tool calls with different IDs", () => {
152+
const currentState: ToolCallState = {
153+
status: "generating",
154+
toolCall: {
155+
id: "call123",
156+
type: "function",
157+
function: {
158+
name: "searchFiles",
159+
arguments: '{"query":"test"}',
160+
},
161+
},
162+
toolCallId: "call123",
163+
parsedArgs: { query: "test" },
164+
};
165+
166+
const delta: ToolCallDelta = {
167+
id: "call456", // Different ID
168+
type: "function",
169+
function: {
170+
name: "readFile",
171+
arguments: '{"path":"file.txt"}',
172+
},
173+
};
174+
175+
const result = addToolCallDeltaToState(delta, currentState);
176+
177+
// Should keep the original state and ignore the new call
178+
expect(result).toBe(currentState);
179+
expect(result.toolCall.id).toBe("call123");
180+
expect(result.toolCall.function.name).toBe("searchFiles");
181+
});
182+
60183
it("should merge function argument deltas correctly", () => {
61184
const currentState: ToolCallState = {
62185
status: "generating",

gui/src/util/toolCallState.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ export function addToolCallDeltaToState(
88
toolCallDelta: ToolCallDelta,
99
currentState: ToolCallState | undefined,
1010
): ToolCallState {
11+
// This prevents multiple tool calls for now, by ignoring new tool call ids
12+
if (
13+
toolCallDelta.id &&
14+
currentState?.toolCallId &&
15+
toolCallDelta.id !== currentState?.toolCallId
16+
) {
17+
return currentState;
18+
}
19+
1120
const currentCall = currentState?.toolCall;
1221

1322
// These will/should not be partially streamed
@@ -21,8 +30,18 @@ export function addToolCallDeltaToState(
2130
const nameDelta = toolCallDelta.function?.name ?? "";
2231
const argsDelta = toolCallDelta.function?.arguments ?? "";
2332

24-
const mergedName =
25-
currentName === nameDelta ? currentName : currentName + nameDelta; // Some models may include the name repeatedly. This doesn't account for an edge case where the name is like "dothisdothis" and it happens to stream name in chunks "dothis" and "dothis" but that's a super edge case
33+
let mergedName = currentName;
34+
if (nameDelta.startsWith(currentName)) {
35+
// Case where model progresssively streams name but full name each time e.g. "readFi" -> "readFil" -> "readFile"
36+
mergedName = nameDelta;
37+
} else if (currentName.startsWith(nameDelta)) {
38+
// Case where model streams in full name each time e.g. readFile -> readFile -> readFile
39+
// do nothing
40+
} else {
41+
// Case where model streams in name in parts e.g. "read" -> "File"
42+
mergedName = currentName + nameDelta;
43+
}
44+
2645
const mergedArgs = currentArgs + argsDelta;
2746

2847
const [_, parsedArgs] = incrementalParseJson(mergedArgs || "{}");

0 commit comments

Comments
 (0)