Skip to content

Commit e90bc0a

Browse files
fix(core): prevent tool call chunks from merging incorrectly in AIMes… (#8433)
1 parent 76400ae commit e90bc0a

File tree

3 files changed

+82
-6
lines changed

3 files changed

+82
-6
lines changed

.changeset/honest-vans-live.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@langchain/core": patch
3+
---
4+
5+
fix(core): prevent tool call chunks from merging incorrectly in AIMes…

langchain-core/src/messages/base.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -418,8 +418,12 @@ export function _mergeDicts(
418418
if (key === "type") {
419419
// Do not merge 'type' fields
420420
continue;
421+
} else if (["id", "output_version", "model_provider"].includes(key)) {
422+
// Keep the incoming value for these fields
423+
merged[key] = value;
424+
} else {
425+
merged[key] += value;
421426
}
422-
merged[key] += value;
423427
} else if (typeof merged[key] === "object" && !Array.isArray(merged[key])) {
424428
merged[key] = _mergeDicts(merged[key], value);
425429
} else if (Array.isArray(merged[key])) {
@@ -435,8 +439,10 @@ export function _mergeDicts(
435439
return merged;
436440
}
437441

438-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
439-
export function _mergeLists(left?: any[], right?: any[]) {
442+
export function _mergeLists<Content extends MessageContentComplex>(
443+
left?: Content[],
444+
right?: Content[]
445+
): Content[] | undefined {
440446
if (left === undefined && right === undefined) {
441447
return undefined;
442448
} else if (left === undefined || right === undefined) {
@@ -446,19 +452,36 @@ export function _mergeLists(left?: any[], right?: any[]) {
446452
for (const item of right) {
447453
if (
448454
typeof item === "object" &&
455+
item !== null &&
449456
"index" in item &&
450457
typeof item.index === "number"
451458
) {
452459
const toMerge = merged.findIndex(
453-
(leftItem) => leftItem.index === item.index
460+
(leftItem) =>
461+
leftItem !== null &&
462+
typeof leftItem === "object" &&
463+
"index" in leftItem &&
464+
leftItem.index === item.index &&
465+
// Only merge if IDs match (or both are undefined)
466+
("id" in leftItem && "id" in item
467+
? leftItem.id === item.id
468+
: !("id" in leftItem) && !("id" in item))
454469
);
455-
if (toMerge !== -1) {
456-
merged[toMerge] = _mergeDicts(merged[toMerge], item);
470+
if (
471+
toMerge !== -1 &&
472+
typeof merged[toMerge] === "object" &&
473+
merged[toMerge] !== null
474+
) {
475+
merged[toMerge] = _mergeDicts(
476+
merged[toMerge] as Record<string, unknown>,
477+
item as Record<string, unknown>
478+
) as Content;
457479
} else {
458480
merged.push(item);
459481
}
460482
} else if (
461483
typeof item === "object" &&
484+
item !== null &&
462485
"text" in item &&
463486
item.text === ""
464487
) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it, expect } from "@jest/globals";
2+
import { AIMessageChunk } from "../ai.js";
3+
4+
describe("AIMessageChunk", () => {
5+
it("should properly merge tool call chunks", () => {
6+
const chunk1 = new AIMessageChunk({
7+
content: "",
8+
tool_call_chunks: [
9+
{
10+
name: "add_new_task",
11+
args: '{"tasks":["buy tomatoes","help child with math"]}',
12+
type: "tool_call_chunk",
13+
index: 0,
14+
id: "9fb5c937-6944-4173-84be-ad1caee1cedd",
15+
},
16+
],
17+
});
18+
const chunk2 = new AIMessageChunk({
19+
content: "",
20+
tool_call_chunks: [
21+
{
22+
name: "add_ideas",
23+
args: '{"ideas":["read about Angular 19 updates"]}',
24+
type: "tool_call_chunk",
25+
index: 0,
26+
id: "5abf542e-87f3-4899-87c6-8f7d9cb6a28d",
27+
},
28+
],
29+
});
30+
31+
const merged = chunk1.concat(chunk2);
32+
expect(merged.tool_call_chunks).toHaveLength(2);
33+
34+
const firstCall = merged.tool_call_chunks?.[0];
35+
expect(firstCall?.name).toBe("add_new_task");
36+
expect(firstCall?.args).toBe(
37+
'{"tasks":["buy tomatoes","help child with math"]}'
38+
);
39+
expect(firstCall?.id).toBe("9fb5c937-6944-4173-84be-ad1caee1cedd");
40+
41+
const secondCall = merged.tool_call_chunks?.[1];
42+
expect(secondCall?.name).toBe("add_ideas");
43+
expect(secondCall?.args).toBe(
44+
'{"ideas":["read about Angular 19 updates"]}'
45+
);
46+
expect(secondCall?.id).toBe("5abf542e-87f3-4899-87c6-8f7d9cb6a28d");
47+
});
48+
});

0 commit comments

Comments
 (0)