Proposal: composable ToolNode middleware (tool-call dedup + wrap_tool_call chaining)

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.

PR: prebuilt: add composable tool middleware utilities and deduplication support by zamaniali1995 · Pull Request #8291 · langchain-ai/langgraph · GitHub

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 langchain AgentMiddleware — 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:

  1. 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.
  2. 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:

  1. langgraph.prebuilt is the wrong home given the v1 migration to create_agent / AgentMiddleware.
  2. 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.
  3. 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.

Thanks again for the review.