Subgraph (using interrupt) restarts instead of resuming from internal breakpoint #639

Closed
opened 2026-02-20 17:41:04 -05:00 by yindo · 2 comments
Owner

Originally created by @michalgala on GitHub (May 22, 2025).

Originally assigned to: @sydney-runkle on GitHub.

Checked other resources

  • This is a bug, not a usage question. For questions, please use GitHub Discussions.
  • 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.

Example Code

from typing import TypedDict, Optional
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver


class State(TypedDict):
    """The graph state from the example using interrupt()"""
    state_counter: int
    human_response: Optional[str]

counter_node_in_subgraph = 0
counter_human_node = 0

def node_in_subgraph(state: State):
    """A node in the sub-graph."""
    global counter_node_in_subgraph
    counter_node_in_subgraph += 1
    print(f"SUBGRAPH: Entered `node_in_subgraph` a total of {counter_node_in_subgraph} times")
    answer = state.get("human_response","None")
    print(f"SUBGRAPH: `node_in_subgraph` has human reponse: '{answer}'")
    return {}

def human_node(state: State):
    """This node would process human input if it ran."""
    global counter_human_node
    counter_human_node += 1
    print(f"SUBGRAPH: Entered `human_node` in sub-graph a total of {counter_human_node} times")
    answer = state.get("human_response")
    print(f"SUBGRAPH: `human_node` got an answer of '{answer}' from state.")
    return {}

checkpointer = MemorySaver()

subgraph_builder = StateGraph(State)
subgraph_builder.add_node("some_node", node_in_subgraph)
subgraph_builder.add_node("human_node", human_node)
subgraph_builder.add_edge(START, "some_node")
subgraph_builder.add_edge("some_node", "human_node")
subgraph_builder.add_edge("human_node", END)

subgraph = subgraph_builder.compile(
    checkpointer=checkpointer,
    interrupt_before=["human_node"]
)

counter_parent_node = 0

def parent_node(state: State, config: dict): # Added config
    """This parent node will invoke the subgraph."""
    global counter_parent_node
    counter_parent_node += 1
    print(f"PARENT: Entered `parent_node` a total of {counter_parent_node} times")
    print(f"  PARENT: state before subgraph.invoke: {state}")

    subgraph_output_state = subgraph.invoke(state, config=config)

    print(f"  PARENT: subgraph_output_state: {subgraph_output_state}")
    # The parent node returns the full state output from the subgraph.
    return subgraph_output_state


builder = StateGraph(State)
builder.add_node("parent_node", parent_node)
builder.add_edge(START, "parent_node")
builder.add_edge("parent_node", END)

graph = builder.compile(checkpointer=checkpointer)
print("Main graph compiled.")

config = {
    "configurable": {
      "thread_id": "1",
    }
}

# Initial run - should interrupt before subgraph's 'human_node'
print("\n--- Initial Run (expecting interrupt) ---")
initial_input = State(state_counter=1, human_response=None) # type: ignore
for chunk in graph.stream(initial_input, config, subgraphs=True):
    print(f"CHUNK Initial: {chunk}")

print(f"\n--- Graph interrupted. Current state (Main Graph): {graph.get_state(config).values} ---")
print(f"--- Next node in Main Graph: {graph.get_state(config).next} ---")
print(f"--- Next node in Sub Graph: {graph.get_state(config, subgraphs=True).tasks[0].state.next} ---")


print('\n--- Resuming ---')
hil_response_value = "35" # The input the human provides

# Update state with the HIL response
graph.update_state(
    config,
    State(state_counter=graph.get_state(config).values["state_counter"],
          human_response=hil_response_value) # type: ignore
)
print(f"State updated with human_response: {hil_response_value}")
print(f"  Current state (Main Graph) before resume stream: {graph.get_state(config).values}")


# Resume by streaming with None as input
for chunk in graph.stream(None, config, subgraphs=True):
    print(f"CHUNK Resumed: {chunk}")

print(f"\n--- Final State (Main Graph): {graph.get_state(config).values} ---")
print(f"--- Final counter_node_in_subgraph: {counter_node_in_subgraph} ---")
print(f"--- Final counter_human_node: {counter_human_node} ---")
print(f"--- Final counter_parent_node: {counter_parent_node} ---")

Error Message and Stack Trace (if applicable)

Main graph compiled.

--- Initial Run (expecting interrupt) ---
PARENT: Entered `parent_node` a total of 1 times
  PARENT: state before subgraph.invoke: {'state_counter': 1, 'human_response': None}
SUBGRAPH: Entered `node_in_subgraph` a total of 1 times
SUBGRAPH: `node_in_subgraph` has human reponse: 'None'
CHUNK Initial: (('parent_node:34b0ebce-0f4f-349e-2e37-3278639278b7',), {'some_node': None})
CHUNK Initial: ((), {'__interrupt__': ()})

--- Graph interrupted. Current state (Main Graph): {'state_counter': 1, 'human_response': None} ---
--- Next node in Main Graph: ('parent_node',) ---
--- Next node in Sub Graph: ('human_node',) ---

--- Resuming ---
State updated with human_response: 35
  Current state (Main Graph) before resume stream: {'state_counter': 1, 'human_response': '35'}
PARENT: Entered `parent_node` a total of 2 times
  PARENT: state before subgraph.invoke: {'state_counter': 1, 'human_response': '35'}
SUBGRAPH: Entered `node_in_subgraph` a total of 2 times
SUBGRAPH: `node_in_subgraph` has human reponse: '35'
CHUNK Resumed: (('parent_node:e104621c-052e-bf14-5fa0-e956f82d4312',), {'some_node': None})
CHUNK Resumed: ((), {'__interrupt__': ()})

--- Final State (Main Graph): {'state_counter': 1, 'human_response': '35'} ---
--- Final counter_node_in_subgraph: 2 ---
--- Final counter_human_node: 0 ---
--- Final counter_parent_node: 2 ---

Description

I have recreated the example from the docs (https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/add-human-in-the-loop/#subgraphs-called-as-functions).
But instead of using interrupt and Command to resume, i am using interrupt_before that i define at the subgraph compilation.

The expected behavior, based on documentation for nested checkpoints and interruptible subgraphs, is that if the subgraph interrupts (e.g., before its human_node), the parent graph's calling node (parent_node) will re-execute upon resuming the main graph. When this re-executing parent_node calls the subgraph again, the subgraph should use its own checkpoint to resume from its internal breakpoint (i.e., start executing its human_node).

However, as you can see in the output, the subgraph is restarting from the entry point node, despite its checkpoint correctly indicating next=('human_node',).
This leads to some_node executing twice and human_node (the HIL target) never executing, effectively breaking the HIL flow within the subgraph.

System Info

System Information

OS: Darwin
OS Version: Darwin Kernel Version 23.6.0: Fri Jul 5 17:55:37 PDT 2024; root:xnu-10063.141.1~2/RELEASE_ARM64_T6030
Python Version: 3.12.2 (main, Mar 7 2024, 22:00:34) [Clang 15.0.0 (clang-1500.1.0.2.5)]

Package Information

langchain_core: 0.3.47
langchain: 0.3.20
langchain_community: 0.3.19
langsmith: 0.3.13
langchain_experimental: 0.3.4
langchain_openai: 0.3.9
langchain_text_splitters: 0.3.7
langgraph_api: 0.2.27
langgraph_cli: 0.2.10
langgraph_license: Installed. No version info available.
langgraph_runtime: Installed. No version info available.
langgraph_runtime_inmem: 0.0.11
langgraph_sdk: 0.1.69
langgraph_supervisor: 0.0.20

Optional packages not installed

langserve

Other Dependencies

aiohttp<4.0.0,>=3.8.3: Installed. No version info available.
async-timeout<5.0.0,>=4.0.0;: Installed. No version info available.
blockbuster: 1.5.24
click: 8.1.8
cloudpickle: 3.1.1
cryptography: 43.0.3
dataclasses-json<0.7,>=0.5.7: Installed. No version info available.
httpx: 0.28.1
httpx-sse<1.0.0,>=0.4.0: Installed. No version info available.
jsonpatch<2.0,>=1.33: Installed. No version info available.
jsonschema-rs: 0.29.1
langchain-anthropic;: Installed. No version info available.
langchain-aws;: Installed. No version info available.
langchain-cohere;: Installed. No version info available.
langchain-community;: Installed. No version info available.
langchain-core<0.4.0,>=0.3.40: Installed. No version info available.
langchain-core<1.0.0,>=0.3.41: Installed. No version info available.
langchain-core<1.0.0,>=0.3.45: Installed. No version info available.
langchain-deepseek;: Installed. No version info available.
langchain-fireworks;: Installed. No version info available.
langchain-google-genai;: Installed. No version info available.
langchain-google-vertexai;: Installed. No version info available.
langchain-groq;: Installed. No version info available.
langchain-huggingface;: Installed. No version info available.
langchain-mistralai;: Installed. No version info available.
langchain-ollama;: Installed. No version info available.
langchain-openai;: Installed. No version info available.
langchain-text-splitters<1.0.0,>=0.3.6: Installed. No version info available.
langchain-together;: Installed. No version info available.
langchain-xai;: Installed. No version info available.
langchain<1.0.0,>=0.3.20: Installed. No version info available.
langgraph: 0.4.5
langgraph-checkpoint: 2.0.26
langgraph-prebuilt<0.2.0,>=0.1.7: Installed. No version info available.
langgraph>=0.3.5: Installed. No version info available.
langsmith-pyo3: Installed. No version info available.
langsmith<0.4,>=0.1.125: Installed. No version info available.
langsmith<0.4,>=0.1.17: Installed. No version info available.
numpy<3,>=1.26.2: Installed. No version info available.
openai<2.0.0,>=1.66.3: Installed. No version info available.
orjson: 3.10.15
packaging: 24.2
packaging<25,>=23.2: Installed. No version info available.
pydantic: 2.10.6
pydantic-settings<3.0.0,>=2.4.0: Installed. No version info available.
pydantic<3.0.0,>=2.5.2;: Installed. No version info available.
pydantic<3.0.0,>=2.7.4: Installed. No version info available.
pydantic<3.0.0,>=2.7.4;: Installed. No version info available.
pyjwt: 2.10.1
pytest: Installed. No version info available.
python-dotenv: 1.0.1
PyYAML>=5.3: Installed. No version info available.
requests: 2.32.3
requests-toolbelt: 1.0.0
requests<3,>=2: Installed. No version info available.
rich: Installed. No version info available.
SQLAlchemy<3,>=1.4: Installed. No version info available.
sse-starlette: 2.1.3
starlette: 0.41.3
structlog: 25.2.0
tenacity: 8.5.0
tenacity!=8.4.0,<10,>=8.1.0: Installed. No version info available.
tenacity!=8.4.0,<10.0.0,>=8.1.0: Installed. No version info available.
tiktoken<1,>=0.7: Installed. No version info available.
truststore: 0.10.1
typing-extensions>=4.7: Installed. No version info available.
watchfiles: 0.20.0
zstandard: 0.23.0

Originally created by @michalgala on GitHub (May 22, 2025). Originally assigned to: @sydney-runkle on GitHub. ### Checked other resources - [x] This is a bug, not a usage question. For questions, please use GitHub Discussions. - [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. ### Example Code ```python from typing import TypedDict, Optional from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import MemorySaver class State(TypedDict): """The graph state from the example using interrupt()""" state_counter: int human_response: Optional[str] counter_node_in_subgraph = 0 counter_human_node = 0 def node_in_subgraph(state: State): """A node in the sub-graph.""" global counter_node_in_subgraph counter_node_in_subgraph += 1 print(f"SUBGRAPH: Entered `node_in_subgraph` a total of {counter_node_in_subgraph} times") answer = state.get("human_response","None") print(f"SUBGRAPH: `node_in_subgraph` has human reponse: '{answer}'") return {} def human_node(state: State): """This node would process human input if it ran.""" global counter_human_node counter_human_node += 1 print(f"SUBGRAPH: Entered `human_node` in sub-graph a total of {counter_human_node} times") answer = state.get("human_response") print(f"SUBGRAPH: `human_node` got an answer of '{answer}' from state.") return {} checkpointer = MemorySaver() subgraph_builder = StateGraph(State) subgraph_builder.add_node("some_node", node_in_subgraph) subgraph_builder.add_node("human_node", human_node) subgraph_builder.add_edge(START, "some_node") subgraph_builder.add_edge("some_node", "human_node") subgraph_builder.add_edge("human_node", END) subgraph = subgraph_builder.compile( checkpointer=checkpointer, interrupt_before=["human_node"] ) counter_parent_node = 0 def parent_node(state: State, config: dict): # Added config """This parent node will invoke the subgraph.""" global counter_parent_node counter_parent_node += 1 print(f"PARENT: Entered `parent_node` a total of {counter_parent_node} times") print(f" PARENT: state before subgraph.invoke: {state}") subgraph_output_state = subgraph.invoke(state, config=config) print(f" PARENT: subgraph_output_state: {subgraph_output_state}") # The parent node returns the full state output from the subgraph. return subgraph_output_state builder = StateGraph(State) builder.add_node("parent_node", parent_node) builder.add_edge(START, "parent_node") builder.add_edge("parent_node", END) graph = builder.compile(checkpointer=checkpointer) print("Main graph compiled.") config = { "configurable": { "thread_id": "1", } } # Initial run - should interrupt before subgraph's 'human_node' print("\n--- Initial Run (expecting interrupt) ---") initial_input = State(state_counter=1, human_response=None) # type: ignore for chunk in graph.stream(initial_input, config, subgraphs=True): print(f"CHUNK Initial: {chunk}") print(f"\n--- Graph interrupted. Current state (Main Graph): {graph.get_state(config).values} ---") print(f"--- Next node in Main Graph: {graph.get_state(config).next} ---") print(f"--- Next node in Sub Graph: {graph.get_state(config, subgraphs=True).tasks[0].state.next} ---") print('\n--- Resuming ---') hil_response_value = "35" # The input the human provides # Update state with the HIL response graph.update_state( config, State(state_counter=graph.get_state(config).values["state_counter"], human_response=hil_response_value) # type: ignore ) print(f"State updated with human_response: {hil_response_value}") print(f" Current state (Main Graph) before resume stream: {graph.get_state(config).values}") # Resume by streaming with None as input for chunk in graph.stream(None, config, subgraphs=True): print(f"CHUNK Resumed: {chunk}") print(f"\n--- Final State (Main Graph): {graph.get_state(config).values} ---") print(f"--- Final counter_node_in_subgraph: {counter_node_in_subgraph} ---") print(f"--- Final counter_human_node: {counter_human_node} ---") print(f"--- Final counter_parent_node: {counter_parent_node} ---") ``` ### Error Message and Stack Trace (if applicable) ```shell Main graph compiled. --- Initial Run (expecting interrupt) --- PARENT: Entered `parent_node` a total of 1 times PARENT: state before subgraph.invoke: {'state_counter': 1, 'human_response': None} SUBGRAPH: Entered `node_in_subgraph` a total of 1 times SUBGRAPH: `node_in_subgraph` has human reponse: 'None' CHUNK Initial: (('parent_node:34b0ebce-0f4f-349e-2e37-3278639278b7',), {'some_node': None}) CHUNK Initial: ((), {'__interrupt__': ()}) --- Graph interrupted. Current state (Main Graph): {'state_counter': 1, 'human_response': None} --- --- Next node in Main Graph: ('parent_node',) --- --- Next node in Sub Graph: ('human_node',) --- --- Resuming --- State updated with human_response: 35 Current state (Main Graph) before resume stream: {'state_counter': 1, 'human_response': '35'} PARENT: Entered `parent_node` a total of 2 times PARENT: state before subgraph.invoke: {'state_counter': 1, 'human_response': '35'} SUBGRAPH: Entered `node_in_subgraph` a total of 2 times SUBGRAPH: `node_in_subgraph` has human reponse: '35' CHUNK Resumed: (('parent_node:e104621c-052e-bf14-5fa0-e956f82d4312',), {'some_node': None}) CHUNK Resumed: ((), {'__interrupt__': ()}) --- Final State (Main Graph): {'state_counter': 1, 'human_response': '35'} --- --- Final counter_node_in_subgraph: 2 --- --- Final counter_human_node: 0 --- --- Final counter_parent_node: 2 --- ``` ### Description I have recreated the example from the docs (https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/add-human-in-the-loop/#subgraphs-called-as-functions). But instead of using _interrupt_ and _Command_ to resume, i am using _interrupt_before_ that i define at the subgraph compilation. The expected behavior, based on documentation for nested checkpoints and interruptible subgraphs, is that if the subgraph interrupts (e.g., before its human_node), the parent graph's calling node (parent_node) will re-execute upon resuming the main graph. When this re-executing parent_node calls the subgraph again, the subgraph should use its own checkpoint to resume from its internal breakpoint (i.e., start executing its human_node). However, as you can see in the output, the subgraph is restarting from the entry point node, despite its checkpoint correctly indicating next=('human_node',). This leads to some_node executing twice and human_node (the HIL target) never executing, effectively breaking the HIL flow within the subgraph. ### System Info System Information ------------------ > OS: Darwin > OS Version: Darwin Kernel Version 23.6.0: Fri Jul 5 17:55:37 PDT 2024; root:xnu-10063.141.1~2/RELEASE_ARM64_T6030 > Python Version: 3.12.2 (main, Mar 7 2024, 22:00:34) [Clang 15.0.0 (clang-1500.1.0.2.5)] Package Information ------------------- > langchain_core: 0.3.47 > langchain: 0.3.20 > langchain_community: 0.3.19 > langsmith: 0.3.13 > langchain_experimental: 0.3.4 > langchain_openai: 0.3.9 > langchain_text_splitters: 0.3.7 > langgraph_api: 0.2.27 > langgraph_cli: 0.2.10 > langgraph_license: Installed. No version info available. > langgraph_runtime: Installed. No version info available. > langgraph_runtime_inmem: 0.0.11 > langgraph_sdk: 0.1.69 > langgraph_supervisor: 0.0.20 Optional packages not installed ------------------------------- > langserve Other Dependencies ------------------ > aiohttp<4.0.0,>=3.8.3: Installed. No version info available. > async-timeout<5.0.0,>=4.0.0;: Installed. No version info available. > blockbuster: 1.5.24 > click: 8.1.8 > cloudpickle: 3.1.1 > cryptography: 43.0.3 > dataclasses-json<0.7,>=0.5.7: Installed. No version info available. > httpx: 0.28.1 > httpx-sse<1.0.0,>=0.4.0: Installed. No version info available. > jsonpatch<2.0,>=1.33: Installed. No version info available. > jsonschema-rs: 0.29.1 > langchain-anthropic;: Installed. No version info available. > langchain-aws;: Installed. No version info available. > langchain-cohere;: Installed. No version info available. > langchain-community;: Installed. No version info available. > langchain-core<0.4.0,>=0.3.40: Installed. No version info available. > langchain-core<1.0.0,>=0.3.41: Installed. No version info available. > langchain-core<1.0.0,>=0.3.45: Installed. No version info available. > langchain-deepseek;: Installed. No version info available. > langchain-fireworks;: Installed. No version info available. > langchain-google-genai;: Installed. No version info available. > langchain-google-vertexai;: Installed. No version info available. > langchain-groq;: Installed. No version info available. > langchain-huggingface;: Installed. No version info available. > langchain-mistralai;: Installed. No version info available. > langchain-ollama;: Installed. No version info available. > langchain-openai;: Installed. No version info available. > langchain-text-splitters<1.0.0,>=0.3.6: Installed. No version info available. > langchain-together;: Installed. No version info available. > langchain-xai;: Installed. No version info available. > langchain<1.0.0,>=0.3.20: Installed. No version info available. > langgraph: 0.4.5 > langgraph-checkpoint: 2.0.26 > langgraph-prebuilt<0.2.0,>=0.1.7: Installed. No version info available. > langgraph>=0.3.5: Installed. No version info available. > langsmith-pyo3: Installed. No version info available. > langsmith<0.4,>=0.1.125: Installed. No version info available. > langsmith<0.4,>=0.1.17: Installed. No version info available. > numpy<3,>=1.26.2: Installed. No version info available. > openai<2.0.0,>=1.66.3: Installed. No version info available. > orjson: 3.10.15 > packaging: 24.2 > packaging<25,>=23.2: Installed. No version info available. > pydantic: 2.10.6 > pydantic-settings<3.0.0,>=2.4.0: Installed. No version info available. > pydantic<3.0.0,>=2.5.2;: Installed. No version info available. > pydantic<3.0.0,>=2.7.4: Installed. No version info available. > pydantic<3.0.0,>=2.7.4;: Installed. No version info available. > pyjwt: 2.10.1 > pytest: Installed. No version info available. > python-dotenv: 1.0.1 > PyYAML>=5.3: Installed. No version info available. > requests: 2.32.3 > requests-toolbelt: 1.0.0 > requests<3,>=2: Installed. No version info available. > rich: Installed. No version info available. > SQLAlchemy<3,>=1.4: Installed. No version info available. > sse-starlette: 2.1.3 > starlette: 0.41.3 > structlog: 25.2.0 > tenacity: 8.5.0 > tenacity!=8.4.0,<10,>=8.1.0: Installed. No version info available. > tenacity!=8.4.0,<10.0.0,>=8.1.0: Installed. No version info available. > tiktoken<1,>=0.7: Installed. No version info available. > truststore: 0.10.1 > typing-extensions>=4.7: Installed. No version info available. > watchfiles: 0.20.0 > zstandard: 0.23.0
yindo added the duplicate label 2026-02-20 17:41:04 -05:00
yindo closed this issue 2026-02-20 17:41:04 -05:00
Author
Owner

@michalgala commented on GitHub (May 22, 2025):

I have changed the subgraph compilation by changing checkpointer=checkpointer to checkpointer=True

Now it resumes as expected.
However, the state in the subgraph is not updated.

--- Resuming ---
State updated with human_response: 35
  Current state (Main Graph) before resume stream: {'state_counter': 1, 'human_response': '35'}
PARENT: Entered `parent_node` a total of 2 times
  PARENT: state before subgraph.invoke: {'state_counter': 1, 'human_response': '35'}
SUBGRAPH: Entered `human_node` in sub-graph a total of 1 times
SUBGRAPH: `human_node` got an answer of 'None' from state.

The state before subgraph.invoke has human_response:35, but then the print in the human node shows that the state in the subgraph has not been updated.

@michalgala commented on GitHub (May 22, 2025): I have changed the subgraph compilation by changing `checkpointer=checkpointer` to `checkpointer=True` Now it resumes as expected. However, the state in the subgraph is not updated. ```` --- Resuming --- State updated with human_response: 35 Current state (Main Graph) before resume stream: {'state_counter': 1, 'human_response': '35'} PARENT: Entered `parent_node` a total of 2 times PARENT: state before subgraph.invoke: {'state_counter': 1, 'human_response': '35'} SUBGRAPH: Entered `human_node` in sub-graph a total of 1 times SUBGRAPH: `human_node` got an answer of 'None' from state. ```` The state before subgraph.invoke has human_response:35, but then the print in the human node shows that the state in the subgraph has not been updated.
Author
Owner

@sydney-runkle commented on GitHub (Jun 11, 2025):

Going to close this as a dupe of https://github.com/langchain-ai/langgraph/issues/4796 at this point, given that the resume behavior is resolved.

@sydney-runkle commented on GitHub (Jun 11, 2025): Going to close this as a dupe of https://github.com/langchain-ai/langgraph/issues/4796 at this point, given that the resume behavior is resolved.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#639