Merge branch 'main' into bb/gui

This commit is contained in:
emozilla
2026-05-20 16:01:41 -04:00
72 changed files with 2726 additions and 742 deletions
+28 -16
View File
@@ -168,7 +168,7 @@ from agent.tool_result_classification import (
file_mutation_result_landed,
)
from agent.trajectory import (
convert_scratchpad_to_think, has_incomplete_scratchpad,
convert_scratchpad_to_think,
save_trajectory as _save_trajectory_to_file,
)
from agent.message_sanitization import (
@@ -1535,23 +1535,35 @@ class AIAgent:
return content.strip()
def _save_session_log(self, messages: List[Dict[str, Any]] = None):
"""
Save the full raw session to a JSON file.
"""Optional per-session JSON snapshot writer.
Stores every message exactly as the agent sees it: user messages,
assistant messages (with reasoning, finish_reason, tool_calls),
tool responses (with tool_call_id, tool_name), and injected system
messages (compression summaries, todo snapshots, etc.).
Gated by ``sessions.write_json_snapshots`` (default False). state.db
is the canonical message store; this writer exists only for users
whose external tooling consumes ``~/.hermes/sessions/session_{sid}.json``
directly. When the flag is off this is a fast no-op.
REASONING_SCRATCHPAD tags are converted to <think> blocks for consistency.
Overwritten after each turn so it always reflects the latest state.
When enabled, rewrites the snapshot after every persistence point with
the full message list (assistant content normalized via
``_clean_session_content`` to convert REASONING_SCRATCHPAD to think
tags). The truncation guard ("don't overwrite a larger log with
fewer messages") is preserved so resume + branch don't clobber a
fuller existing snapshot.
"""
if not getattr(self, "_session_json_enabled", False):
return
messages = messages or self._session_messages
if not messages:
return
# Re-derive the target path each call so /branch and /compress
# session-id changes land in the right file without any re-point
# bookkeeping at the call sites.
try:
log_file = self.logs_dir / f"session_{self.session_id}.json"
except Exception:
return
try:
# Clean assistant content for session logs
cleaned = []
for msg in messages:
if msg.get("role") == "assistant" and msg.get("content"):
@@ -1560,12 +1572,11 @@ class AIAgent:
cleaned.append(msg)
# Guard: never overwrite a larger session log with fewer messages.
# This protects against data loss when --resume loads a session whose
# messages weren't fully written to SQLite — the resumed agent starts
# with partial history and would otherwise clobber the full JSON log.
if self.session_log_file.exists():
# Protects against data loss when a resumed agent starts with
# partial history and would otherwise clobber the full JSON log.
if log_file.exists():
try:
existing = json.loads(self.session_log_file.read_text(encoding="utf-8"))
existing = json.loads(log_file.read_text(encoding="utf-8"))
existing_count = existing.get("message_count", len(existing.get("messages", [])))
if existing_count > len(cleaned):
logging.debug(
@@ -1590,7 +1601,7 @@ class AIAgent:
}
atomic_json_write(
self.session_log_file,
log_file,
entry,
indent=2,
default=str,
@@ -1600,6 +1611,7 @@ class AIAgent:
if self.verbose_logging:
logging.warning(f"Failed to save session log: {e}")
def interrupt(self, message: str = None) -> None:
"""
Request the agent to interrupt its current tool-calling loop.