diff --git a/agent/context_compressor.py b/agent/context_compressor.py index 034eb8f99..f6cfa5b9f 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -87,7 +87,7 @@ class ContextCompressor: parts = [] for msg in turns_to_summarize: role = msg.get("role", "unknown") - content = msg.get("content", "") + content = msg.get("content") or "" if len(content) > 2000: content = content[:1000] + "\n...[truncated]...\n" + content[-500:] tool_calls = msg.get("tool_calls", []) @@ -193,7 +193,7 @@ Write only the summary, starting with "[CONTEXT SUMMARY]:" prefix.""" for i in range(compress_start): msg = messages[i].copy() if i == 0 and msg.get("role") == "system" and self.compression_count == 0: - msg["content"] = msg.get("content", "") + "\n\n[Note: Some earlier conversation turns may be summarized to preserve context space.]" + msg["content"] = (msg.get("content") or "") + "\n\n[Note: Some earlier conversation turns may be summarized to preserve context space.]" compressed.append(msg) compressed.append({"role": "user", "content": summary}) diff --git a/tests/agent/test_context_compressor.py b/tests/agent/test_context_compressor.py index 25e3ac109..393e48204 100644 --- a/tests/agent/test_context_compressor.py +++ b/tests/agent/test_context_compressor.py @@ -115,6 +115,48 @@ class TestCompress: assert result[-2]["content"] == msgs[-2]["content"] +class TestGenerateSummaryNoneContent: + """Regression: content=None (from tool-call-only assistant messages) must not crash.""" + + def test_none_content_does_not_crash(self): + mock_client = MagicMock() + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "[CONTEXT SUMMARY]: tool calls 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) + + messages = [ + {"role": "user", "content": "do something"}, + {"role": "assistant", "content": None, "tool_calls": [ + {"function": {"name": "search"}} + ]}, + {"role": "tool", "content": "result"}, + {"role": "assistant", "content": None}, + {"role": "user", "content": "thanks"}, + ] + + summary = c._generate_summary(messages) + assert isinstance(summary, str) + assert "CONTEXT SUMMARY" in summary + + def test_none_content_in_system_message_compress(self): + """System message with content=None should not crash during compress.""" + with patch("agent.context_compressor.get_model_context_length", return_value=100000), \ + patch("agent.context_compressor.get_text_auxiliary_client", return_value=(None, None)): + c = ContextCompressor(model="test", quiet_mode=True, protect_first_n=2, protect_last_n=2) + + msgs = [{"role": "system", "content": None}] + [ + {"role": "user" if i % 2 == 0 else "assistant", "content": f"msg {i}"} + for i in range(10) + ] + result = c.compress(msgs) + assert len(result) < len(msgs) + + class TestCompressWithClient: def test_summarization_path(self): mock_client = MagicMock()