Files
hermes-agent/tests/agent/test_compression_attempt_telemetry.py
Teknium c99299d746 feat(telemetry): memory and compression rows say why they failed or were skipped
Fleet telemetry shows built-in memory writes failing 34-47% and automatic
compression failing ~15%, and several merged fixes each target one cause
(#133038 for memory, #133054/#133040/#133051/#133032 for compression). Both
metrics only carried outcome, so the fleet could not tell which cause a fix
moved.

- hermes.memory.op.count and hermes.compression.count gain one closed-enum
  dimension, failure_class; `none` on success.
- Memory: every refusal/failure return in tools/memory_tool.py and
  tools/memory_tool_store.py names its class (no_match, ambiguous,
  over_budget, would_empty, drift, read_failed, retry_cap, scan_blocked,
  missing_content, missing_old_text, invalid_args, stale_entry, disabled,
  staged, gate_refused) through a context variable; the result the model
  sees is unchanged. Plugin providers report provider_error / exception.
- Compression: the attempt log's own failure_class, collapsed onto the
  compressor's closed names (exception:<Type> / rollback:<Type> keep only the
  prefix, anything else reads other, a classless failure unknown). Skipped
  rows carry the skip reason, and the skip set is now defined once in the
  contract.
- Package schema v4 / event schema v4; v3 field sets of both metrics stay
  valid so pending rows drain. Doc table rows and the consent copy (CLI
  setup and every Desktop locale's collectedActivity) name the new reason.
2026-10-05 22:07:41 -07:00

286 lines
11 KiB
Python

import json
import logging
import time
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from agent.conversation_compression import _emit_aborted_attempt_telemetry, compress_context
from agent.context_compressor import ContextCompressor
class _TodoStore:
def format_for_injection(self):
return ""
class _Agent:
def __init__(self, compressor):
self.context_compressor = compressor
self.session_id = "session-telemetry-test"
self.platform = "cli"
self.model = "test/main-model"
self.provider = "test-provider"
self.tools = []
self._compression_feasibility_checked = True
self.compression_in_place = False
self._memory_manager = None
self._session_db = None
self._todo_store = _TodoStore()
self._cached_system_prompt = None
def _emit_status(self, _message):
pass
def _emit_warning(self, _message):
pass
def _invalidate_system_prompt(self):
self._cached_system_prompt = None
def _build_system_prompt(self, system_message):
return system_message
def commit_memory_session(self, _messages):
pass
def _messages(secret_text="TOPSECRET_TRANSCRIPT_TEXT"):
msgs = [{"role": "system", "content": "system prompt"}]
for idx in range(10):
msgs.append({"role": "user", "content": f"user message {idx} {secret_text}"})
msgs.append({"role": "assistant", "content": f"assistant reply {idx} {secret_text}"})
return msgs
def _extract_telemetry(caplog):
records = [
record.getMessage()
for record in caplog.records
if "context compression attempt telemetry:" in record.getMessage()
]
assert len(records) == 1
return json.loads(records[0].split("context compression attempt telemetry: ", 1)[1])
def test_compression_attempt_telemetry_is_metadata_only(caplog):
with patch("agent.context_compressor.get_model_context_length", return_value=100_000):
compressor = ContextCompressor(
model="test/main-model",
provider="test-provider",
threshold_percent=0.50,
quiet_mode=True,
config_context_length=100_000,
)
compressor.tail_token_budget = 10
agent = _Agent(compressor)
with patch.object(compressor, "_generate_summary", return_value="SANITIZED SUMMARY"):
with caplog.at_level(logging.INFO, logger="agent.conversation_compression"):
compressed, system_prompt = compress_context(
agent,
_messages(),
"system prompt",
approx_tokens=75_000,
force=True,
)
assert system_prompt == "system prompt"
assert compressed is not None
payload = _extract_telemetry(caplog)
assert payload["event"] == "compression_attempt"
assert payload["attempt_id"]
assert payload["session_id"] == "session-telemetry-test"
assert payload["trigger_source"] == "manual"
assert payload["main_model"] == "test/main-model"
assert payload["main_context_limit"] == 100_000
assert payload["current_estimated_tokens"] == 75_000
assert payload["effective_threshold"] == compressor.threshold_tokens
assert payload["protected_head_tokens"] is not None
assert payload["protected_tail_tokens"] is not None
assert payload["middle_window_tokens"] is not None
assert payload["chunking"] is False
assert payload["chunk_count"] in {0, 1}
assert payload["commit_status"] == "committed"
assert payload["split_status"] == "not_applicable"
assert payload["fallback_used"] is False
assert isinstance(payload["total_duration_ms"], int)
assert isinstance(payload["commit_ms"], int)
assert payload["queue_wait_ms"] is None
assert payload["prompt_build_ms"] is None
assert payload["time_to_first_progress_ms"] is None
assert payload["summary_generation_ms"] is None
raw_log = json.dumps(payload)
assert "TOPSECRET_TRANSCRIPT_TEXT" not in raw_log
assert "SANITIZED SUMMARY" not in raw_log
assert "user message" not in raw_log
assert "assistant reply" not in raw_log
def test_aux_call_telemetry_records_durations_without_content(caplog):
with patch("agent.context_compressor.get_model_context_length", return_value=100_000):
compressor = ContextCompressor(
model="test/main-model",
provider="test-provider",
threshold_percent=0.50,
quiet_mode=True,
config_context_length=100_000,
)
compressor.tail_token_budget = 10
agent = _Agent(compressor)
response = SimpleNamespace(
choices=[SimpleNamespace(message=SimpleNamespace(content="SANITIZED SUMMARY"))]
)
with patch("agent.context_compressor.call_llm", return_value=response):
with caplog.at_level(logging.INFO, logger="agent.conversation_compression"):
compress_context(
agent,
_messages(),
"system prompt",
approx_tokens=75_000,
)
payload = _extract_telemetry(caplog)
assert payload["aux_prompt_tokens"] is not None
# Current main intentionally omits max_tokens from the aux summary call
# (the summary budget is prompt-level guidance only), so no output
# reservation is recorded.
assert payload["aux_output_reservation"] is None
assert isinstance(payload["aux_call_duration_ms"], int)
assert payload["aux_provider"]
assert payload["aux_model"]
raw_log = json.dumps(payload)
assert "TOPSECRET_TRANSCRIPT_TEXT" not in raw_log
assert "SANITIZED SUMMARY" not in raw_log
def test_aux_call_telemetry_records_content_free_phase_timings():
with patch("agent.context_compressor.get_model_context_length", return_value=100_000):
compressor = ContextCompressor(
model="test/main-model",
provider="test-provider",
threshold_percent=0.50,
quiet_mode=True,
config_context_length=100_000,
)
compressor._begin_compression_telemetry(current_tokens=75_000)
compressor._record_aux_compression_call(
prompt_messages=[{"role": "user", "content": "TOPSECRET_TRANSCRIPT_TEXT"}],
max_tokens=1400,
duration_ms=22,
aux_provider="ollama",
aux_model="qwen3:8b",
phase_timings={
"queue_wait_ms": 3,
"prompt_build_ms": 5,
"time_to_first_progress_ms": 7,
"summary_generation_ms": 19,
"commit_ms": 11,
},
)
payload = compressor._last_compression_telemetry
assert payload is not None
assert {key: payload[key] for key in (
"queue_wait_ms",
"prompt_build_ms",
"time_to_first_progress_ms",
"summary_generation_ms",
"commit_ms",
)} == {
"queue_wait_ms": 3,
"prompt_build_ms": 5,
"time_to_first_progress_ms": 7,
"summary_generation_ms": 19,
"commit_ms": 11,
}
assert "TOPSECRET_TRANSCRIPT_TEXT" not in json.dumps(payload)
def test_automatic_compaction_counts_once_in_shared_metrics(monkeypatch):
from hermes_cli.observability import shared_metrics_events
from hermes_cli.observability.shared_metrics_fields import compression_fields
calls = []
monkeypatch.setattr(shared_metrics_events, "record_compression", lambda **kw: calls.append(kw))
with patch("agent.context_compressor.get_model_context_length", return_value=100_000):
compressor = ContextCompressor(
model="test/main-model", provider="test-provider", threshold_percent=0.50, quiet_mode=True,
config_context_length=100_000,
)
compressor.tail_token_budget = 10
agent = _Agent(compressor)
with patch.object(compressor, "_generate_summary", return_value="SANITIZED SUMMARY"):
compress_context(agent, _messages(), "system prompt", approx_tokens=80_000)
assert [compression_fields(**kw) for kw in calls] == [
{"trigger": "auto", "outcome": "success", "context_fill_bucket": "75_to_90", "failure_class": "none"}
]
def test_structural_no_op_is_counted_skipped_with_the_compressors_class(caplog, monkeypatch):
"""A transcript that fits inside the protected tail makes no summary call: the attempt log names
``no_compressible_window`` (not the caller's generic ``no_progress``) and the shared metric counts
it ``skipped``, not ``failed`` (#131412)."""
from hermes_cli.observability import shared_metrics_events
from hermes_cli.observability.shared_metrics_fields import compression_fields
recorded = []
monkeypatch.setattr(shared_metrics_events, "record_compression", lambda **kw: recorded.append(kw))
with patch("agent.context_compressor.get_model_context_length", return_value=100_000):
compressor = ContextCompressor(
model="test/main-model", provider="test-provider", threshold_percent=0.50, quiet_mode=True,
config_context_length=100_000,
)
agent = _Agent(compressor)
messages = _messages()
with patch.object(compressor, "_find_tail_cut_by_tokens", return_value=0):
with caplog.at_level(logging.INFO, logger="agent.conversation_compression"):
compressed, _ = compress_context(agent, messages, "system prompt", approx_tokens=75_000)
assert compressed == messages
payload = _extract_telemetry(caplog)
assert (payload["commit_status"], payload["failure_class"]) == ("aborted", "no_compressible_window")
assert [(f["outcome"], f["failure_class"]) for f in (compression_fields(**kw) for kw in recorded)] == [
("skipped", "no_compressible_window")
]
@pytest.mark.parametrize(
("recorded", "caller", "expected"),
[
# Nothing recorded: the caller's verdict is the only information there is.
(None, "no_progress", "no_progress"),
# Sibling generic verdict: a terminal summary failure keeps its documented class.
("summary_auth_failure", "summary_generation_aborted", "summary_auth_failure"),
# A caller-side event the compressor cannot see still outranks a recorded class.
("no_compressible_window", "attempt_superseded", "attempt_superseded"),
# A snapshot restore put the PREVIOUS attempt's telemetry back: its class is not this attempt's.
(("no_compressible_window", "previous-attempt"), "no_progress", "no_progress"),
],
)
def test_generic_caller_verdicts_defer_to_the_recorded_failure_class(caplog, monkeypatch, recorded, caller, expected):
from hermes_cli.observability import shared_metrics_events
recorded, attempt_id = recorded if isinstance(recorded, tuple) else (recorded, "this-attempt")
monkeypatch.setattr(shared_metrics_events, "finish_compression_attempt", lambda *_args, **_kwargs: None)
compressor = SimpleNamespace(
_last_compression_telemetry={"failure_class": recorded, "attempt_id": attempt_id}, context_length=100_000,
)
agent = SimpleNamespace(
context_compressor=compressor, session_id="session-telemetry-test", _compression_attempt_id="this-attempt",
)
with caplog.at_level(logging.INFO, logger="agent.conversation_compression"):
_emit_aborted_attempt_telemetry(agent, time.monotonic(), caller)
assert _extract_telemetry(caplog)["failure_class"] == expected