From 77da3bbc95feae670632b6b173703a20a06eb2ac Mon Sep 17 00:00:00 2001 From: teknium1 Date: Sun, 8 Mar 2026 23:09:04 -0700 Subject: [PATCH] fix: use correct role for summary message in context compressor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The summary message was always injected as 'user' role, which causes consecutive user messages when the last preserved head message is also 'user'. Some APIs reject this (400 error), and it produces malformed training data. Fix: check the role of the last head message and pick the opposite role for the summary — 'user' after assistant/tool, 'assistant' after user. Based on PR #328 by johnh4098. Closes #328. --- agent/context_compressor.py | 4 +- tests/agent/test_context_compressor.py | 54 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/agent/context_compressor.py b/agent/context_compressor.py index 7a01d796..01aa2af8 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -342,7 +342,9 @@ Write only the summary, starting with "[CONTEXT SUMMARY]:" prefix.""" compressed.append(msg) if summary: - compressed.append({"role": "user", "content": summary}) + last_head_role = messages[compress_start - 1].get("role", "user") if compress_start > 0 else "user" + summary_role = "user" if last_head_role in ("assistant", "tool") else "assistant" + compressed.append({"role": summary_role, "content": summary}) else: if not self.quiet_mode: print(" ⚠️ No summary model available — middle turns dropped without summary") diff --git a/tests/agent/test_context_compressor.py b/tests/agent/test_context_compressor.py index 29b49fd1..12fa374c 100644 --- a/tests/agent/test_context_compressor.py +++ b/tests/agent/test_context_compressor.py @@ -224,6 +224,60 @@ class TestCompressWithClient: for tc in msg["tool_calls"]: assert tc["id"] in answered_ids + def test_summary_role_avoids_consecutive_user_messages(self): + """Summary role should alternate with the last head message to avoid consecutive same-role messages.""" + mock_client = MagicMock() + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "[CONTEXT SUMMARY]: stuff happened" + mock_client.chat.completions.create.return_value = mock_response + + with patch("agent.context_compressor.get_model_context_length", return_value=100000), \ + patch("agent.context_compressor.get_text_auxiliary_client", return_value=(mock_client, "test-model")): + c = ContextCompressor(model="test", quiet_mode=True, protect_first_n=2, protect_last_n=2) + + # Last head message (index 1) is "assistant" → summary should be "user" + msgs = [ + {"role": "user", "content": "msg 0"}, + {"role": "assistant", "content": "msg 1"}, + {"role": "user", "content": "msg 2"}, + {"role": "assistant", "content": "msg 3"}, + {"role": "user", "content": "msg 4"}, + {"role": "assistant", "content": "msg 5"}, + ] + result = c.compress(msgs) + summary_msg = [m for m in result if "CONTEXT SUMMARY" in (m.get("content") or "")] + assert len(summary_msg) == 1 + assert summary_msg[0]["role"] == "user" + + def test_summary_role_avoids_consecutive_user_when_head_ends_with_user(self): + """When last head message is 'user', summary must be 'assistant' to avoid two consecutive user messages.""" + mock_client = MagicMock() + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "[CONTEXT SUMMARY]: stuff happened" + mock_client.chat.completions.create.return_value = mock_response + + with patch("agent.context_compressor.get_model_context_length", return_value=100000), \ + patch("agent.context_compressor.get_text_auxiliary_client", return_value=(mock_client, "test-model")): + c = ContextCompressor(model="test", quiet_mode=True, protect_first_n=3, protect_last_n=2) + + # Last head message (index 2) is "user" → summary should be "assistant" + msgs = [ + {"role": "system", "content": "system prompt"}, + {"role": "user", "content": "msg 1"}, + {"role": "user", "content": "msg 2"}, # last head — user + {"role": "assistant", "content": "msg 3"}, + {"role": "user", "content": "msg 4"}, + {"role": "assistant", "content": "msg 5"}, + {"role": "user", "content": "msg 6"}, + {"role": "assistant", "content": "msg 7"}, + ] + result = c.compress(msgs) + summary_msg = [m for m in result if "CONTEXT SUMMARY" in (m.get("content") or "")] + assert len(summary_msg) == 1 + assert summary_msg[0]["role"] == "assistant" + def test_summarization_does_not_start_tail_with_tool_outputs(self): mock_client = MagicMock() mock_response = MagicMock()