Stop conversations crashing during compaction on tool-call turns (#1777)

context_compactor.maybe_compact built its summary text with
msg.get('content', '')[:2000], which raised
TypeError: 'NoneType' object is not subscriptable on assistant turns
whose content is None (turns that carried only native tool_calls).
Once a conversation crossed the 85% compaction threshold — reached
after only a few turns on small-context local models plus the large
agent prompt — every subsequent message failed ("send more than three
messages and it stops working").

Flatten message content to text first via a _content_as_text helper
(str passthrough, multimodal list blocks joined, None -> "") and
tolerate a missing role. Adds tests/test_context_compactor.py covering
the helper and a >=4-message conversation that forces compaction with
a None-content tool-call turn (fails before this change, passes after).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
clockworksquirrel
2026-06-03 00:25:33 -04:00
committed by GitHub
parent 12696a05ae
commit 2625e97f11
2 changed files with 129 additions and 1 deletions
+21 -1
View File
@@ -15,6 +15,26 @@ from core.models import ChatMessage
logger = logging.getLogger(__name__)
def _content_as_text(content: Any) -> str:
"""Flatten a message's content to plain text.
Handles the three shapes that flow through history: a plain string, a
multimodal list of content blocks (vision/image attachments), and None
(assistant turns that carried only native tool_calls persist content as
None). Returns "" for anything without text so callers can safely slice
the result.
"""
if isinstance(content, str):
return content
if isinstance(content, list):
return " ".join(
b.get("text", "") for b in content
if isinstance(b, dict) and b.get("text")
)
return ""
COMPACT_THRESHOLD = 0.85 # Trigger compaction at 85% of context window
SUMMARY_MAX_TOKENS = 1024
SMALL_CONTEXT_LIMIT = 8192 # Models with context <= this get aggressive trimming
@@ -274,7 +294,7 @@ async def maybe_compact(
# Build the text to summarize
convo_text = "\n".join(
f"{msg['role'].upper()}: {msg.get('content', '')[:2000]}"
f"{msg.get('role', 'user').upper()}: {_content_as_text(msg.get('content'))[:2000]}"
for msg in older
)