Skip to content

fix(langchain): after model send dispatch#10444

Merged
Christian Bromann (christian-bromann) merged 6 commits intomainfrom
cb/fix-v2-aftermodel-send-dispatch
Mar 17, 2026
Merged

fix(langchain): after model send dispatch#10444
Christian Bromann (christian-bromann) merged 6 commits intomainfrom
cb/fix-v2-aftermodel-send-dispatch

Conversation

@christian-bromann
Copy link
Member

@christian-bromann Christian Bromann (christian-bromann) commented Mar 17, 2026

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: with version: "v2" (the default) and afterModel middleware present, all tool calls were silently routed to a single ToolNode invocation — behaving like version: "v1" — instead of dispatching each tool call as a separate Send task.

Root cause

#createAfterModelRouter contained a blanket return TOOLS_NODE_NAME at 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 #createModelRouter does the dispatch), but not for the afterModel path. When afterModel middleware is present, AGENT_NODE_NAME connects to the last afterModel node via a direct edge, bypassing #createModelRouter entirely. The first afterModel node then routes to tools — but it was always doing so as a plain string, not as Send objects.

Fix

#createAfterModelRouter now branches on #toolBehaviorVersion at the end of its routing path:

  • v1: returns TOOLS_NODE_NAME (plain string) — single ToolNode invocation, all calls run via Promise.all
  • v2: returns [Send(TOOLS_NODE_NAME, { ...state, lg_tool_call: toolCall })] per regular tool call — matching what #createModelRouter already does for the no-middleware path, and matching Python LangGraph's post_model_hook_router

No graph construction changes were needed. The path_map for the conditional edge on firstAfterModelNode already declares TOOLS_NODE_NAME as a valid destination, which covers both string returns and Send objects targeting that node.

Breaking change

Any agent using version: "v2" (the default) together with afterModel middleware will now correctly dispatch one Send task per tool call instead of batching all calls into a single node. This changes:

  • Checkpoint granularity: each tool call gets its own checkpoint
  • Interrupt/resume behaviour: runs can now be paused and resumed between individual tool calls in the afterModel path
  • State isolation: each tool call sees its own snapshotted state

Migration: if you rely on the previous parallel-within-a-single-node behaviour, pass version: "v1" explicitly to preserve it. See the expanded version JSDoc on CreateAgentParams for guidance on which option fits your use case.

…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-bot
Copy link

changeset-bot bot commented Mar 17, 2026

🦋 Changeset detected

Latest commit: 552fda7

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

@github-actions
Copy link
Contributor

📦 Dev Release Published

A dev release has been published for this PR!

Package Version
@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

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

View workflow run

@github-actions github-actions bot added the ready label Mar 17, 2026
Base automatically changed from cb/tool-call-invoke-fix to main March 17, 2026 19:55
@christian-bromann Christian Bromann (christian-bromann) merged commit 82d56cb into main Mar 17, 2026
26 checks passed
@christian-bromann Christian Bromann (christian-bromann) deleted the cb/fix-v2-aftermodel-send-dispatch branch March 17, 2026 19:56
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.

2 participants