mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
test: cover fallback dropped-turn handoff
This commit is contained in:
parent
6dc068ef04
commit
042c1d6bb0
2 changed files with 65 additions and 8 deletions
|
|
@ -79,6 +79,7 @@ _SUMMARY_FAILURE_COOLDOWN_SECONDS = 600
|
|||
# only meant to preserve continuity anchors from the dropped window, not to
|
||||
# become another unbounded transcript copy after the LLM summarizer failed.
|
||||
_FALLBACK_SUMMARY_MAX_CHARS = 8_000
|
||||
_FALLBACK_TURN_MAX_CHARS = 700
|
||||
|
||||
|
||||
_PATH_MENTION_RE = re.compile(r"(?:/|~/?|[A-Za-z]:\\)[^\s`'\")\]}<>]+")
|
||||
|
|
@ -574,8 +575,8 @@ class ContextCompressor(ContextEngine):
|
|||
self.quiet_mode = quiet_mode
|
||||
# When True, summary-generation failure aborts compression entirely
|
||||
# (returns messages unchanged, sets _last_compress_aborted=True).
|
||||
# When False (default = historical behavior), insert a static
|
||||
# "summary unavailable" placeholder and drop the middle window.
|
||||
# When False (default = historical behavior), insert a
|
||||
# deterministic "summary unavailable" handoff and drop the middle window.
|
||||
self.abort_on_summary_failure = abort_on_summary_failure
|
||||
|
||||
self.context_length = get_model_context_length(
|
||||
|
|
@ -941,6 +942,23 @@ class ContextCompressor(ContextEngine):
|
|||
tool_actions: list[str] = []
|
||||
relevant_files: list[str] = []
|
||||
blockers: list[str] = []
|
||||
last_dropped_turns: list[str] = []
|
||||
|
||||
def _compact_fallback_turn(value: Any) -> str:
|
||||
text = redact_sensitive_text(_content_text_for_contains(value))
|
||||
text = re.sub(r"\bgh[pousr]_[A-Za-z0-9_]{8,}\b", "[REDACTED]", text)
|
||||
text = re.sub(r"\s+", " ", text).strip()
|
||||
if len(text) > _FALLBACK_TURN_MAX_CHARS:
|
||||
text = text[: _FALLBACK_TURN_MAX_CHARS - 15].rstrip() + " ...[truncated]"
|
||||
return re.sub(r"\bgh[pousr]_[A-Za-z0-9_.-]+", "[REDACTED]", text)
|
||||
|
||||
def _remember_dropped_turn(label: str, text: str, *, limit: int = 8) -> None:
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return
|
||||
last_dropped_turns.append(f"{label}: {text}")
|
||||
if len(last_dropped_turns) > limit:
|
||||
del last_dropped_turns[0]
|
||||
|
||||
def _collect_paths_from_jsonish(obj: Any) -> None:
|
||||
if isinstance(obj, dict):
|
||||
|
|
@ -972,10 +990,20 @@ class ContextCompressor(ContextEngine):
|
|||
|
||||
for msg in turns_to_summarize:
|
||||
role = msg.get("role", "unknown")
|
||||
text = redact_sensitive_text(
|
||||
_content_text_for_contains(msg.get("content"))
|
||||
).strip()
|
||||
text = _compact_fallback_turn(msg.get("content"))
|
||||
_collect_path_mentions(text, relevant_files)
|
||||
|
||||
turn_text = text
|
||||
turn_tool_names: list[str] = []
|
||||
if role == "assistant" and msg.get("tool_calls"):
|
||||
for tc in msg.get("tool_calls") or []:
|
||||
name, _args = _extract_tool_call_name_and_args(tc)
|
||||
turn_tool_names.append(name)
|
||||
if turn_tool_names:
|
||||
prefix = "tool calls: " + ", ".join(turn_tool_names[:6])
|
||||
turn_text = f"{prefix}; {turn_text}" if turn_text else prefix
|
||||
_remember_dropped_turn(str(role).upper(), turn_text)
|
||||
|
||||
if len(text) > 600:
|
||||
text = text[:420].rstrip() + " ... " + text[-160:].lstrip()
|
||||
|
||||
|
|
@ -1073,6 +1101,9 @@ None recoverable from deterministic fallback.
|
|||
## Remaining Work
|
||||
Continue from the most recent unfulfilled user ask and protected tail messages. Verify state with tools before making claims.
|
||||
|
||||
## Last Dropped Turns
|
||||
{_bullets(last_dropped_turns, limit=8)}
|
||||
|
||||
## Critical Context
|
||||
Summary generation was unavailable, so this is a best-effort deterministic fallback for {len(turns_to_summarize)} compacted message(s).{reason_text}"""
|
||||
summary = self._with_summary_prefix(redact_sensitive_text(body.strip()))
|
||||
|
|
@ -1808,9 +1839,9 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
|||
# True → ABORT compression entirely. Return messages unchanged
|
||||
# and set _last_compress_aborted=True so callers can warn
|
||||
# the user and stop the auto-compress retry loop.
|
||||
# False → Fall through to the legacy fallback path below: insert
|
||||
# a static "summary unavailable" placeholder and drop the
|
||||
# middle window. Records _last_summary_fallback_used /
|
||||
# False → Fall through to the default fallback path below: insert
|
||||
# a deterministic "summary unavailable" handoff and drop
|
||||
# the middle window. Records _last_summary_fallback_used /
|
||||
# _last_summary_dropped_count for gateway hygiene to
|
||||
# surface a warning.
|
||||
# Default is False (historical behavior).
|
||||
|
|
|
|||
|
|
@ -881,6 +881,32 @@ class TestSummaryFailureTrackingForGatewayWarning:
|
|||
assert "/repo/src/pkg/module.py" in fallback
|
||||
assert "C:\\work\\pkg\\module.py" in fallback
|
||||
assert "Traceback" in fallback
|
||||
assert "## Last Dropped Turns" in fallback
|
||||
assert "TOOL: Traceback in /repo/src/pkg/module.py: boom" in fallback
|
||||
|
||||
def test_summary_failure_fallback_preserves_last_dropped_turns_without_tail(self):
|
||||
with patch("agent.context_compressor.get_model_context_length", return_value=100000):
|
||||
c = ContextCompressor(model="test", quiet_mode=True, protect_first_n=1, protect_last_n=1)
|
||||
|
||||
msgs = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "Investigate dropped-window request in /tmp/active.py"},
|
||||
{"role": "assistant", "content": "I inspected /tmp/active.py and found the failing branch"},
|
||||
{"role": "tool", "tool_call_id": "call-old", "content": "ValueError: boom in /tmp/active.py"},
|
||||
{"role": "assistant", "content": "Next step is patching /tmp/active.py"},
|
||||
{"role": "user", "content": "Confirm regression coverage for /tmp/active.py"},
|
||||
{"role": "assistant", "content": "Regression note is ready"},
|
||||
{"role": "user", "content": "protected tail request must not be copied from dropped window"},
|
||||
]
|
||||
|
||||
with patch("agent.context_compressor.call_llm", side_effect=Exception("timeout")):
|
||||
result = c.compress(msgs)
|
||||
|
||||
fallback = next(m["content"] for m in result if "Summary generation was unavailable" in m.get("content", ""))
|
||||
assert "## Last Dropped Turns" in fallback
|
||||
assert "ASSISTANT: I inspected /tmp/active.py and found the failing branch" in fallback
|
||||
assert "TOOL: ValueError: boom in /tmp/active.py" in fallback
|
||||
assert "protected tail request must not be copied" not in fallback
|
||||
|
||||
def test_summary_failure_fallback_is_bounded(self):
|
||||
with patch("agent.context_compressor.get_model_context_length", return_value=100000):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue