zylon-ai/private-gpt · error · ValueError

Tool result block references an unknown tool use ID: {block.

Error message

Tool result block references an unknown tool use ID: {block.tool_use_id}

What it means

Raised while validating ChatMessages: a ToolResultBlock references a tool_use_id that was never declared by any preceding ToolUseBlock in the message list. The validator first collects all ToolUseBlock ids (assistant messages only) and then checks each ToolResultBlock against that set; an unmatched reference means the conversation history is inconsistent — a tool result without its originating tool call.

Source

Thrown at private_gpt/server/chat/chat_models.py:403

        tool_uses_ids: set[str] = set()
        tool_results_ids: set[str] = set()
        for message in self.messages:
            if isinstance(message.content, list):
                for block in message.content:
                    if block is None:
                        raise ValueError("Block cannot be None")
                    elif isinstance(block, ToolUseBlock):
                        if message.role != "assistant":
                            raise ValueError(
                                f"Tool use blocks can only be used in assistant messages: {message}"
                            )
                        if block.id in tool_uses_ids:
                            raise ValueError(f"Duplicate tool use ID found: {block.id}")
                        tool_uses_ids.add(block.id)

                    elif isinstance(block, ToolResultBlock):
                        if block.tool_use_id not in tool_uses_ids:
                            raise ValueError(
                                f"Tool result block references an unknown tool use ID: {block.tool_use_id}"
                            )
                        tool_results_ids.add(block.tool_use_id)

                    elif isinstance(block, TLDRBlock):
                        if message.role != "assistant":
                            raise ValueError(
                                f"TLDR blocks can only be used in assistant messages: {message}"
                            )

        if tool_results_ids != tool_uses_ids:
            raise ValueError(
                "Tool result blocks must match the tool use IDs in the same message."
                f" Found tool use IDs: {tool_uses_ids}, but tool result IDs: {tool_results_ids}"
            )

        return self

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Ensure every ToolResultBlock.tool_use_id exactly matches the id of a ToolUseBlock present earlier in the same messages array
  2. When trimming history, remove each tool_use/tool_result pair together, never the tool_use alone
  3. If you inject synthetic tool results, first inject (or keep) the corresponding assistant tool_use block with the same id

Example fix

# before
messages = [
  {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "tu_999", "content": "ok"}]},
]

# after
messages = [
  {"role": "assistant", "content": [{"type": "tool_use", "id": "tu_999", "name": "search", "input": {"q": "x"}}]},
  {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "tu_999", "content": "ok"}]},
]
Defensive patterns

Strategy: validation

Validate before calling

use_ids = {b.id for m in messages if m.role == "assistant" for b in (m.content or []) if isinstance(b, ToolUseBlock)}
result_ids = [b.tool_use_id for m in messages for b in (m.content or []) if isinstance(b, ToolResultBlock)]
missing = [r for r in result_ids if r not in use_ids]
assert not missing, f"tool results without matching tool_use: {missing}"

Type guard

def tool_results_are_grounded(messages: list) -> bool:
    use_ids = set()
    for m in messages:
        blocks = m.get("content", []) if isinstance(m, dict) else (m.content or [])
        for b in blocks:
            t = b.get("type") if isinstance(b, dict) else getattr(b, "type", None)
            if t == "tool_use":
                use_ids.add(b.get("id") if isinstance(b, dict) else b.id)
            if t == "tool_result":
                rid = b.get("tool_use_id") if isinstance(b, dict) else b.tool_use_id
                if rid not in use_ids:
                    return False
    return True

Try / catch

try:
    ChatMessages(messages=msgs)
except ValueError as e:
    if "unknown tool use ID" in str(e):
        drop_orphan_tool_results(msgs)
    else:
        raise

Prevention

When it happens

Trigger: Sending a user/tool message containing a ToolResultBlock whose tool_use_id is misspelled, was generated client-side without a matching ToolUseBlock, or whose assistant tool_use message was dropped (e.g. history truncation removed the assistant turn but kept the result).

Common situations: Sliding-window history pruning that cuts assistant tool_use turns but keeps tool result turns; clients that synthesize tool results manually (e.g. 'error' placeholder results) with arbitrary ids; migrating histories where ids were regenerated.

Related errors


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