langchain-ai / langchain-ai/langgraph

Multiple Tool Results for Single Tool Call with LangGraph Human Approval Flow

Open
#4,397 13 comments 0 reactions 0 assignees View on GitHub
bug external pending
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

![Image](https://github.com/user-attachments/assets/5543084a-f3e6-4528-a975-bb64becda727)

### 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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.