zylon-ai/private-gpt · error · ValueError

Tool message {j} in user block {i} does not follow an assist

Error message

Tool message {j} in user block {i} does not follow an assistant message: {message}

What it means

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.

Source

Thrown at private_gpt/components/chat/processors/chat_history/memory/utils/repairs.py:157

        if len(block) == 1:
            # No more validation needed for a single user message block
            continue

        # Validate the last message in the block
        if strict and block[-1].role != "assistant":
            raise ValueError(
                f"User block {i} does not end with an assistant message: {block[-1]}"
            )
        elif not strict and block[-1].role not in ["assistant", "tool"]:
            raise ValueError(
                f"User block {i} does not end with an assistant or tool message: {block[-1]}"
            )

        # Validate the tool messages
        for j, message in enumerate(block[1:-1]):
            if message.role == "tool":
                if j == 0 or block[j].role != "assistant":
                    raise ValueError(
                        f"Tool message {j} in user block {i} does not follow an assistant message: {message}"
                    )
            elif message.role != "assistant":
                raise ValueError(
                    f"Message {j} in user block {i} is neither an assistant nor a tool: {message}"
                )


def _assert_repair_without_tools(
    chat_history: list[ChatMessage],
) -> None:
    """Assert that the chat history does not contain any tool messages.

    This is used to ensure that the chat history is
    clean and does not contain any tool calls or tool messages.
    """
    if not chat_history:
        return

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Reconstruct ordering so each tool message directly follows the assistant message carrying the matching tool_call
  2. Drop orphan tool messages that have no parent assistant message before repair
  3. Fix the persistence layer to store assistant/tool messages atomically as a pair

Example fix

# before
repaired = repair_with_tools(chat_history)

# after
# drop orphan tool messages (no preceding assistant)
cleaned = []
for m in chat_history:
    if m.role == "tool" and (not cleaned or cleaned[-1].role != "assistant"):
        continue
    cleaned.append(m)
repaired = repair_with_tools(cleaned)
Defensive patterns

Strategy: validation

Validate before calling

def tool_messages_follow_assistant(history: list[ChatMessage]) -> bool:
    for i, m in enumerate(history):
        if m.role == "tool":
            if i == 0 or history[i - 1].role != "assistant":
                return False
    return True

assert tool_messages_follow_assistant(conversation_messages)

Type guard

def is_tool_ordering_valid(history: list[ChatMessage]) -> bool:
    return tool_messages_follow_assistant(history)

Try / catch

try:
    repaired = repair_with_tools(chat_history)
except ValueError as e:
    if "does not follow an assistant message" in str(e):
        cleaned = [m for i, m in enumerate(chat_history) if not (m.role == "tool" and (i == 0 or chat_history[i-1].role != "assistant"))]
        repaired = repair_with_tools(cleaned)
    else:
        raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15). Data as JSON: /api/errors/5d4b0e39b5984e2f. Report an issue: GitHub.