fix(langchain): after model send dispatch#10444
Merged
Christian Bromann (christian-bromann) merged 6 commits intomainfrom Mar 17, 2026
Merged
fix(langchain): after model send dispatch#10444Christian Bromann (christian-bromann) merged 6 commits intomainfrom
Christian Bromann (christian-bromann) merged 6 commits intomainfrom
Conversation
…ool call path When afterModel middleware was present and an interrupted run was resumed, Send tasks regardless of the `version` option. With version:"v1" this caused the remaining calls to be executed one-per-graph-task instead of in parallel via Promise.all inside a single ToolNode invocation, breaking the user's expectation of parallelism. The fix branches on #toolBehaviorVersion in the pendingToolCalls path: v1 returns the plain TOOLS_NODE_NAME string so ToolNode handles the remaining calls with its existing Promise.all path; v2 keeps the Send-per-call dispatch unchanged. This matches Python LangGraph's behaviour, where the post_model_hook router (the analogous path) is only reachable in v2 and therefore always uses Send. Also expands the version JSDoc to explain when each option is the better choice, particularly calling out the checkpoint serialisation that occurs in v2 when tools invoke sub-graphs.
Ensure the afterModel router respects the specified version in the pending tool call path.
🦋 Changeset detectedLatest commit: 552fda7 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 |
aa0b887 to
96e1012
Compare
Contributor
📦 Dev Release PublishedA dev release has been published for this PR!
Install: npm i @langchain/anthropic@1.3.24-dev-1773776071697 @langchain/aws@1.4.0-dev-1773776071697 @langchain/classic@1.0.24-dev-1773776071697 @langchain/community@1.2.0-dev-1773776071697 @langchain/core@1.1.33-dev-1773776071697 @langchain/deepseek@1.0.18-dev-1773776071697 @langchain/google@0.1.7-dev-1773776071697 @langchain/google-common@2.1.26-dev-1773776071697 @langchain/google-gauth@2.1.26-dev-1773776071697 @langchain/google-genai@2.1.26-dev-1773776071697 @langchain/google-vertexai@2.1.26-dev-1773776071697 @langchain/google-vertexai-web@2.1.26-dev-1773776071697 @langchain/google-webauth@2.1.26-dev-1773776071697 @langchain/openai@1.3.0-dev-1773776071697 @langchain/openrouter@0.1.7-dev-1773776071697 @langchain/xai@1.3.10-dev-1773776071697 langchain@1.2.33-dev-1773776071697 --save-exact |
Hunter Lovell (hntrl)
approved these changes
Mar 17, 2026
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.
This is a follow-up to #10443 which fixed
version: "v1"not being respected in the interrupt-resumption path of#createAfterModelRouter. While investigating that fix I discovered a broader gap: withversion: "v2"(the default) andafterModelmiddleware present, all tool calls were silently routed to a singleToolNodeinvocation — behaving likeversion: "v1"— instead of dispatching each tool call as a separateSendtask.Root cause
#createAfterModelRoutercontained a blanketreturn TOOLS_NODE_NAMEat the end of its normal routing path, with a comment explaining that "The Send API is handled at the model_request node level". That was true for the no-middleware path (where#createModelRouterdoes the dispatch), but not for the afterModel path. When afterModel middleware is present,AGENT_NODE_NAMEconnects to the last afterModel node via a direct edge, bypassing#createModelRouterentirely. The first afterModel node then routes to tools — but it was always doing so as a plain string, not asSendobjects.Fix
#createAfterModelRouternow branches on#toolBehaviorVersionat the end of its routing path:v1: returnsTOOLS_NODE_NAME(plain string) — single ToolNode invocation, all calls run viaPromise.allv2: returns[Send(TOOLS_NODE_NAME, { ...state, lg_tool_call: toolCall })]per regular tool call — matching what#createModelRouteralready does for the no-middleware path, and matching Python LangGraph'spost_model_hook_routerNo graph construction changes were needed. The
path_mapfor the conditional edge onfirstAfterModelNodealready declaresTOOLS_NODE_NAMEas a valid destination, which covers both string returns andSendobjects targeting that node.Breaking change
Any agent using
version: "v2"(the default) together withafterModelmiddleware will now correctly dispatch oneSendtask per tool call instead of batching all calls into a single node. This changes:Migration: if you rely on the previous parallel-within-a-single-node behaviour, pass
version: "v1"explicitly to preserve it. See the expandedversionJSDoc onCreateAgentParamsfor guidance on which option fits your use case.