hermes: show recorded tool activity in resumed chats

This commit is contained in:
jenkins 2026-09-13 14:25:25 -05:00
parent 364d1b3503
commit 17d70759e0
2 changed files with 81 additions and 2 deletions

View File

@ -51,6 +51,33 @@ HELPER_REPLACEMENT = '''def _run_activity_messages(session_id: str) -> List[Dict
return entries[-1_000:]
def _present_activity_message(message: Dict[str, Any]) -> Dict[str, Any]:
"""Give a persisted tool-only assistant turn an honest visible summary.
Hermes deliberately stores a blank assistant ``content`` field when the
turn only invokes tools. The tool call records are retained separately,
but a transcript card with neither prose nor a summary looks like missing
history. This projection leaves the stored message intact and names only
the already-recorded tool functions.
"""
item = dict(message)
if item.get("role") != "assistant" or str(item.get("content") or "").strip():
return item
calls = item.get("tool_calls")
if not isinstance(calls, list):
return item
names: List[str] = []
for call in calls:
function = call.get("function") if isinstance(call, dict) else None
name = function.get("name") if isinstance(function, dict) else None
if isinstance(name, str) and name.strip():
names.append(name.strip()[:80])
if names:
item["content"] = "Calling " + ", ".join(names[:8]) + "."
item["observed"] = True
return item
@app.get("/api/sessions/{session_id}/messages")
async def get_session_messages(
session_id: str,
@ -64,8 +91,11 @@ MESSAGES_BEFORE = ''' messages = db.get_messages(sid)
'''
MESSAGES_AFTER = ''' messages = [
*db.get_messages(sid),
*_run_activity_messages(sid),
_present_activity_message(item)
for item in [
*db.get_messages(sid),
*_run_activity_messages(sid),
]
]
messages.sort(key=lambda item: float(item.get("timestamp") or 0.0))
total_messages = len(messages)

View File

@ -2,9 +2,11 @@
from __future__ import annotations
import asyncio
import importlib.util
import json
import sqlite3
import textwrap
from pathlib import Path
import pytest
@ -290,6 +292,53 @@ def test_web_session_activity_patch_projects_bounded_events(tmp_path: Path):
assert 'item[0].get("ended_at") is None' in patched
def test_activity_projection_preserves_user_text_and_labels_tool_only_turns():
"""A saved tool invocation cannot become a blank resumed-chat card."""
module_path = HERMES / "scripts" / "patch_web_session_activity.py"
spec = importlib.util.spec_from_file_location("patch_web_activity_projection", module_path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
helper = module.HELPER_REPLACEMENT.split('@app.get("/api/sessions/', 1)[0]
namespace: dict[str, object] = {}
exec("from typing import Any, Dict, List\n" + helper, namespace)
present = namespace["_present_activity_message"]
first_user = {"role": "user", "content": "Please inspect the saved work."}
second_user = {"role": "user", "content": "Continue from the recorded findings."}
assert present(first_user) == first_user
assert present(second_user) == second_user
tool_only = {
"role": "assistant",
"content": None,
# This is the parsed API shape for the serialized database column.
"tool_calls": [{"function": {"name": "terminal"}}],
}
projected = present(tool_only)
assert projected["content"] == "Calling terminal."
assert projected["observed"] is True
assert tool_only["content"] is None
class FakeDB:
def get_messages(self, session_id):
assert session_id == "resume-fixture"
return [first_user, tool_only, second_user]
response_source = "async def project_messages(db, sid, limit=None):\n" + textwrap.indent(
textwrap.dedent(module.MESSAGES_AFTER), " "
)
exec(response_source, namespace)
namespace["_run_activity_messages"] = lambda _session_id: []
payload = asyncio.run(namespace["project_messages"](FakeDB(), "resume-fixture"))
assert [item["content"] for item in payload["messages"]] == [
"Please inspect the saved work.",
"Calling terminal.",
"Continue from the recorded findings.",
]
assert payload["total_messages"] == 3
def test_web_session_lineage_returns_to_resumed_parent():
"""An ended reviewer must not strand the live view away from its parent."""
module_path = HERMES / "scripts" / "patch_web_session_activity.py"