diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index d78300d4da..d2e4c5a72a 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -33,6 +33,7 @@ from pathlib import Path as _Path sys.path.insert(0, str(_Path(__file__).resolve().parents[3])) +from agent.compression_marker import elide from agent.retry_utils import parse_retry_after_seconds from agent.secret_scope import get_secret from gateway.config import Platform, PlatformConfig @@ -764,8 +765,7 @@ def _serialize_slack_blocks_for_agent(blocks: list, max_chars: int = 6000) -> st payload = json.dumps(_sanitize(inspectable), ensure_ascii=False, indent=2) except Exception: payload = repr(inspectable) - if len(payload) > max_chars: - payload = payload[: max_chars - 18].rstrip() + "\n... [truncated]" + payload = elide(payload, max_chars) return f"[Slack Block Kit payload for this message]\n```json\n{payload}\n```" @@ -4267,8 +4267,7 @@ class SlackAdapter(BasePlatformAdapter): nested_text = "" if blocks_budget > 0: nested_text = _extract_text_from_slack_blocks(att.get("blocks") or []) - if len(nested_text) > blocks_budget: - nested_text = nested_text[:blocks_budget].rstrip() + "\n... [truncated]" + nested_text = elide(nested_text, blocks_budget) if nested_text and nested_text not in body: blocks_budget -= len(nested_text) body = f"{body}\n{nested_text}".strip() if body else nested_text diff --git a/tests/agent/test_compression_marker.py b/tests/agent/test_compression_marker.py index 0ca4c04c9f..e76c84d13f 100644 --- a/tests/agent/test_compression_marker.py +++ b/tests/agent/test_compression_marker.py @@ -19,7 +19,14 @@ from agent.compression_marker import ( elide_middle, ) -AGENT_ROOT = pathlib.Path(__file__).resolve().parents[2] / "agent" +REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] +AGENT_ROOT = REPO_ROOT / "agent" +# Agent-facing renderers outside agent/: Slack payload dumps and trajectory training data. +SCANNED_PATHS = ( + *sorted(AGENT_ROOT.rglob("*.py")), + *sorted((REPO_ROOT / "plugins" / "platforms" / "slack").rglob("*.py")), + REPO_ROOT / "trajectory_compressor.py", +) # Any "...[ truncated]" variant, not just the bare one (e.g. "...[fallback summary truncated]"). IMITABLE_MARKER_RE = re.compile(r"(?:\.\.\.|…)\s?\[[^\]\n]*truncated\]") @@ -68,12 +75,12 @@ def test_no_imitable_truncation_marker_in_agent_strings(): helpers now, so any such literal in a string token is a new imitation surface. """ offenders = [] - for path in sorted(AGENT_ROOT.rglob("*.py")): + for path in SCANNED_PATHS: with tokenize.open(str(path)) as fh: for tok in tokenize.generate_tokens(fh.readline): if tok.type != tokenize.STRING: continue for match in IMITABLE_MARKER_RE.finditer(tok.string): - rel = path.relative_to(AGENT_ROOT.parent) + rel = path.relative_to(REPO_ROOT) offenders.append(f"{rel}:{tok.start[0]}: {match.group()}") assert not offenders, "imitable truncation markers in agent/ strings:\n" + "\n".join(offenders) diff --git a/tests/gateway/test_slack_unfurl_nested_blocks_budget.py b/tests/gateway/test_slack_unfurl_nested_blocks_budget.py index 2bb9eb1ed3..fcbffaf0fe 100644 --- a/tests/gateway/test_slack_unfurl_nested_blocks_budget.py +++ b/tests/gateway/test_slack_unfurl_nested_blocks_budget.py @@ -6,6 +6,7 @@ each carrying its own blocks, so without a shared ceiling the projection grows l attachment count while the top-level ``blocks`` path is capped once. """ +from agent.compression_marker import _COMPRESSION_MARKER_RE from plugins.platforms.slack.adapter import SlackAdapter @@ -27,7 +28,7 @@ def test_nested_block_text_stops_growing_with_attachment_count(): per_attachment_growth = (sizes[20] - sizes[5]) / 15 assert per_attachment_growth < 200, sizes assert sizes[20] < 3 * sizes[1], sizes - assert "[truncated]" in SlackAdapter._append_link_unfurls("intro", [_attachment(i) for i in range(20)]) + assert _COMPRESSION_MARKER_RE.search(SlackAdapter._append_link_unfurls("intro", [_attachment(i) for i in range(20)])) def test_first_attachment_keeps_its_body_and_headers_survive_exhaustion(): diff --git a/tests/test_trajectory_compressor.py b/tests/test_trajectory_compressor.py index c1b643a76d..af8ec8353a 100644 --- a/tests/test_trajectory_compressor.py +++ b/tests/test_trajectory_compressor.py @@ -8,6 +8,8 @@ from unittest.mock import AsyncMock, patch, MagicMock import pytest +from agent.compression_marker import _COMPRESSION_MARKER_RE + from trajectory_compressor import ( CompressionConfig, TrajectoryMetrics, @@ -268,7 +270,7 @@ class TestExtractTurnContent: {"from": "tool", "value": "x" * 5000}, ] content = tc._extract_turn_content_for_summary(trajectory, 0, 1) - assert "...[truncated]..." in content + assert _COMPRESSION_MARKER_RE.search(content) assert len(content) < 5000 def test_empty_range(self): diff --git a/trajectory_compressor.py b/trajectory_compressor.py index 04ae2a7ec1..9e76ec23da 100644 --- a/trajectory_compressor.py +++ b/trajectory_compressor.py @@ -30,6 +30,7 @@ import fire from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn, TimeRemainingColumn from rich.console import Console from hermes_constants import OPENROUTER_BASE_URL, get_hermes_home +from agent.compression_marker import elide_middle from agent.retry_utils import jittered_backoff from hermes_cli.env_loader import load_hermes_dotenv @@ -401,7 +402,7 @@ class TrajectoryCompressor: turn = trajectory[i] value = turn.get("value", "") if len(value) > 3000: - value = value[:1500] + "\n...[truncated]...\n" + value[-500:] + value = elide_middle(value, 1500, 500) parts.append(f"[Turn {i} - {turn.get('from', 'unknown').upper()}]:\n{value}") return "\n\n".join(parts)