create_react_agent fails to resume after external update_state with ToolMessage (tried with astream/ainvoke) #568

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

Originally created by @ericzon on GitHub (Apr 9, 2025).

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

# To run this example, first of all install dependencies with poetry. 
# After that, just run:
# poetry run python minimal_langgraph_test_openai.py

import asyncio
import os
import uuid
import logging
from dotenv import load_dotenv

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from pydantic import BaseModel, Field
from langchain_core.tools import tool

from langgraph.checkpoint.sqlite import SqliteSaver 
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent

load_dotenv()
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

LLM_MODEL = "gpt-4o" 


class DataInputArgs(BaseModel):
    """Schema for arguments passed to the data processing tool."""
    data: str = Field(description="The data string that needs to be processed.")

@tool
def get_data_externally() -> str:
    """
    Retrieves data from a simulated external source when not provided initially by the user. 
    Use this tool FIRST when the user asks to process data but has NOT provided the data in their message.
    This tool obtains the data needed by the 'process_data' tool.
    """
    logger.info("--- SIMULATED TOOL CALL: get_data_externally ---")
    return "Placeholder: Data retrieval initiated externally." 


@tool(args_schema=DataInputArgs)
def process_data(data: str) -> str:
    """
    Processes the provided data string. Requires the 'data' argument containing the string.
    Use this tool ONLY AFTER 'get_data_externally' has successfully run and the data 
    has been provided back to you, or if the user provided the data directly.
    """
    logger.info(f"--- SIMULATED TOOL EXECUTION: process_data ---")
    logger.info(f"  Received data: {data[:100]}...")
    processed_result = f"Successfully processed data: '{data}'"
    logger.info(f"  Returning: {processed_result}")
    return processed_result


llm = ChatOpenAI(model="gpt-4o", temperature=0)


checkpointer = MemorySaver()

tools = [get_data_externally, process_data]

prompt_template = (
    "You are an agent tasked with processing data. "
    "You have the following tools available: 'get_data_externally', 'process_data'.\n"
    "IMPORTANT FLOW: When asked to process data:\n"
    "1. Check if the user provided the data in their message.\n"
    "2. If the data IS provided, directly use 'process_data' with that data.\n"
    "3. If the data IS NOT provided, YOU MUST FIRST use the 'get_data_externally' tool to retrieve the data.\n"
    "4. After 'get_data_externally' runs and you receive the data back (as a ToolMessage), YOU MUST THEN use 'process_data' with the retrieved data.\n"
    "Do not answer directly about processing data if you haven't processed it using the tools and the specific data."
)

graph = create_react_agent(
    llm,
    tools=tools,
    checkpointer=checkpointer,
    prompt=prompt_template,
    interrupt_before=["tools"] 
)

async def simulate_bot_flow():
    thread_id = str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}
    logger.info(f"Starting simulation with Thread ID: {thread_id}")

    user_input = "process the external data" # Input que no incluye los datos
    logger.info(f"Initial User Input: '{user_input}'")

    get_data_tool_call_id = None
    interrupted_for_get_data = False
    
    print("\nRUNNING INITIAL STREAM...")
    try:
        async for chunk in graph.astream({"messages": [("user", user_input)]}, config=config, stream_mode="updates"):
            logger.info(f"Initial Chunk Keys: {list(chunk.keys())}")
            
            agent_chunk = chunk.get('agent')
            if agent_chunk and isinstance(agent_chunk.get('messages', [None])[-1], AIMessage):
                last_ai_msg = agent_chunk['messages'][-1]
                if last_ai_msg.tool_calls:
                    logger.info(f"  Agent wants to call: {last_ai_msg.tool_calls}")
                    if last_ai_msg.tool_calls[0].get('name') == 'get_data_externally':
                        get_data_tool_call_id = last_ai_msg.tool_calls[0].get('id')
                        logger.info(f"  Captured get_data_externally tool_call_id: {get_data_tool_call_id}")

            if "__interrupt__" in chunk:
                logger.info("  >>> Interrupt Detected <<<")
                if get_data_tool_call_id: 
                    interrupted_for_get_data = True
                    logger.info("  Interrupt is for 'get_data_externally'. SIMULATING PAUSE.")
                    break
                else:
                    logger.warning("  Interrupt detected, but couldn't confirm it was for 'get_data_externally'.")
        
        if not interrupted_for_get_data:
            logger.error("Simulation Failed: Did not interrupt for 'get_data_externally'. Check LLM decision/prompt.")
            return

        print("\nSIMULATING EXTERNAL DATA FETCH AND STATE UPDATE...")
        simulated_data_content = f"This is the externally fetched data for {thread_id}"
        logger.info(f"  Simulated data content: '{simulated_data_content}'")
        
        tool_message = ToolMessage(content=simulated_data_content, tool_call_id=get_data_tool_call_id)
        logger.info(f"  Constructed ToolMessage for tool_call_id {get_data_tool_call_id}")

        graph.update_state(config, {"messages": [tool_message]})
        logger.info("  Graph state updated synchronously.")

        try:
            current_state = await graph.aget_state(config)
            logger.info("  --- State Before Resume ---")
            messages = current_state.values.get("messages", [])
            for msg in messages[-3:]: 
                logger.info(f"    {type(msg).__name__}: {str(msg)[:200]}...") # Limitar longitud
            logger.info("  -------------------------")
        except Exception as e:
             logger.error(f"  Error getting state before resume: {e}")

        print("\nATTEMPTING RESUMPTION with astream(None, stream_mode='debug')...")
        resumption_chunks_received = []
        agent_called_process_data = False
        final_response = None

        try:
            async for chunk in graph.astream(None, config=config, stream_mode="debug"):
                logger.info("  +++ Received DEBUG chunk after resume +++")
                logger.info(chunk) 
                resumption_chunks_received.append(chunk)

                if isinstance(chunk, dict) and 'agent' in chunk.get('node', ''):
                     output_messages = chunk.get('output', {}).get('messages', [])
                     if output_messages and isinstance(output_messages[-1], AIMessage):
                          ai_output : AIMessage = output_messages[-1]
                          if ai_output.tool_calls:
                              for tc in ai_output.tool_calls:
                                   if tc.get('name') == 'process_data':
                                       logger.info("  >>> Agent correctly decided to call 'process_data' after resume! <<<")
                                       agent_called_process_data = True
                          elif ai_output.content:
                               logger.warning(f"  Agent produced content instead of calling process_data: {ai_output.content}")
                               final_response = ai_output.content

            logger.info("Resumption stream finished.")

        except Exception as e_resume:
             logger.error(f"ERROR during resumption stream: {e_resume}", exc_info=True)


        print("\n--- SIMULATION RESULTS ---")
        if not resumption_chunks_received:
            logger.error("FAILURE: No chunks received after resumption.")
        elif agent_called_process_data:
            logger.info("SUCCESS (Potentially): Agent decided to call 'process_data'. Further steps (interrupt, execution, final answer) should follow.")
        else:
            logger.warning("FAILURE: Resumption finished, but agent did not call 'process_data'.")
            if final_response:
                logger.warning(f"  Agent's final response was: {final_response}")
            logger.warning("  Check DEBUG chunks above to see what the agent node did (if anything).")


    except Exception as e:
        logger.error(f"Unhandled exception in simulation: {e}", exc_info=True)

if __name__ == "__main__":
    if not os.getenv("OPENAI_API_KEY"):
        print("Error: OPENAI_API_KEY environment variable not set.")
    else:
        asyncio.run(simulate_bot_flow())

Error Message and Stack Trace (if applicable)

2025-04-09 11:38:42,503 - INFO - Starting simulation with Thread ID: e4dc31f9-aa01-4be0-bb5d-fbae01af2861
2025-04-09 11:38:42,503 - INFO - Initial User Input: 'process the external data'

RUNNING INITIAL STREAM...
2025-04-09 11:38:43,847 - INFO - HTTP Request: POST https://... "HTTP/1.1 200 OK"
2025-04-09 11:38:43,855 - INFO - Initial Chunk Keys: ['agent']
2025-04-09 11:38:43,855 - INFO -   Agent wants to call: [{'name': 'get_data_externally', 'args': {}, 'id': 'call_ZSN33LoRAWXnSjL0ya2LhAtQ', 'type': 'tool_call'}]    
2025-04-09 11:38:43,855 - INFO -   Captured get_data_externally tool_call_id: call_ZSN33LoRAWXnSjL0ya2LhAtQ
2025-04-09 11:38:43,855 - INFO - Initial Chunk Keys: ['__interrupt__']
2025-04-09 11:38:43,855 - INFO -   >>> Interrupt Detected <<<
2025-04-09 11:38:43,855 - INFO -   Interrupt is for 'get_data_externally'. SIMULATING PAUSE.

SIMULATING EXTERNAL DATA FETCH AND STATE UPDATE...
2025-04-09 11:38:43,857 - INFO -   Simulated data content: 'This is the externally fetched data for e4dc31f9-aa01-4be0-bb5d-fbae01af2861'
2025-04-09 11:38:43,857 - INFO -   Constructed ToolMessage for tool_call_id call_ZSN33LoRAWXnSjL0ya2LhAtQ
2025-04-09 11:38:43,858 - INFO -   Graph state updated synchronously.
2025-04-09 11:38:43,858 - INFO -   --- State Before Resume ---
2025-04-09 11:38:43,859 - INFO -     HumanMessage: content='process the external data' additional_kwargs={} response_metadata={} id='fe8e6950-181a-4697-9daa-6acca75e532c'...
2025-04-09 11:38:43,859 - INFO -     AIMessage: content='' additional_kwargs={'tool_calls': [{'id': 'call_ZSN33LoRAWXnSjL0ya2LhAtQ', 'function': {'arguments': '{}', 'name': 'get_data_externally'}, 'type': 'function'}], 'refusal': None} response_met...
2025-04-09 11:38:43,859 - INFO -     ToolMessage: content='This is the externally fetched data for e4dc31f9-aa01-4be0-bb5d-fbae01af2861' id='45fa5e80-53aa-4e60-b9db-a95e71be34a7' tool_call_id='call_ZSN33LoRAWXnSjL0ya2LhAtQ'...
2025-04-09 11:38:43,860 - INFO -   -------------------------

ATTEMPTING RESUMPTION with astream(None, stream_mode='debug')...
2025-04-09 11:38:43,861 - INFO -   +++ Received DEBUG chunk after resume +++
2025-04-09 11:38:43,861 - INFO - {'type': 'checkpoint', 'timestamp': '2025-04-09T09:38:43.858381+00:00', 'step': 2, 'payload': {'config': {'tags': [], 'metadata': ChainMap({'thread_id': 'e4dc31f9-aa01-4be0-bb5d-fbae01af2861'}), 'callbacks': None, 'recursion_limit': 25, 'configurable': {'checkpoint_ns': '', 'thread_id': 'e4dc31f9-aa01-4be0-bb5d-fbae01af2861', 'checkpoint_id': '1f015266-d80c-6e09-8002-23d48d4404f4'}}, 'parent_config': {'configurable': {'thread_id': 'e4dc31f9-aa01-4be0-bb5d-fbae01af2861', 'checkpoint_ns': '', 'checkpoint_id': '1f015266-d804-6539-8001-54e10a63b667'}}, 'values': {'messages': [HumanMessage(content='process the external data', additional_kwargs={}, response_metadata={}, id='fe8e6950-181a-4697-9daa-6acca75e532c'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_ZSN33LoRAWXnSjL0ya2LhAtQ', 'function': {'arguments': '{}', 'name': 'get_data_externally'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 314, 'total_tokens': 328, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_65792305e4', 'id': 'chatcmpl-BKMBG0VWT1HORCWdKy6TPMnqyGx5i', 'prompt_filter_results': [{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}], 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-a7fb3038-b000-4510-92ae-1dc87694d33b-0', tool_calls=[{'name': 'get_data_externally', 'args': {}, 'id': 'call_ZSN33LoRAWXnSjL0ya2LhAtQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 314, 'output_tokens': 14, 'total_tokens': 328, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='This is the externally fetched data for e4dc31f9-aa01-4be0-bb5d-fbae01af2861', id='45fa5e80-53aa-4e60-b9db-a95e71be34a7', tool_call_id='call_ZSN33LoRAWXnSjL0ya2LhAtQ')]}, 'metadata': {'source': 'update', 'writes': {'agent': {'messages': [ToolMessage(content='This is the externally fetched data for e4dc31f9-aa01-4be0-bb5d-fbae01af2861', id='45fa5e80-53aa-4e60-b9db-a95e71be34a7', tool_call_id='call_ZSN33LoRAWXnSjL0ya2LhAtQ')]}}, 'step': 2, 'parents': {}, 'thread_id': 'e4dc31f9-aa01-4be0-bb5d-fbae01af2861'}, 'next': [], 'tasks': []}}
2025-04-09 11:38:43,861 - INFO - Resumption stream finished.

--- SIMULATION RESULTS ---
2025-04-09 11:38:43,862 - WARNING - FAILURE: Resumption finished, but agent did not call 'process_data'.
2025-04-09 11:38:43,862 - WARNING -   Check DEBUG chunks above to see what the agent node did (if anything).

Description

Hi,

I'm encountering an issue where a graph created using create_react_agent fails to continue its execution loop after its state is updated externally with a ToolMessage and execution is resumed via astream(None, ...) or ainvoke(None, ...).

The goal is to implement a two-step tool process:

An initial tool (get_data_externally) is called by the agent when data is missing.
Execution is interrupted (interrupt_before=["tools"]).
The application fetches the data externally (simulated in the MRE).
The application updates the graph state using graph.update_state() to add a ToolMessage containing the fetched data, linked to the first tool call ID.
The application resumes execution using graph.astream(None, ...) or graph.ainvoke(None, ...).

Steps to Reproduce:

Run the provided Minimal Reproducible Example script below.

Expected Behavior:

After graph.update_state adds the ToolMessage and graph.astream(None, ...) (or ainvoke) is called:

The graph execution should resume.
The internal routing should follow the tools -> agent edge (which exists in the graph structure, confirmed via visualization).
The agent node (LLM) should be invoked again with the updated state (including the new ToolMessage).
The LLM should see the fetched data in the ToolMessage and, following the prompt, decide to call the second tool (process_data) with this data.
Subsequent chunks (for the process_data tool call, interrupt, execution, final response) should be streamed or included in the final ainvoke result.

Actual Behavior:

After graph.update_state adds the ToolMessage and graph.astream(None, ...) or ainvoke(None, ...) is called:

The graph execution terminates prematurely.
When using astream(None, ..., stream_mode="debug"), the only chunk yielded after resumption is a single {'type': 'checkpoint', ...} chunk, confirming the state update was processed internally by the checkpointer.
No further chunks indicating the execution of the agent node or any subsequent steps are received. The stream finishes immediately after the checkpoint chunk.
When using ainvoke(None, ...), the call completes successfully but returns a final state identical to the state before the ainvoke call (i.e., ending with the injected ToolMessage). It does not execute the process_data tool or generate a final response based on it.
The agent loop does not continue as expected.

Additional Context:

The agent graph structure generated by create_react_agent (confirmed via visualization) correctly includes the tools -> agent loopback edge.
The issue has been reproduced with both MemorySaver and SqliteSaver.
Using ainvoke(None, ...) instead of astream(None, ...) for resumption also fails to continue the agent loop, returning the state immediately after the ToolMessage was added.
Using different prompts (including highly explicit ones describing the flow) did not resolve the issue.

System Info

O.S: Windows 11
Python version: 3.12.3
Model: GPT-4o (2024-05-13)

Main dependencies:

name = "langgraph"
version = "0.3.27"

name = "langchain"
version = "0.3.23"

name = "langchain-openai"
version = "0.3.12"

name = "openai"
version = "1.72.0"

name = "pydantic"
version = "2.11.3"

Originally created by @ericzon on GitHub (Apr 9, 2025). ### 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 # To run this example, first of all install dependencies with poetry. # After that, just run: # poetry run python minimal_langgraph_test_openai.py import asyncio import os import uuid import logging from dotenv import load_dotenv from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, AIMessage, ToolMessage from pydantic import BaseModel, Field from langchain_core.tools import tool from langgraph.checkpoint.sqlite import SqliteSaver from langgraph.checkpoint.memory import MemorySaver from langgraph.prebuilt import create_react_agent load_dotenv() logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) LLM_MODEL = "gpt-4o" class DataInputArgs(BaseModel): """Schema for arguments passed to the data processing tool.""" data: str = Field(description="The data string that needs to be processed.") @tool def get_data_externally() -> str: """ Retrieves data from a simulated external source when not provided initially by the user. Use this tool FIRST when the user asks to process data but has NOT provided the data in their message. This tool obtains the data needed by the 'process_data' tool. """ logger.info("--- SIMULATED TOOL CALL: get_data_externally ---") return "Placeholder: Data retrieval initiated externally." @tool(args_schema=DataInputArgs) def process_data(data: str) -> str: """ Processes the provided data string. Requires the 'data' argument containing the string. Use this tool ONLY AFTER 'get_data_externally' has successfully run and the data has been provided back to you, or if the user provided the data directly. """ logger.info(f"--- SIMULATED TOOL EXECUTION: process_data ---") logger.info(f" Received data: {data[:100]}...") processed_result = f"Successfully processed data: '{data}'" logger.info(f" Returning: {processed_result}") return processed_result llm = ChatOpenAI(model="gpt-4o", temperature=0) checkpointer = MemorySaver() tools = [get_data_externally, process_data] prompt_template = ( "You are an agent tasked with processing data. " "You have the following tools available: 'get_data_externally', 'process_data'.\n" "IMPORTANT FLOW: When asked to process data:\n" "1. Check if the user provided the data in their message.\n" "2. If the data IS provided, directly use 'process_data' with that data.\n" "3. If the data IS NOT provided, YOU MUST FIRST use the 'get_data_externally' tool to retrieve the data.\n" "4. After 'get_data_externally' runs and you receive the data back (as a ToolMessage), YOU MUST THEN use 'process_data' with the retrieved data.\n" "Do not answer directly about processing data if you haven't processed it using the tools and the specific data." ) graph = create_react_agent( llm, tools=tools, checkpointer=checkpointer, prompt=prompt_template, interrupt_before=["tools"] ) async def simulate_bot_flow(): thread_id = str(uuid.uuid4()) config = {"configurable": {"thread_id": thread_id}} logger.info(f"Starting simulation with Thread ID: {thread_id}") user_input = "process the external data" # Input que no incluye los datos logger.info(f"Initial User Input: '{user_input}'") get_data_tool_call_id = None interrupted_for_get_data = False print("\nRUNNING INITIAL STREAM...") try: async for chunk in graph.astream({"messages": [("user", user_input)]}, config=config, stream_mode="updates"): logger.info(f"Initial Chunk Keys: {list(chunk.keys())}") agent_chunk = chunk.get('agent') if agent_chunk and isinstance(agent_chunk.get('messages', [None])[-1], AIMessage): last_ai_msg = agent_chunk['messages'][-1] if last_ai_msg.tool_calls: logger.info(f" Agent wants to call: {last_ai_msg.tool_calls}") if last_ai_msg.tool_calls[0].get('name') == 'get_data_externally': get_data_tool_call_id = last_ai_msg.tool_calls[0].get('id') logger.info(f" Captured get_data_externally tool_call_id: {get_data_tool_call_id}") if "__interrupt__" in chunk: logger.info(" >>> Interrupt Detected <<<") if get_data_tool_call_id: interrupted_for_get_data = True logger.info(" Interrupt is for 'get_data_externally'. SIMULATING PAUSE.") break else: logger.warning(" Interrupt detected, but couldn't confirm it was for 'get_data_externally'.") if not interrupted_for_get_data: logger.error("Simulation Failed: Did not interrupt for 'get_data_externally'. Check LLM decision/prompt.") return print("\nSIMULATING EXTERNAL DATA FETCH AND STATE UPDATE...") simulated_data_content = f"This is the externally fetched data for {thread_id}" logger.info(f" Simulated data content: '{simulated_data_content}'") tool_message = ToolMessage(content=simulated_data_content, tool_call_id=get_data_tool_call_id) logger.info(f" Constructed ToolMessage for tool_call_id {get_data_tool_call_id}") graph.update_state(config, {"messages": [tool_message]}) logger.info(" Graph state updated synchronously.") try: current_state = await graph.aget_state(config) logger.info(" --- State Before Resume ---") messages = current_state.values.get("messages", []) for msg in messages[-3:]: logger.info(f" {type(msg).__name__}: {str(msg)[:200]}...") # Limitar longitud logger.info(" -------------------------") except Exception as e: logger.error(f" Error getting state before resume: {e}") print("\nATTEMPTING RESUMPTION with astream(None, stream_mode='debug')...") resumption_chunks_received = [] agent_called_process_data = False final_response = None try: async for chunk in graph.astream(None, config=config, stream_mode="debug"): logger.info(" +++ Received DEBUG chunk after resume +++") logger.info(chunk) resumption_chunks_received.append(chunk) if isinstance(chunk, dict) and 'agent' in chunk.get('node', ''): output_messages = chunk.get('output', {}).get('messages', []) if output_messages and isinstance(output_messages[-1], AIMessage): ai_output : AIMessage = output_messages[-1] if ai_output.tool_calls: for tc in ai_output.tool_calls: if tc.get('name') == 'process_data': logger.info(" >>> Agent correctly decided to call 'process_data' after resume! <<<") agent_called_process_data = True elif ai_output.content: logger.warning(f" Agent produced content instead of calling process_data: {ai_output.content}") final_response = ai_output.content logger.info("Resumption stream finished.") except Exception as e_resume: logger.error(f"ERROR during resumption stream: {e_resume}", exc_info=True) print("\n--- SIMULATION RESULTS ---") if not resumption_chunks_received: logger.error("FAILURE: No chunks received after resumption.") elif agent_called_process_data: logger.info("SUCCESS (Potentially): Agent decided to call 'process_data'. Further steps (interrupt, execution, final answer) should follow.") else: logger.warning("FAILURE: Resumption finished, but agent did not call 'process_data'.") if final_response: logger.warning(f" Agent's final response was: {final_response}") logger.warning(" Check DEBUG chunks above to see what the agent node did (if anything).") except Exception as e: logger.error(f"Unhandled exception in simulation: {e}", exc_info=True) if __name__ == "__main__": if not os.getenv("OPENAI_API_KEY"): print("Error: OPENAI_API_KEY environment variable not set.") else: asyncio.run(simulate_bot_flow()) ``` ### Error Message and Stack Trace (if applicable) ```shell 2025-04-09 11:38:42,503 - INFO - Starting simulation with Thread ID: e4dc31f9-aa01-4be0-bb5d-fbae01af2861 2025-04-09 11:38:42,503 - INFO - Initial User Input: 'process the external data' RUNNING INITIAL STREAM... 2025-04-09 11:38:43,847 - INFO - HTTP Request: POST https://... "HTTP/1.1 200 OK" 2025-04-09 11:38:43,855 - INFO - Initial Chunk Keys: ['agent'] 2025-04-09 11:38:43,855 - INFO - Agent wants to call: [{'name': 'get_data_externally', 'args': {}, 'id': 'call_ZSN33LoRAWXnSjL0ya2LhAtQ', 'type': 'tool_call'}] 2025-04-09 11:38:43,855 - INFO - Captured get_data_externally tool_call_id: call_ZSN33LoRAWXnSjL0ya2LhAtQ 2025-04-09 11:38:43,855 - INFO - Initial Chunk Keys: ['__interrupt__'] 2025-04-09 11:38:43,855 - INFO - >>> Interrupt Detected <<< 2025-04-09 11:38:43,855 - INFO - Interrupt is for 'get_data_externally'. SIMULATING PAUSE. SIMULATING EXTERNAL DATA FETCH AND STATE UPDATE... 2025-04-09 11:38:43,857 - INFO - Simulated data content: 'This is the externally fetched data for e4dc31f9-aa01-4be0-bb5d-fbae01af2861' 2025-04-09 11:38:43,857 - INFO - Constructed ToolMessage for tool_call_id call_ZSN33LoRAWXnSjL0ya2LhAtQ 2025-04-09 11:38:43,858 - INFO - Graph state updated synchronously. 2025-04-09 11:38:43,858 - INFO - --- State Before Resume --- 2025-04-09 11:38:43,859 - INFO - HumanMessage: content='process the external data' additional_kwargs={} response_metadata={} id='fe8e6950-181a-4697-9daa-6acca75e532c'... 2025-04-09 11:38:43,859 - INFO - AIMessage: content='' additional_kwargs={'tool_calls': [{'id': 'call_ZSN33LoRAWXnSjL0ya2LhAtQ', 'function': {'arguments': '{}', 'name': 'get_data_externally'}, 'type': 'function'}], 'refusal': None} response_met... 2025-04-09 11:38:43,859 - INFO - ToolMessage: content='This is the externally fetched data for e4dc31f9-aa01-4be0-bb5d-fbae01af2861' id='45fa5e80-53aa-4e60-b9db-a95e71be34a7' tool_call_id='call_ZSN33LoRAWXnSjL0ya2LhAtQ'... 2025-04-09 11:38:43,860 - INFO - ------------------------- ATTEMPTING RESUMPTION with astream(None, stream_mode='debug')... 2025-04-09 11:38:43,861 - INFO - +++ Received DEBUG chunk after resume +++ 2025-04-09 11:38:43,861 - INFO - {'type': 'checkpoint', 'timestamp': '2025-04-09T09:38:43.858381+00:00', 'step': 2, 'payload': {'config': {'tags': [], 'metadata': ChainMap({'thread_id': 'e4dc31f9-aa01-4be0-bb5d-fbae01af2861'}), 'callbacks': None, 'recursion_limit': 25, 'configurable': {'checkpoint_ns': '', 'thread_id': 'e4dc31f9-aa01-4be0-bb5d-fbae01af2861', 'checkpoint_id': '1f015266-d80c-6e09-8002-23d48d4404f4'}}, 'parent_config': {'configurable': {'thread_id': 'e4dc31f9-aa01-4be0-bb5d-fbae01af2861', 'checkpoint_ns': '', 'checkpoint_id': '1f015266-d804-6539-8001-54e10a63b667'}}, 'values': {'messages': [HumanMessage(content='process the external data', additional_kwargs={}, response_metadata={}, id='fe8e6950-181a-4697-9daa-6acca75e532c'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_ZSN33LoRAWXnSjL0ya2LhAtQ', 'function': {'arguments': '{}', 'name': 'get_data_externally'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 14, 'prompt_tokens': 314, 'total_tokens': 328, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_65792305e4', 'id': 'chatcmpl-BKMBG0VWT1HORCWdKy6TPMnqyGx5i', 'prompt_filter_results': [{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'self_harm': {'filtered': False, 'severity': 'safe'}, 'sexual': {'filtered': False, 'severity': 'safe'}, 'violence': {'filtered': False, 'severity': 'safe'}}}], 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-a7fb3038-b000-4510-92ae-1dc87694d33b-0', tool_calls=[{'name': 'get_data_externally', 'args': {}, 'id': 'call_ZSN33LoRAWXnSjL0ya2LhAtQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 314, 'output_tokens': 14, 'total_tokens': 328, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='This is the externally fetched data for e4dc31f9-aa01-4be0-bb5d-fbae01af2861', id='45fa5e80-53aa-4e60-b9db-a95e71be34a7', tool_call_id='call_ZSN33LoRAWXnSjL0ya2LhAtQ')]}, 'metadata': {'source': 'update', 'writes': {'agent': {'messages': [ToolMessage(content='This is the externally fetched data for e4dc31f9-aa01-4be0-bb5d-fbae01af2861', id='45fa5e80-53aa-4e60-b9db-a95e71be34a7', tool_call_id='call_ZSN33LoRAWXnSjL0ya2LhAtQ')]}}, 'step': 2, 'parents': {}, 'thread_id': 'e4dc31f9-aa01-4be0-bb5d-fbae01af2861'}, 'next': [], 'tasks': []}} 2025-04-09 11:38:43,861 - INFO - Resumption stream finished. --- SIMULATION RESULTS --- 2025-04-09 11:38:43,862 - WARNING - FAILURE: Resumption finished, but agent did not call 'process_data'. 2025-04-09 11:38:43,862 - WARNING - Check DEBUG chunks above to see what the agent node did (if anything). ``` ### Description Hi, I'm encountering an issue where a graph created using create_react_agent fails to continue its execution loop after its state is updated externally with a ToolMessage and execution is resumed via astream(None, ...) or ainvoke(None, ...). The goal is to implement a two-step tool process: An initial tool (get_data_externally) is called by the agent when data is missing. Execution is interrupted (interrupt_before=["tools"]). The application fetches the data externally (simulated in the MRE). The application updates the graph state using graph.update_state() to add a ToolMessage containing the fetched data, linked to the first tool call ID. The application resumes execution using graph.astream(None, ...) or graph.ainvoke(None, ...). **Steps to Reproduce:** Run the provided Minimal Reproducible Example script below. **Expected Behavior:** After graph.update_state adds the ToolMessage and graph.astream(None, ...) (or ainvoke) is called: The graph execution should resume. The internal routing should follow the tools -> agent edge (which exists in the graph structure, confirmed via visualization). The agent node (LLM) should be invoked again with the updated state (including the new ToolMessage). The LLM should see the fetched data in the ToolMessage and, following the prompt, decide to call the second tool (process_data) with this data. Subsequent chunks (for the process_data tool call, interrupt, execution, final response) should be streamed or included in the final ainvoke result. **Actual Behavior:** After graph.update_state adds the ToolMessage and graph.astream(None, ...) or ainvoke(None, ...) is called: The graph execution terminates prematurely. When using astream(None, ..., stream_mode="debug"), the only chunk yielded after resumption is a single {'type': 'checkpoint', ...} chunk, confirming the state update was processed internally by the checkpointer. No further chunks indicating the execution of the agent node or any subsequent steps are received. The stream finishes immediately after the checkpoint chunk. When using ainvoke(None, ...), the call completes successfully but returns a final state identical to the state before the ainvoke call (i.e., ending with the injected ToolMessage). It does not execute the process_data tool or generate a final response based on it. The agent loop does not continue as expected. **Additional Context:** The agent graph structure generated by create_react_agent (confirmed via visualization) correctly includes the tools -> agent loopback edge. The issue has been reproduced with both MemorySaver and SqliteSaver. Using ainvoke(None, ...) instead of astream(None, ...) for resumption also fails to continue the agent loop, returning the state immediately after the ToolMessage was added. Using different prompts (including highly explicit ones describing the flow) did not resolve the issue. ### System Info O.S: Windows 11 Python version: 3.12.3 Model: GPT-4o (2024-05-13) Main dependencies: name = "langgraph" version = "0.3.27" name = "langchain" version = "0.3.23" name = "langchain-openai" version = "0.3.12" name = "openai" version = "1.72.0" name = "pydantic" version = "2.11.3"
yindo closed this issue 2026-02-20 17:40:45 -05:00
Author
Owner

@vbarda commented on GitHub (Apr 9, 2025):

@ericzon i think you should change your .update_state command to this:

graph.update_state(config, {"messages": [tool_message]}, as_node="tools")
@vbarda commented on GitHub (Apr 9, 2025): @ericzon i think you should change your `.update_state` command to this: ```python graph.update_state(config, {"messages": [tool_message]}, as_node="tools") ```
Author
Owner

@ericzon commented on GitHub (Apr 9, 2025):

Thank you very much @vbarda that was the missing part for making it work.

@ericzon commented on GitHub (Apr 9, 2025): Thank you very much @vbarda that was the missing part for making it work.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#568