Chat: merge consecutive user messages for strict providers

After a non-native tool round, the agent appends tool results as a {role:
'user'} message next to the user's original 'user' prompt, producing two
consecutive 'user' messages. Strict provider APIs (Anthropic/Claude) reject
consecutive same-role messages, so the follow-up generation request fails
silently — search returns sources, then nothing is generated.

_sanitize_llm_messages now merges consecutive 'user' messages (joining their
content). Only user/user is merged; normal chat and agent/tool turns already
alternate and are untouched.

Scoped down per maintainer review: the agent_loop 'output' source-extraction
change is already on main (#898/#901) and the broad-mocking web-sources test
was dropped. Added a focused test that runs consecutive-user messages through
the real _build_anthropic_payload and asserts the payload alternates correctly.
This commit is contained in:
Tatlatat
2026-06-02 18:44:13 +07:00
committed by GitHub
parent fb0f8484d7
commit e084dc993e
2 changed files with 121 additions and 1 deletions
+38 -1
View File
@@ -586,7 +586,44 @@ def _sanitize_llm_messages(messages: List[Dict]) -> List[Dict]:
cleaned.append(item)
elif "content" in item:
cleaned.append(item)
return cleaned
# Merge consecutive user messages to satisfy strict role alternation requirements.
merged = []
for item in cleaned:
if not merged:
merged.append(item)
continue
last = merged[-1]
if last["role"] == "user" and item["role"] == "user":
last_copy = dict(last)
# Content:
last_content = last_copy.get("content")
item_content = item.get("content")
# Convert contents to string if they exist, or keep None/empty
last_str = str(last_content) if last_content is not None else ""
item_str = str(item_content) if item_content is not None else ""
if last_str and item_str:
new_content = f"{last_str}\n\n{item_str}"
elif last_str:
new_content = last_str
else:
new_content = item_str if item_str else None
if new_content is not None:
last_copy["content"] = new_content
elif "content" in last_copy:
del last_copy["content"]
merged[-1] = last_copy
else:
merged.append(item)
return merged
def _normalize_anthropic_url(url: str) -> str:
"""Ensure Anthropic URL points to /v1/messages."""