Skip to content

Commit cc024b3

Browse files
declan-scaleclaude
andcommitted
fix(tutorials): stop at130-langgraph workflow deadlock on graph compile
The 130_langgraph tutorial's workflow called `lg_graph(GRAPH_NAME).compile()` each turn. With the tools node as LangGraph's `ToolNode` (a Runnable) running `execute_in="workflow"`, the temporalio LangGraph plugin wraps it in `wrap_workflow`, whose closure captures the ToolNode. LangGraph's compile-time subgraph detection (`find_subgraph_pregel` -> `get_function_nonlocals` -> `inspect.getsource`) then recurses through that wrapper with no cycle detection and never terminates, tripping Temporal's 2s deadlock detector. The agent never emitted a tool_request and the integration test timed out on every retry. Replace the `ToolNode` tools node with a plain `async def tools_node` that dispatches the requested tool calls. A plain function isn't a Runnable, so the plugin-wired graph compiles in ~2ms instead of hanging, while keeping the intended activity(LLM)/workflow(tools) split. Verified: wired compile drops from non-terminating to 2ms and the hermetic ReAct-loop test still passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bbfb22e commit cc024b3

1 file changed

Lines changed: 30 additions & 3 deletions

File tree

  • examples/tutorials/10_async/10_temporal/130_langgraph/project

examples/tutorials/10_async/10_temporal/130_langgraph/project/graph.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
The router and tools are ``async`` so LangGraph awaits them directly — a sync
1111
callable would be offloaded via ``run_in_executor``, which Temporal's workflow
1212
event loop does not support.
13+
14+
The in-workflow ``tools`` node is a plain ``async`` function rather than
15+
LangGraph's ``ToolNode`` prebuilt on purpose. The plugin wraps an in-workflow
16+
node in ``wrap_workflow``, whose closure captures the wrapped object. When that
17+
object is itself a LangChain ``Runnable`` (as ``ToolNode`` is), LangGraph's
18+
``compile()`` subgraph detection (``find_subgraph_pregel`` →
19+
``get_function_nonlocals``) recurses through that wrapper without cycle
20+
detection and never terminates, tripping Temporal's deadlock detector. A plain
21+
function isn't a ``Runnable``, so compile stays trivial.
1322
"""
1423

1524
from __future__ import annotations
@@ -26,12 +35,14 @@
2635

2736
from langgraph.graph import END, START, StateGraph
2837
from langchain_openai import ChatOpenAI
29-
from langgraph.prebuilt import ToolNode
30-
from langchain_core.messages import SystemMessage
38+
from langchain_core.messages import ToolMessage, SystemMessage
3139
from langgraph.graph.message import add_messages
3240

3341
from project.tools import TOOLS
3442

43+
# Look up tools by name for the in-workflow tools node.
44+
_TOOLS_BY_NAME = {tool.name: tool for tool in TOOLS}
45+
3546
# Name this graph is registered under in the LangGraphPlugin (acp.py / run_worker.py).
3647
GRAPH_NAME = "at130-langgraph"
3748
MODEL_NAME = "gpt-4o"
@@ -58,6 +69,22 @@ async def agent_node(state: AgentState) -> dict[str, Any]:
5869
return {"messages": [await llm.ainvoke(messages)]}
5970

6071

72+
async def tools_node(state: AgentState) -> dict[str, Any]:
73+
"""Run the tool calls the model requested. Runs inline in the workflow.
74+
75+
A plain ``async`` function (not LangGraph's ``ToolNode``) — see the module
76+
docstring for why a ``Runnable`` tools node can't be compiled here.
77+
"""
78+
last = state["messages"][-1]
79+
results: list[Any] = []
80+
for call in getattr(last, "tool_calls", None) or []:
81+
output = await _TOOLS_BY_NAME[call["name"]].ainvoke(call["args"])
82+
results.append(
83+
ToolMessage(content=str(output), tool_call_id=call["id"], name=call["name"])
84+
)
85+
return {"messages": results}
86+
87+
6188
async def route_after_agent(state: AgentState) -> str:
6289
"""Go to the tools node if the model requested tools, else finish (async router)."""
6390
last = state["messages"][-1]
@@ -72,7 +99,7 @@ def build_graph() -> StateGraph:
7299
agent_node,
73100
metadata={"execute_in": "activity", "start_to_close_timeout": timedelta(minutes=5)},
74101
)
75-
builder.add_node("tools", ToolNode(TOOLS), metadata={"execute_in": "workflow"})
102+
builder.add_node("tools", tools_node, metadata={"execute_in": "workflow"})
76103
builder.add_edge(START, "agent")
77104
builder.add_conditional_edges("agent", route_after_agent, {"tools": "tools", END: END})
78105
builder.add_edge("tools", "agent")

0 commit comments

Comments
 (0)