mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(cli): preserve noted staged input on close
This commit is contained in:
parent
475922f2ce
commit
a22a1079a3
4 changed files with 158 additions and 1 deletions
9
cli.py
9
cli.py
|
|
@ -12365,13 +12365,20 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
self._pending_moa_config = None
|
||||
if _moa_cfg is None:
|
||||
_moa_cfg = None
|
||||
# Model/skill notes and voice instructions are API-local. Keep
|
||||
# the original staged input as the durable transcript value so a
|
||||
# close-path marker follows the same dict into turn setup rather
|
||||
# than producing a second noted user row (#63766).
|
||||
_persist_clean_user_message = (
|
||||
message if (_voice_prefix or agent_message != message) else None
|
||||
)
|
||||
try:
|
||||
result = self.agent.run_conversation(
|
||||
user_message=agent_message,
|
||||
conversation_history=self.conversation_history[:-1], # Exclude the message we just added
|
||||
stream_callback=stream_callback,
|
||||
task_id=self.session_id,
|
||||
persist_user_message=message if _voice_prefix else None,
|
||||
persist_user_message=_persist_clean_user_message,
|
||||
moa_config=_moa_cfg,
|
||||
)
|
||||
if getattr(self, "_pending_moa_disable_after_turn", False):
|
||||
|
|
|
|||
|
|
@ -241,6 +241,24 @@ def test_stale_pending_cli_message_does_not_replace_new_turn_input():
|
|||
assert agent._pending_cli_user_message is None
|
||||
|
||||
|
||||
def test_pending_cli_message_uses_clean_override_for_api_local_note():
|
||||
"""A noted API message reuses the clean staged dict and its DB marker."""
|
||||
agent = _FakeAgent()
|
||||
staged = {"role": "user", "content": "clean prompt", "_db_persisted": True}
|
||||
agent._pending_cli_user_message = staged
|
||||
|
||||
ctx = _build(
|
||||
agent,
|
||||
user_message="[MODEL NOTE]\n\nclean prompt",
|
||||
persist_user_message="clean prompt",
|
||||
)
|
||||
|
||||
assert ctx.messages[-1] is staged
|
||||
assert ctx.messages[-1]["content"] == "[MODEL NOTE]\n\nclean prompt"
|
||||
assert ctx.messages[-1]["_db_persisted"] is True
|
||||
assert agent._pending_cli_user_message is None
|
||||
|
||||
|
||||
def test_memory_nudge_fires_at_interval():
|
||||
agent = _FakeAgent()
|
||||
agent._memory_nudge_interval = 1
|
||||
|
|
|
|||
|
|
@ -192,3 +192,42 @@ def test_acknowledged_interrupt_still_requeues_message():
|
|||
queued.append(cli._pending_input.get_nowait())
|
||||
assert any("redirect please" in str(q) for q in queued)
|
||||
assert cli._last_turn_interrupted is True
|
||||
|
||||
|
||||
def test_chat_persists_clean_input_when_a_queued_note_changes_api_message():
|
||||
"""Queued notes remain API-local and preserve close-handoff marker identity."""
|
||||
cli = _make_cli()
|
||||
|
||||
class _NoteAgent(_StubAgent):
|
||||
def __init__(self, session_id):
|
||||
super().__init__(session_id, turn_seconds=0)
|
||||
self.captured = None
|
||||
|
||||
def run_conversation(self, **kwargs):
|
||||
self.captured = kwargs
|
||||
return {
|
||||
"final_response": "done",
|
||||
"messages": [{"role": "assistant", "content": "done"}],
|
||||
"api_calls": 1,
|
||||
"completed": True,
|
||||
"partial": True,
|
||||
"response_previewed": True,
|
||||
}
|
||||
|
||||
agent = _NoteAgent(cli.session_id)
|
||||
cli.agent = agent
|
||||
cli._interrupt_queue = queue.Queue()
|
||||
cli._pending_input = queue.Queue()
|
||||
cli._pending_model_switch_note = "[MODEL SWITCH NOTE]"
|
||||
|
||||
with patch.object(cli, "_ensure_runtime_credentials", return_value=True), \
|
||||
patch.object(cli, "_resolve_turn_agent_config", return_value={
|
||||
"signature": cli._active_agent_route_signature,
|
||||
"model": None, "runtime": None, "request_overrides": None,
|
||||
}), \
|
||||
patch.object(cli, "_init_agent", return_value=True):
|
||||
cli.chat("clean prompt")
|
||||
|
||||
assert agent.captured is not None
|
||||
assert agent.captured["user_message"] == "[MODEL SWITCH NOTE]\n\nclean prompt"
|
||||
assert agent.captured["persist_user_message"] == "clean prompt"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ other tests keep their existing no-arg behaviour.
|
|||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import types
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
|
@ -491,6 +492,98 @@ def test_cli_close_persists_pending_user_when_agent_snapshot_is_empty(tmp_path,
|
|||
assert staged["_db_persisted"] is True
|
||||
|
||||
|
||||
def test_cli_close_preserves_clean_staged_user_across_noted_worker_turn(tmp_path, monkeypatch):
|
||||
"""A noted API-only turn reuses the close-marked clean staged user row."""
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||
|
||||
import cli as cli_mod
|
||||
from hermes_state import SessionDB
|
||||
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
session_id = "cli-close-noted-staged-user"
|
||||
db.create_session(session_id=session_id, source="cli")
|
||||
prefix = [
|
||||
{"role": "user", "content": "old prompt"},
|
||||
{"role": "assistant", "content": "old answer"},
|
||||
]
|
||||
agent = _real_agent(db, session_id, prefix)
|
||||
agent._flush_messages_to_session_db(prefix, [])
|
||||
staged = {"role": "user", "content": "new prompt"}
|
||||
agent._pending_cli_user_message = staged
|
||||
|
||||
cli = object.__new__(cli_mod.HermesCLI)
|
||||
cli.conversation_history = list(prefix) + [staged]
|
||||
cli.session_id = session_id
|
||||
cli.agent = agent
|
||||
|
||||
cli._persist_active_session_before_close()
|
||||
assert staged["_db_persisted"] is True
|
||||
|
||||
# A queued model/skills note changes only the API message. The worker
|
||||
# reuses the marked clean dict, so the normal persistence seam cannot append
|
||||
# a second noted user row.
|
||||
from agent.turn_context import build_turn_context
|
||||
|
||||
agent.quiet_mode = True
|
||||
agent.max_iterations = 1
|
||||
agent.provider = "test"
|
||||
agent.base_url = ""
|
||||
agent.api_key = ""
|
||||
agent.api_mode = "chat_completions"
|
||||
agent.tools = []
|
||||
agent.valid_tool_names = set()
|
||||
agent.enabled_toolsets = None
|
||||
agent.disabled_toolsets = None
|
||||
agent._skip_mcp_refresh = True
|
||||
agent.compression_enabled = False
|
||||
agent.context_compressor = types.SimpleNamespace(protect_first_n=2, protect_last_n=2)
|
||||
agent._memory_store = None
|
||||
agent._memory_manager = None
|
||||
agent._memory_nudge_interval = 0
|
||||
agent._turns_since_memory = 0
|
||||
agent._user_turn_count = 0
|
||||
agent._todo_store = types.SimpleNamespace(has_items=lambda: True)
|
||||
agent._tool_guardrails = types.SimpleNamespace(reset_for_turn=lambda: None)
|
||||
agent._compression_warning = None
|
||||
agent._interrupt_requested = False
|
||||
agent._memory_write_origin = "assistant_tool"
|
||||
agent._stream_context_scrubber = None
|
||||
agent._stream_think_scrubber = None
|
||||
agent._restore_primary_runtime = lambda: None
|
||||
agent._cleanup_dead_connections = lambda: False
|
||||
agent._emit_status = lambda _message: None
|
||||
agent._replay_compression_warning = lambda: None
|
||||
agent._hydrate_todo_store = lambda *_args: None
|
||||
agent._safe_print = lambda *_args: None
|
||||
|
||||
worker = build_turn_context(
|
||||
agent,
|
||||
"[MODEL SWITCH NOTE]\n\nnew prompt",
|
||||
None,
|
||||
prefix,
|
||||
"task",
|
||||
None,
|
||||
"new prompt",
|
||||
None,
|
||||
restore_or_build_system_prompt=lambda *_args: None,
|
||||
install_safe_stdio=lambda: None,
|
||||
sanitize_surrogates=lambda value: value,
|
||||
summarize_user_message_for_log=lambda value: value,
|
||||
set_session_context=lambda _session_id: None,
|
||||
set_current_write_origin=lambda _origin: None,
|
||||
ra=lambda: types.SimpleNamespace(_set_interrupt=lambda *_args: None),
|
||||
)
|
||||
assert worker.messages[-1] is staged
|
||||
assert worker.messages[-1]["content"] == "[MODEL SWITCH NOTE]\n\nnew prompt"
|
||||
|
||||
stored = db.get_messages_as_conversation(session_id)
|
||||
assert [m["content"] for m in stored] == [
|
||||
"old prompt",
|
||||
"old answer",
|
||||
"new prompt",
|
||||
]
|
||||
|
||||
|
||||
def test_cli_close_builds_prompt_before_creating_first_session_row(tmp_path, monkeypatch):
|
||||
"""First-turn close persistence must not leave a NULL prompt snapshot."""
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue