diff --git a/agent/moa_loop.py b/agent/moa_loop.py index d8461e921dc2..28707b2a4f14 100644 --- a/agent/moa_loop.py +++ b/agent/moa_loop.py @@ -491,7 +491,7 @@ def _reference_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: if role == "system": continue if role == "user": - if not text.strip() and content not in (None, "", []): + if not text.strip() and isinstance(content, list) and content: # Structured content with no extractable text (e.g. an # image-only turn). Emitting an empty user message would be # dropped/rejected by strict providers (Anthropic 400s on @@ -499,7 +499,9 @@ def _reference_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: # mode), and silently skipping the turn would break # user/assistant alternation in the advisory view. Substitute # a placeholder so the reference knows a non-text turn - # happened. + # happened. Only structured content qualifies — an empty or + # whitespace-only STRING turn carries nothing and is dropped + # below instead. text = "[user sent non-text content (e.g. an image attachment)]" if not text.strip(): # Genuinely empty user turn (content="" / None). It carries diff --git a/tests/run_agent/test_moa_loop_mode.py b/tests/run_agent/test_moa_loop_mode.py index 659f64da7a94..1c1ab8f71eb7 100644 --- a/tests/run_agent/test_moa_loop_mode.py +++ b/tests/run_agent/test_moa_loop_mode.py @@ -1241,3 +1241,27 @@ def test_reference_guidance_appends_text_part_to_decorated_trailing_user(): assert content[0] == marked_part # The guidance rides as a trailing text part outside the cached span. assert content[1] == {"type": "text", "text": "\n\nREFERENCE BLOCK"} + + +def test_reference_messages_drops_whitespace_only_string_user_turn(): + """A whitespace-only STRING user turn is dropped, not placeholdered. + + The non-text placeholder exists for structured content (image-only turns) + where a real turn happened that the reference should know about. A bare + whitespace string carries nothing — emitting it would 400 strict + providers (Kimi/Moonshot 'role user must not be empty'), and + placeholdering it would fabricate an attachment that never existed. + """ + from agent.moa_loop import _reference_messages + + messages = [ + {"role": "user", "content": " "}, + {"role": "assistant", "content": "a"}, + {"role": "user", "content": "real"}, + ] + + view = _reference_messages(messages) + + assert view[0] == {"role": "assistant", "content": "a"} + assert view[-1] == {"role": "user", "content": "real"} + assert all(str(m["content"]).strip() for m in view)