{"record":{"id":"5d4b0e39b5984e2f","repo":"zylon-ai/private-gpt","slug":"tool-message-j-in-user-block-i-does-not-follow","errorCode":null,"errorMessage":"Tool message {j} in user block {i} does not follow an assistant message: {message}","messagePattern":"Tool message (.+?) in user block (.+?) does not follow an assistant message: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"private_gpt/components/chat/processors/chat_history/memory/utils/repairs.py","lineNumber":157,"sourceCode":"        if len(block) == 1:\n            # No more validation needed for a single user message block\n            continue\n\n        # Validate the last message in the block\n        if strict and block[-1].role != \"assistant\":\n            raise ValueError(\n                f\"User block {i} does not end with an assistant message: {block[-1]}\"\n            )\n        elif not strict and block[-1].role not in [\"assistant\", \"tool\"]:\n            raise ValueError(\n                f\"User block {i} does not end with an assistant or tool message: {block[-1]}\"\n            )\n\n        # Validate the tool messages\n        for j, message in enumerate(block[1:-1]):\n            if message.role == \"tool\":\n                if j == 0 or block[j].role != \"assistant\":\n                    raise ValueError(\n                        f\"Tool message {j} in user block {i} does not follow an assistant message: {message}\"\n                    )\n            elif message.role != \"assistant\":\n                raise ValueError(\n                    f\"Message {j} in user block {i} is neither an assistant nor a tool: {message}\"\n                )\n\n\ndef _assert_repair_without_tools(\n    chat_history: list[ChatMessage],\n) -> None:\n    \"\"\"Assert that the chat history does not contain any tool messages.\n\n    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","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/components/chat/processors/chat_history/memory/utils/repairs.py#L139-L175","documentation":"Tool-message ordering validation inside a user block: for each message in block[1:-1], a 'tool' role message must directly follow an assistant message (the one that issued the tool_call). It raises when a tool message appears at the start of the interior (j==0, i.e. right after the user message) or the preceding block[j] message is not an assistant message.","triggerScenarios":"Calling repair_with_tools() on history where a tool result appears immediately after the user message, or where ordering was shuffled so a tool message follows another tool/user message instead of its assistant.","commonSituations":"Persisted tool results without their parent assistant tool_call message; parallel tool calls reordered during storage; history truncation that cut off the assistant message but kept the tool result.","solutions":["Reconstruct ordering so each tool message directly follows the assistant message carrying the matching tool_call","Drop orphan tool messages that have no parent assistant message before repair","Fix the persistence layer to store assistant/tool messages atomically as a pair"],"exampleFix":"# before\nrepaired = repair_with_tools(chat_history)\n\n# after\n# drop orphan tool messages (no preceding assistant)\ncleaned = []\nfor m in chat_history:\n    if m.role == \"tool\" and (not cleaned or cleaned[-1].role != \"assistant\"):\n        continue\n    cleaned.append(m)\nrepaired = repair_with_tools(cleaned)","handlingStrategy":"validation","validationCode":"def tool_messages_follow_assistant(history: list[ChatMessage]) -> bool:\n    for i, m in enumerate(history):\n        if m.role == \"tool\":\n            if i == 0 or history[i - 1].role != \"assistant\":\n                return False\n    return True\n\nassert tool_messages_follow_assistant(conversation_messages)","typeGuard":"def is_tool_ordering_valid(history: list[ChatMessage]) -> bool:\n    return tool_messages_follow_assistant(history)","tryCatchPattern":"try:\n    repaired = repair_with_tools(chat_history)\nexcept ValueError as e:\n    if \"does not follow an assistant message\" in str(e):\n        cleaned = [m for i, m in enumerate(chat_history) if not (m.role == \"tool\" and (i == 0 or chat_history[i-1].role != \"assistant\"))]\n        repaired = repair_with_tools(cleaned)\n    else:\n        raise","preventionTips":["Store assistant tool_call and its tool result as an atomic pair","Never truncate history between an assistant tool_call and its tool result","Validate tool ordering at persistence time"],"tags":["chat-history","validation","repair","tool-calls","message-order"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}