From 171c2eeadf4b7a745fab176d9af22e8b98895691 Mon Sep 17 00:00:00 2001 From: ScarabSystems Date: Wed, 29 Jul 2026 10:01:44 -0400 Subject: [PATCH 1/4] Python: bound tool result compaction summaries Keep ToolResultCompactionStrategy from re-inserting oversized tool result payloads through the synthetic summary message by bounding the generated digest text. Add regression coverage proving a large tool result is not embedded verbatim, keeps a bounded prefix, and marks truncation. --- .../core/agent_framework/_compaction.py | 26 ++++++++++++++++-- .../core/tests/core/test_compaction.py | 27 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/python/packages/core/agent_framework/_compaction.py b/python/packages/core/agent_framework/_compaction.py index 59abb10a468..d8f5f3f71eb 100644 --- a/python/packages/core/agent_framework/_compaction.py +++ b/python/packages/core/agent_framework/_compaction.py @@ -855,6 +855,11 @@ class ToolResultCompactionStrategy: untouched; older ones are collapsed. """ + _SUMMARY_PREFIX = "[Tool results: " + _SUMMARY_SUFFIX = "]" + _SUMMARY_MAX_CHARS = 4096 + _SUMMARY_TRUNCATION_MARKER = "... [truncated]" + def __init__(self, *, keep_last_tool_call_groups: int = 1) -> None: """Create a tool-result compaction strategy. @@ -916,8 +921,7 @@ async def __call__(self, messages: list[Message]) -> bool: tool_name = call_id_to_name.get(content.call_id or "", "") label = f"{tool_name}: {result_text}" if tool_name else result_text tool_results.append(label.strip()) - summary_label = "; ".join(tool_results) if tool_results else "no results" - summary_text = f"[Tool results: {summary_label}]" + summary_text = self._summary_text(tool_results) summary_id = f"tool_summary_{group_id}" original_message_ids = [msg.message_id for msg in group_msgs if msg.message_id] @@ -948,6 +952,24 @@ async def __call__(self, messages: list[Message]) -> bool: return changed + @classmethod + def _summary_text(cls, tool_results: list[str]) -> str: + summary_label = "; ".join(tool_results) if tool_results else "no results" + summary_text = f"{cls._SUMMARY_PREFIX}{summary_label}{cls._SUMMARY_SUFFIX}" + if len(summary_text) <= cls._SUMMARY_MAX_CHARS: + return summary_text + + allowed_label_chars = ( + cls._SUMMARY_MAX_CHARS + - len(cls._SUMMARY_PREFIX) + - len(cls._SUMMARY_TRUNCATION_MARKER) + - len(cls._SUMMARY_SUFFIX) + ) + if allowed_label_chars <= 0: + return f"{cls._SUMMARY_PREFIX}{cls._SUMMARY_TRUNCATION_MARKER}{cls._SUMMARY_SUFFIX}" + truncated_label = summary_label[:allowed_label_chars].rstrip() + return f"{cls._SUMMARY_PREFIX}{truncated_label}{cls._SUMMARY_TRUNCATION_MARKER}{cls._SUMMARY_SUFFIX}" + def _tool_result_text(value: Any) -> str: if isinstance(value, str): diff --git a/python/packages/core/tests/core/test_compaction.py b/python/packages/core/tests/core/test_compaction.py index 4507d39946f..7d3df6ed761 100644 --- a/python/packages/core/tests/core/test_compaction.py +++ b/python/packages/core/tests/core/test_compaction.py @@ -772,6 +772,33 @@ async def test_tool_result_compaction_preserves_tool_results_in_summary() -> Non assert "found 3 docs" in summary_msgs[0].text # type: ignore[operator] +async def test_tool_result_compaction_bounds_large_summary_payload() -> None: + """Summary text should not embed an oversized tool result verbatim.""" + large_result = "file-start\n" + ("line contents\n" * 1_000) + "file-end" + messages = [ + Message(role="user", contents=["read the file"]), + Message( + role="assistant", + contents=[Content.from_function_call(call_id="c1", name="read_file", arguments="{}")], + ), + _tool_result("c1", large_result), + Message(role="assistant", contents=["done"]), + ] + strategy = ToolResultCompactionStrategy(keep_last_tool_call_groups=0) + annotate_message_groups(messages) + + await strategy(messages) + + summary = next(m for m in included_messages(messages) if (m.text or "").startswith("[Tool results:")) + summary_text = summary.text or "" + assert large_result not in summary_text + assert "file-start" in summary_text + assert "file-end" not in summary_text + assert "[truncated]" in summary_text + assert len(summary_text) <= 4096 + assert len(summary_text) < len(large_result) + + async def test_tool_result_compaction_bidirectional_tracing() -> None: """Summary and originals should link to each other like SummarizationStrategy does.""" messages = [ From b6881b62f5215f0704cdc191671deca2ef53d4ae Mon Sep 17 00:00:00 2001 From: ScarabSystems Date: Wed, 29 Jul 2026 10:33:47 -0400 Subject: [PATCH 2/4] Python: keep excluded tool results out of compaction digests Build ToolResultCompactionStrategy digest content from messages still included in the group so a summary cannot restore payloads that an earlier compaction already excluded. Use the strategy cap constant in the large-payload regression and add coverage for already-excluded tool results. Validation: uv run pytest packages/core/tests/core/test_compaction.py -q -k 'tool_result_compaction'; uv run ruff check packages/core/agent_framework/_compaction.py packages/core/tests/core/test_compaction.py; uv run ruff format --check packages/core/agent_framework/_compaction.py packages/core/tests/core/test_compaction.py; uv run poe test -P core; uv run poe build -P core; env HOME=/tmp/sds-home XDG_CACHE_HOME=/tmp/sds-cache uv run poe test -A. --- .../core/agent_framework/_compaction.py | 5 +-- .../core/tests/core/test_compaction.py | 31 ++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/python/packages/core/agent_framework/_compaction.py b/python/packages/core/agent_framework/_compaction.py index d8f5f3f71eb..cd845aa1f9b 100644 --- a/python/packages/core/agent_framework/_compaction.py +++ b/python/packages/core/agent_framework/_compaction.py @@ -899,9 +899,10 @@ async def __call__(self, messages: list[Message]) -> bool: if group_id in keep_ids: continue group_msgs = grouped.get(group_id, []) + included_group_msgs = [msg for msg in group_msgs if msg.additional_properties.get(EXCLUDED_KEY) is not True] # Build a call_id → function_name map from function_call contents. call_id_to_name: dict[str, str] = {} - for msg in group_msgs: + for msg in included_group_msgs: for content in msg.contents: if content.type == "function_call" and content.call_id and content.name: call_id_to_name[content.call_id] = content.name @@ -909,7 +910,7 @@ async def __call__(self, messages: list[Message]) -> bool: call_id_to_name[content.call_id] = content.tool_name # Collect tool results with the function name for context. tool_results: list[str] = [] - for msg in group_msgs: + for msg in included_group_msgs: for content in msg.contents: if content.type == "function_result": result_text = content.result if isinstance(content.result, str) else str(content.result) diff --git a/python/packages/core/tests/core/test_compaction.py b/python/packages/core/tests/core/test_compaction.py index 7d3df6ed761..9c4ae49f33d 100644 --- a/python/packages/core/tests/core/test_compaction.py +++ b/python/packages/core/tests/core/test_compaction.py @@ -795,10 +795,39 @@ async def test_tool_result_compaction_bounds_large_summary_payload() -> None: assert "file-start" in summary_text assert "file-end" not in summary_text assert "[truncated]" in summary_text - assert len(summary_text) <= 4096 + assert len(summary_text) <= ToolResultCompactionStrategy._SUMMARY_MAX_CHARS assert len(summary_text) < len(large_result) +async def test_tool_result_compaction_ignores_already_excluded_results() -> None: + """Summary text should not restore tool results already excluded from context.""" + excluded_result = "excluded-start\n" + ("excluded line\n" * 1_000) + "excluded-end" + messages = [ + Message(role="user", contents=["read the files"]), + Message( + role="assistant", + contents=[ + Content.from_function_call(call_id="c1", name="read_file", arguments="{}"), + Content.from_function_call(call_id="c2", name="read_file", arguments="{}"), + ], + ), + _tool_result("c1", "included result"), + _tool_result("c2", excluded_result), + Message(role="assistant", contents=["done"]), + ] + strategy = ToolResultCompactionStrategy(keep_last_tool_call_groups=0) + annotate_message_groups(messages) + messages[3].additional_properties[EXCLUDED_KEY] = True + + await strategy(messages) + + summary = next(m for m in included_messages(messages) if (m.text or "").startswith("[Tool results:")) + summary_text = summary.text or "" + assert "included result" in summary_text + assert excluded_result not in summary_text + assert "excluded-start" not in summary_text + + async def test_tool_result_compaction_bidirectional_tracing() -> None: """Summary and originals should link to each other like SummarizationStrategy does.""" messages = [ From f2f0f486b39d2768cfc7f52f52bb27b7685c2755 Mon Sep 17 00:00:00 2001 From: ScarabSystems Date: Wed, 29 Jul 2026 11:07:38 -0400 Subject: [PATCH 3/4] Python: align compaction digest review cleanup Align ToolResultCompactionStrategy's included-message filter with the module's existing EXCLUDED_KEY boolean semantics. Make the large-payload regression size scale from _SUMMARY_MAX_CHARS so it continues to exercise truncation if the digest cap changes. Validation: uv run pytest packages/core/tests/core/test_compaction.py -q -k 'tool_result_compaction'; uv run ruff check packages/core/agent_framework/_compaction.py packages/core/tests/core/test_compaction.py; uv run ruff format --check packages/core/agent_framework/_compaction.py packages/core/tests/core/test_compaction.py; uv run poe test -P core; uv run poe build -P core. --- python/packages/core/agent_framework/_compaction.py | 2 +- python/packages/core/tests/core/test_compaction.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/python/packages/core/agent_framework/_compaction.py b/python/packages/core/agent_framework/_compaction.py index cd845aa1f9b..aadb6655122 100644 --- a/python/packages/core/agent_framework/_compaction.py +++ b/python/packages/core/agent_framework/_compaction.py @@ -899,7 +899,7 @@ async def __call__(self, messages: list[Message]) -> bool: if group_id in keep_ids: continue group_msgs = grouped.get(group_id, []) - included_group_msgs = [msg for msg in group_msgs if msg.additional_properties.get(EXCLUDED_KEY) is not True] + included_group_msgs = [msg for msg in group_msgs if not msg.additional_properties.get(EXCLUDED_KEY, False)] # Build a call_id → function_name map from function_call contents. call_id_to_name: dict[str, str] = {} for msg in included_group_msgs: diff --git a/python/packages/core/tests/core/test_compaction.py b/python/packages/core/tests/core/test_compaction.py index 9c4ae49f33d..5945c8c41dd 100644 --- a/python/packages/core/tests/core/test_compaction.py +++ b/python/packages/core/tests/core/test_compaction.py @@ -774,7 +774,9 @@ async def test_tool_result_compaction_preserves_tool_results_in_summary() -> Non async def test_tool_result_compaction_bounds_large_summary_payload() -> None: """Summary text should not embed an oversized tool result verbatim.""" - large_result = "file-start\n" + ("line contents\n" * 1_000) + "file-end" + payload_line = "line contents\n" + payload_lines = ToolResultCompactionStrategy._SUMMARY_MAX_CHARS // len(payload_line) + 1 + large_result = "file-start\n" + (payload_line * payload_lines) + "file-end" messages = [ Message(role="user", contents=["read the file"]), Message( From 7c146b4ef533525d08ea3e0c1df733b6a06b4521 Mon Sep 17 00:00:00 2001 From: ScarabSystems Date: Thu, 30 Jul 2026 10:06:01 -0400 Subject: [PATCH 4/4] Python: collapse tool result digest scan --- .../core/agent_framework/_compaction.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/python/packages/core/agent_framework/_compaction.py b/python/packages/core/agent_framework/_compaction.py index 269195a1a61..5f9dec5bcdd 100644 --- a/python/packages/core/agent_framework/_compaction.py +++ b/python/packages/core/agent_framework/_compaction.py @@ -1024,23 +1024,20 @@ async def __call__(self, messages: list[Message]) -> bool: if group_id in keep_ids: continue group_msgs = grouped.get(group_id, []) - included_group_msgs = [msg for msg in group_msgs if not msg.additional_properties.get(EXCLUDED_KEY, False)] - # Build a call_id → function_name map from function_call contents. call_id_to_name: dict[str, str] = {} - for msg in included_group_msgs: + tool_results: list[str] = [] + for msg in group_msgs: + if msg.additional_properties.get(EXCLUDED_KEY, False): + continue for content in msg.contents: if content.type == "function_call" and content.call_id and content.name: call_id_to_name[content.call_id] = content.name elif content.type == "mcp_server_tool_call" and content.call_id and content.tool_name: call_id_to_name[content.call_id] = content.tool_name - # Collect tool results with the function name for context. - tool_results: list[str] = [] - for msg in included_group_msgs: - for content in msg.contents: - if content.type == "function_result": + elif content.type == "function_result": result_text = content.result if isinstance(content.result, str) else str(content.result) - func_name = call_id_to_name.get(content.call_id or "", "") - label = f"{func_name}: {result_text}" if func_name else result_text + tool_name = call_id_to_name.get(content.call_id or "", "") + label = f"{tool_name}: {result_text}" if tool_name else result_text tool_results.append(label.strip()) elif content.type == "mcp_server_tool_result": result_text = _tool_result_text(content.output)