{"record":{"id":"02622b8750243101","repo":"zylon-ai/private-gpt","slug":"tool-use-blocks-can-only-be-used-in-assistant-mess","errorCode":null,"errorMessage":"Tool use blocks can only be used in assistant messages: {message}","messagePattern":"Tool use blocks can only be used in assistant messages: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":422,"severity":"error","filePath":"private_gpt/server/chat/chat_models.py","lineNumber":394,"sourceCode":"            tool_names = [tool.name for tool in self.tools]\n            if len(tool_names) != len(set(tool_names)):\n                raise ValueError(\n                    \"Duplicate tool names found in the tools list.\"\n                    f\" Provided tools: {self.tools}\"\n                    f\" Unique tool names: {set(tool_names)}\"\n                )\n\n        # Check tool use and result blocks\n        tool_uses_ids: set[str] = set()\n        tool_results_ids: set[str] = set()\n        for message in self.messages:\n            if isinstance(message.content, list):\n                for block in message.content:\n                    if block is None:\n                        raise ValueError(\"Block cannot be None\")\n                    elif isinstance(block, ToolUseBlock):\n                        if message.role != \"assistant\":\n                            raise ValueError(\n                                f\"Tool use blocks can only be used in assistant messages: {message}\"\n                            )\n                        if block.id in tool_uses_ids:\n                            raise ValueError(f\"Duplicate tool use ID found: {block.id}\")\n                        tool_uses_ids.add(block.id)\n\n                    elif isinstance(block, ToolResultBlock):\n                        if block.tool_use_id not in tool_uses_ids:\n                            raise ValueError(\n                                f\"Tool result block references an unknown tool use ID: {block.tool_use_id}\"\n                            )\n                        tool_results_ids.add(block.tool_use_id)\n\n                    elif isinstance(block, TLDRBlock):\n                        if message.role != \"assistant\":\n                            raise ValueError(\n                                f\"TLDR blocks can only be used in assistant messages: {message}\"\n                            )","sourceCodeStart":376,"sourceCodeEnd":412,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/server/chat/chat_models.py#L376-L412","documentation":"ChatBody enforces Anthropic-style tool semantics: ToolUseBlock entries may only appear in assistant messages. A tool_use block in a user (or system/tool) message is rejected with the whole message printed, because tool calls originate from the model (assistant), never from the caller.","triggerScenarios":"Echoing the assistant's tool_use block back in a user message (instead of a matching ToolResultBlock); clients that copy the full assistant turn including tool_use into the user turn; role mislabeling when replaying tool transcripts.","commonSituations":"Hand-rolled tool loops that mis-model the protocol; converting OpenAI-style 'tool' role messages to this API's block format; replaying logged conversations where roles were flattened.","solutions":["Keep ToolUseBlock only in assistant messages; respond to it with a user message containing ToolResultBlock referencing block.tool_use_id.","When replaying history, verify each tool_use block's parent message has role='assistant'.","Use the SDK's message-conversion helpers rather than manual block placement."],"exampleFix":"# before\n{\"role\":\"user\",\"content\":[{\"type\":\"tool_use\",\"id\":\"t1\",\"name\":\"search\",\"input\":{}}]}\n\n# after\n{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"t1\",\"name\":\"search\",\"input\":{}}]},\n{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"t1\",\"content\":\"ok\"}]}","handlingStrategy":"type-guard","validationCode":"for m in messages:\n    if m['role'] != 'assistant':\n        assert not any(getattr(b, 'type', None) == 'tool_use' for b in m.get('content', []) if isinstance(b, dict)), \\\n            'tool_use blocks belong in assistant messages only'","typeGuard":"def tool_use_roles_valid(msgs: list[dict]) -> bool:\n    return all(\n        m['role'] == 'assistant'\n        for m in msgs\n        for b in (m.get('content') or [])\n        if isinstance(b, dict) and b.get('type') == 'tool_use'\n    )","tryCatchPattern":"except ValidationError as e:\n    if 'Tool use blocks can only' in str(e):\n        move_tool_use_to_assistant(messages); retry()\n    else:\n        raise","preventionTips":["Use SDK helpers to build tool-loop messages instead of manual blocks","Answer tool_use with ToolResultBlock in the next user turn","Replay-logged tool conversations through a role validator before resending"],"tags":["validation","tools","tool-use-blocks","message-roles","protocol"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}