mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
fix(anthropic): prepend user turn when compaction leaves a leading assistant message
Anthropic extracts the system prompt into a separate `system` field and requires messages[0] to be role="user"; a leading assistant turn is rejected with HTTP 400. After a second context compaction the only surviving leading anchor is the system prompt, so a summary/handoff message emitted as role="assistant" becomes the first messages entry once the system prompt is extracted. Anthropic reports this with a misleading error — `messages.N: tool_use ids were found without tool_result blocks immediately after: toolu_...` — even when every tool_use/tool_result pair is adjacent and matched; the real structural defect is the leading assistant role (#52160). This is engine-agnostic: it fires for any producer of a leading-assistant transcript (built-in compressor, the DAG/LCM context engine, session truncation), unlike the compressor-scoped fix in #52167. The native Bedrock Converse adapter already guards the same invariant (convert_messages_to_converse); this mirrors it for the native Anthropic path. Add _ensure_leading_user_turn() to the convert_messages_to_anthropic post-processing chain, scoped to the system-extracted case (the production trigger) so bare assistant-only unit fixtures are unaffected. Adds regression tests (leading-assistant, leading-assistant-with-adjacent-tool_use, and a no-op negative control).
This commit is contained in:
parent
17fa5910be
commit
e71c1137e8
2 changed files with 78 additions and 2 deletions
|
|
@ -2412,6 +2412,12 @@ def _evict_old_screenshots(result: List[Dict[str, Any]]) -> None:
|
|||
]
|
||||
|
||||
|
||||
def _ensure_leading_user_turn(result: List[Dict[str, Any]], system: Any) -> None:
|
||||
"""Anthropic requires messages[0] to be user when system is extracted."""
|
||||
if system is not None and result and result[0].get("role") != "user":
|
||||
result.insert(0, {"role": "user", "content": [{"type": "text", "text": " "}]})
|
||||
|
||||
|
||||
def convert_messages_to_anthropic(
|
||||
messages: List[Dict],
|
||||
base_url: str | None = None,
|
||||
|
|
@ -2470,6 +2476,7 @@ def convert_messages_to_anthropic(
|
|||
|
||||
_strip_orphaned_tool_blocks(result)
|
||||
result = _merge_consecutive_roles(result)
|
||||
_ensure_leading_user_turn(result, system)
|
||||
_manage_thinking_signatures(result, base_url, model)
|
||||
_evict_old_screenshots(result)
|
||||
|
||||
|
|
|
|||
|
|
@ -1121,7 +1121,8 @@ class TestConvertMessages:
|
|||
])
|
||||
|
||||
_, result = convert_messages_to_anthropic(messages)
|
||||
assistant_blocks = result[0]["content"]
|
||||
assistant_msg = next(m for m in result if m["role"] == "assistant")
|
||||
assistant_blocks = assistant_msg["content"]
|
||||
|
||||
assert assistant_blocks[0]["type"] == "text"
|
||||
assert assistant_blocks[0]["text"] == "Hello from assistant"
|
||||
|
|
@ -1207,7 +1208,12 @@ class TestConvertMessages:
|
|||
], native_anthropic=True)
|
||||
|
||||
_, result = convert_messages_to_anthropic(messages)
|
||||
user_msg = [m for m in result if m["role"] == "user"][0]
|
||||
user_msg = next(
|
||||
m for m in result
|
||||
if m["role"] == "user"
|
||||
and isinstance(m["content"], list)
|
||||
and any(b.get("type") == "tool_result" for b in m["content"])
|
||||
)
|
||||
tool_block = user_msg["content"][0]
|
||||
|
||||
assert tool_block["type"] == "tool_result"
|
||||
|
|
@ -1356,6 +1362,69 @@ class TestConvertMessages:
|
|||
assert isinstance(result[0]["content"], list)
|
||||
assert result[0]["content"] == [{"type": "text", "text": "(empty message)"}]
|
||||
|
||||
def test_leading_assistant_after_compaction_gets_user_turn_prepended(self):
|
||||
"""The adapter backstops compactors that emit a leading assistant summary."""
|
||||
messages = [
|
||||
{"role": "system", "content": "You are helpful."},
|
||||
{"role": "assistant", "content": "[Context compaction summary] earlier work…"},
|
||||
{"role": "user", "content": "continue"},
|
||||
]
|
||||
|
||||
system, result = convert_messages_to_anthropic(messages)
|
||||
|
||||
assert system == "You are helpful."
|
||||
assert result[0]["role"] == "user"
|
||||
assert result[0]["content"] == [{"type": "text", "text": " "}]
|
||||
assert result[1]["role"] == "assistant"
|
||||
assert any(
|
||||
m["role"] == "assistant" and "Context compaction summary" in str(m["content"])
|
||||
for m in result
|
||||
)
|
||||
|
||||
def test_leading_user_message_is_not_modified(self):
|
||||
"""A normal transcript that already starts with user must be untouched."""
|
||||
messages = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "hi"},
|
||||
]
|
||||
|
||||
_, result = convert_messages_to_anthropic(messages)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]["role"] == "user"
|
||||
assert result[0]["content"] == "hello"
|
||||
|
||||
def test_leading_assistant_with_tool_use_after_compaction_is_repaired(self):
|
||||
"""Repair the leading role without disturbing adjacent tool pairs."""
|
||||
messages = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "running it",
|
||||
"tool_calls": [
|
||||
{"id": "toolu_1", "function": {"name": "terminal", "arguments": "{}"}},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "toolu_1", "content": "ok"},
|
||||
{"role": "user", "content": "next"},
|
||||
]
|
||||
|
||||
_, result = convert_messages_to_anthropic(messages)
|
||||
|
||||
assert result[0]["role"] == "user"
|
||||
asst_idx = next(
|
||||
i for i, m in enumerate(result)
|
||||
if m["role"] == "assistant"
|
||||
and any(b.get("type") == "tool_use" for b in m["content"] if isinstance(b, dict))
|
||||
)
|
||||
nxt = result[asst_idx + 1]
|
||||
assert nxt["role"] == "user"
|
||||
assert any(
|
||||
isinstance(b, dict) and b.get("type") == "tool_result" and b.get("tool_use_id") == "toolu_1"
|
||||
for b in nxt["content"]
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build kwargs
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue