Parallel Tool Calls with Reducers - Dynamic ToolNode vs Static Graph Definition

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?