fix(agent): preserve user message in _snip_history to prevent GLM error 1214

When _snip_history truncates the message history and the only user message
ends up outside the kept window, providers like GLM reject the resulting
system→assistant sequence with error 1214 ("messages 参数非法").

Two-layer fix:
1. _snip_history now walks backwards through non_system messages to recover
   the nearest user message when none exists in the kept window.
2. _enforce_role_alternation inserts a synthetic user message
   "(conversation continued)" when the first non-system message is a bare
   assistant (no tool_calls), serving as a safety net for any edge cases
   that slip through.

Co-authored-by: darlingbud <darlingbud@users.noreply.github.com>
This commit is contained in:
chengyongru
2026-04-17 16:20:53 +08:00
committed by Xubin Ren
co-authored by darlingbud
parent e9d727c3a5
commit 44b526c4ee
4 changed files with 189 additions and 5 deletions
@@ -195,3 +195,46 @@ class TestEnforceRoleAlternation:
assert result[3]["role"] == "user"
assert "And 3+3?" in result[3]["content"]
assert "(please be quick)" in result[3]["content"]
def test_leading_assistant_after_system_inserts_synthetic_user(self):
"""When the first non-system message is assistant (no tool_calls), a
synthetic user message is inserted to prevent GLM error 1214."""
msgs = [
{"role": "system", "content": "sys"},
{"role": "assistant", "content": "previous reply"},
{"role": "tool", "tool_call_id": "tc_1", "content": "result"},
{"role": "assistant", "content": "after tool"},
]
result = LLMProvider._enforce_role_alternation(msgs)
non_system = [m for m in result if m["role"] != "system"]
assert non_system[0]["role"] == "user"
assert non_system[0]["content"] == "(conversation continued)"
# The original assistant should follow.
assert non_system[1]["role"] == "assistant"
def test_leading_assistant_with_tool_calls_not_patched(self):
"""An assistant message with tool_calls at the start is left as-is
because tool messages will follow and some providers accept this."""
msgs = [
{"role": "system", "content": "sys"},
{"role": "assistant", "content": None, "tool_calls": [
{"id": "tc_1", "type": "function", "function": {"name": "ls", "arguments": "{}"}}
]},
{"role": "tool", "tool_call_id": "tc_1", "content": "result"},
]
result = LLMProvider._enforce_role_alternation(msgs)
non_system = [m for m in result if m["role"] != "system"]
# The assistant has tool_calls so it should NOT be patched.
assert non_system[0]["role"] == "assistant"
assert non_system[0].get("tool_calls") is not None
def test_user_after_system_not_patched(self):
"""Normal system→user sequence is not modified."""
msgs = [
{"role": "system", "content": "sys"},
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "hi"},
]
result = LLMProvider._enforce_role_alternation(msgs)
assert result[1]["role"] == "user"
assert result[1]["content"] == "hello"