A stalled Relay event could hold the compression lease while a mark was published. Use the same timeout as other Relay scope operations so the agent can continue after a stalled publisher. A committed proactive prune estimated both full transcripts even when no Relay scope could receive the mark. Check for a target before doing that work. Signed-off-by: mnajafian-nv <mnajafian@nvidia.com> (cherry picked from commit 808eab610aa161875ad07c79947282fe54da0999)
308 lines
16 KiB
Python
308 lines
16 KiB
Python
"""Content-free ``hermes.compaction`` v1 records for every history rewrite, and their fan-out.
|
|
|
|
One record per ``compress_context`` attempt (built from the attempt telemetry seam), per
|
|
micro-compaction pass, per committed proactive tool-result prune, and per Codex-native thread
|
|
compaction Hermes observes. A persistence-detached fork (background review) shares the live session id
|
|
but rewrites only its own throwaway transcript, so it publishes nothing. The payload is flat because OpenTelemetry exporters flatten only top-level
|
|
keys; every categorical field except the free-form ``summarizer_provider`` / ``summarizer_model`` identifiers
|
|
comes from a closed set with an ``other`` fallback; no field carries message, summary, focus-topic, error or
|
|
path text. Publishing never raises into compaction.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import logging
|
|
import threading
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from agent.model_metadata import estimate_messages_tokens_rough
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SCHEMA_NAME = "hermes.compaction"
|
|
SCHEMA_VERSION = "1"
|
|
# Relay's reserved mark name: it resets the owning agent scope's LLM-history freshness, so it is used only
|
|
# for a committed rewrite of what the model will see next. Everything else is an ordinary attempt mark.
|
|
COMPACTION_MARK = "compaction"
|
|
ATTEMPT_MARK = "compaction.attempt"
|
|
|
|
# The v1 sets list only values this module emits. Later versions may add request-scope truncation kinds.
|
|
_KINDS = frozenset({"summarize", "micro_summarize", "prune_tool_results", "provider_native"})
|
|
_SCOPES = frozenset({"history", "provider"})
|
|
_METHODS = frozenset({
|
|
"llm_summary", "aux_fallback_main", "deterministic_fallback", "deterministic_prune", "provider", "none",
|
|
})
|
|
_TRIGGERS = frozenset({
|
|
"manual", "turn_start_threshold", "pre_api", "post_tool", "idle", "engine_preflight", "overflow",
|
|
"gateway_hygiene", "between_turns", "proactive_prune", "provider", "unknown",
|
|
})
|
|
# Every other known trigger is automatic; an unknown or unlisted one asserts nothing.
|
|
_TRIGGER_CLASSES = {
|
|
"manual": "manual", "overflow": "overflow", "provider": "provider", "unknown": "unknown", "other": "unknown",
|
|
}
|
|
_TRIGGER_CLASS_VALUES = frozenset({"auto", "manual", "overflow", "provider", "unknown"})
|
|
# The error-classifier reasons whose recovery compresses with ``trigger="overflow"``.
|
|
_OVERFLOW_REASONS = frozenset({"context_overflow", "payload_too_large", "long_context_tier"})
|
|
_OUTCOMES = frozenset({"committed", "aborted", "failed", "skipped", "blocked"})
|
|
_SPLIT_STATUSES = frozenset({
|
|
"not_applicable", "in_place_committed", "rotated_committed", "failed_not_indexed", "aborted",
|
|
})
|
|
_TOKEN_COUNT_METHODS = frozenset({"estimate_rough"})
|
|
# Attempt classes beyond the shared-metrics closed set: guards, Codex route exits, lease races, an engine's
|
|
# empty transcript, and the micro-compaction pass outcomes.
|
|
_EXTRA_FAILURE_CLASSES = frozenset({
|
|
"blocked:cooldown", "blocked:structural_backoff", "blocked:ineffective", "blocked:unknown",
|
|
"codex_auto_native", "codex_auto_off", "codex_no_thread", "codex_compaction_failed",
|
|
"session_ownership_lost", "session_ownership_unreadable", "cooldown_state_unreadable", "empty_transcript",
|
|
"summary_model_benched",
|
|
"summarize_failed", "exchange_skipped", "defrag_failed", "stale_generation", "watermark_unavailable",
|
|
})
|
|
# An aborted attempt the compressor ran but deliberately did not commit; other aborts are failures.
|
|
_ABORTED_CLASSES = frozenset({"would_grow", "no_progress", "commit_fence_cancelled"})
|
|
_COMMIT_MODES = frozenset({"in_place_committed", "rotated_committed"})
|
|
_MICRO_OUTCOMES = {
|
|
"absorbed": "committed", "defrag": "committed", "stale_generation": "skipped", "watermark_unavailable": "skipped",
|
|
}
|
|
|
|
# Every v1 key, so consumers see a stable shape; unknown values stay null.
|
|
_FIELDS = (
|
|
"harness", "kind", "scope", "official", "method", "trigger", "trigger_class", "overflow_reason", "outcome",
|
|
"failure_class", "attempt_id", "session_id", "tokens_before", "tokens_after", "tokens_reclaimed",
|
|
"token_count_method", "messages_before", "messages_after", "items_dropped", "context_limit", "threshold_tokens",
|
|
"protected_head_tokens", "middle_window_tokens", "protected_tail_tokens", "duration_ms",
|
|
"summary_generation_ms", "aux_call_duration_ms", "queue_wait_ms", "commit_ms", "summarizer_provider",
|
|
"summarizer_model", "tool_results_pruned", "reasoning_items_pruned", "summary_input_omitted_chars",
|
|
"compression_count", "in_place", "session_rotated", "split_status", "cache_break", "has_focus_topic",
|
|
)
|
|
# Integer fields copied as-is from the attempt record (record key -> payload key).
|
|
_ATTEMPT_INTS = {
|
|
"tokens_before": "tokens_before", "tokens_after": "tokens_after", "tokens_reclaimed": "tokens_reclaimed",
|
|
"messages_before": "messages_before", "messages_after": "messages_after", "items_dropped": "items_dropped",
|
|
"main_context_limit": "context_limit", "effective_threshold": "threshold_tokens",
|
|
"protected_head_tokens": "protected_head_tokens", "middle_window_tokens": "middle_window_tokens",
|
|
"protected_tail_tokens": "protected_tail_tokens", "total_duration_ms": "duration_ms",
|
|
"summary_generation_ms": "summary_generation_ms", "aux_call_duration_ms": "aux_call_duration_ms",
|
|
"queue_wait_ms": "queue_wait_ms", "commit_ms": "commit_ms", "tool_results_pruned": "tool_results_pruned",
|
|
"reasoning_items_pruned": "reasoning_items_pruned", "summary_input_omitted_chars": "summary_input_omitted_chars",
|
|
}
|
|
|
|
_warned_lock = threading.Lock()
|
|
_warned = False
|
|
|
|
|
|
@functools.cache
|
|
def _failure_classes() -> frozenset[str]:
|
|
from hermes_cli.observability.shared_metrics_contract import COMPRESSION_FAILURE_CLASSES
|
|
|
|
return COMPRESSION_FAILURE_CLASSES | _EXTRA_FAILURE_CLASSES
|
|
|
|
|
|
@functools.cache
|
|
def _skip_classes() -> frozenset[str]:
|
|
from hermes_cli.observability.shared_metrics_contract import COMPRESSION_SKIP_CLASSES
|
|
|
|
return COMPRESSION_SKIP_CLASSES
|
|
|
|
|
|
def _closed(value: Any, allowed: frozenset[str], default: str | None = "other") -> str | None:
|
|
if not isinstance(value, str) or not value:
|
|
return None
|
|
return value if value in allowed else default
|
|
|
|
|
|
def _int(value: Any) -> int | None:
|
|
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
|
|
|
|
def failure_class(value: Any) -> str:
|
|
"""Closed failure class: ``exception:<Type>`` / ``rollback:<Type>`` keep only the prefix (the type name
|
|
may be a plugin's); a missing class reads ``none``."""
|
|
if not isinstance(value, str) or not value:
|
|
return "none"
|
|
if value.startswith(("exception:", "rollback:")):
|
|
value = value.split(":", 1)[0]
|
|
return value if value in _failure_classes() else "other"
|
|
|
|
|
|
def event_name(payload: dict[str, Any]) -> str:
|
|
"""``compaction`` only for a committed history or provider rewrite; ``compaction.attempt`` otherwise."""
|
|
committed = payload.get("outcome") == "committed" and payload.get("scope") in {"history", "provider"}
|
|
return COMPACTION_MARK if committed else ATTEMPT_MARK
|
|
|
|
|
|
def _payload(**values: Any) -> dict[str, Any]:
|
|
payload = dict.fromkeys(_FIELDS)
|
|
payload.update(values, harness="hermes")
|
|
payload["kind"] = _closed(payload["kind"], _KINDS)
|
|
payload["scope"] = _closed(payload["scope"], _SCOPES)
|
|
payload["method"] = _closed(payload["method"], _METHODS) or "none"
|
|
payload["trigger"] = _closed(payload["trigger"], _TRIGGERS) or "unknown"
|
|
payload["trigger_class"] = (
|
|
_closed(payload["trigger_class"], _TRIGGER_CLASS_VALUES, None) or _TRIGGER_CLASSES.get(payload["trigger"], "auto")
|
|
)
|
|
payload["outcome"] = _closed(payload["outcome"], _OUTCOMES) or "other"
|
|
payload["token_count_method"] = _closed(payload["token_count_method"], _TOKEN_COUNT_METHODS)
|
|
payload["attempt_id"] = payload["attempt_id"] or uuid.uuid4().hex
|
|
payload["session_id"] = payload["session_id"] or ""
|
|
if payload["cache_break"] is None and payload["scope"] == "history":
|
|
payload["cache_break"] = payload["outcome"] == "committed"
|
|
return payload
|
|
|
|
|
|
def _attempt_outcome(commit_status: Any, failure: str, history_rewritten: Any) -> str:
|
|
# A split that failed after publishing the compacted history still rewrote what the model sees next: it is
|
|
# a committed compaction whose failure_class records the failure.
|
|
if history_rewritten is True:
|
|
return "committed"
|
|
if commit_status != "aborted":
|
|
return commit_status if commit_status in _OUTCOMES else "other"
|
|
if failure in _skip_classes():
|
|
return "skipped"
|
|
return "aborted" if failure in _ABORTED_CLASSES else "failed"
|
|
|
|
|
|
def _attempt_trigger(trigger_source: Any) -> tuple[Any, str | None]:
|
|
"""``(trigger, trigger_class)``. ``auto`` is what an unlabelled automatic caller records: the reason is
|
|
unknown, but the attempt is still known to be automatic."""
|
|
if trigger_source == "auto":
|
|
return "unknown", "auto"
|
|
return trigger_source or "unknown", None
|
|
|
|
|
|
def attempt_payload(record: dict[str, Any], *, compression_count: Any = None) -> dict[str, Any]:
|
|
"""Map one attempt telemetry record (the attempt log line's dict) onto the v1 schema."""
|
|
codex = record.get("route") == "codex_app_server"
|
|
failure = failure_class(record.get("failure_class"))
|
|
split_status = _closed(record.get("split_status"), _SPLIT_STATUSES)
|
|
outcome = _attempt_outcome(record.get("commit_status"), failure, record.get("history_rewritten"))
|
|
trigger, trigger_class = _attempt_trigger(record.get("trigger_source"))
|
|
values: dict[str, Any] = {key: _int(record.get(src)) for src, key in _ATTEMPT_INTS.items()}
|
|
values.update(
|
|
kind="provider_native" if codex else "summarize", scope="provider" if codex else "history", official=True,
|
|
method=record.get("method"), trigger=trigger, trigger_class=trigger_class,
|
|
overflow_reason=_closed(record.get("overflow_reason"), _OVERFLOW_REASONS), outcome=outcome,
|
|
failure_class=failure,
|
|
attempt_id=record.get("attempt_id"), session_id=record.get("session_id"),
|
|
token_count_method=record.get("token_count_method"),
|
|
summarizer_provider=record.get("aux_provider") or None, summarizer_model=record.get("aux_model") or None,
|
|
compression_count=_int(compression_count), split_status=split_status,
|
|
# Commit mode is known only for a fully committed split; anything else leaves both null.
|
|
in_place=split_status == "in_place_committed" if split_status in _COMMIT_MODES else None,
|
|
session_rotated=split_status == "rotated_committed" if split_status in _COMMIT_MODES else None,
|
|
has_focus_topic=record.get("has_focus_topic") if isinstance(record.get("has_focus_topic"), bool) else None,
|
|
)
|
|
if codex:
|
|
values["cache_break"] = None # the Codex thread's provider cache is not Hermes's prompt cache
|
|
return _payload(**values)
|
|
|
|
|
|
def micro_payload(record: dict[str, Any]) -> dict[str, Any]:
|
|
"""Map one micro-compaction telemetry record onto the v1 schema."""
|
|
outcome = _MICRO_OUTCOMES.get(record.get("outcome"), "failed")
|
|
before, after = _int(record.get("tokens_before")), _int(record.get("tokens_after"))
|
|
return _payload(
|
|
kind="micro_summarize", scope="history", official=False, trigger="between_turns", outcome=outcome,
|
|
method="llm_summary" if outcome == "committed" else "none",
|
|
failure_class="none" if outcome == "committed" else failure_class(record.get("outcome")),
|
|
session_id=record.get("session_id"), tokens_before=before, tokens_after=after,
|
|
tokens_reclaimed=before - after if before is not None and after is not None else None,
|
|
token_count_method="estimate_rough", messages_before=_int(record.get("messages_before")),
|
|
messages_after=_int(record.get("messages_after")), duration_ms=_int(record.get("duration_ms")),
|
|
threshold_tokens=_int(record.get("threshold_tokens")), context_limit=_int(record.get("context_limit")),
|
|
summarizer_model=record.get("aux_model") or None,
|
|
)
|
|
|
|
|
|
def prune_payload(
|
|
*, session_id: str, tokens_before: int, tokens_after: int, messages: int, tool_results_pruned: int,
|
|
) -> dict[str, Any]:
|
|
"""A committed proactive tool-result prune: deterministic, no LLM, message count unchanged."""
|
|
return _payload(
|
|
kind="prune_tool_results", scope="history", official=False, trigger="proactive_prune", outcome="committed",
|
|
method="deterministic_prune", failure_class="none", session_id=session_id, tokens_before=tokens_before,
|
|
tokens_after=tokens_after, tokens_reclaimed=tokens_before - tokens_after, token_count_method="estimate_rough",
|
|
messages_before=messages, messages_after=messages, tool_results_pruned=tool_results_pruned,
|
|
)
|
|
|
|
|
|
def provider_native_payload(*, session_id: str, compression_count: Any = None) -> dict[str, Any]:
|
|
"""A compaction the provider ran on its own thread and Hermes only observed."""
|
|
return _payload(
|
|
kind="provider_native", scope="provider", official=False, trigger="provider", outcome="committed",
|
|
method="provider", failure_class="none", session_id=session_id, compression_count=_int(compression_count),
|
|
)
|
|
|
|
|
|
def _publish(
|
|
build: Any, *args: Any, turn_session_id: str | None = None, require_target_session_id: str | None = None,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
"""Build one record and fan it out to the built-in Relay integration. Never raises into compaction: the
|
|
first failure logs at WARNING with its traceback, later ones at DEBUG."""
|
|
global _warned
|
|
try:
|
|
if require_target_session_id is not None:
|
|
from agent.relay_compaction import compaction_target_available
|
|
|
|
if not compaction_target_available(require_target_session_id, turn_session_id=turn_session_id):
|
|
return
|
|
payload = build(*args, **kwargs)
|
|
from agent.relay_compaction import emit_compaction_mark
|
|
|
|
emit_compaction_mark(payload["session_id"], event_name(payload), payload, turn_session_id=turn_session_id)
|
|
except Exception:
|
|
with _warned_lock:
|
|
first, _warned = not _warned, True
|
|
logger.log(logging.WARNING if first else logging.DEBUG, "compaction event publish failed", exc_info=True)
|
|
|
|
|
|
def _publishes_for(agent: Any) -> bool:
|
|
"""A persistence-detached fork (background review) rewrites only its own throwaway transcript."""
|
|
return not getattr(agent, "_persist_disabled", False)
|
|
|
|
|
|
def _turn_session_id(agent: Any) -> str | None:
|
|
"""The session the agent's current turn started in; a rotating commit moves ``agent.session_id`` off it
|
|
mid-turn. ``None`` outside a turn."""
|
|
value = getattr(agent, "_inflight_turn_session_id", None)
|
|
return value if isinstance(value, str) and value else None
|
|
|
|
|
|
def publish_attempt(agent: Any, record: dict[str, Any]) -> None:
|
|
"""Publish one ``compress_context`` attempt record (the attempt telemetry seam calls this)."""
|
|
if _publishes_for(agent):
|
|
_publish(lambda: attempt_payload(
|
|
record, compression_count=getattr(getattr(agent, "context_compressor", None), "compression_count", None),
|
|
), turn_session_id=_turn_session_id(agent))
|
|
|
|
|
|
def publish_micro(record: dict[str, Any], *, turn_session_id: str | None = None) -> None:
|
|
"""Publish one micro-compaction pass record. The finalizer never runs micro-compaction for a fork, and passes
|
|
the session its turn started in (``_turn_session_id``) because the pass may run in a rotated child."""
|
|
_publish(micro_payload, record, turn_session_id=turn_session_id)
|
|
|
|
|
|
def publish_prune(agent: Any, messages: list, pruned: list, tool_results_pruned: int) -> None:
|
|
"""Publish one committed proactive tool-result prune, measured like the attempt record (rough estimate of
|
|
the message list the model sees next)."""
|
|
session_id = getattr(agent, "session_id", None) or ""
|
|
turn_session_id = _turn_session_id(agent)
|
|
if _publishes_for(agent):
|
|
_publish(lambda: prune_payload(
|
|
session_id=session_id, tokens_before=estimate_messages_tokens_rough(messages),
|
|
tokens_after=estimate_messages_tokens_rough(pruned), messages=len(pruned),
|
|
tool_results_pruned=tool_results_pruned,
|
|
), turn_session_id=turn_session_id, require_target_session_id=session_id)
|
|
|
|
|
|
def publish_provider_native(agent: Any) -> None:
|
|
"""Publish one provider-native compaction Hermes observed but did not run."""
|
|
if _publishes_for(agent):
|
|
_publish(
|
|
provider_native_payload, session_id=getattr(agent, "session_id", None) or "",
|
|
compression_count=getattr(getattr(agent, "context_compressor", None), "compression_count", None),
|
|
turn_session_id=_turn_session_id(agent),
|
|
)
|