zylon-ai/private-gpt · error · ValueError

Tool result blocks must match the tool use IDs in the same…

Error message

Tool result blocks must match the tool use IDs in the same message. Found tool use IDs: {tool_uses_ids}, but tool result IDs: {tool_results_ids}

What it means

Final consistency check of the ChatMessages validator: after scanning all messages, the set of tool_use ids and the set of tool_result ids must be identical — every tool call must have exactly one result within the same request. The error lists both sets so you can see which id is missing a result (present in tool uses but not results) or which tool_use was never paired.

Solutions

  1. Append a ToolResultBlock for every ToolUseBlock id shown in 'Found tool use IDs' but missing from 'tool result IDs' (even a synthetic 'skipped' result)
  2. Prune history on tool_use/tool_result pair boundaries so pairing always stays complete
  3. Never submit the assistant tool-call turn before the tool result is available — send both in the same request

Example fix

# before (tool_use tu_1 has no result)
[assistant(tu_1), user("thanks")]

# after
[assistant(tu_1), user(tool_result tu_1 = "skipped"), user("thanks")]
Defensive patterns

Strategy: validation

Validate before calling

use_ids = {b.id for m in messages 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)}
if use_ids != result_ids:
    missing_results = use_ids - result_ids
    for uid in missing_results:
        append_synthetic_tool_result(uid, "skipped")

Type guard

def tool_pairs_are_complete(messages: list) -> bool:
    use_ids, result_ids = set(), set()
    for m in messages:
        for b in (m.get("content", []) if isinstance(m, dict) else (m.content or [])):
            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":
                result_ids.add(b.get("tool_use_id") if isinstance(b, dict) else b.tool_use_id)
    return use_ids == result_ids

Try / catch

try:
    ChatMessages(messages=msgs)
except ValueError as e:
    if "must match the tool use IDs" in str(e):
        backfill_missing_tool_results(msgs)
    else:
        raise

Prevention

When it happens

Trigger: An assistant ToolUseBlock exists but no ToolResultBlock with that id appears anywhere in the messages (result turn was truncated, still pending, or omitted); or the pairing check fires after passing the earlier per-block checks because ids match individually but one direction is incomplete.

Common situations: Truncating history from the tail and cutting the last tool_result; sending an in-flight conversation where the tool result has not been appended yet; hand-crafted histories that include the assistant tool call but forget the result turn.

Related errors


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

Appendix: source

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

                        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)