Repository navigation
Open
Description
Which component is this bug for?
Langchain Instrumentation
📜 Description
_wrap_middleware_hookswraps theAgentMiddlewarebase hooks on the class withwrap_function_wrapper(__init__.py#L298-L322). A wraptFunctionWrapperreturns a new bound wrapper on every attribute access, soAgentMiddleware.before_model is AgentMiddleware.before_modelbecomesFalse.create_agentuses that identity to decide which middleware overrides a hook (factory.py#L1166-L1185). After instrumenting, every middleware looks like it overrides all four hooks, so the graph gains an emptybefore_model/after_modelnode per middleware per model turn. Each node counts as a step againstrecursion_limit.- In our agents with four
wrap_model_callmiddlewares, a model turn went from about 2 steps to about 10, and runs hitGraphRecursionErroraround 5x earlier than without tracing. - Why we're raising this here rather than with LangChain:
- there might be an improvement to LangChain's check that could harden their logic but I think the tracing is meant to observe the agent without changing how it runs, and fix(langchain): Use instance-level wrapping for AgentMiddleware hooks #4000 suggests it can be fixed entirely inside the instrumentation.
- openllmetry's span wrapper also seems to fire only when the base (no-op) hook runs. A middleware that really overrides
before_modelreplaces the base method, so the wrapper never runs and I don't think real hook overrides get anexecute_taskspan today. Could you confirm whether that matches your understanding?
👟 Reproduction steps
- Save the script below as
repro.py. - Run it in a clean environment:
uv run --no-project --with "opentelemetry-instrumentation-langchain==0.62.4" --with "langchain>=1.3" --with opentelemetry-sdk python repro.py
from importlib.metadata import version
import sys
from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, wrap_model_call
from langchain_core.language_models.fake_chat_models import FakeMessagesListChatModel
from langchain_core.messages import AIMessage
@wrap_model_call
def passthrough(request, handler):
return handler(request)
def build_nodes():
model = FakeMessagesListChatModel(responses=[AIMessage(content="done")])
agent = create_agent(model, tools=[], middleware=[passthrough])
return sorted(agent.get_graph().nodes)
print("python", sys.version.split()[0])
for package in ("langchain", "langgraph", "opentelemetry-instrumentation-langchain"):
print(package, version(package))
print("before instrument:", build_nodes())
print("identity before:", AgentMiddleware.before_model is AgentMiddleware.before_model)
from opentelemetry.instrumentation.langchain import LangchainInstrumentor
LangchainInstrumentor().instrument()
print("after instrument: ", build_nodes())
print("identity after:", AgentMiddleware.before_model is AgentMiddleware.before_model)👍 Expected behavior
Instrumenting doesn't change the agent graph. A middleware that only implements wrap_model_call adds no nodes:
after instrument: ['__end__', '__start__', 'model']
identity after: True
👎 Actual Behavior with Screenshots
python 3.14.0
langchain 1.4.6
langgraph 1.2.15
opentelemetry-instrumentation-langchain 0.62.4
before instrument: ['__end__', '__start__', 'model']
identity before: True
after instrument: ['__end__', '__start__', 'model', 'passthrough.after_agent', 'passthrough.after_model', 'passthrough.before_agent', 'passthrough.before_model']
identity after: False
🤖 Python Version
3.14.0. Also seen on 3.12.
📃 Provide any additional context for the Bug.
- Versions where we see it: instrumentation 0.62.1, 0.62.3 and 0.62.4, with langchain 1.2.13, 1.3.15 and 1.4.6.
- fix(langchain): Use instance-level wrapping for AgentMiddleware hooks #4000 looks like it addresses exactly this. Running the script above against its head commit (43d14f5, with
wrapt<2) keeps the graph unchanged and identityTrue. The branch is based on 0.59.1, so it may need a rebase onto main for the wrapt 2 changes from fix(langchain): use positional args for wrap_function_wrapper for wrapt v1/v2 compat, fix BaseChatOpenAI uninstrument #4082. - In the meantime we're working around it downstream by calling
opentelemetry.instrumentation.utils.unwrapon the eightAgentMiddlewarehooks right afterinstrument().
👀 Have you spent some time to check if this bug has been raised before?
- I checked and didn't find similar issue
Metadata
Metadata
Assignees
Labels
No labels
Activity
DEVJHAWAR11 commentedon Oct 16, 2026
Hi, I can reproduce this with the script above. The base hooks get wrapped, so
AgentMiddleware.before_model is AgentMiddleware.before_modelturns False andcreate_agentadds a node per middleware. I'd like to fix it. #4000 looks like the same idea but has been conflicting since April. Would you prefer I rebase and finish that one, or open a fresh PR with a regression test (graph nodes unchanged afterinstrument(), and the hooks still traced when a middleware really overrides them)? Happy to go either way.scampbell-h1 commentedon Oct 16, 2026
One more effect of the same wrapping, which a colleague spotted: it can stop a middleware's hook from running at all.
A middleware hook has a sync and an async version. If a middleware writes only one of them, LangChain normally either runs that one or raises an error. With instrumentation on, LangChain thinks both were written, so it runs the wrapped empty base method in place of the missing one. The hook is skipped silently.
before_modelainvokeabefore_modelinvokeTypeErrorReproduction steps
repro_dispatch.py.Expected
Instrumenting doesn't change which hooks run:
Actual (0.62.4, langchain 1.4.6, Python 3.14.0)
DEVJHAWAR11 commentedon Oct 16, 2026
I opened a PR with a fix: #4629. The base hooks are now plain functions via
functools.wraps, so the identity check, theruntimesignature and the sync/async dispatch all match the un-instrumented behaviour. Thanks @scampbell-h1 for the dispatch repro, it's in the tests. This should replace #4000, which fails with theruntimeerror described above.sdelmas commentedon Oct 18, 2026
I can confirm the skipped hook from the comment above with a standalone repro (scripted chat model, no API key). Versions: langchain 1.3.14, langchain-core 1.4.9, langgraph 1.2.12, opentelemetry-sdk 1.45.1.
before_model(subclass or@before_model) onainvoke: runs 1/1 uninstrumented. On 0.62.1, 0.62.4 and main 94d3f04 it runs 0/1 and raises no error. With the fix(langchain): keep AgentMiddleware hook identity when instrumenting #4629 head (2d0c82c) it runs 1/1.abefore_model(subclass or@before_model) oninvoke: uninstrumented, it raisesTypeError: No synchronous function provided. On 0.62.1, 0.62.4 and main it runs 0/1 with no error. With fix(langchain): keep AgentMiddleware hook identity when instrumenting #4629 it raises the sameTypeError.In the skipped runs an
execute_task <Middleware>.abefore_model(or.before_model) span still appears. It comes from the wrapped base no-op, which runs in place of the user's hook.With #4629 installed, every instrumented case matches the uninstrumented one. One side effect: these middlewares no longer emit any
execute_taskhook span, because no base hook runs. That might be worth a changelog note.Repro:
create_agentwith one middleware and a fake chat model that answers immediately. Each case runs in a fresh process, once withLangchainInstrumentor().instrument()and once without, and counts hook calls against model calls.