diff --git a/agent/context_compressor.py b/agent/context_compressor.py index 1cf3bb1e358..4bccda13808 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -1768,7 +1768,15 @@ This compaction should PRIORITISE preserving all information related to the focu # retry (_generate_summary recursion) re-enters harmlessly. with aux_interrupt_protection(): response = call_llm(**call_kwargs) - content = response.choices[0].message.content + # ``_validate_llm_response`` only guarantees ``choices[0].message`` + # exists, not that it's an object with ``.content``. Some + # OpenAI-compatible proxies / local backends return a dict- or + # str-shaped message; coerce defensively instead of crashing. + message = response.choices[0].message + if isinstance(message, dict): + content = message.get("content") + else: + content = getattr(message, "content", message) # Handle cases where content is not a string (e.g., dict from llama.cpp) if not isinstance(content, str): content = str(content) if content else "" diff --git a/tests/agent/test_context_compressor.py b/tests/agent/test_context_compressor.py index 5edb412e507..cd23d13480c 100644 --- a/tests/agent/test_context_compressor.py +++ b/tests/agent/test_context_compressor.py @@ -510,6 +510,24 @@ class TestNonStringContent: assert summary is None assert c._summary_failure_cooldown_until > 0 + def test_string_message_coerced_to_summary_content(self): + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message = "plain summary text" + + with patch("agent.context_compressor.get_model_context_length", return_value=100000): + c = ContextCompressor(model="test", quiet_mode=True) + + messages = [ + {"role": "user", "content": "do something"}, + {"role": "assistant", "content": "ok"}, + ] + + with patch("agent.context_compressor.call_llm", return_value=mock_response): + summary = c._generate_summary(messages) + + assert summary == f"{SUMMARY_PREFIX}\nplain summary text" + def test_summary_call_does_not_force_temperature(self): mock_response = MagicMock() mock_response.choices = [MagicMock()]