mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: persist reasoning across gateway session turns (schema v6) (#2974)
feat: persist reasoning across gateway session turns (schema v6) Tested against OpenAI Codex (direct), Anthropic (direct + OAI-compat), and OpenRouter → 6 backends. All reasoning field types (reasoning, reasoning_details, codex_reasoning_items) round-trip through the DB correctly.
This commit is contained in:
parent
5dbe2d9d73
commit
42fec19151
5 changed files with 173 additions and 11 deletions
|
|
@ -177,6 +177,91 @@ class TestMessageStorage:
|
|||
messages = db.get_messages("s1")
|
||||
assert messages[0]["finish_reason"] == "stop"
|
||||
|
||||
def test_reasoning_persisted_and_restored(self, db):
|
||||
"""Reasoning text is stored for assistant messages and restored by
|
||||
get_messages_as_conversation() so providers receive coherent multi-turn
|
||||
reasoning context."""
|
||||
db.create_session(session_id="s1", source="telegram")
|
||||
db.append_message("s1", role="user", content="create a cron job")
|
||||
db.append_message(
|
||||
"s1",
|
||||
role="assistant",
|
||||
content=None,
|
||||
tool_calls=[{"function": {"name": "cronjob", "arguments": "{}"}, "id": "c1", "type": "function"}],
|
||||
reasoning="I should call the cronjob tool to schedule this.",
|
||||
)
|
||||
db.append_message("s1", role="tool", content='{"job_id": "abc"}', tool_call_id="c1")
|
||||
|
||||
conv = db.get_messages_as_conversation("s1")
|
||||
assert len(conv) == 3
|
||||
# reasoning must be present on the assistant message
|
||||
assistant = conv[1]
|
||||
assert assistant["role"] == "assistant"
|
||||
assert assistant.get("reasoning") == "I should call the cronjob tool to schedule this."
|
||||
# user and tool messages must NOT carry reasoning
|
||||
assert "reasoning" not in conv[0]
|
||||
assert "reasoning" not in conv[2]
|
||||
|
||||
def test_reasoning_details_persisted_and_restored(self, db):
|
||||
"""reasoning_details (structured array) is round-tripped through JSON
|
||||
serialization in the DB."""
|
||||
db.create_session(session_id="s1", source="telegram")
|
||||
details = [
|
||||
{"type": "reasoning.summary", "summary": "Thinking about tools"},
|
||||
{"type": "reasoning.encrypted_content", "encrypted_content": "abc123"},
|
||||
]
|
||||
db.append_message(
|
||||
"s1",
|
||||
role="assistant",
|
||||
content="Hello",
|
||||
reasoning="Thinking about what to say",
|
||||
reasoning_details=details,
|
||||
)
|
||||
|
||||
conv = db.get_messages_as_conversation("s1")
|
||||
assert len(conv) == 1
|
||||
msg = conv[0]
|
||||
assert msg["reasoning"] == "Thinking about what to say"
|
||||
assert msg["reasoning_details"] == details
|
||||
|
||||
def test_reasoning_not_set_for_non_assistant(self, db):
|
||||
"""reasoning is never leaked onto user or tool messages."""
|
||||
db.create_session(session_id="s1", source="telegram")
|
||||
db.append_message("s1", role="user", content="hi")
|
||||
db.append_message("s1", role="assistant", content="hello", reasoning=None)
|
||||
|
||||
conv = db.get_messages_as_conversation("s1")
|
||||
assert "reasoning" not in conv[0]
|
||||
assert "reasoning" not in conv[1]
|
||||
|
||||
def test_reasoning_empty_string_not_restored(self, db):
|
||||
"""Empty string reasoning is treated as absent."""
|
||||
db.create_session(session_id="s1", source="cli")
|
||||
db.append_message("s1", role="assistant", content="hi", reasoning="")
|
||||
|
||||
conv = db.get_messages_as_conversation("s1")
|
||||
assert "reasoning" not in conv[0]
|
||||
|
||||
def test_codex_reasoning_items_persisted_and_restored(self, db):
|
||||
"""codex_reasoning_items (encrypted blobs for Codex Responses API) are
|
||||
round-tripped through JSON serialization in the DB."""
|
||||
db.create_session(session_id="s1", source="cli")
|
||||
codex_items = [
|
||||
{"type": "reasoning", "id": "rs_abc", "encrypted_content": "enc_blob_123"},
|
||||
{"type": "reasoning", "id": "rs_def", "encrypted_content": "enc_blob_456"},
|
||||
]
|
||||
db.append_message(
|
||||
"s1",
|
||||
role="assistant",
|
||||
content="Done",
|
||||
codex_reasoning_items=codex_items,
|
||||
)
|
||||
|
||||
conv = db.get_messages_as_conversation("s1")
|
||||
assert len(conv) == 1
|
||||
assert conv[0]["codex_reasoning_items"] == codex_items
|
||||
assert conv[0]["codex_reasoning_items"][0]["encrypted_content"] == "enc_blob_123"
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# FTS5 search
|
||||
|
|
@ -737,7 +822,7 @@ class TestSchemaInit:
|
|||
def test_schema_version(self, db):
|
||||
cursor = db._conn.execute("SELECT version FROM schema_version")
|
||||
version = cursor.fetchone()[0]
|
||||
assert version == 5
|
||||
assert version == 6
|
||||
|
||||
def test_title_column_exists(self, db):
|
||||
"""Verify the title column was created in the sessions table."""
|
||||
|
|
@ -793,12 +878,12 @@ class TestSchemaInit:
|
|||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Open with SessionDB — should migrate to v5
|
||||
# Open with SessionDB — should migrate to v6
|
||||
migrated_db = SessionDB(db_path=db_path)
|
||||
|
||||
# Verify migration
|
||||
cursor = migrated_db._conn.execute("SELECT version FROM schema_version")
|
||||
assert cursor.fetchone()[0] == 5
|
||||
assert cursor.fetchone()[0] == 6
|
||||
|
||||
# Verify title column exists and is NULL for existing sessions
|
||||
session = migrated_db.get_session("existing")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue