feat(observability): observer-grade telemetry hooks + NeMo-Relay plugin

Adds backend-neutral observer hooks for plugins: session, turn, API
request, tool, approval, and subagent lifecycle events with stable
correlation IDs (session_id, task_id, turn_id, api_request_id,
tool_call_id, parent/child subagent ids). Extends VALID_HOOKS with
api_request_error and subagent_start.

Hot path is zero-cost when no plugin subscribes: has_hook()/presence
checks gate all payload construction, request payloads are returned
by reference when no middleware rewrites, and the sanitized response
payload no longer embeds raw response objects.

Bundles the optional NeMo-Relay observability plugin
(plugins/observability/nemo_relay) as an in-repo consumer of the new
hooks, peer to the existing langfuse plugin. Fails open when the
optional nemo-relay package is not installed.

Authored-by: Bryan Bednarski <bbednarski@nvidia.com>
Salvaged from #29722 onto current main.
This commit is contained in:
Bryan Bednarski
2026-06-03 06:36:46 -07:00
committed by Teknium
parent a78c73f3aa
commit 0d9b7132ff
26 changed files with 3188 additions and 140 deletions
+16
View File
@@ -49,6 +49,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Union
from hermes_constants import get_hermes_home
from utils import env_var_enabled
from hermes_cli.config import cfg_get
OBSERVER_SCHEMA_VERSION = "hermes.observer.v1"
def get_bundled_plugins_dir() -> Path:
@@ -137,10 +138,12 @@ VALID_HOOKS: Set[str] = {
"post_llm_call",
"pre_api_request",
"post_api_request",
"api_request_error",
"on_session_start",
"on_session_end",
"on_session_finalize",
"on_session_reset",
"subagent_start",
"subagent_stop",
# Gateway pre-dispatch hook. Fired once per incoming MessageEvent
# after the internal-event guard but BEFORE auth/pairing and agent
@@ -1551,6 +1554,7 @@ class PluginManager:
are reused. All injected context is ephemeral — never
persisted to session DB.
"""
kwargs.setdefault("telemetry_schema_version", OBSERVER_SCHEMA_VERSION)
callbacks = self._hooks.get(hook_name, [])
results: List[Any] = []
for cb in callbacks:
@@ -1567,6 +1571,10 @@ class PluginManager:
)
return results
def has_hook(self, hook_name: str) -> bool:
"""Return True when at least one callback is registered for a hook."""
return bool(self._hooks.get(hook_name))
# -----------------------------------------------------------------------
# Introspection
# -----------------------------------------------------------------------
@@ -1647,6 +1655,10 @@ def invoke_hook(hook_name: str, **kwargs: Any) -> List[Any]:
return get_plugin_manager().invoke_hook(hook_name, **kwargs)
def has_hook(hook_name: str) -> bool:
"""Return True when a hook has registered callbacks."""
return get_plugin_manager().has_hook(hook_name)
_thread_tool_whitelist = threading.local()
@@ -1669,6 +1681,8 @@ def get_pre_tool_call_block_message(
task_id: str = "",
session_id: str = "",
tool_call_id: str = "",
turn_id: str = "",
api_request_id: str = "",
) -> Optional[str]:
"""Check ``pre_tool_call`` hooks for a blocking directive.
@@ -1693,6 +1707,8 @@ def get_pre_tool_call_block_message(
task_id=task_id,
session_id=session_id,
tool_call_id=tool_call_id,
turn_id=turn_id,
api_request_id=api_request_id,
)
for result in hook_results: