Skip to content

fix(langchain): prevent llmToolSelectorMiddleware from leaking into message stream#10160

Merged
Christian Bromann (christian-bromann) merged 6 commits into
langchain-ai:mainfrom
JadenKim-dev:fix-middleware-stream
May 16, 2026
Merged

fix(langchain): prevent llmToolSelectorMiddleware from leaking into message stream#10160
Christian Bromann (christian-bromann) merged 6 commits into
langchain-ai:mainfrom
JadenKim-dev:fix-middleware-stream

Conversation

@JadenKim-dev

@JadenKim-dev Youngho Kim (JadenKim-dev) commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Summary

llmToolSelectorMiddleware internally calls structuredModel.invoke() to select relevant tools. When agent.stream() is used with streamMode: "messages", LangGraph injects a StreamMessagesHandler into config.callbacks and stores it in AsyncLocalStorage. 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/core merges the explicit config with the implicit config from AsyncLocalStorage. If config.callbacks is undefined, the StreamMessagesHandler from the parent context is inherited. Passing callbacks: [] (an empty array) breaks this inheritance because a non-undefined value always overrides the implicit one.

LangSmith tracing is unaffected — it is injected via LangChainTracer.getTraceableRunTree() inside _configureSync, not through config.callbacks.

Fix

Build an explicit config using mergeConfigs:

const baseConfig: RunnableConfig = pickRunnableConfigKeys(request.runtime) ?? {};
const config = mergeConfigs(baseConfig, {
  metadata: { lc_source: "llmToolSelector" },
  callbacks: [],
});
  • callbacks: [] prevents StreamMessagesHandler from being inherited via AsyncLocalStorage
  • pickRunnableConfigKeys inherits the parent config (tags, metadata, configurable, etc.) from runtime
  • lc_source: "llmToolSelector" tags the call for observability, consistent with summarizationMiddleware

Fixes #10042

Test plan

  • Unit tests: yarn test src/agents/middleware/tests/llmToolSelector.test.ts
  • Integration test verifying no stream leak: OPENAI_API_KEY=... yarn vitest run --mode int src/agents/middleware/tests/llmToolSelector.int.test.ts

@changeset-bot

changeset-bot Bot commented Feb 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff99621

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
langchain Patch

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".
…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.
@JadenKim-dev Youngho Kim (JadenKim-dev) marked this pull request as draft February 26, 2026 13:41
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.
@JadenKim-dev Youngho Kim (JadenKim-dev) marked this pull request as ready for review February 26, 2026 13:50
@JadenKim-dev Youngho Kim (JadenKim-dev) changed the title fix(langchain): prevent llmToolSelectorMiddleware from leaking into message stream fix(agents): prevent llmToolSelectorMiddleware from leaking into message stream Feb 28, 2026
christian-bromann

This comment was marked as outdated.

@christian-bromann Christian Bromann (christian-bromann) changed the title fix(agents): prevent llmToolSelectorMiddleware from leaking into message stream fix(langchain): prevent llmToolSelectorMiddleware from leaking into message stream May 3, 2026
});

const agent = createAgent({
model: new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

…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.
@JadenKim-dev

Youngho Kim (JadenKim-dev) commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

Christian Bromann (@christian-bromann)
Replaced the integration test with FakeListChatModel so it no longer needs a real API call.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

Thanks for the update!

@christian-bromann Christian Bromann (christian-bromann) merged commit bba900c into langchain-ai:main May 16, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JS LLM Tool Selector Middleware: internal structuredModel.invoke() is streamed to UI

2 participants