Files
hermes-agent/hermes_cli/lifecycle.py
Todd Dailey 727e7342cf fix(gateway): await async pre_gateway_dispatch callbacks on the event loop
`GatewayInboundMixin._hm_pre_gateway_dispatch_hook` was a plain `def` calling
the sync `hermes_cli.lifecycle.invoke_hook` from the async `_hm_admit_event`,
so an `async def pre_gateway_dispatch` callback was resolved through
`resolve_plugin_command_result` on a helper thread with its own loop: the
gateway loop blocked for the callback's whole duration and any loop-bound await
(an `asyncio.Event` set by a loop task, a loop-bound aiohttp session,
`asyncio.to_thread`) could never complete, failing at 30s.

Add `PluginManager.ainvoke_hook` (+ `hermes_cli.plugins.ainvoke_hook` /
`hermes_cli.lifecycle.ainvoke_hook`): same payload narrowing (shared
`_hook_callback_kwargs`), observer + isolation semantics and result contract as
`invoke_hook`, but awaitable results are awaited on the caller's loop.
`pre_gateway_dispatch` stays intentionally unbounded. The inbound hook becomes
`async def` and `_hm_admit_event` awaits it; the sync `invoke_hook` is untouched
for every other caller. Existing tests that stubbed the hook synchronously are
adapted to the async seam.

Fixes #110241
Salvages #110265

(cherry picked from commit 22bb10d305c7992f43bbfdf1481ad0ef0015b2b7)
2026-09-21 23:47:51 -07:00

73 lines
2.2 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 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)