zylon-ai/private-gpt · error · ValueError

Chat history contains a tool message: {message.content}. Rea

Error message

Chat history contains a tool message: {message.content}. Reason: {reason or 'Unknown'}

What it means

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.

Source

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

    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

    def is_a_potential_tool_message(msg: ChatMessage) -> tuple[bool, str | None]:
        if msg.role == "tool":
            return True, "Tool message detected"
        if msg.additional_kwargs.get("tool_calls", None):
            return True, "Tool call detected"
        if msg.additional_kwargs.get("tool_call_id", None):
            return True, "Tool call ID detected"
        return False, None

    for message in chat_history:
        is_tool, reason = is_a_potential_tool_message(message)
        if is_tool:
            raise ValueError(
                f"Chat history contains a tool message: {message.content}. Reason: {reason or 'Unknown'}"
            )


def _assert_only_one_user_block(
    chat_history: list[ChatMessage],
) -> None:
    """Assert that the chat history contains exactly one user block.

    This is used to ensure that the chat history
    is clean and contains only one user block.
    """
    if not chat_history:
        return

    matrix = get_user_blocks(chat_history)
    if not matrix:
        raise ValueError("Chat history does not contain any user messages.")

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Use repair_with_tools() instead when the history legitimately contains tool flows
  2. Strip 'tool_calls' and 'tool_call_id' from additional_kwargs and drop role=='tool' messages before calling repair_without_tools()
  3. On the producer side, don't persist tool metadata when the target path is tool-free

Example fix

# before
repaired = repair_without_tools(chat_history)

# after
scrubbed = [
    m for m in chat_history if m.role != "tool"
]
for m in scrubbed:
    m.additional_kwargs.pop("tool_calls", None)
    m.additional_kwargs.pop("tool_call_id", None)
repaired = repair_without_tools(scrubbed)
Defensive patterns

Strategy: validation

Validate before calling

def is_tool_free(history: list[ChatMessage]) -> bool:
    return not any(
        m.role == "tool"
        or m.additional_kwargs.get("tool_calls")
        or m.additional_kwargs.get("tool_call_id")
        for m in history
    )

assert is_tool_free(conversation_messages)

Type guard

def is_tool_free(history: list[ChatMessage]) -> bool:
    return not any(
        m.role == "tool"
        or m.additional_kwargs.get("tool_calls")
        or m.additional_kwargs.get("tool_call_id")
        for m in history
    )

Try / catch

try:
    repaired = repair_without_tools(chat_history)
except ValueError as e:
    if "contains a tool message" in str(e):
        repaired = repair_with_tools(chat_history)  # correct path for tool histories
    else:
        raise

Prevention

When it happens

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

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

Related errors


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