{"record":{"id":"3e6f04c1ac781c06","repo":"deepset-ai/haystack","slug":"expected-one-toolexecutiondecision-for-each-tool-c","errorCode":null,"errorMessage":"Expected one ToolExecutionDecision for each tool call, but received {len(tool_execution_decisions)} decisions for {len(tool_calls)} tool calls.","messagePattern":"Expected one ToolExecutionDecision for each tool call, but received (.+?) decisions for (.+?) tool calls\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"haystack/hooks/human_in_the_loop/strategies.py","lineNumber":561,"sourceCode":"\ndef _apply_tool_execution_decisions(\n    tool_call_messages: list[ChatMessage], tool_execution_decisions: list[ToolExecutionDecision]\n) -> tuple[list[ChatMessage], list[ChatMessage]]:\n    \"\"\"\n    Apply the tool execution decisions to the tool call messages.\n\n    :param tool_call_messages: The tool call messages to apply the decisions to.\n    :param tool_execution_decisions: The tool execution decisions to apply.\n    :returns:\n        A tuple containing:\n        - A list of rejection messages for rejected tool calls. These are pairs of tool call and tool call result\n          messages.\n        - A list of tool call messages for confirmed or modified tool calls. If tool parameters were modified,\n          a user message explaining the modification is included before the tool call message.\n    \"\"\"\n    tool_calls = [tc for message in tool_call_messages for tc in (message.tool_calls or [])]\n    if len(tool_calls) != len(tool_execution_decisions):\n        raise ValueError(\n            f\"Expected one ToolExecutionDecision for each tool call, but received {len(tool_execution_decisions)} \"\n            f\"decisions for {len(tool_calls)} tool calls.\"\n        )\n\n    # Create lookup for decisions that have a tool_call_id\n    decision_by_id = {d.tool_call_id: d for d in tool_execution_decisions if d.tool_call_id}\n\n    # Create a lookup for decisions that don't have a tool_call_id. We use tool name instead.\n    decisions_without_id = [d for d in tool_execution_decisions if not d.tool_call_id]\n    decision_by_name = {d.tool_name: d for d in decisions_without_id if d.tool_name}\n\n    def make_assistant_message(chat_message: ChatMessage, tool_calls: list[ToolCall]) -> ChatMessage:\n        return ChatMessage.from_assistant(\n            text=chat_message.text,\n            meta=chat_message.meta,\n            name=chat_message.name,\n            tool_calls=tool_calls,\n            reasoning=chat_message.reasoning,","sourceCodeStart":543,"sourceCodeEnd":579,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/hooks/human_in_the_loop/strategies.py#L543-L579","documentation":"_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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ndecisions = [decide(first_call)]  # only one decision\napply(messages_with_two_calls, decisions)  # ValueError\n// after\ndecisions = [decide(tc) for tc in all_tool_calls]  # one per tool call\napply(messages, decisions)","handlingStrategy":"validation","validationCode":"tool_calls = [tc for m in tool_call_messages for tc in (m.tool_calls or [])]\nassert len(tool_calls) == len(tool_execution_decisions), \\\n    f\"need {len(tool_calls)} decisions, got {len(tool_execution_decisions)}\"","typeGuard":null,"tryCatchPattern":"try:\n    result = _apply_tool_execution_decisions(tool_call_messages, decisions)\nexcept ValueError as e:\n    if \"Expected one ToolExecutionDecision\" in str(e):\n        decisions = [default_decision(tc) for tc in all_tool_calls]\n        result = _apply_tool_execution_decisions(tool_call_messages, decisions)\n    else:\n        raise","preventionTips":["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"],"tags":["python","validation","hooks","human-in-the-loop","tools"],"backgroundTag":"decision-count-mismatch","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}