Interrupts missing in thread_state on self-hosted LangGraph (1.x) #1032

Open
opened 2026-02-20 17:42:49 -05:00 by yindo · 2 comments
Owner

Originally created by @akbindal on GitHub (Oct 30, 2025).

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.

Example Code

import asyncio
from uuid import uuid4

from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain_core.tools import tool
from langgraph.types import Command, interrupt
from langgraph_sdk import get_client
from langchain_openai import ChatOpenAI

LANGGRAPH_SERVER_URL="http://SERVER.URL"

# Tool that generates an interrupt
@tool
def request_human_feedback(question: str) -> str:
    """Request feedback from a human user."""
    feedback = interrupt(
        {
            "type": "human_feedback_request",
            "question": question,
            "message": f"Please provide feedback on: {question}",
        }
    )
    return f"Human feedback received: {feedback}"


@tool
def return_four_coffee() -> str:
    """Return four coffee."""
    raise Exception("This is a test error")
    return 4


# Create the agent with checkpointer for interrupts
model = ChatOpenAI()  # model="gpt-4o-mini")
tools = [request_human_feedback, return_four_coffee]

agent = create_agent(
    model=model,
    tools=tools,
    system_prompt="You are a helpful assistant that can request human feedback when needed.",
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "return_four_coffee": {
                    "allowed_decisions": ["approve", "edit", "reject"],
                }
            },
            description_prefix="Review return four",
        )
    ],
)

async def make_agent(config):
    return agent.with_config(config)

async def run_agent_with_client(thread_id: str, user_input: str):
    """Run the agent with client and handle interrupts."""
    client = get_client(url=LANGGRAPH_SERVER_URL)
    config = {"configurable": {"thread_id": thread_id}}

    print(f"🚀 Starting agent run for: {user_input}")

    interrupt_detected = False
    # create thread if not exists
    await client.threads.create(thread_id=thread_id)
    # Stream the agent's execution
    streaming_interrupted_detected = False
    async for chunk in client.runs.stream(
        thread_id,
        "test_agent",
        input={"messages": [{"role": "user", "content": user_input}]},
        config=config,
        stream_mode="updates",
    ):
        print(f"📄 Agent processing: {chunk}")

        # Check if this chunk indicates an interrupt
        if hasattr(chunk, "data") and chunk.data and "__interrupt__" in chunk.data:
            interrupts = chunk.data["__interrupt__"]
            print(f"⏸️ INTERRUPT DETECTED DURING STREAMING: {interrupts}")
            streaming_interrupted_detected = True
    print(
        f"✅ Streaming complete. Interrupt detected: {streaming_interrupted_detected}"
    )

    # Always get the thread state at the end to check for interrupts
    try:
        thread_state = await client.threads.get_state(thread_id)

        # Check for interrupts in the thread state
        interrupts_in_state = thread_state.get("interrupts", [])
        print(f"📊 Interrupt from Thread State: {interrupts_in_state}")

        if interrupts_in_state:
            interrupt_detected = True
            print(f"🚨 INTERRUPTS FOUND IN THREAD STATE: {interrupts_in_state}")
            for interrupt in interrupts_in_state:
                print(f"   - Interrupt: {interrupt.get('value', 'No value')}")
        else:
            print("✨ No interrupts found in thread state")

        # interrupt detected in both streaming and thread state

        return (
            thread_state,
            streaming_interrupted_detected,
            interrupt_detected,
        )

    except Exception as e:
        print(f"❌ Error getting thread state: {e}")
        return None, interrupt_detected, []


async def test_interrupt_detection_via_client(user_input: str):
    try:
        (
            thread_state,
            streaming_interrupted_detected,
            interrupt_detected,
        ) = await run_agent_with_client(
            str(uuid4()),
            user_input,
        )
        print(
            f"📈 Results: streaming_interrupted_detected={streaming_interrupted_detected}, thread_state_interrupt_detected={interrupt_detected}"
        )

        if streaming_interrupted_detected and interrupt_detected:
            print("✅ Interrupt detected in both streaming and thread state")
        else:
            print("❌ Interrupt not detected in both streaming and thread state")

    except Exception as e:
        print(f"❌ Client test failed: {e}")
    print()


# Demo function
async def demo():

    # Test 3: Client-based streaming with interrupt detection
    print("Test 1: Agent streaming with tool interrupt detection")
    await test_interrupt_detection_via_client(
        "Please request human feedback on my project idea: building an AI assistant."
    )

    print("--------------------------------")
    print("Test 2: Agent streaming with HITL interrupt detection")
    await test_interrupt_detection_via_client(
        "Please return four coffee",
    )

    print("🎉 Demo complete! The agent successfully handled interrupts and streaming.")


if __name__ == "__main__":
    asyncio.run(demo())

Error Message and Stack Trace (if applicable)

=== LangGraph Agent with Interrupt Demo ===

Test 1: Agent streaming with tool interrupt detection
🚀 Starting agent run for: Please request human feedback on my project idea: building an AI assistant.
📄 Agent processing: StreamPart(event='metadata', data={'run_id': '019a3440-4902-76ae-bef5-a9720c8b6f7d', 'attempt': 1})
📄 Agent processing: StreamPart(event='updates', data={'model': {'messages': [{'content': '', 'additional_kwargs': {'refusal': None}, 'response_metadata': {'token_usage': {'completion_tokens': 31, 'prompt_tokens': 87, 'total_tokens': 118, '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_provider': 'openai', 'model_name': 'gpt-4.1-2025-04-14', 'system_fingerprint': 'fp_f99638a8d7', 'id': 'chatcmpl-CWIOBWryHlzVdv3pv6gxxwkcsPNTj', 'prompt_filter_results': [{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, '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': {}}, 'type': 'ai', 'name': None, 'id': 'lc_run--64407d7b-e822-4a83-bae7-d4669d9c8d10-0', 'tool_calls': [{'name': 'request_human_feedback', 'args': {'question': "Can you provide feedback on the user's project idea: building an AI assistant?"}, 'id': 'call_Vxn7OKXErfGksT9eIxIzAM1B', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 87, 'output_tokens': 31, 'total_tokens': 118, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}}]}})
📄 Agent processing: StreamPart(event='updates', data={'HumanInTheLoopMiddleware.after_model': None})
📄 Agent processing: StreamPart(event='updates', data={'__interrupt__': [{'value': {'type': 'human_feedback_request', 'question': "Can you provide feedback on the user's project idea: building an AI assistant?", 'message': "Please provide feedback on: Can you provide feedback on the user's project idea: building an AI assistant?"}, 'id': '79ffa8cc8a8fcbacadc4fbf1d72f79f8'}]})
⏸️ INTERRUPT DETECTED DURING STREAMING: [{'value': {'type': 'human_feedback_request', 'question': "Can you provide feedback on the user's project idea: building an AI assistant?", 'message': "Please provide feedback on: Can you provide feedback on the user's project idea: building an AI assistant?"}, 'id': '79ffa8cc8a8fcbacadc4fbf1d72f79f8'}]
✅ Streaming complete. Interrupt detected: True
📊 Interrupt from Thread State: []
✨ No interrupts found in thread state
📈 Results: streaming_interrupted_detected=True, thread_state_interrupt_detected=False
❌ Interrupt not detected in both streaming and thread state

--------------------------------
Test 2: Agent streaming with HITL interrupt detection
🚀 Starting agent run for: Please return four coffee
📄 Agent processing: StreamPart(event='metadata', data={'run_id': '019a3440-4c26-7327-924f-6b834b3fcb9e', 'attempt': 1})
📄 Agent processing: StreamPart(event='updates', data={'model': {'messages': [{'content': '', 'additional_kwargs': {'refusal': None}, 'response_metadata': {'token_usage': {'completion_tokens': 13, 'prompt_tokens': 77, 'total_tokens': 90, '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_provider': 'openai', 'model_name': 'gpt-4.1-2025-04-14', 'system_fingerprint': 'fp_f99638a8d7', 'id': 'chatcmpl-CWIODJG7JPryZbIT3Q4Poguo40d4d', 'prompt_filter_results': [{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, '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': {}}, 'type': 'ai', 'name': None, 'id': 'lc_run--4b0bf439-bf20-457f-a6a6-fc3544578a56-0', 'tool_calls': [{'name': 'return_four_coffee', 'args': {}, 'id': 'call_FUJX75L9uMgmlBP95s3M9Dbd', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 77, 'output_tokens': 13, 'total_tokens': 90, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}}]}})
📄 Agent processing: StreamPart(event='updates', data={'__interrupt__': [{'value': {'action_requests': [{'name': 'return_four_coffee', 'args': {}, 'description': 'Review return four\n\nTool: return_four_coffee\nArgs: {}'}], 'review_configs': [{'action_name': 'return_four_coffee', 'allowed_decisions': ['approve', 'edit', 'reject']}]}, 'id': 'e48b045619fe273421789bad3c1ab1b0'}]})
⏸️ INTERRUPT DETECTED DURING STREAMING: [{'value': {'action_requests': [{'name': 'return_four_coffee', 'args': {}, 'description': 'Review return four\n\nTool: return_four_coffee\nArgs: {}'}], 'review_configs': [{'action_name': 'return_four_coffee', 'allowed_decisions': ['approve', 'edit', 'reject']}]}, 'id': 'e48b045619fe273421789bad3c1ab1b0'}]
✅ Streaming complete. Interrupt detected: True
📊 Interrupt from Thread State: []
✨ No interrupts found in thread state
📈 Results: streaming_interrupted_detected=True, thread_state_interrupt_detected=False
❌ Interrupt not detected in both streaming and thread state

Description

I'm facing a bug where interrupts are not reflected in thread_state when running on the self-hosted LangGraph OSS (Docker) setup.

Behavior

  • On Docker (self-hosted):
    → The stream correctly emits the interrupt,
    → but the thread_state.interrupts array is empty.
  • On local (via langgraph dev):
    → Works correctly — interrupts appear in both the stream and thread_state.

Repro

  • Script to reproduce: gist link

  • Logs attached:

  • Tested with different interrupt sources — both HITL middleware interrupt and tool-triggered interrupt — same issue observed.

System Info

Langgraph Docker Container -->

System Information

OS: Linux
OS Version: #87-Ubuntu SMP PREEMPT_DYNAMIC Mon Sep 22 18:03:36 UTC 2025
Python Version: 3.12.12 (main, Oct 21 2025, 02:11:22) [GCC 14.2.0]

Package Information

langchain_core: 1.0.2
langchain: 1.0.3
langchain_community: 0.4.1
langsmith: 0.4.38
langchain_anthropic: 1.0.0
langchain_classic: 1.0.0
langchain_mcp_adapters: 0.1.11
langchain_openai: 1.0.1
langchain_text_splitters: 1.0.0
langgraph_api: 0.4.47
langgraph_cli: 0.4.4
langgraph_runtime_inmem: 0.14.1
langgraph_sdk: 0.2.9

Optional packages not installed

langserve

Other Dependencies

aiohttp: 3.13.2
anthropic: 0.72.0
async-timeout: Installed. No version info available.
blockbuster: 1.5.25
claude-agent-sdk: Installed. No version info available.
click: 8.3.0
cloudpickle: 3.1.1
cryptography: 44.0.3
dataclasses-json: 0.6.7
grpcio: 1.76.0
grpcio-tools: 1.75.1
httpx: 0.28.1
httpx-sse: 0.4.3
jsonpatch: 1.33
jsonschema-rs: 0.29.1
langchain-aws: 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-perplexity: Installed. No version info available.
langchain-together: Installed. No version info available.
langchain-xai: Installed. No version info available.
langgraph: 1.0.2
langgraph-checkpoint: 2.1.2
langsmith-pyo3: Installed. No version info available.
mcp: 1.19.0
numpy: 2.1.3
openai: 2.6.1
openai-agents: Installed. No version info available.
opentelemetry-api: 1.38.0
opentelemetry-exporter-otlp-proto-http: 1.38.0
opentelemetry-sdk: 1.38.0
orjson: 3.10.16
packaging: 24.2
protobuf: 6.33.0
pydantic: 2.12.3
pydantic-settings: 2.11.0
pyjwt: 2.10.1
pytest: Installed. No version info available.
python-dotenv: 1.2.1
pyyaml: 6.0.3
PyYAML: 6.0.3
requests: 2.32.5
requests-toolbelt: 1.0.0
rich: 14.2.0
SQLAlchemy: 2.0.44
sqlalchemy: 2.0.44
sse-starlette: 2.1.3
starlette: 0.49.1
structlog: 25.5.0
tenacity: 9.1.2
tiktoken: 0.12.0
truststore: 0.10.4
typing-extensions: 4.15.0
uvicorn: 0.38.0
vcrpy: Installed. No version info available.
watchfiles: 1.1.1
zstandard: 0.25.0

Originally created by @akbindal on GitHub (Oct 30, 2025). ### 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. ### Example Code ```python import asyncio from uuid import uuid4 from langchain.agents import create_agent from langchain.agents.middleware import HumanInTheLoopMiddleware from langchain_core.tools import tool from langgraph.types import Command, interrupt from langgraph_sdk import get_client from langchain_openai import ChatOpenAI LANGGRAPH_SERVER_URL="http://SERVER.URL" # Tool that generates an interrupt @tool def request_human_feedback(question: str) -> str: """Request feedback from a human user.""" feedback = interrupt( { "type": "human_feedback_request", "question": question, "message": f"Please provide feedback on: {question}", } ) return f"Human feedback received: {feedback}" @tool def return_four_coffee() -> str: """Return four coffee.""" raise Exception("This is a test error") return 4 # Create the agent with checkpointer for interrupts model = ChatOpenAI() # model="gpt-4o-mini") tools = [request_human_feedback, return_four_coffee] agent = create_agent( model=model, tools=tools, system_prompt="You are a helpful assistant that can request human feedback when needed.", middleware=[ HumanInTheLoopMiddleware( interrupt_on={ "return_four_coffee": { "allowed_decisions": ["approve", "edit", "reject"], } }, description_prefix="Review return four", ) ], ) async def make_agent(config): return agent.with_config(config) async def run_agent_with_client(thread_id: str, user_input: str): """Run the agent with client and handle interrupts.""" client = get_client(url=LANGGRAPH_SERVER_URL) config = {"configurable": {"thread_id": thread_id}} print(f"🚀 Starting agent run for: {user_input}") interrupt_detected = False # create thread if not exists await client.threads.create(thread_id=thread_id) # Stream the agent's execution streaming_interrupted_detected = False async for chunk in client.runs.stream( thread_id, "test_agent", input={"messages": [{"role": "user", "content": user_input}]}, config=config, stream_mode="updates", ): print(f"📄 Agent processing: {chunk}") # Check if this chunk indicates an interrupt if hasattr(chunk, "data") and chunk.data and "__interrupt__" in chunk.data: interrupts = chunk.data["__interrupt__"] print(f"⏸️ INTERRUPT DETECTED DURING STREAMING: {interrupts}") streaming_interrupted_detected = True print( f"✅ Streaming complete. Interrupt detected: {streaming_interrupted_detected}" ) # Always get the thread state at the end to check for interrupts try: thread_state = await client.threads.get_state(thread_id) # Check for interrupts in the thread state interrupts_in_state = thread_state.get("interrupts", []) print(f"📊 Interrupt from Thread State: {interrupts_in_state}") if interrupts_in_state: interrupt_detected = True print(f"🚨 INTERRUPTS FOUND IN THREAD STATE: {interrupts_in_state}") for interrupt in interrupts_in_state: print(f" - Interrupt: {interrupt.get('value', 'No value')}") else: print("✨ No interrupts found in thread state") # interrupt detected in both streaming and thread state return ( thread_state, streaming_interrupted_detected, interrupt_detected, ) except Exception as e: print(f"❌ Error getting thread state: {e}") return None, interrupt_detected, [] async def test_interrupt_detection_via_client(user_input: str): try: ( thread_state, streaming_interrupted_detected, interrupt_detected, ) = await run_agent_with_client( str(uuid4()), user_input, ) print( f"📈 Results: streaming_interrupted_detected={streaming_interrupted_detected}, thread_state_interrupt_detected={interrupt_detected}" ) if streaming_interrupted_detected and interrupt_detected: print("✅ Interrupt detected in both streaming and thread state") else: print("❌ Interrupt not detected in both streaming and thread state") except Exception as e: print(f"❌ Client test failed: {e}") print() # Demo function async def demo(): # Test 3: Client-based streaming with interrupt detection print("Test 1: Agent streaming with tool interrupt detection") await test_interrupt_detection_via_client( "Please request human feedback on my project idea: building an AI assistant." ) print("--------------------------------") print("Test 2: Agent streaming with HITL interrupt detection") await test_interrupt_detection_via_client( "Please return four coffee", ) print("🎉 Demo complete! The agent successfully handled interrupts and streaming.") if __name__ == "__main__": asyncio.run(demo()) ``` ### Error Message and Stack Trace (if applicable) ```shell === LangGraph Agent with Interrupt Demo === Test 1: Agent streaming with tool interrupt detection 🚀 Starting agent run for: Please request human feedback on my project idea: building an AI assistant. 📄 Agent processing: StreamPart(event='metadata', data={'run_id': '019a3440-4902-76ae-bef5-a9720c8b6f7d', 'attempt': 1}) 📄 Agent processing: StreamPart(event='updates', data={'model': {'messages': [{'content': '', 'additional_kwargs': {'refusal': None}, 'response_metadata': {'token_usage': {'completion_tokens': 31, 'prompt_tokens': 87, 'total_tokens': 118, '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_provider': 'openai', 'model_name': 'gpt-4.1-2025-04-14', 'system_fingerprint': 'fp_f99638a8d7', 'id': 'chatcmpl-CWIOBWryHlzVdv3pv6gxxwkcsPNTj', 'prompt_filter_results': [{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, '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': {}}, 'type': 'ai', 'name': None, 'id': 'lc_run--64407d7b-e822-4a83-bae7-d4669d9c8d10-0', 'tool_calls': [{'name': 'request_human_feedback', 'args': {'question': "Can you provide feedback on the user's project idea: building an AI assistant?"}, 'id': 'call_Vxn7OKXErfGksT9eIxIzAM1B', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 87, 'output_tokens': 31, 'total_tokens': 118, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}}]}}) 📄 Agent processing: StreamPart(event='updates', data={'HumanInTheLoopMiddleware.after_model': None}) 📄 Agent processing: StreamPart(event='updates', data={'__interrupt__': [{'value': {'type': 'human_feedback_request', 'question': "Can you provide feedback on the user's project idea: building an AI assistant?", 'message': "Please provide feedback on: Can you provide feedback on the user's project idea: building an AI assistant?"}, 'id': '79ffa8cc8a8fcbacadc4fbf1d72f79f8'}]}) ⏸️ INTERRUPT DETECTED DURING STREAMING: [{'value': {'type': 'human_feedback_request', 'question': "Can you provide feedback on the user's project idea: building an AI assistant?", 'message': "Please provide feedback on: Can you provide feedback on the user's project idea: building an AI assistant?"}, 'id': '79ffa8cc8a8fcbacadc4fbf1d72f79f8'}] ✅ Streaming complete. Interrupt detected: True 📊 Interrupt from Thread State: [] ✨ No interrupts found in thread state 📈 Results: streaming_interrupted_detected=True, thread_state_interrupt_detected=False ❌ Interrupt not detected in both streaming and thread state -------------------------------- Test 2: Agent streaming with HITL interrupt detection 🚀 Starting agent run for: Please return four coffee 📄 Agent processing: StreamPart(event='metadata', data={'run_id': '019a3440-4c26-7327-924f-6b834b3fcb9e', 'attempt': 1}) 📄 Agent processing: StreamPart(event='updates', data={'model': {'messages': [{'content': '', 'additional_kwargs': {'refusal': None}, 'response_metadata': {'token_usage': {'completion_tokens': 13, 'prompt_tokens': 77, 'total_tokens': 90, '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_provider': 'openai', 'model_name': 'gpt-4.1-2025-04-14', 'system_fingerprint': 'fp_f99638a8d7', 'id': 'chatcmpl-CWIODJG7JPryZbIT3Q4Poguo40d4d', 'prompt_filter_results': [{'prompt_index': 0, 'content_filter_results': {'hate': {'filtered': False, 'severity': 'safe'}, 'jailbreak': {'filtered': False, 'detected': False}, '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': {}}, 'type': 'ai', 'name': None, 'id': 'lc_run--4b0bf439-bf20-457f-a6a6-fc3544578a56-0', 'tool_calls': [{'name': 'return_four_coffee', 'args': {}, 'id': 'call_FUJX75L9uMgmlBP95s3M9Dbd', 'type': 'tool_call'}], 'invalid_tool_calls': [], 'usage_metadata': {'input_tokens': 77, 'output_tokens': 13, 'total_tokens': 90, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}}]}}) 📄 Agent processing: StreamPart(event='updates', data={'__interrupt__': [{'value': {'action_requests': [{'name': 'return_four_coffee', 'args': {}, 'description': 'Review return four\n\nTool: return_four_coffee\nArgs: {}'}], 'review_configs': [{'action_name': 'return_four_coffee', 'allowed_decisions': ['approve', 'edit', 'reject']}]}, 'id': 'e48b045619fe273421789bad3c1ab1b0'}]}) ⏸️ INTERRUPT DETECTED DURING STREAMING: [{'value': {'action_requests': [{'name': 'return_four_coffee', 'args': {}, 'description': 'Review return four\n\nTool: return_four_coffee\nArgs: {}'}], 'review_configs': [{'action_name': 'return_four_coffee', 'allowed_decisions': ['approve', 'edit', 'reject']}]}, 'id': 'e48b045619fe273421789bad3c1ab1b0'}] ✅ Streaming complete. Interrupt detected: True 📊 Interrupt from Thread State: [] ✨ No interrupts found in thread state 📈 Results: streaming_interrupted_detected=True, thread_state_interrupt_detected=False ❌ Interrupt not detected in both streaming and thread state ``` ### Description I'm facing a bug where **interrupts are not reflected in `thread_state`** when running on the **self-hosted LangGraph OSS (Docker)** setup. #### Behavior * On **Docker (self-hosted)**: → The **stream correctly emits the interrupt**, → but the **`thread_state.interrupts` array is empty**. * On **local (via `langgraph dev`)**: → Works correctly — interrupts appear in both the stream and `thread_state`. #### Repro * Script to reproduce: [gist link](https://gist.github.com/akbindal/2fd42aa1a5fe7729c22dbd6024ce89b2) * Logs attached: * [local-logs.txt](https://github.com/user-attachments/files/23228981/local-logs.txt) (agent running via `langgraph dev`) * [server-logs.txt](https://github.com/user-attachments/files/23228980/server-logs.txt) (agent running via Docker container) * Tested with different interrupt sources — both HITL middleware interrupt and tool-triggered interrupt — same issue observed. ### System Info Langgraph Docker Container --> System Information ------------------ > OS: Linux > OS Version: #87-Ubuntu SMP PREEMPT_DYNAMIC Mon Sep 22 18:03:36 UTC 2025 > Python Version: 3.12.12 (main, Oct 21 2025, 02:11:22) [GCC 14.2.0] Package Information ------------------- > langchain_core: 1.0.2 > langchain: 1.0.3 > langchain_community: 0.4.1 > langsmith: 0.4.38 > langchain_anthropic: 1.0.0 > langchain_classic: 1.0.0 > langchain_mcp_adapters: 0.1.11 > langchain_openai: 1.0.1 > langchain_text_splitters: 1.0.0 > langgraph_api: 0.4.47 > langgraph_cli: 0.4.4 > langgraph_runtime_inmem: 0.14.1 > langgraph_sdk: 0.2.9 Optional packages not installed ------------------------------- > langserve Other Dependencies ------------------ > aiohttp: 3.13.2 > anthropic: 0.72.0 > async-timeout: Installed. No version info available. > blockbuster: 1.5.25 > claude-agent-sdk: Installed. No version info available. > click: 8.3.0 > cloudpickle: 3.1.1 > cryptography: 44.0.3 > dataclasses-json: 0.6.7 > grpcio: 1.76.0 > grpcio-tools: 1.75.1 > httpx: 0.28.1 > httpx-sse: 0.4.3 > jsonpatch: 1.33 > jsonschema-rs: 0.29.1 > langchain-aws: 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-perplexity: Installed. No version info available. > langchain-together: Installed. No version info available. > langchain-xai: Installed. No version info available. > langgraph: 1.0.2 > langgraph-checkpoint: 2.1.2 > langsmith-pyo3: Installed. No version info available. > mcp: 1.19.0 > numpy: 2.1.3 > openai: 2.6.1 > openai-agents: Installed. No version info available. > opentelemetry-api: 1.38.0 > opentelemetry-exporter-otlp-proto-http: 1.38.0 > opentelemetry-sdk: 1.38.0 > orjson: 3.10.16 > packaging: 24.2 > protobuf: 6.33.0 > pydantic: 2.12.3 > pydantic-settings: 2.11.0 > pyjwt: 2.10.1 > pytest: Installed. No version info available. > python-dotenv: 1.2.1 > pyyaml: 6.0.3 > PyYAML: 6.0.3 > requests: 2.32.5 > requests-toolbelt: 1.0.0 > rich: 14.2.0 > SQLAlchemy: 2.0.44 > sqlalchemy: 2.0.44 > sse-starlette: 2.1.3 > starlette: 0.49.1 > structlog: 25.5.0 > tenacity: 9.1.2 > tiktoken: 0.12.0 > truststore: 0.10.4 > typing-extensions: 4.15.0 > uvicorn: 0.38.0 > vcrpy: Installed. No version info available. > watchfiles: 1.1.1 > zstandard: 0.25.0
yindo added the bugpending labels 2026-02-20 17:42:49 -05:00
Author
Owner

@Ujjwal-Bajpayee commented on GitHub (Nov 1, 2025):

Hi @mrkn @nfcampos @snikch @akbindal I’ve gone through the issue and reproduced the behavior, the thread_state.interrupts array stays empty when
running LangGraph in a self-hosted setup, even though interrupts appear correctly in the stream.

I want to work on a fix for this issue. I plan to trace how interrupts are appended to the thread_state during normal runs and identify where the self-hosted flow diverges. Once confirmed, I’ll propose a patch and add tests to ensure parity between langgraph dev and self-hosted environments.

Please let me know if it’s okay for me to take this up. Thanks!

@Ujjwal-Bajpayee commented on GitHub (Nov 1, 2025): Hi @mrkn @nfcampos @snikch @akbindal I’ve gone through the issue and reproduced the behavior, the thread_state.interrupts array stays empty when running LangGraph in a self-hosted setup, even though interrupts appear correctly in the stream. I want to work on a fix for this issue. I plan to trace how interrupts are appended to the thread_state during normal runs and identify where the self-hosted flow diverges. Once confirmed, I’ll propose a patch and add tests to ensure parity between langgraph dev and self-hosted environments. Please let me know if it’s okay for me to take this up. Thanks!
Author
Owner

@akbindal commented on GitHub (Nov 3, 2025):

Thanks @Ujjwal-Bajpayee for looking into the issue. The interrupt behavior is expected to be consistent across durability modes. From my analysis, the regression appears in langgraph-server versions post-4.43 (4.43 functions correctly). In the latest builds, the graph checkpointer reliably persists interrupts to Postgres. The root cause likely lies in how langgraph-api reconstructs the thread StateSnapshot from the graph checkpoint.

fyi: leaving a comment on PR also for langchain team

@akbindal commented on GitHub (Nov 3, 2025): Thanks @Ujjwal-Bajpayee for looking into the issue. The interrupt behavior is expected to be consistent across durability modes. From my analysis, the regression appears in `langgraph-server` versions **post-4.43** (4.43 functions correctly). In the latest builds, the graph checkpointer reliably persists interrupts to Postgres. The root cause likely lies in how `langgraph-api` reconstructs the thread `StateSnapshot` from the graph checkpoint. fyi: leaving a comment on PR also for langchain team
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#1032