Context:
I have a custom React agent with a state schema using Annotated reducers:
class AssistantState(TypedDict):
activity_log: Annotated[Dict[str, List[Dict]], merge_activity_logs] # Custom reducer
messages: Annotated[Sequence[BaseMessage], add_messages]
My tools return Command objects with activity log updates:
@tool
async def update_artifact_field(...):
return Command(update={
"messages": [tool_message],
"activity_log": {"field_generation": [new_entry], ...}
})
Problem:
When 3 parallel tool calls execute, only the last tool’s activity log appears in state. The merge_activity_logs reducer isn’t being applied.
Root Cause:
I create ToolNode dynamically inside a node function (not at graph definition time) because I need to filter tools at runtime (regular/transfer/long-running):
async def regular_tools_node(state):
# Filter tool calls dynamically
regular_tool_calls = [tc for tc in tool_calls if not tc['name'].startswith('transfer_')]
# Create ToolNode on-the-fly
regular_node = ToolNode(regular_tools)
result = await regular_node.ainvoke(filtered_state)
# ❌ Returns list of Commands - no reducer merging happens
Current Solution:
Manually apply reducers when processing Commands:
key_reducers = {'activity_log': merge_activity_logs}
for command in result: # List of Commands
for key, value in command.update.items():
if key in key_reducers:
old_value = state_updates.get(key)
merged_value = key_reducers[key](old_value, value) # Manual merge
state_updates[key] = merged_value
Question:
Is there a way to make dynamically-created ToolNode instances aware of the parent graph’s state schema and reducers? Or is manual reducer application the correct pattern when tools are filtered/routed dynamically at runtime?
This works but feels like I’m reimplementing what LangGraph should do automatically. Any best practices here?