mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-11 08:42:11 +00:00
refactor: keep anthropic_content_blocks in-memory only (no state.db column)
Drop the hermes_state.py column + persistence plumbing from the salvaged interleaved-thinking fix. The ordered-block channel covers the failure window in-memory (turn replayed within the live conversation loop). A session reloaded from disk after a crash falls back to reconstruction; if that replay 400s, the thinking-signature recovery (#43667) strips reasoning_details and retries — one degraded call in a rare resume path instead of a schema column. Replaces the DB-roundtrip test with a fallback-shape test.
This commit is contained in:
parent
7a1eed8268
commit
efcbbde48c
3 changed files with 26 additions and 70 deletions
|
|
@ -488,7 +488,6 @@ CREATE TABLE IF NOT EXISTS messages (
|
|||
reasoning TEXT,
|
||||
reasoning_content TEXT,
|
||||
reasoning_details TEXT,
|
||||
anthropic_content_blocks TEXT,
|
||||
codex_reasoning_items TEXT,
|
||||
codex_message_items TEXT,
|
||||
platform_message_id TEXT,
|
||||
|
|
@ -2241,7 +2240,6 @@ class SessionDB:
|
|||
reasoning: str = None,
|
||||
reasoning_content: str = None,
|
||||
reasoning_details: Any = None,
|
||||
anthropic_content_blocks: Any = None,
|
||||
codex_reasoning_items: Any = None,
|
||||
codex_message_items: Any = None,
|
||||
platform_message_id: str = None,
|
||||
|
|
@ -2264,10 +2262,6 @@ class SessionDB:
|
|||
json.dumps(reasoning_details)
|
||||
if reasoning_details else None
|
||||
)
|
||||
anthropic_content_blocks_json = (
|
||||
json.dumps(anthropic_content_blocks)
|
||||
if anthropic_content_blocks else None
|
||||
)
|
||||
codex_items_json = (
|
||||
json.dumps(codex_reasoning_items)
|
||||
if codex_reasoning_items else None
|
||||
|
|
@ -2290,10 +2284,9 @@ class SessionDB:
|
|||
cursor = conn.execute(
|
||||
"""INSERT INTO messages (session_id, role, content, tool_call_id,
|
||||
tool_calls, tool_name, timestamp, token_count, finish_reason,
|
||||
reasoning, reasoning_content, reasoning_details, anthropic_content_blocks,
|
||||
codex_reasoning_items,
|
||||
reasoning, reasoning_content, reasoning_details, codex_reasoning_items,
|
||||
codex_message_items, platform_message_id, observed)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
session_id,
|
||||
role,
|
||||
|
|
@ -2307,7 +2300,6 @@ class SessionDB:
|
|||
reasoning,
|
||||
reasoning_content,
|
||||
reasoning_details_json,
|
||||
anthropic_content_blocks_json,
|
||||
codex_items_json,
|
||||
codex_message_items_json,
|
||||
platform_message_id,
|
||||
|
|
@ -2356,9 +2348,6 @@ class SessionDB:
|
|||
role = msg.get("role", "unknown")
|
||||
tool_calls = msg.get("tool_calls")
|
||||
reasoning_details = msg.get("reasoning_details") if role == "assistant" else None
|
||||
anthropic_content_blocks = (
|
||||
msg.get("anthropic_content_blocks") if role == "assistant" else None
|
||||
)
|
||||
codex_reasoning_items = (
|
||||
msg.get("codex_reasoning_items") if role == "assistant" else None
|
||||
)
|
||||
|
|
@ -2369,9 +2358,6 @@ class SessionDB:
|
|||
reasoning_details_json = (
|
||||
json.dumps(reasoning_details) if reasoning_details else None
|
||||
)
|
||||
anthropic_content_blocks_json = (
|
||||
json.dumps(anthropic_content_blocks) if anthropic_content_blocks else None
|
||||
)
|
||||
codex_items_json = (
|
||||
json.dumps(codex_reasoning_items) if codex_reasoning_items else None
|
||||
)
|
||||
|
|
@ -2388,10 +2374,9 @@ class SessionDB:
|
|||
conn.execute(
|
||||
"""INSERT INTO messages (session_id, role, content, tool_call_id,
|
||||
tool_calls, tool_name, timestamp, token_count, finish_reason,
|
||||
reasoning, reasoning_content, reasoning_details, anthropic_content_blocks,
|
||||
codex_reasoning_items,
|
||||
reasoning, reasoning_content, reasoning_details, codex_reasoning_items,
|
||||
codex_message_items, platform_message_id, observed)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(
|
||||
session_id,
|
||||
role,
|
||||
|
|
@ -2405,7 +2390,6 @@ class SessionDB:
|
|||
msg.get("reasoning") if role == "assistant" else None,
|
||||
msg.get("reasoning_content") if role == "assistant" else None,
|
||||
reasoning_details_json,
|
||||
anthropic_content_blocks_json,
|
||||
codex_items_json,
|
||||
codex_message_items_json,
|
||||
platform_msg_id,
|
||||
|
|
@ -2748,7 +2732,6 @@ class SessionDB:
|
|||
rows = self._conn.execute(
|
||||
"SELECT role, content, tool_call_id, tool_calls, tool_name, "
|
||||
"finish_reason, reasoning, reasoning_content, reasoning_details, "
|
||||
"anthropic_content_blocks, "
|
||||
"codex_reasoning_items, codex_message_items, platform_message_id, observed "
|
||||
f"FROM messages WHERE session_id IN ({placeholders})"
|
||||
f"{active_clause} ORDER BY id",
|
||||
|
|
@ -2796,12 +2779,6 @@ class SessionDB:
|
|||
except (json.JSONDecodeError, TypeError):
|
||||
logger.warning("Failed to deserialize reasoning_details, falling back to None")
|
||||
msg["reasoning_details"] = None
|
||||
if row["anthropic_content_blocks"]:
|
||||
try:
|
||||
msg["anthropic_content_blocks"] = json.loads(row["anthropic_content_blocks"])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
logger.warning("Failed to deserialize anthropic_content_blocks, falling back to None")
|
||||
msg["anthropic_content_blocks"] = None
|
||||
if row["codex_reasoning_items"]:
|
||||
try:
|
||||
msg["codex_reasoning_items"] = json.loads(row["codex_reasoning_items"])
|
||||
|
|
|
|||
|
|
@ -1597,7 +1597,6 @@ class AIAgent:
|
|||
reasoning=msg.get("reasoning") if role == "assistant" else None,
|
||||
reasoning_content=msg.get("reasoning_content") if role == "assistant" else None,
|
||||
reasoning_details=msg.get("reasoning_details") if role == "assistant" else None,
|
||||
anthropic_content_blocks=msg.get("anthropic_content_blocks") if role == "assistant" else None,
|
||||
codex_reasoning_items=msg.get("codex_reasoning_items") if role == "assistant" else None,
|
||||
codex_message_items=msg.get("codex_message_items") if role == "assistant" else None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -176,59 +176,39 @@ class TestInterleavedThinkingBlockOrder:
|
|||
"in the latest assistant message cannot be modified'."
|
||||
)
|
||||
|
||||
def test_interleaved_order_survives_db_roundtrip(self, tmp_path):
|
||||
"""The ordered-block channel must survive SQLite persistence + reload.
|
||||
def test_replay_falls_back_gracefully_without_ordered_blocks(self):
|
||||
"""Without the ordered-block channel, conversion must not crash.
|
||||
|
||||
This is the exact path that fails after a gateway crash: the session
|
||||
is reloaded from state.db via get_messages_as_conversation, then
|
||||
replayed. If the verbatim block list is dropped or not deserialized,
|
||||
the reconstruction reorders signed thinking blocks -> HTTP 400.
|
||||
The channel is intentionally NOT persisted to state.db (in-memory
|
||||
only): a session reloaded from disk after a crash loses the field
|
||||
and falls back to reconstruction. That replay may take one HTTP 400,
|
||||
which the thinking-signature recovery (#43667) absorbs by stripping
|
||||
reasoning_details and retrying. This test pins the fallback shape:
|
||||
conversion still produces a valid assistant message from the
|
||||
parallel reasoning_details + tool_calls fields.
|
||||
"""
|
||||
import hermes_state
|
||||
|
||||
response = _interleaved_response()
|
||||
original_order = _original_block_order(response)
|
||||
|
||||
transport = get_transport("anthropic_messages")
|
||||
normalized = transport.normalize_response(response)
|
||||
assistant_msg = _stored_assistant_message(normalized)
|
||||
# Simulate a disk reload: the in-memory-only channel is gone.
|
||||
assistant_msg.pop("anthropic_content_blocks", None)
|
||||
|
||||
db = hermes_state.SessionDB(tmp_path / "state.db")
|
||||
sid = "sess_roundtrip"
|
||||
db.create_session(sid, source="test")
|
||||
db.append_message(
|
||||
session_id=sid,
|
||||
role="assistant",
|
||||
content=assistant_msg["content"],
|
||||
tool_calls=assistant_msg["tool_calls"],
|
||||
reasoning_details=assistant_msg.get("reasoning_details"),
|
||||
anthropic_content_blocks=assistant_msg.get("anthropic_content_blocks"),
|
||||
)
|
||||
db.append_message(session_id=sid, role="tool", tool_call_id="toolu_1", content="a ok")
|
||||
db.append_message(session_id=sid, role="tool", tool_call_id="toolu_2", content="b ok")
|
||||
|
||||
# Reload via the conversation-restore path used on resume / crash recovery.
|
||||
loaded = db.get_messages_as_conversation(sid)
|
||||
reloaded_assistant = [m for m in loaded if m.get("role") == "assistant"]
|
||||
assert reloaded_assistant, "no assistant message after DB reload"
|
||||
# The ordered-block channel must come back as a deserialized list.
|
||||
blocks = reloaded_assistant[0].get("anthropic_content_blocks")
|
||||
assert isinstance(blocks, list) and len(blocks) == 4, (
|
||||
"anthropic_content_blocks was not persisted/deserialized correctly"
|
||||
)
|
||||
|
||||
messages = [
|
||||
assistant_msg,
|
||||
{"role": "tool", "tool_call_id": "toolu_1", "content": "a ok"},
|
||||
{"role": "tool", "tool_call_id": "toolu_2", "content": "b ok"},
|
||||
]
|
||||
_system, anthropic_messages = convert_messages_to_anthropic(
|
||||
loaded, base_url=None, model="claude-opus-4-8",
|
||||
messages, base_url=None, model="claude-opus-4-8",
|
||||
)
|
||||
assistant_out = [m for m in anthropic_messages if m.get("role") == "assistant"]
|
||||
assert assistant_out, "no assistant message in converted output"
|
||||
replayed_order = _replayed_block_order(assistant_out[-1]["content"])
|
||||
|
||||
assert replayed_order == original_order, (
|
||||
"Interleaved block order was lost across the SQLite round-trip.\n"
|
||||
f" original: {original_order}\n"
|
||||
f" replayed: {replayed_order}"
|
||||
)
|
||||
content = assistant_out[-1]["content"]
|
||||
assert isinstance(content, list) and content, "fallback produced empty content"
|
||||
# Reconstruction keeps both tool_use blocks (answered by results).
|
||||
tool_ids = [b.get("id") for b in content if isinstance(b, dict) and b.get("type") == "tool_use"]
|
||||
assert set(tool_ids) == {"toolu_1", "toolu_2"}
|
||||
|
||||
|
||||
class TestInterleavedReplayCredentialRedaction:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue