I opened PR #8291 to add a few small, dependency-free utilities to langgraph.prebuilt for two patterns that teams frequently re-implement when working with ToolNode:
build_tool_call_key, deduplicate_tool_calls, and deduplicate_tool_calls_in_state
Remove duplicate parallel tool calls from the trailing AIMessage (first occurrence wins).
chain_tool_wrappers and chain_async_tool_wrappers
Compose multiple wrap_tool_call / awrap_tool_call interceptors into a single wrapper. Under async execution, synchronous wrappers are transparently run in a worker thread so they don’t block the event loop.
These utilities build entirely on the existing ToolCallRequest / ToolCallWrapper APIs and do not require any changes to ToolNode itself. The accompanying tests are included and passing.
Would this be a direction the maintainers would welcome in langgraph.prebuilt? If so, I’d be happy to link an approved issue so PR #8291 can be reopened.
Thanks for putting this together — the PR is clean, dependency-free, and the test coverage is genuinely nice to see. A few honest notes on direction so you don’t invest further in the wrong spot, plus where I think the valuable part should land.
Short version: the dedup idea is the keeper; the wrapper-chaining is already covered by the framework; and langgraph.prebuilt is the wrong home for new public API here.
On placement. Tool-call interception is deliberately consolidating in the langchain package’s agent middleware. In LangGraph v1, create_react_agent (and AgentState) in langgraph.prebuilt are deprecated in favor of create_agent from langchain.agents, and ToolNode / ToolCallWrapper now steer you toward AgentMiddleware.wrap_tool_call. So adding new public helpers to langgraph.prebuilt runs against that migration — see the migration guide and custom middleware docs.
On chain_tool_wrappers / chain_async_tool_wrappers.create_agent already composes multiple middleware wrap_tool_call / awrap_tool_call hooks into a single wrapper for you — you add more middleware and they chain in declaration order (outermost first). So this half is already handled by the framework; no separate utility needed.
On the deduplication — this is the valuable, novel bit. There’s no parallel-duplicate-tool-call dedup anywhere in langchain or langgraph today (existing middleware covers retry, call limits, tool selection, and HITL — not this). It has a real home as a small langchainAgentMiddleware — a wrap_tool_call/pre-execution hook that collapses identical parallel calls, first-occurrence-wins, which is essentially your build_tool_call_key logic.
Suggested path:
Open an issue on the langchain repo proposing a tool-call-dedup middleware, with your motivating use case. That puts it in front of the middleware maintainers and also satisfies the require-issue-link check that auto-closed #8291.
Once a maintainer approves the direction, port the dedup logic into a middleware there.
Reframed that way, there’s a solid, mergeable contribution here. Thanks again for pushing on it!
Thanks @dariel.datoon — this is very helpful, and I appreciate the clear direction.
I agree on all three points:
langgraph.prebuilt is the wrong home given the v1 migration to create_agent / AgentMiddleware.
chain_tool_wrappers / chain_async_tool_wrappers are redundant with how create_agent already composes middleware wrap_tool_call hooks — I won’t pursue that half further.
Parallel duplicate tool-call dedup is the valuable piece — I’ll port build_tool_call_key and first-occurrence-wins collapse into a small AgentMiddleware on the langchain side.
Per your suggested path, I opened a feature request on the langchain repo with the motivating use case:
I’m planning to implement this as message-level dedup on the trailing AIMessage (likely via after_model, unless you prefer it as a wrap_tool_call batch hook) and will wait for maintainer approval before opening a PR.