langchain-ai / langchain-ai/langgraph
Multiple Tool Results for Single Tool Call with LangGraph Human Approval Flow
- Dominant language
- Python
- Stars
- 41.8k
- Forks
- 7.1k
- Avg merge
- 23h 7m
- Merged PRs (30d)
- 30
Description
### Checked other resources
- [x] This is a bug, not a usage question. For questions, please use GitHub Discussions.
- [x] I added a clear and detailed title that summarizes the issue.
- [x] I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
- [x] I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.
### Example Code
```python
def routing_node_factory(agent_name, safe, sensitive):
def routing_node(state: GraphState) -> Command:
ai_message = state["messages"][-1]
tool_calls = getattr(ai_message, "tool_calls", []) or []
tools_requested = [call["name"] for call in tool_calls]
safe_tool_names = [t.name for t in safe]
sensitive_tool_names = [t.name for t in sensitive]
for tool in tools_requested:
if tool in sensitive_tool_names:
return Command(goto="human_approval", update={"next": f"{agent_name}_sensitive"})
if tool in safe_tool_names:
return Command(goto=f"{agent_name}_safe", update={"next": f"{agent_name}_safe"})
return Command(goto=END, update={"next": END})
return routing_node
def tool_result_handler(state: GraphState) -> GraphState:
tool_use_message = next(
(m for m in reversed(state["messages"]) if hasattr(m, "tool_calls") and m.tool_calls),
None
)
if not tool_use_message:
return state
existing_result_ids = {
getattr(m, "tool_call_id", None)
for m in state["messages"]
if isinstance(m, ToolMessage)
}
new_results = []
for tool_call in tool_use_message.tool_calls:
call_id = tool_call["id"]
if call_id in existing_result_ids:
continue
tool_result_message = next(
(m for m in state["messages"] if isinstance(m, ToolMessage) and m.tool_call_id == call_id),
None
)
if tool_result_message:
new_results.append(tool_result_message)
break
if not new_results:
return state
return GraphState(messages=state["messages"] + new_results)
```
### Error Message and Stack Trace (if applicable)
```shell
InvokeModelWithResponseStream operation: messages.4: Too many tool_result blocks found: 3, expected 1. The number of tool_result blocks must match the number of tool_use blocks in the previous message.
```
### Description
I'm experiencing an issue with my LangGraph implementation that uses a supervisor pattern to route tool calls to different agents, with a distinction between "safe" and "sensitive" tools. When a sensitive tool is called (one requiring human approval), I'm getting an error:
InvokeModelWithResponseStream operation: messages.4: Too many tool_result blocks found: 3, expected 1. The number of tool_result blocks must match the number of tool_use blocks in the previous message.
Interestingly, safe tools (which don't require human approval) work perfectly fine with the same architecture. The problem seems to be that after a sensitive tool is properly executed and returns a result, the system re-enters the same agent and executes the same tool twice more, resulting in three tool results for a single tool call.
My implementation follows a pattern where I have multiple specialized agents (math, documents, tasks, boards, etc.), each with access to both "safe" tools that execute directly and "sensitive" tools that require human approval before execution. A supervisor node routes tool calls to the appropriate agent based on the tool name, and a routing node for each agent determines if the tool is safe or sensitive.
For sensitive tools, the flow goes through a human approval node before execution. After tool execution, results go through a tool result handler that adds the result to the message history. The issue only occurs with sensitive tools that require human approval - the architecture works perfectly with safe tools.
I suspect there might be an issue with how the human approval flow is connected in the graph, possibly causing loops or duplicate executions of sensitive tools.
my structure is the one attached

### System Info
System Information
------------------
> OS: Windows
> OS Version: 10.0.26100
> Python Version: 3.12.6 (tags/v3.12.6:a4a2d2b, Sep 6 2024, 20:11:23) [MSC v.1940 64 bit (AMD64)]
Package Information
-------------------
> langchain_core: 0.2.43
> langchain: 0.2.17
> langchain_community: 0.2.19
> langsmith: 0.1.147
> langchain_aws: 0.1.18
> langchain_experimental: 0.0.64
> langchain_text_splitters: 0.2.4
> langgraph: 0.3.28
Optional packages not installed
-------------------------------
> langserve
Other Dependencies
------------------
> aiohttp: 3.11.16
> async-timeout: Installed. No version info available.
> boto3: 1.34.162
> dataclasses-json: 0.6.7
> httpx: 0.27.2
> jsonpatch: 1.33
> langgraph-checkpoint: 2.0.24
> langgraph-prebuilt: 0.1.8
> langgraph-sdk: 0.1.61
> langsmith-pyo3: Installed. No version info available.
> numpy: 1.26.4
> orjson: 3.10.16
> packaging: 24.2
> pydantic: 2.11.3
> PyYAML: 6.0.2
> requests: 2.32.3
> requests-toolbelt: 1.0.0
> SQLAlchemy: 2.0.40
> tenacity: 8.5.0
> typing-extensions: 4.13.2
> xxhash: 3.5.0
Contributor guide
Research direction
Start with the supplied routing_node_factory and tool_result_handler functions, then trace the sensitive-tool path through human approval and execution. Reproduce the InvokeModelWithResponseStream error and inspect whether the graph re-enters the agent or appends duplicate ToolMessage results. Done means one tool_result block is produced for each tool_use block after approval.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- ai
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100