#11648·langchainjs

createAgent().streamEvents() inside a LangGraph node delivers config callbacks twice when LangSmith tracing is enabled

Author: MatgonzattiCreated Sep 15, 2026Updated Sep 15, 2026
Labelsbug

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 very descriptive title to this issue.
  • I searched the LangChain.js documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain.js rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).

Example Code

javascript
// npm i [email protected] @langchain/[email protected] @langchain/[email protected]
// LANGCHAIN_TRACING_V2=true LANGSMITH_API_KEY=any LANGSMITH_ENDPOINT=http://127.0.0.1:9 node repro.mjs
import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
import { AIMessageChunk, HumanMessage } from "@langchain/core/messages";
import { FakeStreamingChatModel } from "@langchain/core/utils/testing";
import { Annotation, END, START, StateGraph } from "@langchain/langgraph";
import { createAgent } from "langchain";

class TokenCounter extends BaseCallbackHandler {
  name = "token_counter";
  tokens = 0;
  handleLLMNewToken(token) {
    if (token) this.tokens += 1;
  }
}

const chunks = ["Hello", "!", " How are you?"];

async function streamAgent() {
  const counter = new TokenCounter();
  let streamedChunks = 0;
  const agent = createAgent({
    model: new FakeStreamingChatModel({
      chunks: chunks.map((content) => new AIMessageChunk({ content })),
    }),
    tools: [],
  });
  const events = agent.streamEvents(
    { messages: [new HumanMessage("hi")] },
    { version: "v2", callbacks: [counter] },
  );
  for await (const event of events) {
    if (event.event === "on_chat_model_stream" && event.data.chunk.text) streamedChunks += 1;
  }
  return { handlerTokens: counter.tokens, streamedChunks };
}

console.log("agent called directly:", await streamAgent());

const graph = new StateGraph(Annotation.Root({ question: Annotation() }))
  .addNode("run_agent", async () => {
    console.log("agent inside a graph node:", await streamAgent());
    return {};
  })
  .addEdge(START, "run_agent")
  .addEdge("run_agent", END)
  .compile();

await graph.invoke({ question: "hi" });

Error Message and Stack Trace (if applicable)

With LANGCHAIN_TRACING_V2=true:

agent called directly: { handlerTokens: 3, streamedChunks: 3 }
Error in handler EventStreamCallbackHandler, handleChainEnd: Error: onChainEnd: Run ID 01a0a721-86e6-73a8-9329-f6c6004611a3 not found in run map.
Error in handler EventStreamCallbackHandler, handleLLMEnd: Error: onLLMEnd: Run ID 01a0a721-86e7-7279-950a-4b83fda07aa1 not found in run map.
Error in handler EventStreamCallbackHandler, handleChainEnd: Error: onChainEnd: Run ID 01a0a721-86e7-7279-950a-47df7826a84f not found in run map.
Error in handler EventStreamCallbackHandler, handleChainEnd: Error: onChainEnd: Run ID 01a0a721-86e6-73a8-9329-f0f5ac430c25 not found in run map.
agent inside a graph node: { handlerTokens: 6, streamedChunks: 6 }

With LANGCHAIN_TRACING_V2=false both lines print { handlerTokens: 3, streamedChunks: 3 } and no errors are logged. (The LANGSMITH fetch failures in the real output only come from pointing the endpoint at a closed local port.)

Description

What I'm doing: streaming a createAgent() agent with agent.streamEvents(input, { version: "v2", callbacks: [handler] }) from inside a node of an outer StateGraph, with LangSmith tracing enabled through LANGCHAIN_TRACING_V2=true (as in production).

Expected: every handler passed in callbacks receives each token once, and streamEvents yields each on_chat_model_stream chunk once — exactly what happens when the same agent is called outside a graph node, or when tracing is disabled.

Actual: the handler receives every token twice, streamEvents yields every chunk twice (the same chunk object back to back), and EventStreamCallbackHandler logs Run ID ... not found in run map for chain/LLM end events.

It only happens when all three are true:

  1. LangSmith tracing is enabled;
  2. the agent runs inside a LangGraph node (Pregel inside Pregel);
  3. handlers are passed explicitly in the streamEvents config.

Not affected: model.streamEvents(...) inside the same node, the agent called directly, the same setup with tracing disabled, and a handler passed only to the outer graph.invoke(...) (inherited instead of explicit).

Where I think it comes from (from reading @langchain/core 1.2.x, not confirmed): AsyncLocalStorageProvider.runWithConfig stores the run config in runTree.extra[LC_CHILD_KEY]. With tracing enabled that run tree comes from the inherited LangChainTracer (getRunTreeWithTracingConfig(parentRunId)), while with tracing disabled it is a fresh <runnable_lambda> tree. ensureConfig then reads that implicit config and mergeConfigs concatenates both callback managers without deduplicating handlers (handlers: baseCallbacks.handlers.concat(providedCallbacks.handlers) in runnables/config.js), so the explicit handlers — including the internal event-stream handler — end up registered twice for the agent's inner runs.

Workaround on our side: not passing callbacks to streamEvents for an agent that runs inside a graph node.

System Info

  • langchain 1.5.11, @langchain/core 1.2.11, @langchain/langgraph 1.4.15 (latest at the time of writing); also reproduced on langchain 1.5.10, @langchain/core 1.2.9, @langchain/langgraph 1.4.13
  • Node.js v24.20.0
  • macOS 27.0 (arm64)
  • npm

Source: langchain-ai/langchainjs