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:
def deep_merge_dicts(d1: dict, d2: dict) -> dict:
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)
else:
d1[key] = value # <-- lists are overwritten here
return d1
Reproduction
- Create a tool that appends to a list in
tool_context.state:
def my_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:
def deep_merge_dicts(d1: dict, d2: dict) -> dict:
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
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