fix(moa): scope the non-text placeholder to structured content only

Follow-up to the cherry-picked empty-user-turn drop: the placeholder
introduced in 8582f35d9 fired for whitespace-only STRING turns too
(content='   ' flattens to non-stripping text but isn't in the
(None, '', []) exclusion set), fabricating an attachment note for a turn
that carried nothing. Gate the placeholder on isinstance(content, list)
so only genuinely structured (e.g. image-only) turns get it; empty and
whitespace-only string turns now fall through to the drop path.

Edge cases verified: trailing empty user turn still ends the view on the
synthetic advisory marker; an all-empty transcript degenerates to [].
This commit is contained in:
Teknium 2026-07-14 05:31:07 -07:00
parent b4c2c4f922
commit b013ed03e5
2 changed files with 28 additions and 2 deletions

View file

@ -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

View file

@ -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)