Cross-Subgraph State Mixup #1129

Closed
opened 2026-02-20 17:43:11 -05:00 by yindo · 3 comments
Owner

Originally created by @ben-pulse on GitHub (Jan 28, 2026).

Checked other resources

  • This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • I added a clear and detailed title that summarizes the issue.
  • I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.

Description

My application has two separate graphs:

  • foo
  • bar

The application is threaded though, essentially a multi-agent with a supervisor.

And we use a checkpointer to allow interrupts and resumes.

We noticed though that the state's "next" field only has the node name, and our two subgraphs both have nodes with the same name.

In fact if we call agent.get_state(), both agents say that their "next" node is going to be called.

Our expected behavior is only the agent which was interrupted should have this value.

If this is a user error on my part, then perhaps the https://docs.langchain.com/oss/python/langgraph/use-subgraphs can be updated to clarify it?

Output of the 3 threads after running example:

Thread 1:

>>> agent_foo.get_state(config=config1).next
()
>>> agent_bar.get_state(config=config1).next
()

Thread 2:

Correctly only agent_foo has next state.

>>> agent_foo.get_state(config=config2).next
('do_foo_work',)
>>> agent_bar.get_state(config=config2).next
()

Thread 3:

Unexpectedly, both agents have next state (even though agent_bar has never been invoked).

>>> agent_foo.get_state(config=config3).next
('format_report',)
>>> agent_bar.get_state(config=config3).next
('format_report',)

Example Code

from typing_extensions import TypedDict
from langgraph.graph.state import StateGraph, START
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.runnables import RunnableConfig


class ResearchState(TypedDict):
    fail_in: str | None
    initial_research: str
    formatted_report: str

def do_foo_work(state: ResearchState):
    if state["fail_in"] == "do_foo_work":
        raise ValueError("Failing in do_foo_work")
    return {"initial_research": f"Foo Research"}

def do_bar_work(state: ResearchState):
    if state["fail_in"] == "do_bar_work":
        raise ValueError("Failing in do_bar_work")
    return {"initial_research": f"Bar Research"}

def format_report(state: ResearchState):
    if state["fail_in"] == "format_report":
        raise ValueError("Failing in format_repot")
    return {"formatted_report": f"This is the formatted report of {state["initial_research"]}"}


checkpointer = MemorySaver()

############ 
# Foo Graph
############ 

foo_builder = StateGraph(ResearchState)
foo_builder.add_node(do_foo_work)
foo_builder.add_node(format_report)
foo_builder.add_edge(START, "do_foo_work")
foo_builder.add_edge("do_foo_work", "format_report")
agent_foo = foo_builder.compile(checkpointer=checkpointer)

############ 
# Bar Graph
############ 

bar_builder = StateGraph(ResearchState)
bar_builder.add_node(do_bar_work)
bar_builder.add_node(format_report)
bar_builder.add_edge(START, "do_bar_work")
bar_builder.add_edge("do_bar_work", "format_report")
agent_bar = bar_builder.compile(checkpointer=checkpointer)

############ 
# Usage
############ 

# Thread 1: Call "foo" successfully
config1: RunnableConfig = {"configurable": {"thread_id": "thread-1"}}
agent_foo.invoke({"fail_in": None, "initial_research": "", "formatted_report": ""}, config=config1)

agent_foo.get_state(config=config1).next
agent_bar.get_state(config=config1).next


# Thread 2: Call "foo" fails in "do_foo_work" node

config2: RunnableConfig = {"configurable": {"thread_id": "thread-2"}}
agent_foo.invoke({"fail_in": "do_foo_work", "initial_research": "", "formatted_report": ""}, config=config2)

agent_foo.get_state(config=config2).next
agent_bar.get_state(config=config2).next

# Thread 3: Call "foo" fails in "format_report" node

config3: RunnableConfig = {"configurable": {"thread_id": "thread-3"}}
agent_foo.invoke({"fail_in": "format_report", "initial_research": "", "formatted_report": ""}, config=config3)

agent_foo.get_state(config=config3).next
agent_bar.get_state(config=config3).next

System Info

  • langgraph 1.0.1
  • langchain 0.3.27
  • langchain-core 0.3.81
  • python 3.12
Originally created by @ben-pulse on GitHub (Jan 28, 2026). ## Checked other resources - [x] This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/). - [x] I added a clear and detailed title that summarizes the issue. - [x] I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example). - [x] I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue. ## Description My application has two separate graphs: - foo - bar The application is threaded though, essentially a multi-agent with a supervisor. And we use a checkpointer to allow interrupts and resumes. We noticed though that the state's "next" field only has the node name, and our two subgraphs both have nodes with the same name. In fact if we call `agent.get_state()`, both agents say that their "next" node is going to be called. Our expected behavior is only the agent which was interrupted should have this value. If this is a user error on my part, then perhaps the https://docs.langchain.com/oss/python/langgraph/use-subgraphs can be updated to clarify it? ### Output of the 3 threads after running example: #### Thread 1: ``` >>> agent_foo.get_state(config=config1).next () >>> agent_bar.get_state(config=config1).next () ``` #### Thread 2: Correctly only agent_foo has next state. ``` >>> agent_foo.get_state(config=config2).next ('do_foo_work',) >>> agent_bar.get_state(config=config2).next () ``` #### Thread 3: Unexpectedly, both agents have `next` state (even though `agent_bar` has never been invoked). ``` >>> agent_foo.get_state(config=config3).next ('format_report',) >>> agent_bar.get_state(config=config3).next ('format_report',) ``` ## Example Code ```python from typing_extensions import TypedDict from langgraph.graph.state import StateGraph, START from langgraph.checkpoint.memory import MemorySaver from langchain_core.runnables import RunnableConfig class ResearchState(TypedDict): fail_in: str | None initial_research: str formatted_report: str def do_foo_work(state: ResearchState): if state["fail_in"] == "do_foo_work": raise ValueError("Failing in do_foo_work") return {"initial_research": f"Foo Research"} def do_bar_work(state: ResearchState): if state["fail_in"] == "do_bar_work": raise ValueError("Failing in do_bar_work") return {"initial_research": f"Bar Research"} def format_report(state: ResearchState): if state["fail_in"] == "format_report": raise ValueError("Failing in format_repot") return {"formatted_report": f"This is the formatted report of {state["initial_research"]}"} checkpointer = MemorySaver() ############ # Foo Graph ############ foo_builder = StateGraph(ResearchState) foo_builder.add_node(do_foo_work) foo_builder.add_node(format_report) foo_builder.add_edge(START, "do_foo_work") foo_builder.add_edge("do_foo_work", "format_report") agent_foo = foo_builder.compile(checkpointer=checkpointer) ############ # Bar Graph ############ bar_builder = StateGraph(ResearchState) bar_builder.add_node(do_bar_work) bar_builder.add_node(format_report) bar_builder.add_edge(START, "do_bar_work") bar_builder.add_edge("do_bar_work", "format_report") agent_bar = bar_builder.compile(checkpointer=checkpointer) ############ # Usage ############ # Thread 1: Call "foo" successfully config1: RunnableConfig = {"configurable": {"thread_id": "thread-1"}} agent_foo.invoke({"fail_in": None, "initial_research": "", "formatted_report": ""}, config=config1) agent_foo.get_state(config=config1).next agent_bar.get_state(config=config1).next # Thread 2: Call "foo" fails in "do_foo_work" node config2: RunnableConfig = {"configurable": {"thread_id": "thread-2"}} agent_foo.invoke({"fail_in": "do_foo_work", "initial_research": "", "formatted_report": ""}, config=config2) agent_foo.get_state(config=config2).next agent_bar.get_state(config=config2).next # Thread 3: Call "foo" fails in "format_report" node config3: RunnableConfig = {"configurable": {"thread_id": "thread-3"}} agent_foo.invoke({"fail_in": "format_report", "initial_research": "", "formatted_report": ""}, config=config3) agent_foo.get_state(config=config3).next agent_bar.get_state(config=config3).next ``` ## System Info - langgraph 1.0.1 - langchain 0.3.27 - langchain-core 0.3.81 - python 3.12
yindo added the bugpending labels 2026-02-20 17:43:11 -05:00
yindo closed this issue 2026-02-20 17:43:11 -05:00
Author
Owner

@f4roukb commented on GitHub (Feb 1, 2026):

Both of your agents are sharing the same checkpointer, and the same thread_id through the same config, so changes to the checkpoint from one agent are visible to the other agent. After each node finishes execution, the state is checkpointed by the agent owning those nodes, and the latest checkpoint is loaded by the second agent when you call get_state.

You need to ensure that your agents run with distinct checkpointers.

@f4roukb commented on GitHub (Feb 1, 2026): Both of your agents are sharing the same checkpointer, and the same `thread_id` through the same config, so changes to the checkpoint from one agent are visible to the other agent. After each node finishes execution, the state is checkpointed by the agent owning those nodes, and the latest checkpoint is loaded by the second agent when you call `get_state`. You need to ensure that your agents run with distinct checkpointers.
Author
Owner

@ben-pulse commented on GitHub (Feb 1, 2026):

Thanks my intention is to show how 2 subgraphs of a single agent work. It shows them sharing a checkpointer here too: https://docs.langchain.com/oss/python/langgraph/use-subgraphs

@ben-pulse commented on GitHub (Feb 1, 2026): Thanks my intention is to show how 2 subgraphs of a single agent work. It shows them sharing a checkpointer here too: https://docs.langchain.com/oss/python/langgraph/use-subgraphs
Author
Owner

@ben-pulse commented on GitHub (Feb 2, 2026):

@f4roukb thanks for the advice, I will close this issue.

I've tried a few times with building a single langgraph compiled state graph that includes both subgraphs, but each time I do not reproduce the issue. The problem is therefore in our custom implementation of the supervisor node, which is why in the contrived example I provided I did not include it.

So I agree with you, the easiest fix here is to use a separate checkpointer for each of the subgraphs.

@ben-pulse commented on GitHub (Feb 2, 2026): @f4roukb thanks for the advice, I will close this issue. I've tried a few times with building a single langgraph compiled state graph that includes both subgraphs, but each time I do not reproduce the issue. The problem is therefore in our custom implementation of the supervisor node, which is why in the contrived example I provided I did not include it. So I agree with you, the easiest fix here is to use a separate checkpointer for each of the subgraphs.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#1129