{"record":{"id":"47c8abdf4717f765","repo":"zylon-ai/private-gpt","slug":"chat-history-contains-a-tool-message-message-con","errorCode":null,"errorMessage":"Chat history contains a tool message: {message.content}. Reason: {reason or 'Unknown'}","messagePattern":"Chat history contains a tool message: (.+?)\\. Reason: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"private_gpt/components/chat/processors/chat_history/memory/utils/repairs.py","lineNumber":189,"sourceCode":"    This is used to ensure that the chat history is\n    clean and does not contain any tool calls or tool messages.\n    \"\"\"\n    if not chat_history:\n        return\n\n    def is_a_potential_tool_message(msg: ChatMessage) -> tuple[bool, str | None]:\n        if msg.role == \"tool\":\n            return True, \"Tool message detected\"\n        if msg.additional_kwargs.get(\"tool_calls\", None):\n            return True, \"Tool call detected\"\n        if msg.additional_kwargs.get(\"tool_call_id\", None):\n            return True, \"Tool call ID detected\"\n        return False, None\n\n    for message in chat_history:\n        is_tool, reason = is_a_potential_tool_message(message)\n        if is_tool:\n            raise ValueError(\n                f\"Chat history contains a tool message: {message.content}. Reason: {reason or 'Unknown'}\"\n            )\n\n\ndef _assert_only_one_user_block(\n    chat_history: list[ChatMessage],\n) -> None:\n    \"\"\"Assert that the chat history contains exactly one user block.\n\n    This is used to ensure that the chat history\n    is clean and contains only one user block.\n    \"\"\"\n    if not chat_history:\n        return\n\n    matrix = get_user_blocks(chat_history)\n    if not matrix:\n        raise ValueError(\"Chat history does not contain any user messages.\")","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/components/chat/processors/chat_history/memory/utils/repairs.py#L171-L207","documentation":"Raised by _assert_repair_without_tools (used by repair_without_tools) after repair: the history must be free of tool artifacts. A message counts as a tool artifact if role=='tool', if additional_kwargs contains 'tool_calls', or if it contains 'tool_call_id'. Any hit means tool content survived into a tool-free pipeline.","triggerScenarios":"Calling repair_without_tools() on history that includes tool results or assistant messages carrying tool_calls/tool_call_id in additional_kwargs — for example feeding an agent-with-tools transcript into a no-tools model path.","commonSituations":"Switching a conversation from a tool-enabled agent to a plain chat model; persisted assistant messages retaining tool_calls metadata; mixing RAG 'sources' kwargs that accidentally use the tool_calls key.","solutions":["Use repair_with_tools() instead when the history legitimately contains tool flows","Strip 'tool_calls' and 'tool_call_id' from additional_kwargs and drop role=='tool' messages before calling repair_without_tools()","On the producer side, don't persist tool metadata when the target path is tool-free"],"exampleFix":"# before\nrepaired = repair_without_tools(chat_history)\n\n# after\nscrubbed = [\n    m for m in chat_history if m.role != \"tool\"\n]\nfor m in scrubbed:\n    m.additional_kwargs.pop(\"tool_calls\", None)\n    m.additional_kwargs.pop(\"tool_call_id\", None)\nrepaired = repair_without_tools(scrubbed)","handlingStrategy":"validation","validationCode":"def is_tool_free(history: list[ChatMessage]) -> bool:\n    return not any(\n        m.role == \"tool\"\n        or m.additional_kwargs.get(\"tool_calls\")\n        or m.additional_kwargs.get(\"tool_call_id\")\n        for m in history\n    )\n\nassert is_tool_free(conversation_messages)","typeGuard":"def is_tool_free(history: list[ChatMessage]) -> bool:\n    return not any(\n        m.role == \"tool\"\n        or m.additional_kwargs.get(\"tool_calls\")\n        or m.additional_kwargs.get(\"tool_call_id\")\n        for m in history\n    )","tryCatchPattern":"try:\n    repaired = repair_without_tools(chat_history)\nexcept ValueError as e:\n    if \"contains a tool message\" in str(e):\n        repaired = repair_with_tools(chat_history)  # correct path for tool histories\n    else:\n        raise","preventionTips":["Pick repair_with_tools vs repair_without_tools based on whether tools were used in the session","Scrub tool_calls/tool_call_id kwargs when exporting to tool-free pipelines","Keep tool metadata namespaced so accidental collisions can't occur"],"tags":["chat-history","tool-calls","validation","repair"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}