fix(langchain): prevent llmToolSelectorMiddleware from leaking into message stream#10160
Merged
Christian Bromann (christian-bromann) merged 6 commits intoMay 16, 2026
Conversation
🦋 Changeset detectedLatest commit: ff99621 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
…essage stream
Pass `{ callbacks: [] }` to the internal structuredModel.invoke() call so
LangGraph's streaming callbacks are not inherited, preventing the tool
selection response from appearing as an assistant message when using
agent.stream() with streamMode "messages".
0953c10 to
6c305b4
Compare
6 tasks
…erit runtime config Merge the parent runnable config from runtime so LangSmith tracing and other callback-based consumers can properly track the internal tool selection call, while still overriding callbacks with an empty array to prevent streaming events from leaking to the UI.
Previously `callbacks: []` was spread after `config` in the invoke call, making the override intent implicit. Moving it into `mergeConfigs` as the second argument makes it clear that the empty array intentionally overrides any inherited callbacks from AsyncLocalStorage context.
Christian Bromann (christian-bromann)
requested changes
May 6, 2026
| }); | ||
|
|
||
| const agent = createAgent({ | ||
| model: new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }), |
There was a problem hiding this comment.
We shouldn't need a real model for this and instead just use a mock as documented in https://docs.langchain.com/oss/javascript/langchain/test/unit-testing
b9fdd0f to
abc6088
Compare
…istChatModel Replace the llmToolSelector integration test that required a real OpenAI API call with a unit test using FakeListChatModel, so the streaming isolation check runs without network access or API keys.
Contributor
Author
|
Christian Bromann (@christian-bromann) |
Christian Bromann (christian-bromann)
approved these changes
May 16, 2026
Christian Bromann (christian-bromann)
left a comment
Member
There was a problem hiding this comment.
LGTM 👍
Thanks for the update!
bba900c
into
langchain-ai:main
29 checks passed
Hunter Lovell (hntrl)
added a commit
that referenced
this pull request
May 18, 2026
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @langchain/anthropic@1.4.0 ### Minor Changes - [#10777](#10777) [`0cfcfc6`](0cfcfc6) Thanks [@jonaslalin](https://github.com/jonaslalin)! - feat(anthropic): support strict tool calling for custom tools ## langchain@1.4.1 ### Patch Changes - [#10879](#10879) [`eb480cb`](eb480cb) Thanks [@vignesh-gep](https://github.com/vignesh-gep)! - fix(langchain/createAgent): throw on terminal `providerStrategy` parse failure instead of silently resolving with `structuredResponse: undefined` When `createAgent` was configured with `responseFormat` resolving to a `providerStrategy` (either passed explicitly or auto-promoted from a bare Zod / JSON schema for models whose profile reports `structuredOutput: true`), and the model produced a terminal response (no `tool_calls`) whose text could not be JSON-parsed or did not satisfy the schema, the agent silently exited with no `structuredResponse`, surfacing later as `TypeError: Cannot read properties of undefined`. The agent now throws a `StructuredOutputParsingError` in that case while still allowing the agent loop to continue when tool calls are present. Closes [#10878](#10878). - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. - [#10160](#10160) [`bba900c`](bba900c) Thanks [@JadenKim-dev](https://github.com/JadenKim-dev)! - fix(langchain): prevent llmToolSelectorMiddleware from leaking into message stream ## @langchain/classic@1.0.33 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. - Updated dependencies \[[`229a7ad`](229a7ad), [`36fb0ef`](36fb0ef)]: - @langchain/openai@1.4.6 ## @langchain/core@1.1.47 ### Patch Changes - [#10906](#10906) [`f61b345`](f61b345) Thanks [@hntrl](https://github.com/hntrl)! - feat(core): add uuid v6 utility support Add `v6` UUID generation support to `@langchain/core/utils/uuid` by vendoring the upstream uuidjs `v6` implementation and its `v1ToV6` helper, exporting `v6` from the UUID utils index, and adding tests for deterministic generation, buffer/offset behavior, validation/versioning, and ordering. - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. - [#10792](#10792) [`3682268`](3682268) Thanks [@Genmin](https://github.com/Genmin)! - fix(core): apply v1 message casting after implicit streaming aggregation - [#10901](#10901) [`f26fc4a`](f26fc4a) Thanks [@christian-bromann](https://github.com/christian-bromann)! - fix(testing): share fakeModel invocation state across bindTools instances ## @langchain/aws@1.3.8 ### Patch Changes - [#10653](#10653) [`e8d72d3`](e8d72d3) Thanks [@muhammadosama984](https://github.com/muhammadosama984)! - fix(aws): preserve Bedrock tool call identity in callback-streamed chunks ## @langchain/cloudflare@1.0.6 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. ## @langchain/deepseek@1.0.26 ### Patch Changes - [#10556](#10556) [`9076f06`](9076f06) Thanks [@muhammadosama984](https://github.com/muhammadosama984)! - refactor(deepseek,xai): remove redundant reasoning_content overrides - Updated dependencies \[[`229a7ad`](229a7ad), [`36fb0ef`](36fb0ef)]: - @langchain/openai@1.4.6 ## @langchain/exa@1.0.2 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. ## @langchain/fireworks@0.1.4 ### Patch Changes - Updated dependencies \[[`229a7ad`](229a7ad), [`36fb0ef`](36fb0ef)]: - @langchain/openai@1.4.6 ## @langchain/google@0.1.12 ### Patch Changes - [#10704](#10704) [`f7e50fb`](f7e50fb) Thanks [@afirstenberg](https://github.com/afirstenberg)! - Adds support for flex and priority pricing. Adds support for custom headers. Thanks to @Nico385412 and @pawel-twardziak for their insight and contributions. ## @langchain/google-common@2.1.31 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. - [#9912](#9912) [`7be1da4`](7be1da4) Thanks [@yukukotani](https://github.com/yukukotani)! - fix missing stream usage metadata on Gemini ## @langchain/google-gauth@2.1.31 ### Patch Changes - Updated dependencies \[[`a640079`](a640079), [`7be1da4`](7be1da4)]: - @langchain/google-common@2.1.31 ## @langchain/google-vertexai@2.1.31 ### Patch Changes - Updated dependencies \[]: - @langchain/google-gauth@2.1.31 ## @langchain/google-vertexai-web@2.1.31 ### Patch Changes - Updated dependencies \[]: - @langchain/google-webauth@2.1.31 ## @langchain/google-webauth@2.1.31 ### Patch Changes - Updated dependencies \[[`a640079`](a640079), [`7be1da4`](7be1da4)]: - @langchain/google-common@2.1.31 ## @langchain/groq@1.2.1 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. ## @langchain/mongodb@1.2.1 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. ## @langchain/neo4j@0.1.5 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. - Updated dependencies \[[`a640079`](a640079)]: - @langchain/classic@1.0.33 ## @langchain/openai@1.4.6 ### Patch Changes - [#10902](#10902) [`229a7ad`](229a7ad) Thanks [@christian-bromann](https://github.com/christian-bromann)! - fix(openai): preserve v1 assistant tool calls - [#10895](#10895) [`36fb0ef`](36fb0ef) Thanks [@BertBR](https://github.com/BertBR)! - fix(openai): guard bare `JSON.parse` in Responses API converter against trailing non-whitespace characters `convertResponsesDeltaToChatGenerationChunk` previously called `JSON.parse(msg.text)` directly when `response.text.format.type === "json_schema"`. Some models (observed with `gpt-5-mini` on `service_tier: "auto"`) intermittently emit trailing non-whitespace characters (extra tokens, control characters) after a valid JSON object, causing a `SyntaxError` that propagates as an unhandled exception and kills the entire streaming response mid-flight. The parse is now wrapped in a `try`/`catch`: on failure, `additional_kwargs.parsed` is left undefined, the stream completes normally, and the existing `withStructuredOutput` pipeline handles the typed failure — `includeRaw: true` returns `{ raw, parsed: null }` via its `withFallbacks` wrapper, `includeRaw: false` throws a typed `OutputParserException` that the caller can catch and retry. Closes [#10894](#10894). ## @langchain/openrouter@0.2.5 ### Patch Changes - Updated dependencies \[[`229a7ad`](229a7ad), [`36fb0ef`](36fb0ef)]: - @langchain/openai@1.4.6 ## @langchain/perplexity@0.2.1 ### Patch Changes - [#10884](#10884) [`06fa847`](06fa847) Thanks [@jliounis](https://github.com/jliounis)! - Add useResponsesApi flag to ChatPerplexity ## @langchain/pinecone@1.0.3 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. ## @langchain/qdrant@1.0.3 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. ## @langchain/redis@1.1.3 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. ## @langchain/together-ai@0.1.4 ### Patch Changes - Updated dependencies \[[`229a7ad`](229a7ad), [`36fb0ef`](36fb0ef)]: - @langchain/openai@1.4.6 ## @langchain/weaviate@1.0.3 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. ## @langchain/xai@1.3.18 ### Patch Changes - [#10872](#10872) [`a640079`](a640079) Thanks [@hntrl](https://github.com/hntrl)! - chore(deps): remove redundant @types/uuid declarations Remove `@types/uuid` from package manifests that rely on `@langchain/core/utils/uuid` or do not require uuid type stubs directly, and refresh the lockfile entries accordingly. - [#10556](#10556) [`9076f06`](9076f06) Thanks [@muhammadosama984](https://github.com/muhammadosama984)! - refactor(deepseek,xai): remove redundant reasoning_content overrides - Updated dependencies \[[`229a7ad`](229a7ad), [`36fb0ef`](36fb0ef)]: - @langchain/openai@1.4.6 ## @langchain/google-genai@2.1.31 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Hunter Lovell <hunter@hntrl.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
llmToolSelectorMiddlewareinternally callsstructuredModel.invoke()to select relevant tools. Whenagent.stream()is used withstreamMode: "messages", LangGraph injects aStreamMessagesHandlerintoconfig.callbacksand stores it inAsyncLocalStorage. Without an explicit config override, this handler is inherited by the internal invoke call, causing the tool selection response ({"tools":["..."]}) to appear as an assistant message in the UI stream.Root cause
ensureConfig()in@langchain/coremerges the explicit config with the implicit config fromAsyncLocalStorage. Ifconfig.callbacksisundefined, theStreamMessagesHandlerfrom the parent context is inherited. Passingcallbacks: [](an empty array) breaks this inheritance because a non-undefinedvalue always overrides the implicit one.LangSmith tracing is unaffected — it is injected via
LangChainTracer.getTraceableRunTree()inside_configureSync, not throughconfig.callbacks.Fix
Build an explicit config using
mergeConfigs:callbacks: []preventsStreamMessagesHandlerfrom being inherited viaAsyncLocalStoragepickRunnableConfigKeysinherits the parent config (tags, metadata, configurable, etc.) fromruntimelc_source: "llmToolSelector"tags the call for observability, consistent withsummarizationMiddlewareFixes #10042
Test plan
yarn test src/agents/middleware/tests/llmToolSelector.test.tsOPENAI_API_KEY=... yarn vitest run --mode int src/agents/middleware/tests/llmToolSelector.int.test.ts