Files
hermes-agent/agent/conversation_compression_feasibility.py
yoniebans 8e7cf55eb6 fix(agent): a task with its own URL re-probes when that URL's pin changes
The route identity read a provider entry's context pin only for a task that names the entry. A task with `provider: custom` and its own `base_url` returned early, but the probe still reads the pin of the entry at that URL, so lowering only that pin (900K to 80K) left a warm agent's trigger on the old window. The identity now carries the pin for the route's own URL too.

Reported-by: greptile-apps[bot]
2026-11-05 12:09:34 +01:00

128 lines
6.5 KiB
Python

"""When the compression feasibility probe runs: after a main-runtime change, once a request first reaches
the smallest summariser window, and again when a warm agent meets a compression route other than the one
it probed. The probe itself is ``check_compression_model_feasibility`` in ``agent.conversation_compression``."""
from __future__ import annotations
import logging
from typing import Any
logger = logging.getLogger("agent.conversation_compression")
def _probe(agent: Any) -> None:
from agent.conversation_compression import check_compression_model_feasibility
check_compression_model_feasibility(agent)
def _mark_checked(agent: Any, route: tuple) -> None:
"""Latch the probe as done for *route*, the compression route read before the probe ran: an edit that
lands during the probe is then a moved route on the next request, not a measured one."""
agent._compression_feasibility_checked = True
agent._compression_feasibility_route = route
def compression_route_identity() -> tuple:
"""The configured compression route a probe measures: the task's provider, model, URL and context
length, the URL of a ``providers:`` / ``custom_providers:`` entry the task names, and the context pin
a provider entry sets for the compression model at the route's URL. A config this
cannot read yields ``("unreadable",)``: the main request carries on and the probe reports the problem."""
try:
return _route_identity()
except Exception:
logger.debug("Compression route unreadable for the feasibility latch", exc_info=True)
return ("unreadable",)
def _route_identity() -> tuple:
from agent.auxiliary_task_config import _get_auxiliary_task_config
cfg = _get_auxiliary_task_config("compression")
route = tuple(str(cfg.get(k) or "") for k in ("provider", "model", "base_url", "context_length"))
provider = route[0].strip()
from hermes_cli.config import get_custom_provider_context_length
if route[2]:
url, model, named = route[2], route[1], ()
elif not provider or provider in ("auto", "main", "custom"):
return route
else:
from hermes_cli.runtime_provider_custom import _get_named_custom_provider
entry = _get_named_custom_provider(provider, resolve_credentials=False) or {}
url, model = str(entry.get("base_url") or ""), route[1] or str(entry.get("model") or "")
named = (url, str(entry.get("model") or ""))
# The context pin the probe reads for this model at this URL (a matching entry's per-model, else entry-level).
pin = get_custom_provider_context_length(model=model, base_url=url) if url and model else None
return route + named + (str(pin or ""),)
def _drop_stale_ceiling(agent: Any) -> None:
"""A warm agent met a compression route other than the one it probed: the ceiling it installed and
any failure cooldown belong to the old summariser, so the trigger returns to the main window and the
cooldown is cleared (in memory and in the session row) before the new probe."""
agent._compression_feasibility_checked = False
compressor = agent.context_compressor
clear_cooldown = getattr(compressor, "_clear_compression_failure_cooldown", None)
if callable(clear_cooldown):
clear_cooldown()
if getattr(compressor, "_aux_context_ceiling", None) is not None:
compressor._aux_context_ceiling = None
compressor.update_model(compressor.model, compressor.context_length, base_url=compressor.base_url,
api_key=compressor.api_key, provider=compressor.provider,
api_mode=compressor.api_mode)
def revalidate_compression_feasibility(agent: Any) -> None:
"""Re-run the aux feasibility probe after the main runtime changed (model switch, fallback activation,
primary restore). ``update_model()`` already voided the previous ceiling; probing now clamps the trigger
before the first compaction on the new window rather than after it (#114707). A probe failure leaves the
latch unset so the lazy probe at the next compaction re-raises hard rejections."""
agent._compression_feasibility_checked = False
if not getattr(agent, "context_compressor", None):
return
route = compression_route_identity()
try:
_probe(agent)
except Exception as exc:
logger.debug("Compression feasibility re-check deferred to the next compaction: %s", exc, exc_info=True)
return
_mark_checked(agent, route)
def ensure_compression_feasibility_checked(agent: Any, estimated_tokens: int) -> None:
"""Run the deferred aux feasibility probe once a request first reaches ``MINIMUM_CONTEXT_LENGTH`` — the
smallest window any summariser may have — so an aux clamp lands before the first compaction fires on the
main-window threshold instead of after it (#114707). Below that size no summariser can be too small, so
short sessions keep the probe-free cold start (#28957). A probe failure leaves the latch unset for the
lazy probe in ``compress_context`` to re-raise hard rejections. A warm agent whose compression route moved
since its last probe (side tasks follow the profile's config.yaml) probes again."""
if not getattr(agent, "context_compressor", None):
return
route = compression_route_identity()
if getattr(agent, "_compression_feasibility_checked", False):
probed = getattr(agent, "_compression_feasibility_route", None)
if probed is None or route == probed:
return
_drop_stale_ceiling(agent)
from agent.model_metadata import MINIMUM_CONTEXT_LENGTH
if int(estimated_tokens or 0) < MINIMUM_CONTEXT_LENGTH:
return
try:
_probe(agent)
except Exception as exc:
logger.debug("Compression feasibility probe deferred to the first compaction: %s", exc, exc_info=True)
return
_mark_checked(agent, route)
def lazy_compression_feasibility_check(agent: Any) -> None:
"""The probe ``compress_context`` runs when nothing probed yet, or the route moved since the probe
that recorded one; a hard rejection propagates to the caller. A latch with no recorded route (set
before route tracking, or by a caller marking the agent checked) is trusted as it always was."""
route = compression_route_identity()
if getattr(agent, "_compression_feasibility_checked", False):
probed = getattr(agent, "_compression_feasibility_route", None)
if probed is None or route == probed:
return
_drop_stale_ceiling(agent)
_probe(agent)
_mark_checked(agent, route)