diff --git a/libs/core/langchain_core/utils/_merge.py b/libs/core/langchain_core/utils/_merge.py index 251f9260b2d..ba7df2043cb 100644 --- a/libs/core/langchain_core/utils/_merge.py +++ b/libs/core/langchain_core/utils/_merge.py @@ -117,7 +117,15 @@ def merge_lists(left: list | None, *others: list | None) -> list | None: to_merge = [ i for i, e_left in enumerate(merged) - if "index" in e_left and e_left["index"] == e["index"] + if ( + "index" in e_left + and e_left["index"] == e["index"] # index matches + and ( # IDs not inconsistent + e_left.get("id") is None + or e.get("id") is None + or e_left["id"] == e["id"] + ) + ) ] if to_merge: # TODO: Remove this once merge_dict is updated with special diff --git a/libs/core/tests/unit_tests/test_messages.py b/libs/core/tests/unit_tests/test_messages.py index c5a6b157286..1955cef093b 100644 --- a/libs/core/tests/unit_tests/test_messages.py +++ b/libs/core/tests/unit_tests/test_messages.py @@ -916,6 +916,47 @@ def test_merge_tool_calls() -> None: assert len(merged) == 2 +def test_merge_tool_calls_parallel_same_index() -> None: + """Test parallel tool calls with same index but different IDs.""" + # Two parallel tool calls with the same index but different IDs + left = create_tool_call_chunk( + name="read_file", args='{"path": "foo.txt"}', id="tooluse_ABC", index=0 + ) + right = create_tool_call_chunk( + name="search_text", args='{"query": "bar"}', id="tooluse_DEF", index=0 + ) + merged = merge_lists([left], [right]) + assert merged is not None + assert len(merged) == 2 + assert merged[0]["name"] == "read_file" + assert merged[0]["id"] == "tooluse_ABC" + assert merged[1]["name"] == "search_text" + assert merged[1]["id"] == "tooluse_DEF" + + # Streaming continuation: same index, id=None on continuation chunk + # should still merge correctly with the original chunk + first = create_tool_call_chunk(name="tool1", args="", id="id1", index=0) + continuation = create_tool_call_chunk( + name=None, args='{"key": "value"}', id=None, index=0 + ) + merged = merge_lists([first], [continuation]) + assert merged is not None + assert len(merged) == 1 + assert merged[0]["name"] == "tool1" + assert merged[0]["args"] == '{"key": "value"}' + assert merged[0]["id"] == "id1" + + # Three parallel tool calls all with the same index + tc1 = create_tool_call_chunk(name="tool_a", args="{}", id="id_a", index=0) + tc2 = create_tool_call_chunk(name="tool_b", args="{}", id="id_b", index=0) + tc3 = create_tool_call_chunk(name="tool_c", args="{}", id="id_c", index=0) + merged = merge_lists([tc1], [tc2], [tc3]) + assert merged is not None + assert len(merged) == 3 + assert [m["name"] for m in merged] == ["tool_a", "tool_b", "tool_c"] + assert [m["id"] for m in merged] == ["id_a", "id_b", "id_c"] + + def test_tool_message_serdes() -> None: message = ToolMessage( "foo", artifact={"bar": {"baz": 123}}, tool_call_id="1", status="error"