Issue Description:
Problem:
When using Annotated[List[Dict], operator.add] for state fields that are updated by tools via Command(update=…), the state accumulates exponentially instead of properly merging list items. Each tool update appears to wrap the previous state in a new array, causing nested concatenation rather than flat list merging.
Reproduction Steps:
-
Define a state field: current_context_source: Annotated[List[Dict[str, str]], operator.add]
-
Create tools that update this field: Command(update={“current_context_source”: [{“id”: “A”, “value”: [“source1”]}]})
-
Run multiple queries through the graph
-
Observe exponential growth: [A] → [[A], B] → [[[A], B], C]
Expected Behavior:
python
# Query 1: [{“id”: “A”, “value”: [“source1”]}]
# Query 2: [{“id”: “A”, “value”: [“source1”]}, {“id”: “B”, “value”: [“source2”]}]
# Query 3: [{“id”: “A”, “value”: [“source1”]}, {“id”: “B”, “value”: [“source2”]}, {“id”: “C”, “value”: [“source3”]}]
Actual Behavior:
python
# Query 1: [{“id”: “A”, “value”: [“source1”]}]
# Query 2: [{“id”: “A”, “value”: [“source1”]}, {“id”: “A”, “value”: [“source1”]}, {“id”: “B”, “value”: [“source2”]}]
# Query 3: [{“id”: “A”, “value”: [“source1”]}, {“id”: “A”, “value”: [“source1”]}, {“id”: “B”, “value”: [“source2”]}, {“id”: “A”, “value”: [“source1”]}, {“id”: “A”, “value”: [“source1”]}, {“id”: “B”, “value”: [“source2”]}, {“id”: “C”, “value”: [“source3”]}]
Code Example:
python
class MyState(AgentState):
**current_context_source: Annotated\[List\[Dict\[str, str\]\], operator.add\] = Field(*default_factory*=list)**
@tool(“my_tool”)
async def my_tool(query_id: str):
***return***** Command(*update*={**
**"current_context_source": \[{"id": query_id, "value": \["source"\]}\]**
**})**
Attempted Solutions (All Failed):
1. Custom Lambda Reducer:
python
current_context_source: Annotated[List[Dict], lambda x, y: (x or []) + (y or [])]
Result: Same behavior as operator.add - still creates nested arrays
2. Messages-Style Manual Concatenation:
python
# In tools
existing_sources = state.get(“current_context_source”, [])
“current_context_source”: existing_sources + [{“id”: query_id, “value”: sources}]
Result: Failed because tools don’t have access to state parameter
3. Different Reducer Functions:
-
operator.setitem - Overwrites instead of merging
-
operator.add - Same nested behavior
-
Custom functions - All produce same nested concatenation
4. State Structure Changes:
-
Tried different Annotated syntax
-
Attempted to flatten in custom reducers
-
Modified update patterns
All approaches failed because the issue appears to be in LangGraph’s internal handling of Command(update=…) with operator.add reducers.
Root Cause Analysis:
The issue seems to be that LangGraph internally wraps tool updates in arrays before applying the operator.add reducer, causing:
python
# What we send: [{“id”: “A”, “value”: [“source1”]}]
# What LangGraph does internally: existing + [new_array]
# Instead of: existing + new_items
Workaround:
Currently using response-level deduplication:
python
# Deduplicate at response creation time
unique_sources =
seen_ids = set()
for source in raw_sources:
*if* source\["id"\] not in seen_ids:
unique_sources.append(source)
seen_ids.add(source\["id"\])
Request:
Please investigate the operator.add reducer behavior with tool updates and provide a proper solution for flat list merging in state management. Also explain why this does not happen with the in built messafes state in AgentState ?