How to interrupt a subgraph, and insert a message, and then rerun graph? #168

Closed
opened 2026-02-20 17:30:09 -05:00 by yindo · 8 comments
Owner

Originally created by @gbaian10 on GitHub (Aug 5, 2024).

Checked other resources

  • I added a very descriptive title to this issue.
  • I searched the LangGraph/LangChain documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangGraph/LangChain rather than my code.
  • I am sure this is better as an issue rather than a GitHub discussion, since this is a LangGraph bug and not a design question.

Example Code

import uuid
from typing import Annotated, Literal, TypedDict

from dotenv import load_dotenv
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint import BaseCheckpointSaver, MemorySaver
from langgraph.graph import END, START, StateGraph, add_messages
from langgraph.graph.state import CompiledStateGraph

load_dotenv()


class State(TypedDict, total=False):
    messages: Annotated[list, add_messages]
    reply: str


def print_node(state: State, config: RunnableConfig) -> State:
    curr_node = config["metadata"]["langgraph_node"]
    print(f"This Node is '{curr_node}'")
    if curr_node == "1_A":
        return {"messages": [("ai", "Do you want to go to B or C?")]}
    return {"messages": [("ai", curr_node)]}


def go_to_b_or_c(state: State) -> Literal["B", "C"]:
    reply = state["reply"]
    if reply == "B":
        return "B"
    if reply == "C":
        return "C"
    raise ValueError("Invalid input")


def create_sub_graph(
    i: int, checkpointer: BaseCheckpointSaver | None = None
) -> CompiledStateGraph:
    A = f"{i}_A"
    REPLY = f"{i}_reply"
    B = f"{i}_B"
    C = f"{i}_C"

    builder = StateGraph(State)

    builder.add_node(A, print_node)
    builder.add_node(REPLY, lambda state: {"reply": state["messages"][-1].content})
    builder.add_node(B, print_node)
    builder.add_node(C, print_node)

    builder.add_edge(START, A)
    builder.add_edge(A, REPLY)
    builder.add_conditional_edges(REPLY, go_to_b_or_c, {"B": B, "C": C})
    builder.add_edge(B, END)
    builder.add_edge(C, END)

    return builder.compile(interrupt_before=[REPLY], checkpointer=checkpointer)


def run_graph(graph: CompiledStateGraph) -> None:
    config = {"configurable": {"thread_id": str(uuid.uuid4())}}
    for event in graph.stream({"messages": []}, config=config, stream_mode="values"):
        print(event)
        
    print(f"{'-' * 20} interrupt {'-' * 20}")
    graph.update_state(config, {"messages": [("human", "B")]})
    
    for event in graph.stream(None, config=config, stream_mode="values"):
        print(event)


if __name__ == "__main__":
    # check subgraph can run
    run_graph(create_sub_graph(1, MemorySaver()))

    print("\n" + "=" * 50 + "\n")

    main_builder = StateGraph(State)
    main_builder.add_node("entry", lambda _: {"messages": [("ai", "main_entry")]})
    main_builder.add_node("sub_app_1", create_sub_graph(1))
    main_builder.add_node("exit", lambda _: {"messages": [("ai", "main_exit")]})

    main_builder.add_edge(START, "entry")
    main_builder.add_edge("entry", "sub_app_1")
    main_builder.add_edge("sub_app_1", "exit")
    main_builder.add_edge("exit", END)

    app = main_builder.compile(checkpointer=MemorySaver())
    # app.get_graph(xray=1).draw_mermaid_png(output_file_path="example.png")
    run_graph(app)

Stdout

{'messages': []}
This Node is '1_A'
{'messages': [AIMessage(content='Do you want to go to B or C?', id='6dc577cb-2eed-4bb9-8423-bacebe1330a5')]}
-------------------- interrupt --------------------
{'messages': [AIMessage(content='Do you want to go to B or C?', id='6dc577cb-2eed-4bb9-8423-bacebe1330a5'), HumanMessage(content='B', id='b6a7b5a1-a146-47a9-b282-1cd133dd8dfb')], 'reply': 'B'}
This Node is '1_B'
{'messages': [AIMessage(content='Do you want to go to B or C?', id='6dc577cb-2eed-4bb9-8423-bacebe1330a5'), HumanMessage(content='B', id='b6a7b5a1-a146-47a9-b282-1cd133dd8dfb'), AIMessage(content='1_B', id='49b28ebc-e7b3-4d84-ae48-99a7b536fad7')], 'reply': 'B'}

==================================================

{'messages': []}
{'messages': [AIMessage(content='main_entry', id='380f46da-651d-47e0-a19e-48d298914ab8')]}
This Node is '1_A'
-------------------- interrupt --------------------
This Node is '1_A'

Description

I have already read How to create subgraphs .

But I don't know how to correctly use interrupt.

image

⬇️ interrupt before
interrupt_before

⬇️ interrupt after
image

Is this the expected behavior?

I'm not sure how the subgraph should operate correctly. Does it need to insert the same or a different checkpointer?
Or will it automatically use the parent's checkpointer?
Which checkpointer do I need to insert new information into?

System Info

langgraph==0.1.19
langchain==0.2.12
langchain-core==0.2.28

platform==linux
python-version==3.10.12

Originally created by @gbaian10 on GitHub (Aug 5, 2024). ### Checked other resources - [X] I added a very descriptive title to this issue. - [X] I searched the [LangGraph](https://langchain-ai.github.io/langgraph/)/LangChain documentation with the integrated search. - [X] I used the GitHub search to find a similar question and didn't find it. - [X] I am sure that this is a bug in LangGraph/LangChain rather than my code. - [X] I am sure this is better as an issue [rather than a GitHub discussion](https://github.com/langchain-ai/langgraph/discussions/new/choose), since this is a LangGraph bug and not a design question. ### Example Code ```python import uuid from typing import Annotated, Literal, TypedDict from dotenv import load_dotenv from langchain_core.runnables import RunnableConfig from langgraph.checkpoint import BaseCheckpointSaver, MemorySaver from langgraph.graph import END, START, StateGraph, add_messages from langgraph.graph.state import CompiledStateGraph load_dotenv() class State(TypedDict, total=False): messages: Annotated[list, add_messages] reply: str def print_node(state: State, config: RunnableConfig) -> State: curr_node = config["metadata"]["langgraph_node"] print(f"This Node is '{curr_node}'") if curr_node == "1_A": return {"messages": [("ai", "Do you want to go to B or C?")]} return {"messages": [("ai", curr_node)]} def go_to_b_or_c(state: State) -> Literal["B", "C"]: reply = state["reply"] if reply == "B": return "B" if reply == "C": return "C" raise ValueError("Invalid input") def create_sub_graph( i: int, checkpointer: BaseCheckpointSaver | None = None ) -> CompiledStateGraph: A = f"{i}_A" REPLY = f"{i}_reply" B = f"{i}_B" C = f"{i}_C" builder = StateGraph(State) builder.add_node(A, print_node) builder.add_node(REPLY, lambda state: {"reply": state["messages"][-1].content}) builder.add_node(B, print_node) builder.add_node(C, print_node) builder.add_edge(START, A) builder.add_edge(A, REPLY) builder.add_conditional_edges(REPLY, go_to_b_or_c, {"B": B, "C": C}) builder.add_edge(B, END) builder.add_edge(C, END) return builder.compile(interrupt_before=[REPLY], checkpointer=checkpointer) def run_graph(graph: CompiledStateGraph) -> None: config = {"configurable": {"thread_id": str(uuid.uuid4())}} for event in graph.stream({"messages": []}, config=config, stream_mode="values"): print(event) print(f"{'-' * 20} interrupt {'-' * 20}") graph.update_state(config, {"messages": [("human", "B")]}) for event in graph.stream(None, config=config, stream_mode="values"): print(event) if __name__ == "__main__": # check subgraph can run run_graph(create_sub_graph(1, MemorySaver())) print("\n" + "=" * 50 + "\n") main_builder = StateGraph(State) main_builder.add_node("entry", lambda _: {"messages": [("ai", "main_entry")]}) main_builder.add_node("sub_app_1", create_sub_graph(1)) main_builder.add_node("exit", lambda _: {"messages": [("ai", "main_exit")]}) main_builder.add_edge(START, "entry") main_builder.add_edge("entry", "sub_app_1") main_builder.add_edge("sub_app_1", "exit") main_builder.add_edge("exit", END) app = main_builder.compile(checkpointer=MemorySaver()) # app.get_graph(xray=1).draw_mermaid_png(output_file_path="example.png") run_graph(app) ``` ### Stdout ```plain {'messages': []} This Node is '1_A' {'messages': [AIMessage(content='Do you want to go to B or C?', id='6dc577cb-2eed-4bb9-8423-bacebe1330a5')]} -------------------- interrupt -------------------- {'messages': [AIMessage(content='Do you want to go to B or C?', id='6dc577cb-2eed-4bb9-8423-bacebe1330a5'), HumanMessage(content='B', id='b6a7b5a1-a146-47a9-b282-1cd133dd8dfb')], 'reply': 'B'} This Node is '1_B' {'messages': [AIMessage(content='Do you want to go to B or C?', id='6dc577cb-2eed-4bb9-8423-bacebe1330a5'), HumanMessage(content='B', id='b6a7b5a1-a146-47a9-b282-1cd133dd8dfb'), AIMessage(content='1_B', id='49b28ebc-e7b3-4d84-ae48-99a7b536fad7')], 'reply': 'B'} ================================================== {'messages': []} {'messages': [AIMessage(content='main_entry', id='380f46da-651d-47e0-a19e-48d298914ab8')]} This Node is '1_A' -------------------- interrupt -------------------- This Node is '1_A' ``` ### Description I have already read [How to create subgraphs](https://langchain-ai.github.io/langgraph/how-tos/subgraph/) . But I don't know how to correctly use interrupt. ![image](https://github.com/user-attachments/assets/19f2ce08-4d91-4d36-9c5a-5b6e4d76cb1f) ⬇️ interrupt before ![interrupt_before](https://github.com/user-attachments/assets/71a8cdf0-2008-415f-b5c9-5bf33e42d960) ⬇️ interrupt after ![image](https://github.com/user-attachments/assets/1013b205-4e58-41db-ab10-e4829537639d) Is this the expected behavior? I'm not sure how the subgraph should operate correctly. Does it need to insert the same or a different checkpointer? Or will it automatically use the parent's checkpointer? Which checkpointer do I need to insert new information into? ### System Info langgraph==0.1.19 langchain==0.2.12 langchain-core==0.2.28 platform==linux python-version==3.10.12
yindo closed this issue 2026-02-20 17:30:09 -05:00
Author
Owner

@vbarda commented on GitHub (Aug 5, 2024):

@gbaian10 we are working on full support for updating subgraph state on interrupt -- stay tuned! but to answer your question, subgraphs will automatically use the parent's checkpointer, you won't have to pass checkpointer separately to the subgraph

@vbarda commented on GitHub (Aug 5, 2024): @gbaian10 we are working on full support for updating subgraph state on interrupt -- stay tuned! but to answer your question, subgraphs will automatically use the parent's checkpointer, you won't have to pass checkpointer separately to the subgraph
Author
Owner

@gbaian10 commented on GitHub (Aug 5, 2024):

@vbarda Thank you for your reply.
Additionally, I found that when trying this, draw_mermaid_png might produce unexpected results in subgraph.

from typing import Annotated, TypedDict

from dotenv import load_dotenv
from langgraph.graph import END, START, StateGraph, add_messages
from langgraph.graph.state import CompiledStateGraph

load_dotenv()


class State(TypedDict, total=False):
    messages: Annotated[list, add_messages]


def nothing(_: State) -> None:
    return


def is_continue(state: State) -> bool:
    return state["messages"][-1].content in ["yes", "y"]


def create_sub_graph(i: int) -> CompiledStateGraph:
    A = f"{i}_A"
    B = f"{i}_B"

    builder = StateGraph(State)

    builder.add_node(A, nothing)
    builder.add_node(B, nothing)

    builder.add_edge(START, A)
    builder.add_conditional_edges(A, is_continue, {True: B, False: END})
    builder.add_edge(B, END)

    return builder.compile(interrupt_before=[A])


if __name__ == "__main__":
    sub_graph = create_sub_graph(i=1)
    sub_graph.get_graph(xray=1).draw_mermaid_png(output_file_path="example_1.png")

    main_builder = StateGraph(State)
    main_builder.add_node("sub_app", create_sub_graph(1))
    main_builder.add_edge(START, "sub_app")
    main_builder.add_edge("sub_app", END)
    main_graph = main_builder.compile()
    main_graph.get_graph(xray=1).draw_mermaid_png(output_file_path="example_2.png")

⬇️ example_1.png (Correct)
image

⬇️ example_2.png (Incorrect)
image

The __end__ node in the subgraph disappeared, and the False path also disappeared.

@gbaian10 commented on GitHub (Aug 5, 2024): @vbarda Thank you for your reply. Additionally, I found that when trying this, `draw_mermaid_png` might produce unexpected results in subgraph. ```py from typing import Annotated, TypedDict from dotenv import load_dotenv from langgraph.graph import END, START, StateGraph, add_messages from langgraph.graph.state import CompiledStateGraph load_dotenv() class State(TypedDict, total=False): messages: Annotated[list, add_messages] def nothing(_: State) -> None: return def is_continue(state: State) -> bool: return state["messages"][-1].content in ["yes", "y"] def create_sub_graph(i: int) -> CompiledStateGraph: A = f"{i}_A" B = f"{i}_B" builder = StateGraph(State) builder.add_node(A, nothing) builder.add_node(B, nothing) builder.add_edge(START, A) builder.add_conditional_edges(A, is_continue, {True: B, False: END}) builder.add_edge(B, END) return builder.compile(interrupt_before=[A]) if __name__ == "__main__": sub_graph = create_sub_graph(i=1) sub_graph.get_graph(xray=1).draw_mermaid_png(output_file_path="example_1.png") main_builder = StateGraph(State) main_builder.add_node("sub_app", create_sub_graph(1)) main_builder.add_edge(START, "sub_app") main_builder.add_edge("sub_app", END) main_graph = main_builder.compile() main_graph.get_graph(xray=1).draw_mermaid_png(output_file_path="example_2.png") ``` ⬇️ example_1.png (Correct) ![image](https://github.com/user-attachments/assets/b57a5848-cc86-46dd-b53d-c35e7a731bc3) ⬇️ example_2.png (Incorrect) ![image](https://github.com/user-attachments/assets/71c0380b-46bf-4f8b-8fa9-37245e23a4a7) The `__end__` node in the subgraph disappeared, and the `False path` also disappeared.
Author
Owner

@vbarda commented on GitHub (Sep 4, 2024):

We've added support for adding subgraph interrupts, resuming the subgraphs from the interrupts and updating the subgraph state. please check out this how-to guide for more information https://langchain-ai.github.io/langgraph/how-tos/subgraphs-manage-state/

@vbarda commented on GitHub (Sep 4, 2024): We've added support for adding subgraph interrupts, resuming the subgraphs from the interrupts and updating the subgraph state. please check out this how-to guide for more information https://langchain-ai.github.io/langgraph/how-tos/subgraphs-manage-state/
Author
Owner

@Arslan-Mehmood1 commented on GitHub (Oct 18, 2024):

@gbaian10 we are working on full support for updating subgraph state on interrupt -- stay tuned! but to answer your question, subgraphs will automatically use the parent's checkpointer, you won't have to pass checkpointer separately to the subgraph

it should be mentioned in documentation.

@Arslan-Mehmood1 commented on GitHub (Oct 18, 2024): > @gbaian10 we are working on full support for updating subgraph state on interrupt -- stay tuned! but to answer your question, subgraphs will automatically use the parent's checkpointer, you won't have to pass checkpointer separately to the subgraph it should be mentioned in documentation.
Author
Owner

@vbarda commented on GitHub (Oct 18, 2024):

@Arslan-Mehmood1 we added notes about this to this how-to guide https://langchain-ai.github.io/langgraph/how-tos/subgraph-persistence/. let me know if anything else is missing!

@vbarda commented on GitHub (Oct 18, 2024): @Arslan-Mehmood1 we added notes about this to this how-to guide https://langchain-ai.github.io/langgraph/how-tos/subgraph-persistence/. let me know if anything else is missing!
Author
Owner

@Arslan-Mehmood1 commented on GitHub (Oct 18, 2024):

@vbarda
Got it thanks.
There's also a bug I encountered yesterday and would like to report here

@Arslan-Mehmood1 commented on GitHub (Oct 18, 2024): @vbarda Got it thanks. There's also a bug I encountered yesterday and would like to report here
Author
Owner

@vbarda commented on GitHub (Oct 18, 2024):

@vbarda Got it thanks. There's also a bug I encountered yesterday and would like to report here

Please create a new issue with the bug!

@vbarda commented on GitHub (Oct 18, 2024): > @vbarda Got it thanks. There's also a bug I encountered yesterday and would like to report here Please create a new issue with the bug!
Author
Owner

@anmol-aidora commented on GitHub (Nov 26, 2024):

We've added support for adding subgraph interrupts, resuming the subgraphs from the interrupts and updating the subgraph state. please check out this how-to guide for more information https://langchain-ai.github.io/langgraph/how-tos/subgraphs-manage-state/

Hi @vbarda

In the examples, you are using different thread ids for every time that you are streaming the graph. How does it know where to resume from in that case?

image
@anmol-aidora commented on GitHub (Nov 26, 2024): > We've added support for adding subgraph interrupts, resuming the subgraphs from the interrupts and updating the subgraph state. please check out this how-to guide for more information https://langchain-ai.github.io/langgraph/how-tos/subgraphs-manage-state/ Hi @vbarda In the examples, you are using different thread ids for every time that you are streaming the graph. How does it know where to resume from in that case? <img width="980" alt="image" src="https://github.com/user-attachments/assets/ea59830c-2192-4ba6-9f57-cc0d7c601326">
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#168