mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-26 11:40:26 +00:00
Merge pull request #2915
This commit is contained in:
+49
-4
@@ -5060,11 +5060,56 @@ def _run_agent_streaming(
|
||||
# Notify the frontend that compression happened
|
||||
if _compressed:
|
||||
visible_after = visible_messages_for_anchor(s.messages, auto_compression=True)
|
||||
s.compression_anchor_visible_idx = (
|
||||
max(0, len(visible_after) - 1) if visible_after else None
|
||||
)
|
||||
# Find the LAST [CONTEXT COMPACTION] marker in s.messages
|
||||
# and count visible messages before it. This is the correct
|
||||
# anchor — it points to the compression boundary regardless
|
||||
# of how many turns have been added since the boundary was
|
||||
# established. Using len(visible_before)-1 is fragile when
|
||||
# _previous_messages doesn't include markers or when extra
|
||||
# messages accumulate between compression and the done event.
|
||||
_last_marker_raw_idx = None
|
||||
for _mi, _m in enumerate(s.messages):
|
||||
if _is_context_compression_marker(_m):
|
||||
_last_marker_raw_idx = _mi
|
||||
if _last_marker_raw_idx is not None:
|
||||
_visible_before_marker = visible_messages_for_anchor(
|
||||
s.messages[:_last_marker_raw_idx], auto_compression=True,
|
||||
)
|
||||
s.compression_anchor_visible_idx = max(0, len(_visible_before_marker) - 1)
|
||||
logger.info(
|
||||
'[ANCHOR-MARKER] session=%s marker_raw=%d vis_before=%d anchor=%d',
|
||||
getattr(s, 'session_id', '?'),
|
||||
_last_marker_raw_idx,
|
||||
len(_visible_before_marker),
|
||||
s.compression_anchor_visible_idx,
|
||||
)
|
||||
else:
|
||||
# Fallback: use pre-turn display messages
|
||||
visible_before = visible_messages_for_anchor(
|
||||
_previous_messages, auto_compression=True,
|
||||
)
|
||||
if visible_before:
|
||||
s.compression_anchor_visible_idx = max(0, len(visible_before) - 1)
|
||||
elif visible_after:
|
||||
s.compression_anchor_visible_idx = 0
|
||||
else:
|
||||
s.compression_anchor_visible_idx = None
|
||||
logger.info(
|
||||
'[ANCHOR-FALLBACK] session=%s vis_before=%d anchor=%d',
|
||||
getattr(s, 'session_id', '?'),
|
||||
len(visible_before) if visible_before else 0,
|
||||
s.compression_anchor_visible_idx if s.compression_anchor_visible_idx is not None else -1,
|
||||
)
|
||||
# Pick anchor_msg for _compression_anchor_message_key
|
||||
_anchor_vis_idx = s.compression_anchor_visible_idx
|
||||
if _anchor_vis_idx is not None and visible_after and _anchor_vis_idx < len(visible_after):
|
||||
anchor_msg = visible_after[_anchor_vis_idx]
|
||||
elif visible_after:
|
||||
anchor_msg = visible_after[-1]
|
||||
else:
|
||||
anchor_msg = None
|
||||
s.compression_anchor_message_key = (
|
||||
_compression_anchor_message_key(visible_after[-1]) if visible_after else None
|
||||
_compression_anchor_message_key(anchor_msg) if anchor_msg else None
|
||||
)
|
||||
s.compression_anchor_summary = _compact_summary_text(
|
||||
_compression_summary_from_messages(s.messages)
|
||||
|
||||
@@ -97,3 +97,101 @@ def test_compression_summary_ignores_tool_output_that_mentions_compression():
|
||||
|
||||
assert _compression_summary_from_messages([marker, skill_tool_output]) == marker["content"]
|
||||
assert _compression_summary_from_messages([skill_tool_output]) is None
|
||||
|
||||
|
||||
def test_marker_based_anchor_calculation():
|
||||
"""Verify that the marker-based anchor logic used in streaming.py's done
|
||||
handler correctly computes compression_anchor_visible_idx from the last
|
||||
[CONTEXT COMPACTION] marker position. This prevents the reference card
|
||||
from being pushed behind the render window after subsequent turns."""
|
||||
messages = [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "first reply"},
|
||||
{"role": "tool", "content": "tool output"},
|
||||
{"role": "user", "content": "second question"},
|
||||
{"role": "assistant", "content": "second reply"},
|
||||
{"role": "assistant", "content": "[CONTEXT COMPACTION \u2014 REFERENCE ONLY] summary"},
|
||||
{"role": "user", "content": "third question"},
|
||||
{"role": "assistant", "content": "third reply"},
|
||||
{"role": "tool", "content": "more tool output"},
|
||||
{"role": "user", "content": "fourth question"},
|
||||
{"role": "assistant", "content": "fourth reply"},
|
||||
]
|
||||
|
||||
# Simulate the fix logic from streaming.py
|
||||
_last_marker_raw_idx = None
|
||||
for _mi, _m in enumerate(messages):
|
||||
if is_context_compression_marker(_m):
|
||||
_last_marker_raw_idx = _mi
|
||||
assert _last_marker_raw_idx == 5, "expected marker at index 5"
|
||||
|
||||
_visible_before_marker = visible_messages_for_anchor(
|
||||
messages[:_last_marker_raw_idx], auto_compression=True,
|
||||
)
|
||||
anchor = max(0, len(_visible_before_marker) - 1)
|
||||
# Visible before marker: 4 messages (hello, first reply, second question, second reply)
|
||||
# tool at index 2 is filtered out. Anchor = 3 (last visible before marker)
|
||||
assert anchor == 3, f"expected anchor 3, got {anchor}"
|
||||
|
||||
# Verify the anchor points to the right message
|
||||
full_vis = visible_messages_for_anchor(messages, auto_compression=True)
|
||||
# full_vis: hello, first reply, second question, second reply, third question, third reply, fourth question, fourth reply = 8
|
||||
assert len(full_vis) == 8, f"expected 8, got {len(full_vis)}"
|
||||
assert full_vis[anchor]["content"] == "second reply"
|
||||
|
||||
# After additional turns, anchor stays at marker boundary
|
||||
messages_extended = messages + [
|
||||
{"role": "user", "content": "fifth question"},
|
||||
{"role": "assistant", "content": "fifth reply"},
|
||||
{"role": "tool", "content": "hidden"},
|
||||
]
|
||||
full_vis_ext = visible_messages_for_anchor(messages_extended, auto_compression=True)
|
||||
# The anchor should still point to the same position in full_vis_ext
|
||||
assert full_vis_ext[anchor]["content"] == "second reply"
|
||||
assert anchor < len(full_vis_ext) - 3 # anchor is not at the end
|
||||
|
||||
|
||||
def test_marker_based_anchor_multiple_compressions():
|
||||
"""With multiple compression markers, the anchor uses the LAST marker."""
|
||||
messages = [
|
||||
{"role": "user", "content": "A"},
|
||||
{"role": "assistant", "content": "B"},
|
||||
{"role": "assistant", "content": "[CONTEXT COMPACTION \u2014 REFERENCE ONLY] first"},
|
||||
{"role": "user", "content": "C"},
|
||||
{"role": "assistant", "content": "D"},
|
||||
{"role": "assistant", "content": "[CONTEXT COMPACTION \u2014 REFERENCE ONLY] second"},
|
||||
{"role": "user", "content": "E"},
|
||||
{"role": "assistant", "content": "F"},
|
||||
]
|
||||
# Find last marker
|
||||
_last_marker_raw_idx = None
|
||||
for _mi, _m in enumerate(messages):
|
||||
if is_context_compression_marker(_m):
|
||||
_last_marker_raw_idx = _mi
|
||||
assert _last_marker_raw_idx == 5
|
||||
|
||||
_visible_before_marker = visible_messages_for_anchor(
|
||||
messages[:_last_marker_raw_idx], auto_compression=True,
|
||||
)
|
||||
anchor = max(0, len(_visible_before_marker) - 1)
|
||||
# Visible before last marker at raw[5]: messages[0:5] = [A, B, marker, C, D]
|
||||
# After filtering: A, B, C, D = 4 visible. Anchor = 3 (last visible = D)
|
||||
assert anchor == 3, f"expected anchor 3, got {anchor}"
|
||||
assert _visible_before_marker[anchor]["content"] == "D"
|
||||
|
||||
|
||||
def test_marker_based_anchor_fallback_when_no_marker():
|
||||
"""When no compression marker exists, fall back to old behavior."""
|
||||
messages = [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "reply"},
|
||||
]
|
||||
_last_marker_raw_idx = None
|
||||
for _mi, _m in enumerate(messages):
|
||||
if is_context_compression_marker(_m):
|
||||
_last_marker_raw_idx = _mi
|
||||
assert _last_marker_raw_idx is None
|
||||
|
||||
# Should fall through (not crash) - verified by the old path
|
||||
visible_after = visible_messages_for_anchor(messages, auto_compression=True)
|
||||
assert len(visible_after) > 0
|
||||
|
||||
Reference in New Issue
Block a user