#11360·langchainjs

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)

Author: serhiykrupkaCreated Aug 13, 2026Updated Sep 10, 2026

Example code

javascript
// 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_V2 unset → 0 warnings
  • LANGCHAIN_TRACING_V2=true4 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