Description
When multiple tool calls run in parallel and each writes to the same state_delta key containing a list value, merge_parallel_function_response_events silently drops all but the last value.
Root Cause
deep_merge_dicts in flows/llm_flows/functions.py (line ~822) only recurses into dict values. Lists hit the else branch and get overwritten:
defdeep_merge_dicts(d1: dict, d2: dict) ->dict:
forkey, valueind2.items():
ifkeyind1andisinstance(d1[key], dict) andisinstance(value, dict):
d1[key] =deep_merge_dicts(d1[key], value)
else:
d1[key] =value# <-- lists are overwritten herereturnd1
Reproduction
- Create a tool that appends to a list in
tool_context.state:
defmy_tool(tool_context, item):
items=tool_context.state.get("items") or []
items=list(items)
items.append(item)
tool_context.state["items"] =items- Have the LLM call this tool multiple times in a single response (parallel execution via
asyncio.gather in handle_function_calls_live) - Each parallel call gets its own
ToolContext with a separate state_delta - Tool A's delta:
{"state_delta": {"items": ["a"]}} - Tool B's delta:
{"state_delta": {"items": ["b"]}} - After merge:
{"state_delta": {"items": ["b"]}} — item "a" is lost
Impact
Any application that accumulates list state across parallel tool calls loses data. In our case, drafting multiple emails in a single agent turn results in only ~2 out of 10 emails persisting.
Suggested Fix
deep_merge_dicts should concatenate lists instead of overwriting:
defdeep_merge_dicts(d1: dict, d2: dict) ->dict:
forkey, valueind2.items():
ifkeyind1andisinstance(d1[key], dict) andisinstance(value, dict):
d1[key] =deep_merge_dicts(d1[key], value)
elifkeyind1andisinstance(d1[key], list) andisinstance(value, list):
d1[key] =d1[key] +valueelse:
d1[key] =valuereturnd1
Environment
google-adk 1.18.0- Python 3.12
- Using
DatabaseSessionService with LiteLLM + OpenAI models
Description
When multiple tool calls run in parallel and each writes to the same
state_deltakey containing a list value,merge_parallel_function_response_eventssilently drops all but the last value.Root Cause
deep_merge_dictsinflows/llm_flows/functions.py(line ~822) only recurses into dict values. Lists hit theelsebranch and get overwritten:Reproduction
tool_context.state:asyncio.gatherinhandle_function_calls_live)ToolContextwith a separatestate_delta{"state_delta": {"items": ["a"]}}{"state_delta": {"items": ["b"]}}{"state_delta": {"items": ["b"]}}— item "a" is lostImpact
Any application that accumulates list state across parallel tool calls loses data. In our case, drafting multiple emails in a single agent turn results in only ~2 out of 10 emails persisting.
Suggested Fix
deep_merge_dictsshould concatenate lists instead of overwriting:Environment
google-adk1.18.0DatabaseSessionServicewithLiteLLM+ OpenAI models