Before interrupt_after, is it necessary to first decide which node (execute the path function)? #206

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

Originally created by @gbaian10 on GitHub (Aug 25, 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

from collections.abc import Callable
from typing import Annotated, Literal, TypedDict

from langchain_core.messages import AnyMessage
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, StateGraph, add_messages


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


def ai_message(msg: str) -> Callable[..., State]:
    def handler(_: State) -> State:
        return {"messages": [("ai", msg)]}

    return handler


def route(state: State) -> Literal["A", "B"]:
    print("This is the ROUTE function")
    # This will be printed twice
    # The first one is before `interrupt_after` and the second one is in `update`
    return "A" if state["reply"] == "A" else "B"


graph_builder = StateGraph(State)
graph_builder.set_entry_point("enter")
# It must be initialized first; otherwise, an KeyError will occur when the route function tries to get 'reply'.
graph_builder.add_node("enter", lambda _: {"reply": ""})
graph_builder.add_node("send_question", ai_message("Do you want to go to A or B?"))
graph_builder.add_node("A", ai_message("You are in A"))
graph_builder.add_node("B", ai_message("You are in B"))

graph_builder.add_edge("enter", "send_question")
graph_builder.add_conditional_edges("send_question", route)
graph_builder.add_edge("A", END)
graph_builder.add_edge("B", END)
memory = MemorySaver()
app = graph_builder.compile(checkpointer=memory, interrupt_after=["send_question"])


def print_divider(text: str) -> None:
    print("=" * 20 + text + "=" * 20)


thread_config = {"configurable": {"thread_id": "1"}}
print(app.invoke({"messages": []}, thread_config))

print_divider("interrupt_after")
print("next_node is ", app.get_state(thread_config).next)  # it will print ('B', )

print_divider("update")
app.update_state(thread_config, {"reply": "A"})
print(app.invoke(None, thread_config))

Error Message and Stack Trace (if applicable)

This is the ROUTE function
{'messages': [AIMessage(content='Do you want to go to A or B?', id='20531714-03d8-4e81-a74b-badea641d187')], 'reply': ''}
====================interrupt_after====================
next_node is  ('B',)
====================update====================
This is the ROUTE function
{'messages': [AIMessage(content='Do you want to go to A or B?', id='20531714-03d8-4e81-a74b-badea641d187'), AIMessage(content='You are in A', id='f761d343-f602-4236-b1ea-128b4d16022a')], 'reply': 'A'}

Description

Before each interrupt_after, it always needs to decide the next node to go to, so it must first execute the path function of add_conditional_edges.

When the graph is interrupted and execution resumes, the path function is executed again, determining a new path.
At this point, the path function executed during the interruption seems redundant, as the displayed next node might be incorrect.
However, I remember that in earlier versions, resuming the graph execution didn't re-execute this function. (But I think the old way was less intuitive, and the new approach is better.)

Is it possible to design it so that when encountering add_conditional_edges with interrupt_after, the path function isn't executed first, and the next_node points to multiple nodes?

In the example above, if it always has to execute first, I would have to initialize reply beforehand; otherwise, it would encounter a KeyError.
However, this pre-execution of the function doesn't make any sense since it seems to execute first and obtain a result that might not be the final choice.
And it will re-select and possibly change the final path when resuming execution.

System Info

langgraph==0.2.14
langgraph-checkpoint==1.0.5

platform==windows
python-version==3.12.5

Originally created by @gbaian10 on GitHub (Aug 25, 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 from collections.abc import Callable from typing import Annotated, Literal, TypedDict from langchain_core.messages import AnyMessage from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import END, StateGraph, add_messages class State(TypedDict, total=False): messages: Annotated[list[AnyMessage], add_messages] reply: str def ai_message(msg: str) -> Callable[..., State]: def handler(_: State) -> State: return {"messages": [("ai", msg)]} return handler def route(state: State) -> Literal["A", "B"]: print("This is the ROUTE function") # This will be printed twice # The first one is before `interrupt_after` and the second one is in `update` return "A" if state["reply"] == "A" else "B" graph_builder = StateGraph(State) graph_builder.set_entry_point("enter") # It must be initialized first; otherwise, an KeyError will occur when the route function tries to get 'reply'. graph_builder.add_node("enter", lambda _: {"reply": ""}) graph_builder.add_node("send_question", ai_message("Do you want to go to A or B?")) graph_builder.add_node("A", ai_message("You are in A")) graph_builder.add_node("B", ai_message("You are in B")) graph_builder.add_edge("enter", "send_question") graph_builder.add_conditional_edges("send_question", route) graph_builder.add_edge("A", END) graph_builder.add_edge("B", END) memory = MemorySaver() app = graph_builder.compile(checkpointer=memory, interrupt_after=["send_question"]) def print_divider(text: str) -> None: print("=" * 20 + text + "=" * 20) thread_config = {"configurable": {"thread_id": "1"}} print(app.invoke({"messages": []}, thread_config)) print_divider("interrupt_after") print("next_node is ", app.get_state(thread_config).next) # it will print ('B', ) print_divider("update") app.update_state(thread_config, {"reply": "A"}) print(app.invoke(None, thread_config)) ``` ### Error Message and Stack Trace (if applicable) ```shell This is the ROUTE function {'messages': [AIMessage(content='Do you want to go to A or B?', id='20531714-03d8-4e81-a74b-badea641d187')], 'reply': ''} ====================interrupt_after==================== next_node is ('B',) ====================update==================== This is the ROUTE function {'messages': [AIMessage(content='Do you want to go to A or B?', id='20531714-03d8-4e81-a74b-badea641d187'), AIMessage(content='You are in A', id='f761d343-f602-4236-b1ea-128b4d16022a')], 'reply': 'A'} ``` ### Description Before each `interrupt_after`, it always needs to decide the next node to go to, so it must first execute the path function of `add_conditional_edges`. When the graph is interrupted and execution resumes, the path function is executed again, determining a new path. At this point, the path function executed during the interruption seems redundant, as the displayed next node might be incorrect. However, I remember that in earlier versions, resuming the graph execution didn't re-execute this function. (But I think the old way was less intuitive, and the new approach is better.) Is it possible to design it so that when encountering `add_conditional_edges` with `interrupt_after`, the path function isn't executed first, and the `next_node` points to multiple nodes? In the example above, if it always has to execute first, I would have to initialize `reply` beforehand; otherwise, it would encounter a KeyError. However, this pre-execution of the function doesn't make any sense since it seems to execute first and obtain a result that might not be the final choice. And it will re-select and possibly change the final path when resuming execution. ### System Info langgraph==0.2.14 langgraph-checkpoint==1.0.5 platform==windows python-version==3.12.5
yindo closed this issue 2026-02-20 17:31:32 -05:00
Author
Owner

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

@gbaian10 it's actually not resuming that re-executes the conditional edge, it's the call to update_state, which is by design:

  • first call to route allows you to resume from the interrupt without having to update the state (otherwise you wouldn't know which nodes are next)
  • second call to route (via update_state) allows you to potentially change next nodes to run

in your case you have 2 options:

  • setting interrupt before send_question (if that would work for your case)
  • alternatively, if you still need to do interrupt_after you can just do state.get("reply") in route function and you won't need to add a separate enter node
@vbarda commented on GitHub (Aug 26, 2024): @gbaian10 it's actually not resuming that re-executes the conditional edge, it's the call to `update_state`, which is by design: * first call to `route` allows you to resume from the interrupt without having to update the state (otherwise you wouldn't know which nodes are next) * second call to `route` (via `update_state`) allows you to potentially change next nodes to run in your case you have 2 options: * setting interrupt before `send_question` (if that would work for your case) * alternatively, if you still need to do `interrupt_after` you can just do `state.get("reply")` in `route` function and you won't need to add a separate `enter` node
Author
Owner

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

@vbarda

Indeed, from my past attempts, I learned that it must first decide the next point before officially interrupting.
Initially, I thought the graph would execute the route and officially decide where to go only after resuming execution.
I don’t fully understand the underlying logic yet; is it possible that the next point can't point to multiple nodes?

As for the second execution of the route, I just learned that it is triggered by update_state.
I always thought it was triggered by the resumed execution of invoke.


Using interrupt_before in this situation does not meet the requirements because if you use interrupt_before, the user won't receive the actual question ("Do you want to go to A or B?").

In previous versions, it seems that update_state did not trigger the second route execution, correct?

In this situation, using before or after won't achieve the goal.
I can only accomplish this task by appending additional points before or after the interrupt node.

Using state.get("reply") is a great idea! I never thought of something so simple. However, if it doesn't re-execute upon resuming, it still won't work.
(There should have been a situation in the past where update_state didn't trigger execution, right? I hope I'm not mistaken.)

Anyway, thank you for your response!

@gbaian10 commented on GitHub (Aug 26, 2024): @vbarda Indeed, from my past attempts, I learned that it must first decide the next point before officially interrupting. Initially, I thought the graph would execute the route and officially decide where to go only after resuming execution. I don’t fully understand the underlying logic yet; is it possible that the next point can't point to multiple nodes? As for the second execution of the route, I just learned that it is triggered by `update_state`. I always thought it was triggered by the resumed execution of invoke. --- Using `interrupt_before` in this situation does not meet the requirements because if you use `interrupt_before`, the user won't receive the actual question ("Do you want to go to A or B?"). In previous versions, it seems that `update_state` did not trigger the second route execution, correct? In this situation, using `before` or `after` won't achieve the goal. I can only accomplish this task by appending additional points before or after the interrupt node. Using `state.get("reply")` is a great idea! I never thought of something so simple. However, if it doesn't re-execute upon resuming, it still won't work. (There should have been a situation in the past where `update_state` didn't trigger execution, right? I hope I'm not mistaken.) Anyway, thank you for your response!
Author
Owner

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

In previous versions, it seems that update_state did not trigger the second route execution, correct?
i am not sure, but the current behavior of re-executing the conditional edge callable on update_state is definitely the intended behavior

good point about user receiving the actual question -- i think what you'll need to do then is just introduce a node for collecting user input, similar to this https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/wait-user-input/. in that case interrupt_before should be sufficient

@vbarda commented on GitHub (Aug 26, 2024): > In previous versions, it seems that update_state did not trigger the second route execution, correct? i am not sure, but the current behavior of re-executing the conditional edge callable on `update_state` is definitely the intended behavior good point about user receiving the actual question -- i think what you'll need to do then is just introduce a node for collecting user input, similar to this https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/wait-user-input/. in that case `interrupt_before` should be sufficient
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#206