Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/google/adk/flows/llm_flows/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1151,10 +1151,17 @@ def __build_response_event(


def deep_merge_dicts(d1: dict, d2: dict) -> dict:
"""Recursively merges d2 into d1."""
"""Recursively merges d2 into d1.

For dict values, merges recursively. For list values, concatenates instead of
overwriting so that parallel tool calls don't silently drop list entries
(e.g. state_delta lists from concurrent function responses).
"""
for key, value in d2.items():
if key in d1 and isinstance(d1[key], dict) and isinstance(value, dict):
d1[key] = deep_merge_dicts(d1[key], value)
elif key in d1 and isinstance(d1[key], list) and isinstance(value, list):
d1[key] = d1[key] + value
else:
d1[key] = value
return d1
Expand Down
83 changes: 83 additions & 0 deletions tests/unittests/flows/llm_flows/test_functions_parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
# limitations under the License.

from google.adk.agents.llm_agent import Agent
from google.adk.events.event import Event
from google.adk.events.event_actions import EventActions
from google.adk.flows.llm_flows.functions import deep_merge_dicts
from google.adk.flows.llm_flows.functions import merge_parallel_function_response_events
from google.adk.tools.tool_context import ToolContext
from google.genai import types
import pytest
Expand Down Expand Up @@ -105,3 +108,83 @@ async def transfer_to_agent(
},
transfer_to_agent='test_sub_agent',
)


def test_deep_merge_dicts_concatenates_lists():
"""Test that deep_merge_dicts concatenates list values instead of overwriting."""
d1 = {'state_delta': {'items': ['a']}}
d2 = {'state_delta': {'items': ['b']}}
result = deep_merge_dicts(d1, d2)
assert result['state_delta']['items'] == ['a', 'b']


def test_deep_merge_dicts_overwrites_non_list_non_dict():
"""Test that deep_merge_dicts still overwrites scalar values."""
d1 = {'key': 'old'}
d2 = {'key': 'new'}
result = deep_merge_dicts(d1, d2)
assert result['key'] == 'new'


def test_deep_merge_dicts_merges_nested_dicts():
"""Test that deep_merge_dicts recursively merges nested dicts."""
d1 = {'a': {'b': 1, 'c': 2}}
d2 = {'a': {'b': 3, 'd': 4}}
result = deep_merge_dicts(d1, d2)
assert result == {'a': {'b': 3, 'c': 2, 'd': 4}}


def test_deep_merge_dicts_handles_mixed_list_and_non_list():
"""Test that deep_merge_dicts overwrites when types differ (list vs non-list)."""
d1 = {'key': 'not_a_list'}
d2 = {'key': ['a', 'b']}
result = deep_merge_dicts(d1, d2)
assert result['key'] == ['a', 'b']

d1 = {'key': ['a', 'b']}
d2 = {'key': 'not_a_list'}
result = deep_merge_dicts(d1, d2)
assert result['key'] == 'not_a_list'


def test_merge_parallel_function_response_events_merges_state_delta_lists():
"""Test that parallel events with list state_delta values are concatenated, not overwritten."""
invocation_id = 'base_invocation_123'

event1 = Event(
invocation_id=invocation_id,
author='tool',
content=types.Content(
role='user',
parts=[
types.Part(
function_response=types.FunctionResponse(
name='func_1',
response={'result': 'ok'},
)
)
],
),
actions=EventActions(state_delta={'items': ['a']}),
)

event2 = Event(
invocation_id=invocation_id,
author='tool',
content=types.Content(
role='user',
parts=[
types.Part(
function_response=types.FunctionResponse(
name='func_2',
response={'result': 'ok'},
)
)
],
),
actions=EventActions(state_delta={'items': ['b']}),
)

merged_event = merge_parallel_function_response_events([event1, event2])

assert merged_event.actions.state_delta == {'items': ['a', 'b']}