mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
The auto-continue recovery note was typed only after run_conversation returned, so its row sat untyped for the whole turn — and permanently when the continuation was itself killed, which is the case it exists for. persist_user_display_kind stamps the type on the live message before the crash persist writes it, in the same insert as the content. The flush also carries display_metadata through, which it was dropping.
106 lines
3.4 KiB
Python
106 lines
3.4 KiB
Python
"""A synthesized turn's row is typed when it is WRITTEN, not when it ends.
|
|
|
|
Synthetic user turns (the crash-recovery note, delegation completions) are
|
|
model-facing scaffolding that must read as a timeline event, never as a user
|
|
bubble. The type used to be stamped only after ``run_conversation`` returned,
|
|
which left two holes:
|
|
|
|
* the row sits untyped for the whole turn, so any surface reading the
|
|
transcript mid-turn paints the raw ``[System note: …]`` text as if the user
|
|
had typed it;
|
|
* a turn killed before it concludes never reaches the stamp at all — and the
|
|
auto-continue turn is precisely the one that gets killed twice, so its note
|
|
stays a user bubble forever.
|
|
|
|
``persist_user_display_kind`` moves the typing to turn start, where the
|
|
crash-resilience persist writes it in the same insert as the content.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
import tempfile
|
|
import types
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from agent.turn_context import build_turn_context
|
|
from hermes_state import SessionDB
|
|
from run_agent import AIAgent
|
|
|
|
NOTE = "[System note: Your previous turn was interrupted mid-run …]\n\nkeep going"
|
|
|
|
|
|
@pytest.fixture()
|
|
def agent_db():
|
|
tmp = tempfile.mkdtemp(prefix="synthetic_display_kind_")
|
|
db = SessionDB(Path(tmp) / "state.db")
|
|
sid = "sess-synthetic"
|
|
db.create_session(session_id=sid, source="desktop", model="test-model")
|
|
agent = AIAgent(
|
|
api_key="test-key",
|
|
base_url="https://openrouter.ai/api/v1",
|
|
quiet_mode=True,
|
|
skip_context_files=True,
|
|
skip_memory=True,
|
|
session_db=db,
|
|
session_id=sid,
|
|
)
|
|
agent._session_db_created = True
|
|
agent._cached_system_prompt = "SYSTEM"
|
|
agent._skip_mcp_refresh = True
|
|
try:
|
|
yield agent, db, sid
|
|
finally:
|
|
db.close()
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
|
|
def _build(agent, **overrides):
|
|
kwargs = dict(
|
|
agent=agent,
|
|
user_message=NOTE,
|
|
system_message=None,
|
|
conversation_history=None,
|
|
task_id=None,
|
|
stream_callback=None,
|
|
persist_user_message=None,
|
|
restore_or_build_system_prompt=lambda *a, **k: None,
|
|
install_safe_stdio=lambda: None,
|
|
sanitize_surrogates=lambda s: s,
|
|
summarize_user_message_for_log=lambda s: s,
|
|
set_session_context=lambda _sid: None,
|
|
set_current_write_origin=lambda _o: None,
|
|
ra=lambda: types.SimpleNamespace(_set_interrupt=lambda *a, **k: None),
|
|
)
|
|
kwargs.update(overrides)
|
|
with patch("agent.auxiliary_client.set_runtime_main", lambda *a, **k: None):
|
|
return build_turn_context(**kwargs)
|
|
|
|
|
|
def test_row_is_typed_by_the_turn_start_persist(agent_db):
|
|
agent, db, sid = agent_db
|
|
|
|
_build(
|
|
agent,
|
|
persist_user_display_kind="auto_continue",
|
|
persist_user_display_metadata={"attempt": 2},
|
|
)
|
|
|
|
row, = [r for r in db.get_messages_as_conversation(sid) if r["role"] == "user"]
|
|
# Typed before the turn ran — a crash from here on still reads as an event.
|
|
assert row["display_kind"] == "auto_continue"
|
|
assert row["display_metadata"] == {"attempt": 2}
|
|
# The model's copy is untouched: same role, same content.
|
|
assert row["content"] == NOTE
|
|
|
|
|
|
def test_a_real_user_turn_stays_untyped(agent_db):
|
|
agent, db, sid = agent_db
|
|
|
|
_build(agent, user_message="keep going")
|
|
|
|
row, = [r for r in db.get_messages_as_conversation(sid) if r["role"] == "user"]
|
|
assert row.get("display_kind") is None
|