fix(sdk): propagate goto and graph in Commands returned by tools (#3391)

Resolves https://github.com/langchain-ai/deepagents/issues/2500

Co-authored-by: Mason Daugherty <github@mdrxy.com>
Co-authored-by: Mason Daugherty <mason@langchain.dev>
This commit is contained in:
ccurme
2026-05-29 16:19:24 -04:00
committed by GitHub
parent 8dd7d95f4c
commit d92aef68f7
2 changed files with 59 additions and 2 deletions
@@ -2004,7 +2004,11 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
resolved_backend,
)
processed_messages.append(processed_message)
return Command(update={**update, "messages": processed_messages})
return Command(
goto=tool_result.goto,
graph=tool_result.graph,
update={**update, "messages": processed_messages},
)
msg = f"Unreachable code reached in _intercept_large_tool_result: for tool_result of type {type(tool_result)}"
raise AssertionError(msg)
@@ -2039,7 +2043,11 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
resolved_backend,
)
processed_messages.append(processed_message)
return Command(update={**update, "messages": processed_messages})
return Command(
goto=tool_result.goto,
graph=tool_result.graph,
update={**update, "messages": processed_messages},
)
msg = f"Unreachable code reached in _aintercept_large_tool_result: for tool_result of type {type(tool_result)}"
raise AssertionError(msg)
@@ -21,7 +21,9 @@ from langchain_core.runnables import Runnable
from langchain_core.tools import BaseTool, tool
from langgraph.channels.delta import DeltaChannel
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.store.memory import InMemoryStore
from langgraph.types import Command
from pydantic import Field
from deepagents.backends import CompositeBackend, FilesystemBackend
@@ -3608,6 +3610,53 @@ class TestDeltaChannels:
assert any("hello.txt" in k for k in files)
def test_tool_command_parent_handoff_preserved() -> None:
# A tool returning Command(goto=..., graph=Command.PARENT) must propagate routing
# through FilesystemMiddleware so multi-agent handoffs reach the sibling node.
@tool
def transfer_to_b(runtime: ToolRuntime) -> Command:
"""Transfer to agent_b."""
return Command(
goto="agent_b",
graph=Command.PARENT,
update={
"messages": [
ToolMessage(content="transferred", tool_call_id=runtime.tool_call_id),
],
},
)
fake_model = FakeChatModelWithHistory(
messages=iter(
[
AIMessage(
content="",
tool_calls=[{"id": "call_xfer", "name": "transfer_to_b", "args": {}}],
),
]
)
)
agent_a = create_deep_agent(model=fake_model, tools=[transfer_to_b])
visited: list[str] = []
def agent_b_node(_state: dict) -> dict:
visited.append("agent_b")
return {}
parent = StateGraph(dict)
parent.add_node("agent_a", agent_a)
parent.add_node("agent_b", agent_b_node)
parent.add_edge(START, "agent_a")
parent.add_edge("agent_a", END)
parent.add_edge("agent_b", END)
compiled = parent.compile()
compiled.invoke({"messages": [HumanMessage(content="hi")]})
assert visited == ["agent_b"]
def test_invalid_tool_call_patched_on_next_turn() -> None:
# Turn 1: model truncates and emits an invalid tool call (no matching ToolMessage
# will be produced because agents only route on `tool_calls`).