{"record":{"id":"c36b925a7431be60","repo":"deepset-ai/haystack","slug":"no-unused-toolexecutiondecision-matches-tool-call","errorCode":null,"errorMessage":"No unused ToolExecutionDecision matches tool call {tc.tool_name!r} with ID {tc.id!r}. Set tool_call_id to match decisions to tool calls reliably.","messagePattern":"No unused ToolExecutionDecision matches tool call (.+?) with ID (.+?)\\. Set tool_call_id to match decisions to tool calls reliably\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"haystack/hooks/human_in_the_loop/strategies.py","lineNumber":591,"sourceCode":"    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,\n        )\n\n    new_tool_call_messages = []\n    rejection_messages = []\n    for chat_msg in tool_call_messages:\n        new_tool_calls = []\n        for tc in chat_msg.tool_calls or []:\n            ted = decision_by_id.pop(tc.id or \"\", None)\n            if ted is None:\n                ted = decision_by_name.pop(tc.tool_name, None)\n            if ted is None:\n                raise ValueError(\n                    f\"No unused ToolExecutionDecision matches tool call {tc.tool_name!r} with ID {tc.id!r}. \"\n                    \"Set tool_call_id to match decisions to tool calls reliably.\"\n                )\n\n            classified_decision = _classify_decision(tool_call=tc, decision=ted)\n            final_args = ted.final_tool_params or {}\n\n            if classified_decision == \"reject\":\n                # rejected tool call\n                tool_result_text = ted.feedback or REJECTION_FEEDBACK_TEMPLATE.format(tool_name=tc.tool_name)\n                rejection_messages.extend(\n                    [\n                        make_assistant_message(chat_msg, [tc]),\n                        ChatMessage.from_tool(tool_result=tool_result_text, origin=tc, error=True),\n                    ]\n                )\n                continue\n","sourceCodeStart":573,"sourceCodeEnd":609,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/hooks/human_in_the_loop/strategies.py#L573-L609","documentation":"When matching decisions to tool calls, the code first looks up by tool_call_id, then falls back to unused decisions by tool_name. If no remaining decision matches either key for a given tool call, it cannot safely determine what the user decided, so a ValueError is raised advising to set tool_call_id.","triggerScenarios":"Applying decisions whose tool_call_id values do not match the tool calls' ids and whose tool_name values differ (or were already consumed) — e.g. decisions built for a different request, stale decisions reused across runs, or names altered by modification.","commonSituations":"Reusing a saved decision list in a new Agent run where tool call ids changed; custom strategy returning decisions with wrong/renamed tool_name; multiple same-name tool calls with ambiguous id-less decisions.","solutions":["Set tool_call_id on each ToolExecutionDecision to the exact id of the corresponding tool call","Verify tool_name on each decision matches the tool call's tool_name when ids are absent","Regenerate decisions from the same tool call messages instead of reusing stale ones"],"exampleFix":"// before\nToolExecutionDecision(tool_name=\"search\", ...)  # no tool_call_id, name mismatch\n// after\nToolExecutionDecision(tool_name=\"search\", tool_call_id=tool_call.id, ...)","handlingStrategy":"validation","validationCode":"ids = {tc.id for m in tool_call_messages for tc in (m.tool_calls or []) if tc.id}\nfor d in tool_execution_decisions:\n    assert d.tool_call_id in ids, f\"decision for {d.tool_name!r} has unknown tool_call_id {d.tool_call_id!r}\"","typeGuard":"def decision_matches(decision, tool_call) -> bool:\n    return bool(decision.tool_call_id and decision.tool_call_id == tool_call.id) or \\\n        decision.tool_name == tool_call.tool_name","tryCatchPattern":"try:\n    result = _apply_tool_execution_decisions(messages, decisions)\nexcept ValueError as e:\n    if \"No unused ToolExecutionDecision\" in str(e):\n        decisions = [replace(d, tool_call_id=tc.id) for d, tc in zip(decisions, all_tool_calls)]\n        result = _apply_tool_execution_decisions(messages, decisions)\n    else:\n        raise","preventionTips":["Always set tool_call_id on decisions from the originating tool call","Regenerate decisions per run instead of caching across runs","Keep tool_name consistent with the actual registered tool"],"tags":["python","validation","hooks","human-in-the-loop","tools"],"backgroundTag":"tool-call-id-mismatch","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}