mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
Finn's state-layer hook missed the paths Desktop actually uses: single delete and archive go through the canonical mutation receipt (mutate_runtime_session), not SessionDB.delete_session, so a plugin never heard about either. Archive (the "this is done" signal #132639 asked for) fired nowhere. - mutate_runtime_session fires after its write commits for operation 'delete' and for a real unarchived -> archived sidebar flip; an exact retry replays the receipt and stays silent. - set_session_archived fires reason="archive" on a real flip, so TUI session.archive, `hermes sessions archive` and deferred TUI archives are covered from one place. Repeats, un-archives, the idle auto-archive sweep and profile adoption (notify=False) stay silent. - Payload is now session_id + reason + profile on every path (profile from the store's home), deleted_ids when more than one row went; primary id is the one the caller asked for, not the alphabetically first. - One fail-open seam (emit_session_deleted) calls lifecycle.invoke_hook directly; the extra notify_session_deleted wrapper is gone. - Tests trimmed to two invariants against the real hook registry. Co-authored-by: Lucas Piera <pieralukasz@gmail.com>
98 lines
3.1 KiB
Python
98 lines
3.1 KiB
Python
"""Hermes lifecycle dispatch for first-party observers and plugins."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, List
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _observe(hook_name: str, **kwargs: Any) -> None:
|
|
from agent.safe_worker_policy import safe_worker_enabled
|
|
|
|
if safe_worker_enabled():
|
|
return
|
|
try:
|
|
from hermes_cli.observability import observe_lifecycle
|
|
|
|
observe_lifecycle(hook_name, **kwargs)
|
|
except Exception:
|
|
logger.warning("Built-in observability hook failed", exc_info=True)
|
|
|
|
|
|
def _plugin_hooks(hook_name: str, **kwargs: Any) -> list[Any]:
|
|
from hermes_cli import plugins
|
|
|
|
return plugins.invoke_hook(hook_name, **kwargs)
|
|
|
|
|
|
def invoke_hook(hook_name: str, **kwargs: Any) -> list[Any]:
|
|
"""Notify first-party observers, then invoke compatibility plugin hooks."""
|
|
from agent.safe_worker_policy import safe_worker_enabled
|
|
|
|
if safe_worker_enabled():
|
|
return []
|
|
_observe(hook_name, **kwargs)
|
|
return _plugin_hooks(hook_name, **kwargs)
|
|
|
|
|
|
async def ainvoke_hook(hook_name: str, **kwargs: Any) -> list[Any]:
|
|
""":func:`invoke_hook` for callers on an event loop: same observers-then-plugins
|
|
composition, with ``async def`` plugin callbacks awaited on that loop."""
|
|
_observe(hook_name, **kwargs)
|
|
from hermes_cli import plugins
|
|
|
|
return await plugins.ainvoke_hook(hook_name, **kwargs)
|
|
|
|
|
|
def has_hook(hook_name: str) -> bool:
|
|
"""Return whether a first-party observer or plugin consumes a hook."""
|
|
from agent.safe_worker_policy import safe_worker_enabled
|
|
|
|
if safe_worker_enabled():
|
|
return False
|
|
try:
|
|
from hermes_cli.observability import handles_hook
|
|
|
|
if handles_hook(hook_name):
|
|
return True
|
|
except Exception:
|
|
logger.warning("Unable to inspect built-in observability hooks", exc_info=True)
|
|
|
|
from hermes_cli import plugins
|
|
|
|
return plugins.has_hook(hook_name)
|
|
|
|
|
|
def session_end_messages(results: Any) -> list[str]:
|
|
"""User-facing text from ``on_session_finalize`` results: a non-empty ``str`` or ``{"message": str}``.
|
|
|
|
Surfaces show these out-of-band (CLI print, TUI system line, gateway chat send) — never as a model
|
|
turn. ``on_session_end`` is per-turn and stays observer-only."""
|
|
messages: list[str] = []
|
|
for result in results or ():
|
|
text = result.get("message") if isinstance(result, dict) else result
|
|
if isinstance(text, str) and text.strip():
|
|
messages.append(text.strip())
|
|
return messages
|
|
|
|
|
|
def finalize_session(**kwargs: Any) -> list[Any]:
|
|
"""Notify observers and hard-close one core-owned Relay conversation."""
|
|
_observe("on_session_finalize", **kwargs)
|
|
|
|
session_id = str(kwargs.get("session_id") or "")
|
|
if session_id:
|
|
try:
|
|
from agent import relay_runtime
|
|
|
|
relay_runtime.SESSION_COORDINATOR.finalize_conversation(
|
|
profile_key=relay_runtime.current_profile_key(),
|
|
session_id=session_id,
|
|
)
|
|
except Exception:
|
|
logger.warning("Core Relay session finalization failed", exc_info=True)
|
|
|
|
return _plugin_hooks("on_session_finalize", **kwargs)
|