Skip to content

Commit 9dec085

Browse files
jsonbaileyclaude
andcommitted
fix: Prevent fan-out when functional and handoff tools coexist on same node
A static loop-back edge from the tools node conflicted with Command(goto=child) emitted by handoff tools, causing both to fire as a fan-out. Fix by replacing the static edge with a conditional edge in the mixed case: after the ToolNode runs, route back to the parent only when the last message is from a functional tool; route to END when it is from a handoff (Command already handles routing to the target agent). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f0cccd1 commit 9dec085

1 file changed

Lines changed: 27 additions & 8 deletions

File tree

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langgraph_agent_graph_runner.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,6 @@ async def invoke(state: WorkflowState) -> dict:
184184
graph_structure.append(node_desc)
185185

186186
if all_tools:
187-
# ToolNode handles Command returns from handoff tools, routing to the target
188-
# node. For functional tools it returns normal ToolMessages and we loop back.
189-
# tools_condition exits to END when no tool is called.
190187
tools_node_key = f"{node_key}__tools"
191188
agent_builder.add_node(tools_node_key, ToolNode(all_tools))
192189

@@ -199,12 +196,34 @@ async def invoke(state: WorkflowState) -> dict:
199196
tools_condition,
200197
{"tools": tools_node_key, END: after_loop},
201198
)
199+
elif not tool_fns:
200+
# Only handoff tools: no loop-back needed.
201+
# Command(goto=child_key) handles routing to the target.
202+
agent_builder.add_conditional_edges(
203+
node_key,
204+
tools_condition,
205+
{"tools": tools_node_key, END: END},
206+
)
202207
else:
203-
# Handoff tools use Command(goto=child_key) — LangGraph routes to the
204-
# target directly without any extra edge. Functional tools (if any)
205-
# return normal ToolMessages and must loop back so the LLM sees the result.
206-
if tool_fns:
207-
agent_builder.add_edge(tools_node_key, node_key)
208+
# Both functional and handoff tools. A static loop-back edge would
209+
# fan-out with Command(goto=child_key) from handoff tools, so use a
210+
# conditional edge that only loops back for functional tool results.
211+
handoff_names_set = frozenset(getattr(t, 'name', '') for t in handoff_fns)
212+
213+
def make_after_tools_router(parent_key: str, ht_names: frozenset):
214+
def route(state: WorkflowState) -> str:
215+
for msg in reversed(state['messages']):
216+
if hasattr(msg, 'name') and msg.name:
217+
return END if msg.name in ht_names else parent_key
218+
break
219+
return parent_key
220+
return route
221+
222+
agent_builder.add_conditional_edges(
223+
tools_node_key,
224+
make_after_tools_router(node_key, handoff_names_set),
225+
{node_key: node_key, END: END},
226+
)
208227
agent_builder.add_conditional_edges(
209228
node_key,
210229
tools_condition,

0 commit comments

Comments
 (0)