diff --git a/run_agent.py b/run_agent.py index 5bc644e45c8..04e52c37241 100644 --- a/run_agent.py +++ b/run_agent.py @@ -3529,6 +3529,19 @@ class AIAgent: # instead of returning structured reasoning fields. Only fall back # to inline extraction when no structured reasoning was found. content = getattr(assistant_message, "content", None) + if not reasoning_parts and isinstance(content, list): + # DeepSeek V4 Pro (and compatible providers) return content as a + # list of typed blocks, e.g.: + # [{"type": "thinking", "thinking": "..."}, {"type": "output", ...}] + # Without this branch the thinking text is silently dropped and the + # next turn fails with HTTP 400 ("thinking must be passed back"). + # Refs #21944. + for block in content: + if isinstance(block, dict) and block.get("type") == "thinking": + thinking_text = block.get("thinking") or block.get("text") or "" + thinking_text = thinking_text.strip() + if thinking_text and thinking_text not in reasoning_parts: + reasoning_parts.append(thinking_text) if not reasoning_parts and isinstance(content, str) and content: inline_patterns = ( r"(.*?)", diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index 6df71b51f90..5bc485e0711 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -517,6 +517,42 @@ class TestExtractReasoning: msg = _mock_assistant_msg(content=content) assert agent._extract_reasoning(msg) == expected + def test_content_list_thinking_blocks_extracted(self, agent): + """DeepSeek V4 Pro returns content as a typed-block list (issue #21944). + + Without this branch thinking text is silently dropped → HTTP 400 on + the next turn ("thinking must be passed back to the API"). + """ + msg = _mock_assistant_msg( + content=[ + {"type": "thinking", "thinking": "deep analysis here"}, + {"type": "output", "text": "final answer"}, + ] + ) + result = agent._extract_reasoning(msg) + assert result == "deep analysis here" + + def test_content_list_non_thinking_blocks_ignored(self, agent): + """Non-thinking blocks in a content list must not be treated as reasoning.""" + msg = _mock_assistant_msg( + content=[ + {"type": "text", "text": "just a regular response"}, + ] + ) + assert agent._extract_reasoning(msg) is None + + def test_content_list_thinking_prefers_structured_field(self, agent): + """Structured ``reasoning`` field wins over content-list thinking blocks.""" + msg = _mock_assistant_msg( + reasoning="from structured field", + content=[ + {"type": "thinking", "thinking": "from content list"}, + ], + ) + result = agent._extract_reasoning(msg) + # structured field was found first → content-list branch skipped + assert result == "from structured field" + class TestCleanSessionContent: def test_none_passthrough(self):