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

  1. Return exactly one ToolExecutionDecision (approve/reject/modify) per tool call in your strategy
  2. If some calls should be skipped, emit an explicit decision (e.g. reject or approve) for them rather than omitting
  3. 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

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


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/3e6f04c1ac781c06. Report an issue: GitHub.