mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 13:26:40 +00:00
With compression.prepare_ahead, compress() re-planned the tail cut and threw the completed background summary away whenever the fresh cut landed before the summary's boundary (the no-LLM prune made tail rows cheaper, tool-group alignment moved the cut), then wrote a second summary inline while the user waited. That breaks the one-summary rule: once the background summary exists it is used, and everything after its boundary is the tail. The splice check no longer consults the fresh cut at all. A summary is still discarded only when the messages it covers changed (rewind, shape change, other session, moved head). The pass planned its own window with a full tail, so the tail is never empty. The now-unreachable inside_tail reason leaves the (unreleased) prepared-compaction metric contract.
67 lines
3.6 KiB
Python
67 lines
3.6 KiB
Python
"""Compaction window planning for ContextCompressor: the head/middle/tail boundaries that compress() and
|
|
the opt-in prepared-compaction pass (agent/prepared_compaction.py) both plan, and the splice of a prepared
|
|
summary into that window."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Optional
|
|
|
|
from agent.prepared_compaction import PreparedCompaction, PreparedSummary
|
|
|
|
|
|
class CompactionWindowMixin:
|
|
"""Window planning shared by compress() and the background pass."""
|
|
|
|
# Set by agent_init when compression.prepare_ahead is on; None leaves compress() inline as before.
|
|
prepared_compaction: Optional[PreparedCompaction] = None
|
|
|
|
def _compress_window(self, messages: list[dict[str, Any]]) -> tuple[int, int]:
|
|
"""Return ``(compress_start, compress_end)`` for the summarizable middle."""
|
|
compress_start = self._align_boundary_forward(messages, self._protect_head_size(messages))
|
|
compress_end = self._find_tail_cut_by_tokens(messages, compress_start)
|
|
# A role collision can merge the summary into the first tail row; keep an actionable user
|
|
# event out of that slot by retaining an older assistant/tool bridge.
|
|
latest_actionable_idx = self._find_last_user_message_idx(messages, 0)
|
|
if compress_end == latest_actionable_idx:
|
|
bridge_idx = latest_actionable_idx - 1
|
|
bridge_role = messages[bridge_idx].get("role") if bridge_idx >= 0 else None
|
|
if bridge_role == "tool":
|
|
bridge_idx = self._align_boundary_backward(messages, latest_actionable_idx)
|
|
elif bridge_role != "assistant":
|
|
bridge_idx = -1
|
|
if bridge_idx > compress_start:
|
|
compress_end = bridge_idx
|
|
return compress_start, compress_end
|
|
|
|
def _plan_compaction_window(self, messages: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], int, int, int]:
|
|
"""Phases 1-2 of compress(): ``(working, pruned_count, compress_start, compress_end)``.
|
|
``working`` is a pruned copy without blank echoes; the boundaries index into it."""
|
|
working, pruned_count = self._prune_old_tool_results(
|
|
messages, protect_tail_count=self.protect_last_n, protect_tail_tokens=self.tail_token_budget,
|
|
)
|
|
working = self._drop_blank_echoes(working)
|
|
compress_start, compress_end = self._compress_window(working)
|
|
return working, pruned_count, compress_start, compress_end
|
|
|
|
def _take_prepared_summary(
|
|
self, messages: list[dict[str, Any]], compress_start: int, *, fresh: bool, provider_context: bool,
|
|
) -> Optional[PreparedSummary]:
|
|
"""The summary prepared in the background for this window, which an automatic compaction splices at
|
|
ITS boundary; every newer message stays outside the summary, in the tail. ``fresh`` compactions
|
|
(manual, focus, overflow recovery, memory-provider context) summarize anew, and once a memory
|
|
provider supplies context no further summaries are prepared (each would be thrown away)."""
|
|
if self.prepared_compaction is None:
|
|
return None
|
|
if provider_context:
|
|
self.prepared_compaction.stop_for_provider_context()
|
|
return self.prepared_compaction.take(
|
|
messages, self._session_id, compress_start, eligible=not (fresh or provider_context),
|
|
)
|
|
|
|
def _adopt_prepared_summary(self, prepared: PreparedSummary) -> str:
|
|
"""Adopt a prepared summary as an inline summary call would have left it; returns the handoff text."""
|
|
from agent.prepared_compaction import adopt
|
|
|
|
adopt(self, prepared)
|
|
return prepared.summary
|