Skip to content

Graph lifecycle managers dispatch to instrumentor-injected handlers: _filter_graph_handlers runs before BaseCallbackManager.__init__ #8613

Description

Checked other resources

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • 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 rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package).
  • This is not related to the langchain-community package.
  • I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

Related Issues / PRs

Reproduction Steps / Example Code (Python)

# pip install "langgraph==1.2.2" langgraph-checkpoint "openinference-instrumentation-langchain==0.1.67"
import logging
from typing import TypedDict

logging.basicConfig(level=logging.WARNING)

from openinference.instrumentation.langchain import LangChainInstrumentor

LangChainInstrumentor().instrument()

from langgraph.callbacks import GraphCallbackHandler, _GraphCallbackManager
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt


class State(TypedDict):
    n: int


def node(state: State) -> State:
    interrupt("gate")
    return {"n": state["n"] + 1}


builder = StateGraph(State)
builder.add_node("node", node)
builder.add_edge(START, "node")
builder.add_edge("node", END)
graph = builder.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "1"}}
graph.invoke({"n": 0}, config)              # pauses  -> on_interrupt dispatch
graph.invoke(Command(resume="go"), config)  # resumes -> on_resume dispatch

handlers = _GraphCallbackManager.configure(None, run_id=None).handlers
print("handlers in the graph lifecycle manager:", [type(h).__name__ for h in handlers])
print("all GraphCallbackHandler?", all(isinstance(h, GraphCallbackHandler) for h in handlers))

Error Message and Stack Trace (if applicable)

WARNING:langchain_core.callbacks.manager:Error in OpenInferenceTracer.on_interrupt callback: AttributeError("'OpenInferenceTracer' object has no attribute 'on_interrupt'")
WARNING:langchain_core.callbacks.manager:Error in OpenInferenceTracer.on_resume callback: AttributeError("'OpenInferenceTracer' object has no attribute 'on_resume'")
handlers in the graph lifecycle manager: ['OpenInferenceTracer']
all GraphCallbackHandler? False

Pinning openinference-instrumentation-langchain==0.1.70 (which added no-op on_interrupt/on_resume to its tracer purely to silence this) removes the two warnings but not the cause — the last two lines are unchanged:

handlers in the graph lifecycle manager: ['OpenInferenceTracer']
all GraphCallbackHandler? False

Description

_GraphCallbackManager / _AsyncGraphCallbackManager are documented to carry only GraphCallbackHandler instances, and every entry point (configure(), copy(), the cross-type constructors) runs _filter_graph_handlers to enforce that. But the filter runs on the constructor arguments, and _init_base_manager then calls BaseCallbackManager.__init__ — which is exactly where LangChain instrumentors monkey-patch to append their handler to every callback manager. So the injection lands after the filter, and the invariant the class documents is not true of the constructed object.

The consequence is that on_interrupt/on_resume dispatch through handle_event to a handler that never implements them, so every interrupt and every resume logs an AttributeError at WARNING. It's swallowed (raise_error is False), so it is noise rather than breakage — but it is per-interrupt and per-resume. In our production workload (human-in-the-loop agents, so every turn is gated) that was a few hundred lines/day across the Celery workers.

This is the sequel to #7543. That issue reported the hard-failure version of the same collision: in 1.1.7 the managers overrode add_handler to raise TypeError("handlers must inherit GraphCallbackHandler"), and the injected handler crashed graph invocation. In 1.2.2 that override is gone, so the injected handler is now accepted silently — the crash became a warning, but the handler still ends up in a manager that then dispatches graph lifecycle events to it.

Worth noting the failure isn't specific to one vendor or to the two callbacks that exist today. Any instrumentor that patches BaseCallbackManager.__init__ hits it (openinference and openllmetry both do), and each new graph lifecycle callback added to this API reopens it for every instrumentor that hasn't yet shipped a matching stub. The current state is that the ecosystem works around a LangGraph invariant by adding no-op methods for LangGraph-private events to unrelated tracers.

Suggested fix — re-apply the existing predicate where it can't be bypassed, either at the end of _init_base_manager:

    BaseCallbackManager.__init__(manager, ...)
    manager.handlers = _filter_graph_handlers(manager.handlers)
    manager.inheritable_handlers = _filter_graph_handlers(manager.inheritable_handlers)
    manager.run_id = run_id

or at the dispatch sites, if you'd rather leave the handler list untouched:

    def on_interrupt(self, event: GraphInterruptEvent) -> None:
        handle_event(_filter_graph_handlers(self.handlers), "on_interrupt", None, event)

Either restores what the class already documents. Happy to open a PR with tests if you'd like — just say which shape you prefer.

System Info

langgraph==1.2.2
langgraph-checkpoint==4.2.0
langchain-core==1.5.4
openinference-instrumentation-langchain==0.1.67 (and 0.1.70)
Python 3.13.14
macOS-26.5.2-arm64

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions