Skip to content

fix(langchain): respect version:"v1" in afterModel router's pending tool call path#10443

Merged
Christian Bromann (christian-bromann) merged 3 commits intomainfrom
cb/tool-call-invoke-fix
Mar 17, 2026
Merged

fix(langchain): respect version:"v1" in afterModel router's pending tool call path#10443
Christian Bromann (christian-bromann) merged 3 commits intomainfrom
cb/tool-call-invoke-fix

Conversation

@christian-bromann

createAgent accepts a version option ("v1" | "v2", defaulting to "v2") that controls how tool calls are dispatched. With version: "v1" all tool calls from a single AIMessage are executed concurrently inside one ToolNode invocation via Promise.all, which is important for agents whose tools invoke sub-graphs — the v2 Send-per-task approach can serialize those calls due to LangGraph's per-task checkpoint writes.

This PR fixes a bug where version: "v1" was not fully respected when afterModel middleware was present and an interrupted run was being resumed.

Bug

#createAfterModelRouter contains a pendingToolCalls branch that fires during interrupt resumption — when some tool calls from an AIMessage have already completed and only a subset remain. That branch unconditionally dispatched each pending call as a separate Send task regardless of #toolBehaviorVersion:

// Before — always Send, ignoring version
if (pendingToolCalls && pendingToolCalls.length > 0) {
  return pendingToolCalls.map(
    (toolCall) => new Send(TOOLS_NODE_NAME, { ...state, lg_tool_call: toolCall })
  );
}

With version: "v1" this silently switched back to v2-style dispatch for the resumed portion of the run, breaking the parallelism guarantee.

Fix

The pendingToolCalls path now branches on #toolBehaviorVersion:

  • v1: returns TOOLS_NODE_NAME as a plain string. ToolNode.run() receives the full message state, filters out already-completed calls internally, and runs the remainder via Promise.all.
  • v2: keeps the existing Send-per-call dispatch unchanged.

This matches the Python LangGraph implementation where the equivalent post_model_hook_router only dispatches via Send and is structurally unreachable from version: "v1".

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

changeset-bot bot commented Mar 17, 2026

🦋 Changeset detected

Latest commit: 0ae443b

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

Ensure the afterModel router respects the specified version in the pending tool call path.
@github-actions github-actions bot added the ready label Mar 17, 2026
@christian-bromann Christian Bromann (christian-bromann) merged commit ff6822e into main Mar 17, 2026
30 checks passed
@christian-bromann Christian Bromann (christian-bromann) deleted the cb/tool-call-invoke-fix branch March 17, 2026 19:55
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