Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions agent/manual_compression_feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""User-facing summaries for manual compression commands."""

from __future__ import annotations

from typing import Any, Sequence


def summarize_manual_compression(
before_messages: Sequence[dict[str, Any]],
after_messages: Sequence[dict[str, Any]],
before_tokens: int,
after_tokens: int,
) -> dict[str, Any]:
"""Return consistent user-facing feedback for manual compression."""
before_count = len(before_messages)
after_count = len(after_messages)
noop = list(after_messages) == list(before_messages)

if noop:
headline = f"No changes from compression: {before_count} messages"
if after_tokens == before_tokens:
token_line = (
f"Rough transcript estimate: ~{before_tokens:,} tokens (unchanged)"
)
else:
token_line = (
f"Rough transcript estimate: ~{before_tokens:,} β†’ "
f"~{after_tokens:,} tokens"
)
else:
headline = f"Compressed: {before_count} β†’ {after_count} messages"
token_line = (
f"Rough transcript estimate: ~{before_tokens:,} β†’ "
f"~{after_tokens:,} tokens"
)

note = None
if not noop and after_count < before_count and after_tokens > before_tokens:
note = (
"Note: fewer messages can still raise this rough transcript estimate "
"when compression rewrites the transcript into denser summaries."
)

return {
"noop": noop,
"headline": headline,
"token_line": token_line,
"note": note,
}
22 changes: 15 additions & 7 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5754,21 +5754,29 @@ def _manual_compress(self):
original_count = len(self.conversation_history)
try:
from agent.model_metadata import estimate_messages_tokens_rough
approx_tokens = estimate_messages_tokens_rough(self.conversation_history)
from agent.manual_compression_feedback import summarize_manual_compression
original_history = list(self.conversation_history)
approx_tokens = estimate_messages_tokens_rough(original_history)
print(f"πŸ—œοΈ Compressing {original_count} messages (~{approx_tokens:,} tokens)...")

compressed, new_system = self.agent._compress_context(
self.conversation_history,
compressed, _ = self.agent._compress_context(
original_history,
self.agent._cached_system_prompt or "",
approx_tokens=approx_tokens,
)
self.conversation_history = compressed
new_count = len(self.conversation_history)
new_tokens = estimate_messages_tokens_rough(self.conversation_history)
print(
f" βœ… Compressed: {original_count} β†’ {new_count} messages "
f"(~{approx_tokens:,} β†’ ~{new_tokens:,} tokens)"
summary = summarize_manual_compression(
original_history,
self.conversation_history,
approx_tokens,
new_tokens,
)
icon = "πŸ—œοΈ" if summary["noop"] else "βœ…"
print(f" {icon} {summary['headline']}")
print(f" {summary['token_line']}")
if summary["note"]:
print(f" {summary['note']}")

except Exception as e:
print(f" ❌ Compression failed: {e}")
Expand Down
22 changes: 17 additions & 5 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5000,6 +5000,7 @@ async def _handle_compress_command(self, event: MessageEvent) -> str:

try:
from run_agent import AIAgent
from agent.manual_compression_feedback import summarize_manual_compression
from agent.model_metadata import estimate_messages_tokens_rough

runtime_kwargs = _resolve_runtime_agent_kwargs()
Expand Down Expand Up @@ -5027,6 +5028,13 @@ async def _handle_compress_command(self, event: MessageEvent) -> str:
)
tmp_agent._print_fn = lambda *a, **kw: None

compressor = tmp_agent.context_compressor
compress_start = compressor.protect_first_n
compress_start = compressor._align_boundary_forward(msgs, compress_start)
compress_end = compressor._find_tail_cut_by_tokens(msgs, compress_start)
if compress_start >= compress_end:
return "Nothing to compress yet (the transcript is still all protected context)."

loop = asyncio.get_event_loop()
compressed, _ = await loop.run_in_executor(
None,
Expand All @@ -5047,13 +5055,17 @@ async def _handle_compress_command(self, event: MessageEvent) -> str:
self.session_store.update_session(
session_entry.session_key, last_prompt_tokens=0
)
new_count = len(compressed)
new_tokens = estimate_messages_tokens_rough(compressed)

return (
f"πŸ—œοΈ Compressed: {original_count} β†’ {new_count} messages\n"
f"~{approx_tokens:,} β†’ ~{new_tokens:,} tokens"
summary = summarize_manual_compression(
msgs,
compressed,
approx_tokens,
new_tokens,
)
lines = [f"πŸ—œοΈ {summary['headline']}", summary["token_line"]]
if summary["note"]:
lines.append(summary["note"])
return "\n".join(lines)
except Exception as e:
logger.warning("Manual compress failed: %s", e)
return f"Compression failed: {e}"
Expand Down
66 changes: 66 additions & 0 deletions tests/cli/test_manual_compress.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Tests for CLI manual compression messaging."""

from unittest.mock import MagicMock, patch

from tests.cli.test_cli_init import _make_cli


def _make_history() -> list[dict[str, str]]:
return [
{"role": "user", "content": "one"},
{"role": "assistant", "content": "two"},
{"role": "user", "content": "three"},
{"role": "assistant", "content": "four"},
]


def test_manual_compress_reports_noop_without_success_banner(capsys):
shell = _make_cli()
history = _make_history()
shell.conversation_history = history
shell.agent = MagicMock()
shell.agent.compression_enabled = True
shell.agent._cached_system_prompt = ""
shell.agent._compress_context.return_value = (list(history), "")

def _estimate(messages):
assert messages == history
return 100

with patch("agent.model_metadata.estimate_messages_tokens_rough", side_effect=_estimate):
shell._manual_compress()

output = capsys.readouterr().out
assert "No changes from compression" in output
assert "βœ… Compressed" not in output
assert "Rough transcript estimate: ~100 tokens (unchanged)" in output


def test_manual_compress_explains_when_token_estimate_rises(capsys):
shell = _make_cli()
history = _make_history()
compressed = [
history[0],
{"role": "assistant", "content": "Dense summary that still counts as more tokens."},
history[-1],
]
shell.conversation_history = history
shell.agent = MagicMock()
shell.agent.compression_enabled = True
shell.agent._cached_system_prompt = ""
shell.agent._compress_context.return_value = (compressed, "")

def _estimate(messages):
if messages == history:
return 100
if messages == compressed:
return 120
raise AssertionError(f"unexpected transcript: {messages!r}")

with patch("agent.model_metadata.estimate_messages_tokens_rough", side_effect=_estimate):
shell._manual_compress()

output = capsys.readouterr().out
assert "βœ… Compressed: 4 β†’ 3 messages" in output
assert "Rough transcript estimate: ~100 β†’ ~120 tokens" in output
assert "denser summaries" in output
121 changes: 121 additions & 0 deletions tests/gateway/test_compress_command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Tests for gateway /compress user-facing messaging."""

from datetime import datetime
from unittest.mock import MagicMock, patch

import pytest

from gateway.config import GatewayConfig, Platform, PlatformConfig
from gateway.platforms.base import MessageEvent
from gateway.session import SessionEntry, SessionSource, build_session_key


def _make_source() -> SessionSource:
return SessionSource(
platform=Platform.TELEGRAM,
user_id="u1",
chat_id="c1",
user_name="tester",
chat_type="dm",
)


def _make_event(text: str = "/compress") -> MessageEvent:
return MessageEvent(text=text, source=_make_source(), message_id="m1")


def _make_history() -> list[dict[str, str]]:
return [
{"role": "user", "content": "one"},
{"role": "assistant", "content": "two"},
{"role": "user", "content": "three"},
{"role": "assistant", "content": "four"},
]


def _make_runner(history: list[dict[str, str]]):
from gateway.run import GatewayRunner

runner = object.__new__(GatewayRunner)
runner.config = GatewayConfig(
platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="***")}
)
session_entry = SessionEntry(
session_key=build_session_key(_make_source()),
session_id="sess-1",
created_at=datetime.now(),
updated_at=datetime.now(),
platform=Platform.TELEGRAM,
chat_type="dm",
)
runner.session_store = MagicMock()
runner.session_store.get_or_create_session.return_value = session_entry
runner.session_store.load_transcript.return_value = history
runner.session_store.rewrite_transcript = MagicMock()
runner.session_store.update_session = MagicMock()
runner.session_store._save = MagicMock()
return runner


@pytest.mark.asyncio
async def test_compress_command_reports_noop_without_success_banner():
history = _make_history()
runner = _make_runner(history)
agent_instance = MagicMock()
agent_instance.context_compressor.protect_first_n = 0
agent_instance.context_compressor._align_boundary_forward.return_value = 0
agent_instance.context_compressor._find_tail_cut_by_tokens.return_value = 2
agent_instance.session_id = "sess-1"
agent_instance._compress_context.return_value = (list(history), "")

def _estimate(messages):
assert messages == history
return 100

with (
patch("gateway.run._resolve_runtime_agent_kwargs", return_value={"api_key": "test-key"}),
patch("gateway.run._resolve_gateway_model", return_value="test-model"),
patch("run_agent.AIAgent", return_value=agent_instance),
patch("agent.model_metadata.estimate_messages_tokens_rough", side_effect=_estimate),
):
result = await runner._handle_compress_command(_make_event())

assert "No changes from compression" in result
assert "Compressed:" not in result
assert "Rough transcript estimate: ~100 tokens (unchanged)" in result


@pytest.mark.asyncio
async def test_compress_command_explains_when_token_estimate_rises():
history = _make_history()
compressed = [
history[0],
{"role": "assistant", "content": "Dense summary that still counts as more tokens."},
history[-1],
]
runner = _make_runner(history)
agent_instance = MagicMock()
agent_instance.context_compressor.protect_first_n = 0
agent_instance.context_compressor._align_boundary_forward.return_value = 0
agent_instance.context_compressor._find_tail_cut_by_tokens.return_value = 2
agent_instance.session_id = "sess-1"
agent_instance._compress_context.return_value = (compressed, "")

def _estimate(messages):
if messages == history:
return 100
if messages == compressed:
return 120
raise AssertionError(f"unexpected transcript: {messages!r}")

with (
patch("gateway.run._resolve_runtime_agent_kwargs", return_value={"api_key": "test-key"}),
patch("gateway.run._resolve_gateway_model", return_value="test-model"),
patch("run_agent.AIAgent", return_value=agent_instance),
patch("agent.model_metadata.estimate_messages_tokens_rough", side_effect=_estimate),
):
result = await runner._handle_compress_command(_make_event())

assert "Compressed: 4 β†’ 3 messages" in result
assert "Rough transcript estimate: ~100 β†’ ~120 tokens" in result
assert "denser summaries" in result