mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(session): restore clean API-local turn content
This commit is contained in:
parent
32bdc67e10
commit
8341d775a9
4 changed files with 115 additions and 9 deletions
|
|
@ -228,6 +228,15 @@ def finalize_turn(
|
|||
if _tail_role != "assistant":
|
||||
messages.append({"role": "assistant", "content": final_response})
|
||||
|
||||
# The model has completed its request, so replace API-local
|
||||
# voice/model/skill guidance with the clean user input before writing the
|
||||
# final durable snapshot and returning the continuation history. Earlier
|
||||
# turn-start flushes use the DB-only override because their messages are
|
||||
# still needed for the API request; this finalizer runs after that request
|
||||
# is complete (#48677 / #63766).
|
||||
_apply_override = getattr(agent, "_apply_persist_user_message_override", None)
|
||||
if callable(_apply_override):
|
||||
_apply_override(messages)
|
||||
agent._persist_session(messages, conversation_history)
|
||||
except Exception as _persist_err:
|
||||
_cleanup_errors.append(f"persist_session: {_persist_err}")
|
||||
|
|
|
|||
16
run_agent.py
16
run_agent.py
|
|
@ -1663,14 +1663,14 @@ class AIAgent:
|
|||
msg = messages[idx]
|
||||
if isinstance(msg, dict) and msg.get("role") == "user":
|
||||
# Text-only call paths may pass a synthetic API-facing prompt
|
||||
# and a cleaner transcript string separately. Multimodal
|
||||
# turns, however, keep image/audio blocks in the live
|
||||
# messages list that is still used for the API request after
|
||||
# early crash-resilience persistence. Do not replace those
|
||||
# blocks with the text-only persistence override before the
|
||||
# model call is built. The paired timestamp override still
|
||||
# applies — it is metadata, not content.
|
||||
if override is not None and not isinstance(msg.get("content"), list):
|
||||
# and a cleaner transcript string separately. Before the API
|
||||
# call, a plain-text override must not replace native image/audio
|
||||
# blocks. A list override, however, is the original clean
|
||||
# multimodal payload (for example before a queued /model note)
|
||||
# and must replace the API-local list once the turn is final.
|
||||
if override is not None and (
|
||||
not isinstance(msg.get("content"), list) or isinstance(override, list)
|
||||
):
|
||||
msg["content"] = override
|
||||
if timestamp is not None:
|
||||
msg["timestamp"] = timestamp
|
||||
|
|
|
|||
|
|
@ -51,7 +51,14 @@ class FakeAgent:
|
|||
pass
|
||||
|
||||
def _persist_session(self, messages, conversation_history):
|
||||
self.persisted_messages = list(messages)
|
||||
# Capture the durable write before finalization restores API-local
|
||||
# guidance to the returned/live transcript.
|
||||
self.persisted_messages = [dict(message) for message in messages]
|
||||
|
||||
def _apply_persist_user_message_override(self, messages):
|
||||
from run_agent import AIAgent
|
||||
|
||||
return AIAgent._apply_persist_user_message_override(self, messages)
|
||||
|
||||
def _file_mutation_verifier_enabled(self):
|
||||
return False
|
||||
|
|
@ -69,6 +76,78 @@ class FakeAgent:
|
|||
pass
|
||||
|
||||
|
||||
def test_finalizer_restores_clean_api_local_text_before_return(monkeypatch):
|
||||
"""One-shot CLI notes do not replay through same-process history."""
|
||||
monkeypatch.setattr("hermes_cli.plugins.invoke_hook", lambda *_a, **_kw: [])
|
||||
agent = FakeAgent()
|
||||
messages = [
|
||||
{"role": "user", "content": "[MODEL SWITCH NOTE]\n\nclean prompt"},
|
||||
{"role": "assistant", "content": "Done."},
|
||||
]
|
||||
agent._persist_user_message_idx = 0
|
||||
agent._persist_user_message_override = "clean prompt"
|
||||
agent._persist_user_message_timestamp = None
|
||||
|
||||
result = finalize_turn(
|
||||
agent,
|
||||
final_response="Done.",
|
||||
api_call_count=1,
|
||||
interrupted=False,
|
||||
failed=False,
|
||||
messages=messages,
|
||||
conversation_history=[],
|
||||
effective_task_id="task",
|
||||
turn_id="turn",
|
||||
user_message="[MODEL SWITCH NOTE]\n\nclean prompt",
|
||||
original_user_message="clean prompt",
|
||||
_should_review_memory=False,
|
||||
_turn_exit_reason="text_response(finish_reason=stop)",
|
||||
)
|
||||
|
||||
assert agent.persisted_messages[0]["content"] == "clean prompt"
|
||||
assert result["messages"][0]["content"] == "clean prompt"
|
||||
|
||||
|
||||
def test_finalizer_restores_clean_api_local_multimodal_before_return(monkeypatch):
|
||||
"""A queued note does not remain in the next-turn native image payload."""
|
||||
monkeypatch.setattr("hermes_cli.plugins.invoke_hook", lambda *_a, **_kw: [])
|
||||
agent = FakeAgent()
|
||||
clean_content = [
|
||||
{"type": "text", "text": "Describe the image"},
|
||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}},
|
||||
]
|
||||
api_content = [
|
||||
{"type": "text", "text": "[MODEL SWITCH NOTE]\n\nDescribe the image"},
|
||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}},
|
||||
]
|
||||
messages = [
|
||||
{"role": "user", "content": api_content},
|
||||
{"role": "assistant", "content": "Done."},
|
||||
]
|
||||
agent._persist_user_message_idx = 0
|
||||
agent._persist_user_message_override = clean_content
|
||||
agent._persist_user_message_timestamp = None
|
||||
|
||||
result = finalize_turn(
|
||||
agent,
|
||||
final_response="Done.",
|
||||
api_call_count=1,
|
||||
interrupted=False,
|
||||
failed=False,
|
||||
messages=messages,
|
||||
conversation_history=[],
|
||||
effective_task_id="task",
|
||||
turn_id="turn",
|
||||
user_message=api_content,
|
||||
original_user_message=clean_content,
|
||||
_should_review_memory=False,
|
||||
_turn_exit_reason="text_response(finish_reason=stop)",
|
||||
)
|
||||
|
||||
assert agent.persisted_messages[0]["content"] == clean_content
|
||||
assert result["messages"][0]["content"] == clean_content
|
||||
|
||||
|
||||
def test_final_response_closes_tool_tail_before_persistence(monkeypatch):
|
||||
"""A recovered/previewed final response must be durable in session history.
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,24 @@ def test_persist_user_message_override_preserves_multimodal_turns(agent):
|
|||
assert messages == [{"role": "user", "content": multimodal_content}]
|
||||
|
||||
|
||||
def test_persist_user_message_override_restores_clean_multimodal_note(agent):
|
||||
clean_content = [
|
||||
{"type": "text", "text": "Describe this screenshot"},
|
||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}},
|
||||
]
|
||||
api_content = [
|
||||
{"type": "text", "text": "[MODEL SWITCH NOTE]\n\nDescribe this screenshot"},
|
||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}},
|
||||
]
|
||||
messages = [{"role": "user", "content": api_content}]
|
||||
agent._persist_user_message_idx = 0
|
||||
agent._persist_user_message_override = clean_content
|
||||
|
||||
agent._apply_persist_user_message_override(messages)
|
||||
|
||||
assert messages == [{"role": "user", "content": clean_content}]
|
||||
|
||||
|
||||
def test_flush_persist_override_replaces_api_local_multimodal_note(agent):
|
||||
"""A note-added multimodal API payload stores the original clean content."""
|
||||
clean_content = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue