Files
hermes-agent/hermes_cli/lifecycle.py
teknium1 dacb06ea93 feat(plugins): show on_session_finalize return messages to the user
A plugin's on_session_finalize hook can now return a str or {"message": str}
and Hermes shows it at the session boundary without starting a turn or
rewriting the model reply:

- CLI /new: printed before the new session; CLI quit: printed above the
  exit summary; one-shot -q/-z: printed to stderr (stdout stays the answer)
- TUI: session.close returns the messages; /new shows them as system lines
- Gateway /new and shutdown: sent to the owning chat via the adapter, while
  adapters are still connected

on_session_end stays observer-only: it fires after every turn, not at a
session boundary. Gateway finalize dispatch moves to
gateway/run_shutdown_session_end.py.
2026-10-08 04:11:24 -07:00

86 lines
2.8 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:
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."""
_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."""
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)