hi @padanes
The parallel run happens because both routes are being scheduled:
-
Your tool returns a
Command(goto="SOP_executor", ...), which dynamically routes toSOP_executor. -
You also have a static edge
workflow.add_edge("tools", "supervisor"). Static edges are written every time thetoolsnode finishes, so the graph also schedulessupervisoragain. Result:SOP_executorandsupervisorrun in parallel on the next step.
Two changes should fix this:
- Do not keep an unconditional edge from
toolstosupervisorwhen you want a handoff.
- Remove
workflow.add_edge("tools", "supervisor"). Thegoto="SOP_executor"will take control and send execution there, sosupervisorwill not be scheduled again. - If you also want the normal “tool → supervisor” loop for other tools, wrap the
ToolNodeto default back to supervisor only when the tool returns a regularToolMessage(not aCommand):
from langgraph.types import Command
from langchain_core.messages import ToolMessage
def wrap_tool_call(request, execute):
result = execute(request)
if isinstance(result, ToolMessage):
return Command(
update={"messages": [result]},
goto="supervisor",
graph=Command.PARENT,
)
return result
workflow.add_node("tools", ToolNode(tools, wrap_tool_call=wrap_tool_call))
# Note: keep the tools->supervisor static edge removed.
- OPTIONAL: When returning a
Commandfrom a tool (inside a subgraph), target the parent graph explicitly.
return Command(
goto="SOP_executor",
update={**runtime.state, "messages": runtime.state["messages"] + [tool_message]},
graph=Command.PARENT, # important when running from within a subgraph
)
Why this is expected in LangGraph
-
A
Command.gotoadds dynamic routing; it does not implicitly disable existing static edges. With both in place, both branches are scheduled. -
Returning
graph=Command.PARENTfrom a tool is the documented way to navigate in the parent graph from within a tool execution inside a subgraph.