mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
Extends the conversation=<id> Portal tag (salvaged from PR #65183 by @J-SUPHA) from main-loop-only to every LLM call in a conversation: - agent/portal_tags.py: ContextVar-based conversation context. nous_portal_tags() falls back to the ambient id when no explicit session_id is passed, so every aux tag site (auxiliary_client, chat_completion_helpers summary path, web_tools) inherits the tag with zero per-call-site plumbing. Ambient id wins over explicit per-segment ids since it carries the lineage root. - hermes_state.py: SessionDB.get_conversation_root() — public wrapper over the lineage walk; returns the ROOT session id, so one user-facing conversation keeps a single conversation= value across context-compression rotation, and delegate subagent trees tag as their parent conversation. - run_agent.py: run_conversation() publishes the root id for the turn and resets it in finally. _conversation_root_id() resolves via _parent_session_id for subagents. - agent/moa_loop.py: MoA reference fan-out workers now run under propagate_context_to_thread so advisor slots attribute to the acting conversation (also fixes approval-callback propagation on that path). - agent/title_generator.py: bare title thread republishes the context from its session id (spawned after turn reset). Tests: ContextVar semantics, cross-context isolation, thread-hop propagation, lineage-root resolution incl. cycle guard.
57 lines
2 KiB
Python
57 lines
2 KiB
Python
"""Tests for SessionDB.get_conversation_root — stable conversation id resolution.
|
|
|
|
The conversation root is the Nous Portal ``conversation=`` tag value: one
|
|
stable id per user-facing conversation, surviving context-compression
|
|
session rotation and covering delegate subagent trees.
|
|
"""
|
|
import pytest
|
|
|
|
from hermes_state import SessionDB
|
|
|
|
|
|
@pytest.fixture
|
|
def db(tmp_path):
|
|
return SessionDB(tmp_path / "state.db")
|
|
|
|
|
|
def test_root_of_standalone_session_is_itself(db):
|
|
db.create_session("solo", source="cli")
|
|
assert db.get_conversation_root("solo") == "solo"
|
|
|
|
|
|
def test_root_of_unknown_session_is_itself(db):
|
|
# No DB row at all (e.g. subagent's first turn before create_session).
|
|
assert db.get_conversation_root("ghost") == "ghost"
|
|
|
|
|
|
def test_root_follows_compression_rotation_chain(db):
|
|
# root -> seg2 -> seg3 (two compression rotations)
|
|
db.create_session("root", source="cli")
|
|
db.create_session("seg2", source="cli", parent_session_id="root")
|
|
db.create_session("seg3", source="cli", parent_session_id="seg2")
|
|
assert db.get_conversation_root("seg3") == "root"
|
|
assert db.get_conversation_root("seg2") == "root"
|
|
assert db.get_conversation_root("root") == "root"
|
|
|
|
|
|
def test_root_covers_delegate_child_sessions(db):
|
|
db.create_session("parent", source="cli")
|
|
db.create_session("child", source="delegate", parent_session_id="parent")
|
|
assert db.get_conversation_root("child") == "parent"
|
|
|
|
|
|
def test_root_handles_parent_cycle_without_hanging(db):
|
|
# Defensive: a corrupted parent chain with a cycle must terminate.
|
|
db.create_session("a", source="cli")
|
|
db.create_session("b", source="cli", parent_session_id="a")
|
|
with db._lock:
|
|
db._conn.execute(
|
|
"UPDATE sessions SET parent_session_id = ? WHERE id = ?", ("b", "a")
|
|
)
|
|
db._conn.commit()
|
|
root = db.get_conversation_root("b")
|
|
assert root in ("a", "b") # terminated, returned a chain member
|
|
|
|
|
|
def test_root_empty_session_id_passthrough(db):
|
|
assert db.get_conversation_root("") == ""
|