deepset-ai/haystack · error
Expected one ToolExecutionDecision for each tool call, but r
Error message
Expected one ToolExecutionDecision for each tool call, but received {len(tool_execution_decisions)} decisions for {len(tool_calls)} tool calls. What it means
_apply_tool_execution_decisions requires a strict 1:1 correspondence between tool calls found in the tool call messages and the ToolExecutionDecision list produced by confirmation strategies. A count mismatch means the decision list was truncated, duplicated, or built from different tool calls, so a ValueError is raised before any decision is applied.
Source
Thrown at haystack/hooks/human_in_the_loop/strategies.py:561
def _apply_tool_execution_decisions(
tool_call_messages: list[ChatMessage], tool_execution_decisions: list[ToolExecutionDecision]
) -> tuple[list[ChatMessage], list[ChatMessage]]:
"""
Apply the tool execution decisions to the tool call messages.
:param tool_call_messages: The tool call messages to apply the decisions to.
:param tool_execution_decisions: The tool execution decisions to apply.
:returns:
A tuple containing:
- A list of rejection messages for rejected tool calls. These are pairs of tool call and tool call result
messages.
- A list of tool call messages for confirmed or modified tool calls. If tool parameters were modified,
a user message explaining the modification is included before the tool call message.
"""
tool_calls = [tc for message in tool_call_messages for tc in (message.tool_calls or [])]
if len(tool_calls) != len(tool_execution_decisions):
raise ValueError(
f"Expected one ToolExecutionDecision for each tool call, but received {len(tool_execution_decisions)} "
f"decisions for {len(tool_calls)} tool calls."
)
# Create lookup for decisions that have a tool_call_id
decision_by_id = {d.tool_call_id: d for d in tool_execution_decisions if d.tool_call_id}
# Create a lookup for decisions that don't have a tool_call_id. We use tool name instead.
decisions_without_id = [d for d in tool_execution_decisions if not d.tool_call_id]
decision_by_name = {d.tool_name: d for d in decisions_without_id if d.tool_name}
def make_assistant_message(chat_message: ChatMessage, tool_calls: list[ToolCall]) -> ChatMessage:
return ChatMessage.from_assistant(
text=chat_message.text,
meta=chat_message.meta,
name=chat_message.name,
tool_calls=tool_calls,
reasoning=chat_message.reasoning,View on GitHub (pinned to e318778c9b)
Solutions
- Return exactly one ToolExecutionDecision (approve/reject/modify) per tool call in your strategy
- If some calls should be skipped, emit an explicit decision (e.g. reject or approve) for them rather than omitting
- Ensure the tool_call_messages list passed in matches the ones the decisions were generated from
Example fix
// before decisions = [decide(first_call)] # only one decision apply(messages_with_two_calls, decisions) # ValueError // after decisions = [decide(tc) for tc in all_tool_calls] # one per tool call apply(messages, decisions)
Defensive patterns
Strategy: validation
Validate before calling
tool_calls = [tc for m in tool_call_messages for tc in (m.tool_calls or [])]
assert len(tool_calls) == len(tool_execution_decisions), \
f"need {len(tool_calls)} decisions, got {len(tool_execution_decisions)}" Try / catch
try:
result = _apply_tool_execution_decisions(tool_call_messages, decisions)
except ValueError as e:
if "Expected one ToolExecutionDecision" in str(e):
decisions = [default_decision(tc) for tc in all_tool_calls]
result = _apply_tool_execution_decisions(tool_call_messages, decisions)
else:
raise Prevention
- Build decisions by iterating every tool call found in the messages
- Never filter decisions before applying them; encode skip as an explicit decision
- Test custom strategies against multi-tool-call (parallel) messages
When it happens
Trigger: Passing tool_execution_decisions whose length differs from the number of tool calls extracted from tool_call_messages, e.g. dropping a decision after filtering, or messages containing multiple tool calls while the strategy returned one decision.
Common situations: Custom confirmation strategies that skip tool calls instead of returning a decision for each; parallel tool calls (n>1) where the user only answered one prompt; combining decisions from separate runs.
Related errors
- No unused ToolExecutionDecision matches tool call {tc.tool_n
- Tool execution requires at least one tool.
- `function` must be a synchronous function. '{function.__name
- `async_function` must be a coroutine function defined with `
- Hook function '{func.__name__}' must take a single parameter
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/3e6f04c1ac781c06.
Report an issue: GitHub.