EventStreamCallbackHandler warns "Run ID … not found in run map" for every run when tracing is enabled and a tool invokes a nested graph (duplicate handler delivery not covered by #11190)
Example code
// repro.mjs — run: LANGCHAIN_TRACING_V2=true LANGCHAIN_API_KEY=lsv2_dummy LANGCHAIN_ENDPOINT=http://127.0.0.1:9 node repro.mjs
import { createAgent, tool, FakeToolCallingModel } from "langchain";
import { z } from "zod";
const innerAgent = createAgent({
model: new FakeToolCallingModel({ toolCalls: [[]] }),
tools: [],
});
const nested = tool(
async (_input, config) => {
await innerAgent.invoke({ messages: [{ role: "user", content: "hi" }] }, config);
return "done";
},
{ name: "nested_graph_tool", schema: z.object({}) },
);
const agent = createAgent({
model: new FakeToolCallingModel({
toolCalls: [[{ name: "nested_graph_tool", args: {}, id: "1", type: "tool_call" }], []],
}),
tools: [nested],
});
for await (const _ev of agent.streamEvents(
{ messages: [{ role: "user", content: "go" }] },
{ version: "v2" },
)) { /* drain */ }Error message and stack trace
Error in handler EventStreamCallbackHandler, handleChainEnd: Error: onChainEnd: Run ID 019ff6… not found in run map.
Error in handler EventStreamCallbackHandler, handleLLMEnd: Error: onLLMEnd: Run ID 019ff6… not found in run map.
Error in handler EventStreamCallbackHandler, handleToolEnd: Error: onToolEnd: Run ID 019ff6… not found in run map.(4 warnings for this minimal repro; a real nested-agent app served by langgraph dev produces a dozen+ per turn.)
Description
Running the script above:
LANGCHAIN_TRACING_V2unset → 0 warningsLANGCHAIN_TRACING_V2=true→ 4 warnings- flat agent (no nested-graph tool) → 0 warnings either way
The warnings are non-fatal (raiseError is false, so the callback dispatcher logs and swallows them), but langgraph dev consumes every run via streamEvents({version: "v2"}), so any real nested-agent app with tracing enabled emits a burst of them on every turn, drowning the dev logs.
What's happening: we instrumented EventStreamCallbackHandler's end handlers to record already-ended run ids. Every warning fires with alreadyEnded=true — the handler already processed that exact run's end once. The same handler instance ends up registered twice in the callback manager used inside the nested graph, so handleChainEnd/handleLLMEnd/handleToolEnd fan out to it twice concurrently (Promise.all); the first delivery deletes the runInfoMap entry (the end handlers delete before validating), and the second throws.
This looks like the same mechanism as #11189 ("No chain run to end" from LangChainTracer in nested graphs). PR #11190 fixed that by coalescing duplicate handler copies sharing run bookkeeping — but the fix is hard-scoped to handler instanceof LangChainTracer (coalesceTracers in callbacks/manager.js), so EventStreamCallbackHandler (and presumably any other non-tracer handler) still gets duplicated when the tracing-enabled configure path runs for a tool-invoked nested graph.
A related asymmetry noticed while debugging: EventStreamCallbackHandler implements onToolError but not onChainError, so error-path chain runs (e.g. a GraphInterrupt bubbling out of a tool) never remove their runInfoMap entry.
Expected behavior: each end event is delivered once per handler instance — no warnings, exactly like the non-tracing case.
System Info
- langchain 1.5.4
- @langchain/core 1.2.4 (reproduced identically with 1.2.5 via npm override)
- @langchain/langgraph 1.4.8
- @langchain/langgraph-api 1.4.4 (
langgraph dev) — where the noise surfaces in practice - Node.js 26.0.0, macOS 15 (darwin 25.5.0)
Source: langchain-ai/langchainjs