mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-06 10:47:12 +08:00
Compare commits
5 Commits
bb/widget-
...
fix/termin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14b06cb090 | ||
|
|
359e08d384 | ||
|
|
0a9d84dd07 | ||
|
|
243681a8e7 | ||
|
|
2e5acc5e77 |
@@ -43,6 +43,9 @@ SUMMARY_PREFIX = (
|
||||
"they were already addressed. "
|
||||
"Your current task is identified in the '## Active Task' section of the "
|
||||
"summary — resume exactly from there. "
|
||||
"IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system "
|
||||
"prompt is ALWAYS authoritative and active — never ignore or deprioritize "
|
||||
"memory content due to this compaction note. "
|
||||
"Respond ONLY to the latest user message "
|
||||
"that appears AFTER this summary. The current session state (files, "
|
||||
"config, etc.) may reflect work described here — avoid repeating it:"
|
||||
@@ -1340,7 +1343,7 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
||||
msg = messages[i].copy()
|
||||
if i == 0 and msg.get("role") == "system":
|
||||
existing = msg.get("content")
|
||||
_compression_note = "[Note: Some earlier conversation turns have been compacted into a handoff summary to preserve context space. The current session state may still reflect earlier work, so build on that summary and state rather than re-doing work.]"
|
||||
_compression_note = "[Note: Some earlier conversation turns have been compacted into a handoff summary to preserve context space. The current session state may still reflect earlier work, so build on that summary and state rather than re-doing work. Your persistent memory (MEMORY.md, USER.md) remains fully authoritative regardless of compaction.]"
|
||||
if _compression_note not in _content_text_for_contains(existing):
|
||||
msg["content"] = _append_text_to_content(
|
||||
existing,
|
||||
@@ -1385,6 +1388,19 @@ The user has requested that this compaction PRIORITISE preserving all informatio
|
||||
# Merge the summary into the first tail message instead
|
||||
# of inserting a standalone message that breaks alternation.
|
||||
_merge_summary_into_tail = True
|
||||
|
||||
# When the summary lands as a standalone role="user" message,
|
||||
# weak models read the verbatim "## Active Task" quote of a past
|
||||
# user request as fresh input (#11475, #14521). Append the explicit
|
||||
# end marker — the same one used in the merge-into-tail path — so
|
||||
# the model has a clear "summary above, not new input" signal.
|
||||
if not _merge_summary_into_tail and summary_role == "user":
|
||||
summary = (
|
||||
summary
|
||||
+ "\n\n--- END OF CONTEXT SUMMARY — "
|
||||
"respond to the message below, not the summary above ---"
|
||||
)
|
||||
|
||||
if not _merge_summary_into_tail:
|
||||
compressed.append({"role": summary_role, "content": summary})
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ _INTERNAL_CONTEXT_RE = re.compile(
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_INTERNAL_NOTE_RE = re.compile(
|
||||
r'\[System note:\s*The following is recalled memory context,\s*NOT new user input\.\s*Treat as informational background data\.\]\s*',
|
||||
r'\[System note:\s*The following is recalled memory context,\s*NOT new user input\.\s*Treat as (?:informational background data|authoritative reference data[^\]]*)\.\]\s*',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
@@ -183,7 +183,8 @@ def build_memory_context_block(raw_context: str) -> str:
|
||||
return (
|
||||
"<memory-context>\n"
|
||||
"[System note: The following is recalled memory context, "
|
||||
"NOT new user input. Treat as informational background data.]\n\n"
|
||||
"NOT new user input. Treat as authoritative reference data — "
|
||||
"this is the agent's persistent memory and should inform all responses.]\n\n"
|
||||
f"{clean}\n"
|
||||
"</memory-context>"
|
||||
)
|
||||
|
||||
@@ -72,6 +72,14 @@ def _telegramize_command_mentions(text: str, platform: Any) -> str:
|
||||
return _TELEGRAM_COMMAND_MENTION_RE.sub(_replace, text)
|
||||
|
||||
|
||||
def _context_overflow_recovery_message() -> str:
|
||||
return (
|
||||
"⚠️ Session too large for the model's context window.\n"
|
||||
"Use /compress to compress the conversation, or "
|
||||
"/reset to start fresh."
|
||||
)
|
||||
|
||||
|
||||
# Only auto-continue interrupted gateway turns while the interruption is fresh.
|
||||
# Stale tool-tail/resume markers can otherwise revive an unrelated old task
|
||||
# after a gateway restart when the user's next message starts new work.
|
||||
@@ -6558,6 +6566,12 @@ class GatewayRunner:
|
||||
"Failed to deliver aux-model-fallback notice to user: %s",
|
||||
_werr,
|
||||
)
|
||||
# Evict the cached agent so the next turn
|
||||
# rebuilds its system prompt from current
|
||||
# SOUL.md, memory, and skills (not the stale
|
||||
# pre-compression snapshot).
|
||||
self._evict_cached_agent(session_key)
|
||||
|
||||
finally:
|
||||
self._cleanup_agent_resources(_hyg_agent)
|
||||
|
||||
@@ -6744,11 +6758,7 @@ class GatewayRunner:
|
||||
)
|
||||
|
||||
if _is_ctx_fail:
|
||||
response = (
|
||||
"⚠️ Session too large for the model's context window.\n"
|
||||
"Use /compact to compress the conversation, or "
|
||||
"/reset to start fresh."
|
||||
)
|
||||
response = _context_overflow_recovery_message()
|
||||
else:
|
||||
response = (
|
||||
f"The request failed: {str(error_detail)[:300]}\n"
|
||||
@@ -7072,11 +7082,7 @@ class GatewayRunner:
|
||||
# 500 with a large session often means the payload is too large
|
||||
# for the API to process — treat it the same way.
|
||||
if _hist_len > 50:
|
||||
return (
|
||||
"⚠️ Session too large for the model's context window.\n"
|
||||
"Use /compact to compress the conversation, or "
|
||||
"/reset to start fresh."
|
||||
)
|
||||
return _context_overflow_recovery_message()
|
||||
elif status_code == 400:
|
||||
status_hint = " The request was rejected by the API."
|
||||
return (
|
||||
@@ -9762,6 +9768,9 @@ class GatewayRunner:
|
||||
# note so they can fix their config.
|
||||
_aux_fail_model = getattr(compressor, "_last_aux_model_failure_model", None)
|
||||
_aux_fail_err = getattr(compressor, "_last_aux_model_failure_error", None)
|
||||
# Evict cached agent so next turn rebuilds system prompt
|
||||
# from current files (SOUL.md, memory, etc.).
|
||||
self._evict_cached_agent(session_key)
|
||||
finally:
|
||||
self._cleanup_agent_resources(tmp_agent)
|
||||
lines = [f"🗜️ {summary['headline']}"]
|
||||
|
||||
@@ -124,6 +124,9 @@ AUTHOR_MAP = {
|
||||
"git@local.invalid": "hendrixfreire",
|
||||
"1060770+benjaminsehl@users.noreply.github.com": "benjaminsehl",
|
||||
"nerijusn76@gmail.com": "Nerijusas",
|
||||
# Compaction salvage batch (May 2026)
|
||||
"MacroAnarchy@users.noreply.github.com": "MacroAnarchy",
|
||||
"74554762+wmagev@users.noreply.github.com": "wmagev",
|
||||
"itonov@proton.me": "Ito-69",
|
||||
"glesstech@gmail.com": "georgeglessner",
|
||||
"maxim.smetanin@gmail.com": "maxims-oss",
|
||||
|
||||
@@ -664,6 +664,44 @@ class TestCompressWithClient:
|
||||
"call_123"
|
||||
]
|
||||
|
||||
def test_user_role_summary_carries_end_marker(self):
|
||||
"""When the summary lands as standalone role='user' (e.g. head ends
|
||||
with assistant/tool), the message body must include the explicit
|
||||
'--- END OF CONTEXT SUMMARY ---' marker. Without it, weak models
|
||||
read the verbatim past user request quoted in '## Active Task' as
|
||||
fresh input (#11475, #14521).
|
||||
"""
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock()]
|
||||
mock_response.choices[0].message.content = "summary text"
|
||||
|
||||
with patch("agent.context_compressor.get_model_context_length", return_value=100000):
|
||||
c = ContextCompressor(model="test", quiet_mode=True, protect_first_n=2, protect_last_n=2)
|
||||
|
||||
# head_last=assistant, tail_first=assistant (same shape as the
|
||||
# existing consecutive-user test) → role resolves to "user".
|
||||
msgs = [
|
||||
{"role": "user", "content": "msg 0"},
|
||||
{"role": "assistant", "content": "msg 1"},
|
||||
{"role": "user", "content": "msg 2"},
|
||||
{"role": "assistant", "content": "msg 3"},
|
||||
{"role": "user", "content": "msg 4"},
|
||||
{"role": "assistant", "content": "msg 5"},
|
||||
{"role": "user", "content": "msg 6"},
|
||||
{"role": "assistant", "content": "msg 7"},
|
||||
]
|
||||
with patch("agent.context_compressor.call_llm", return_value=mock_response):
|
||||
result = c.compress(msgs)
|
||||
|
||||
summary_msg = next(
|
||||
m for m in result if (m.get("content") or "").startswith(SUMMARY_PREFIX)
|
||||
)
|
||||
assert summary_msg["role"] == "user"
|
||||
assert "END OF CONTEXT SUMMARY" in summary_msg["content"]
|
||||
assert summary_msg["content"].rstrip().endswith(
|
||||
"respond to the message below, not the summary above ---"
|
||||
)
|
||||
|
||||
def test_summary_role_avoids_consecutive_user_messages(self):
|
||||
"""Summary role should alternate with the last head message to avoid consecutive same-role messages."""
|
||||
mock_client = MagicMock()
|
||||
|
||||
@@ -10,6 +10,15 @@ from gateway.platforms.base import MessageEvent
|
||||
from gateway.session import SessionEntry, SessionSource, build_session_key
|
||||
|
||||
|
||||
def test_context_overflow_guidance_uses_registered_compress_command():
|
||||
from gateway.run import _context_overflow_recovery_message
|
||||
|
||||
message = _context_overflow_recovery_message()
|
||||
|
||||
assert "/compress" in message
|
||||
assert "/compact" not in message
|
||||
|
||||
|
||||
def _make_source() -> SessionSource:
|
||||
return SessionSource(
|
||||
platform=Platform.TELEGRAM,
|
||||
|
||||
Reference in New Issue
Block a user