hermes-agent/tests/plugins/test_nemo_relay_plugin.py
Alex Fournier 70caf30203 fix(observability): coordinate Relay plugin lifecycle
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-07-22 07:22:21 -07:00

1458 lines
49 KiB
Python

"""Tests for the bundled observability/nemo_relay plugin."""
from __future__ import annotations
import asyncio
import contextvars
import gc
import importlib
import json
import sys
import warnings
from pathlib import Path
from types import SimpleNamespace
import pytest
import yaml
from hermes_cli import lifecycle, plugins as plugin_api
from hermes_cli.observability import relay_runtime, relay_shared_metrics
from hermes_cli.plugins import PluginManager
REPO_ROOT = Path(__file__).resolve().parents[2]
PLUGIN_DIR = REPO_ROOT / "plugins" / "observability" / "nemo_relay"
class _FakeNemoRelay:
def __init__(self):
self.events = []
self._callbacks = {}
self._llm_starts = {}
self._scope_serial = 0
self._scope_context = contextvars.ContextVar(
"fake_nemo_relay_scope", default=None
)
self.ScopeType = SimpleNamespace(Agent="agent", Function="function")
self.scope = SimpleNamespace(
push=self._scope_push,
pop=self._scope_pop,
event=self._scope_event,
)
self.llm = SimpleNamespace(
call=self._llm_call,
call_end=self._llm_call_end,
execute=self._llm_execute,
)
self.tools = SimpleNamespace(
call=self._tool_call,
call_end=self._tool_call_end,
execute=self._tool_execute,
request_intercepts=self._tool_request_intercepts,
)
self.plugin = SimpleNamespace(
initialize=self._plugin_initialize,
clear=self._plugin_clear,
initialize_with_dynamic_plugins=self._plugin_initialize_with_dynamic,
)
self.subscribers = SimpleNamespace(
register=self._register_subscriber,
deregister=self._deregister_subscriber,
flush=self._flush_subscribers,
)
self.LLMRequest = _FakeLLMRequest
self.AtofExporterConfig = _FakeAtofExporterConfig
self.AtofExporterMode = SimpleNamespace(Append="append", Overwrite="overwrite")
self.AtofExporter = self._make_atof_exporter
self.AtifExporter = self._make_atif_exporter
self.get_scope_stack = self._get_scope_stack
def _scope_push(self, name, scope_type, **kwargs):
self._scope_serial += 1
handle = ("scope", name, self._scope_serial)
self._scope_context.set(handle)
self.events.append(("scope.push", name, scope_type, kwargs))
return handle
def _scope_pop(self, handle, **kwargs):
self.events.append(("scope.pop", handle, kwargs))
def _scope_event(self, name, **kwargs):
self.events.append(("scope.event", name, kwargs))
def _get_scope_stack(self):
current = self._scope_context.get()
self.events.append(("scope.sync", current))
return current
def _llm_call(self, name, request, **kwargs):
handle = ("llm", name)
self._llm_starts[handle] = kwargs
self.events.append(("llm.call", name, request.content, kwargs))
return handle
def _llm_call_end(self, handle, response, **kwargs):
self.events.append(("llm.call_end", handle, response, kwargs))
start = self._llm_starts.pop(handle, {})
event = SimpleNamespace(
kind="scope",
category="llm",
name=handle[1],
scope_category="end",
category_profile={"model_name": start.get("model_name")},
metadata={
**(start.get("metadata") or {}),
**(kwargs.get("metadata") or {}),
"otel.status_code": "OK",
},
data=response,
)
for callback in list(self._callbacks.values()):
callback(event)
def _llm_execute(self, name, request, func, **kwargs):
self.events.append(("llm.execute.start", name, request.content, kwargs))
handle = self._llm_call(name, request, **kwargs)
result = func(_FakeLLMRequest(request.headers, {"intercepted": True, **request.content}))
self._llm_call_end(
handle,
result,
**{key: value for key, value in kwargs.items() if key != "handle"},
)
self.events.append(("llm.execute.end", name, result, kwargs))
return result
def _tool_call(self, name, args, **kwargs):
handle = ("tool", name)
self.events.append(("tool.call", name, args, kwargs))
return handle
def _tool_call_end(self, handle, result, **kwargs):
self.events.append(("tool.call_end", handle, result, kwargs))
def _tool_execute(self, name, args, func, **kwargs):
self.events.append(("tool.execute.start", name, args, kwargs))
handle = self._tool_call(name, args, **kwargs)
result = func(args)
self._tool_call_end(
handle,
result,
**{key: value for key, value in kwargs.items() if key != "handle"},
)
self.events.append(("tool.execute.end", name, result, kwargs))
return result
def _tool_request_intercepts(self, name, args):
self.events.append(("tool.request_intercepts", name, args))
return {"intercepted": True, **args}
def _make_atof_exporter(self, config):
return _FakeAtofExporter(self.events, config)
def _make_atif_exporter(self, session_id, agent_name, agent_version, **kwargs):
return _FakeAtifExporter(self.events, session_id, agent_name, agent_version, kwargs)
async def _plugin_initialize(self, config):
self.events.append(("plugin.initialize", config))
return {"diagnostics": []}
async def _plugin_clear(self):
self.events.append(("plugin.clear",))
async def _plugin_initialize_with_dynamic(self, config, dynamic_plugins):
self.events.append(("plugin.activate_dynamic", config, dynamic_plugins))
return _FakePluginActivation(self.events)
def _register_subscriber(self, name, callback):
self._callbacks[name] = callback
self.events.append(("subscribers.register", name))
def _deregister_subscriber(self, name):
self._callbacks.pop(name, None)
self.events.append(("subscribers.deregister", name))
def _flush_subscribers(self):
self.events.append(("subscribers.flush",))
class _FakePluginActivation:
def __init__(self, events):
self.events = events
self.report = {"diagnostics": []}
async def close(self):
self.events.append(("plugin.activation.close",))
class _FakeLLMRequest:
def __init__(self, headers, content):
self.headers = headers
self.content = content
class _FakeAtofExporterConfig:
def __init__(self):
self.output_directory = ""
self.filename = "events.jsonl"
self.mode = "append"
class _FakeAtofExporter:
def __init__(self, events, config):
self.events = events
self.config = config
def register(self, name):
self.events.append(("atof.register", name, self.config.output_directory, self.config.filename))
def deregister(self, name):
self.events.append(("atof.deregister", name, self.config.output_directory, self.config.filename))
return True
class _FakeAtifExporter:
def __init__(self, events, session_id, agent_name, agent_version, kwargs):
self.events = events
self.session_id = session_id
self.agent_name = agent_name
self.agent_version = agent_version
self.kwargs = kwargs
def register(self, name):
self.events.append(("atif.register", name, self.session_id))
def deregister(self, name):
self.events.append(("atif.deregister", name, self.session_id))
return True
def export_json(self):
self.events.append(("atif.export", self.session_id))
return json.dumps({"session_id": self.session_id, "agent_name": self.agent_name})
def _fresh_plugin(monkeypatch, fake):
existing = sys.modules.get("plugins.observability.nemo_relay")
if existing is not None:
existing.reset_for_tests()
relay_shared_metrics._reset_for_tests()
relay_runtime._reset_for_tests()
monkeypatch.setattr(relay_runtime, "_load_nemo_relay", lambda: fake)
monkeypatch.setitem(sys.modules, "nemo_relay", fake)
sys.modules.pop("plugins.observability.nemo_relay", None)
plugin = importlib.import_module("plugins.observability.nemo_relay")
plugin.reset_for_tests()
return plugin
def _enable_dynamic_plugin(tmp_path, monkeypatch) -> Path:
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
f"""
version = 1
[[dynamic_plugins]]
plugin_id = "fixture"
kind = "rust_dynamic"
manifest_ref = "{(tmp_path / "fixture" / "relay-plugin.toml").as_posix()}"
[dynamic_plugins.config]
mode = "test"
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
return plugins_toml
def test_manifest_fields():
data = yaml.safe_load((PLUGIN_DIR / "plugin.yaml").read_text())
assert data["name"] == "nemo_relay"
assert set(data["hooks"]) == {
"on_session_start",
"on_session_end",
"on_session_finalize",
"on_session_reset",
"pre_llm_call",
"post_llm_call",
"pre_approval_request",
"post_approval_response",
"subagent_start",
"subagent_stop",
}
def test_nemo_relay_plugin_is_discoverable_as_bundled_plugin(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test"))
manager = PluginManager()
manager.discover_and_load()
loaded = manager._plugins["observability/nemo_relay"]
assert loaded.manifest.name == "nemo_relay"
assert loaded.manifest.source == "bundled"
assert not loaded.enabled
def test_nemo_relay_plugin_uses_nemo_relay_runtime(monkeypatch):
fake_relay = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake_relay)
plugin.on_session_start(session_id="s1")
assert any(event[0] == "scope.push" for event in fake_relay.events)
def test_nemo_relay_plugin_exports_core_managed_llm_and_tool_events(
tmp_path,
monkeypatch,
):
from agent import relay_llm, relay_tools
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
monkeypatch.setenv("HERMES_NEMO_RELAY_ATOF_ENABLED", "1")
monkeypatch.setenv("HERMES_NEMO_RELAY_ATOF_OUTPUT_DIRECTORY", str(tmp_path / "atof"))
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_ENABLED", "1")
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY", str(tmp_path / "atif"))
base = {
"session_id": "s1",
"task_id": "t1",
"turn_id": "turn-1",
"telemetry_schema_version": "hermes.observer.v1",
}
plugin.on_session_start(**base, model="demo-model", platform="cli")
coordinator = relay_runtime.SESSION_COORDINATOR
lease = coordinator.acquire_conversation(
profile_key=relay_runtime.current_profile_key(),
session_id="s1",
platform="cli",
)
turn = coordinator.begin_turn(
lease,
turn_id="turn-1",
task_id="t1",
)
relay_llm.execute(
{"messages": [{"role": "user", "content": "hi"}]},
lambda request: {
"assistant_message": {"role": "assistant", "content": "hello"},
"request": request,
},
session_id="s1",
name="openai",
model_name="demo-model",
metadata={"api_request_id": "api-1", "api_mode": "custom"},
)
relay_tools.execute(
"read_file",
{"path": "x"},
lambda _args: {"ok": True},
session_id="s1",
metadata={"tool_call_id": "tool-1"},
)
coordinator.end_turn(turn, outcome="success")
coordinator.release_conversation(lease)
plugin.on_session_end(**base, completed=True, interrupted=False)
plugin.on_session_finalize(**base, reason="shutdown")
event_names = [event[0] for event in fake.events]
assert "atof.register" in event_names
assert "atif.register" in event_names
assert event_names.count("llm.call") == 1
assert event_names.count("llm.call_end") == 1
assert event_names.count("tool.call") == 1
assert event_names.count("tool.call_end") == 1
assert "scope.pop" in event_names
assert (tmp_path / "atif" / "hermes-atif-s1.json").exists()
def test_shared_metrics_and_rich_plugin_share_one_core_session(
tmp_path,
monkeypatch,
):
from agent import relay_llm
fake = _FakeNemoRelay()
hermes_home = tmp_path / "hermes-home"
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_ENABLED", "1")
monkeypatch.setenv(
"HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY", str(tmp_path / "atif")
)
monkeypatch.setattr(
"hermes_cli.config.load_config_readonly",
lambda: {"telemetry": {"shared_metrics": {"enabled": True}}},
)
plugin = _fresh_plugin(monkeypatch, fake)
manager = PluginManager()
class _Context:
def register_hook(self, name, callback):
manager._hooks.setdefault(name, []).append(callback)
plugin.register(_Context())
monkeypatch.setattr(plugin_api, "_plugin_manager", manager)
event = {
"session_id": "s1",
"task_id": "t1",
"api_request_id": "api-1",
"provider": "anthropic",
"model": "claude-sonnet",
"platform": "cli",
}
coordinator = relay_runtime.SESSION_COORDINATOR
lease = coordinator.acquire_conversation(
profile_key=relay_runtime.current_profile_key(),
session_id="s1",
platform="cli",
model=event["model"],
)
lifecycle.invoke_hook("on_session_start", **event)
turn = coordinator.begin_turn(
lease,
turn_id="turn-1",
task_id="t1",
)
lifecycle.invoke_hook(
"pre_api_request",
**event,
request={"body": {"messages": [{"role": "user", "content": "hi"}]}},
)
relay_llm.execute(
{"messages": [{"role": "user", "content": "hi"}]},
lambda _request: {
"assistant_message": {"role": "assistant", "content": "hello"}
},
session_id="s1",
name="anthropic",
model_name="claude-sonnet",
metadata={"api_request_id": "api-1", "api_mode": "custom"},
)
lifecycle.invoke_hook(
"post_api_request",
**event,
response={"assistant_message": {"role": "assistant", "content": "hello"}},
)
coordinator.end_turn(turn, outcome="success")
coordinator.release_conversation(lease)
lifecycle.finalize_session(session_id="s1")
session_pushes = [
item
for item in fake.events
if item[0] == "scope.push" and item[1] == relay_runtime.SESSION_SCOPE
]
assert len(session_pushes) == 1
register_metrics = next(
index
for index, item in enumerate(fake.events)
if item[0] == "subscribers.register"
and item[1].startswith("hermes.nemo_relay.shared_metrics.")
)
register_atif = next(
index for index, item in enumerate(fake.events) if item[0] == "atif.register"
)
open_session = fake.events.index(session_pushes[0])
assert register_metrics < register_atif < open_session
plugin_runtime = plugin._get_runtime()
assert plugin_runtime is not None
assert not plugin_runtime.sessions
assert relay_runtime.get_session_handle("s1") is None
packages = list(
(hermes_home / "telemetry" / "shared_metrics" / "outbox").glob("*.json")
)
assert len(packages) == 1
package = json.loads(packages[0].read_text(encoding="utf-8"))
assert package["metrics"][0]["name"] == "hermes.model_call.count"
assert package["metrics"][0]["value"] == 1
assert (tmp_path / "atif" / "hermes-atif-s1.json").exists()
def test_nemo_relay_plugin_emits_approval_marks(monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugin.on_pre_approval_request(session_id="s1", approval_id="approval-1", tool_name="shell")
plugin.on_post_approval_response(session_id="s1", approval_id="approval-1", approved=True)
mark_names = [event[1] for event in fake.events if event[0] == "scope.event"]
assert "hermes.approval.request" in mark_names
assert "hermes.approval.response" in mark_names
def test_nemo_relay_plugin_metadata_promotes_trajectory_and_subagent_ids(monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugin.on_pre_llm_call(
session_id="parent-session",
task_id="task-1",
turn_id="turn-1",
telemetry_schema_version="hermes.observer.v1",
)
plugin.on_subagent_start(
parent_session_id="parent-session",
parent_turn_id="turn-1",
parent_subagent_id="parent-sa",
child_session_id="child-session",
child_subagent_id="child-sa",
child_role="leaf",
telemetry_schema_version="hermes.observer.v1",
)
plugin.on_subagent_stop(
parent_session_id="parent-session",
parent_turn_id="turn-1",
child_session_id="child-session",
child_role="leaf",
child_status="completed",
telemetry_schema_version="hermes.observer.v1",
)
turn_mark = next(event for event in fake.events if event[0] == "scope.event" and event[1] == "hermes.turn.start")
turn_metadata = turn_mark[2]["metadata"]
assert turn_metadata["session_id"] == "parent-session"
assert turn_metadata["trajectory_id"] == "parent-session"
start_mark = next(event for event in fake.events if event[0] == "scope.event" and event[1] == "hermes.subagent.start")
start_metadata = start_mark[2]["metadata"]
assert start_metadata["parent_session_id"] == "parent-session"
assert start_metadata["parent_trajectory_id"] == "parent-session"
assert start_metadata["child_session_id"] == "child-session"
assert start_metadata["child_trajectory_id"] == "child-session"
assert start_metadata["child_subagent_id"] == "child-sa"
assert start_metadata["child_role"] == "leaf"
stop_mark = next(event for event in fake.events if event[0] == "scope.event" and event[1] == "hermes.subagent.stop")
assert stop_mark[2]["metadata"]["child_status"] == "completed"
def test_nemo_relay_plugin_reuses_core_parented_child_scope_for_embedded_atif(monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugin.on_session_start(session_id="parent-session")
plugin.on_subagent_start(
parent_session_id="parent-session",
parent_turn_id="turn-1",
child_session_id="child-session",
child_subagent_id="child-sa",
child_role="leaf",
telemetry_schema_version="hermes.observer.v1",
)
runtime = plugin._get_runtime()
assert runtime is not None
assert runtime.host.get_session("child-session") is None
runtime.host.register_subagent(
{
"parent_session_id": "parent-session",
"child_session_id": "child-session",
}
)
plugin.on_session_start(session_id="child-session")
session_pushes = [
event
for event in fake.events
if event[0] == "scope.push" and event[1] == relay_runtime.SESSION_SCOPE
]
assert len(session_pushes) == 2
child_push = session_pushes[1]
child_kwargs = child_push[3]
assert child_kwargs["handle"] == runtime.sessions["parent-session"].handle
assert child_kwargs["metadata"]["nemo_relay_scope_role"] == "subagent"
assert "session_id" not in child_kwargs["metadata"]
assert "subagent_id" not in child_kwargs["metadata"]
assert runtime.sessions["child-session"].parent_session_id == "parent-session"
runtime.host.unregister_subagent({"child_session_id": "child-session"})
assert runtime.host.get_session("child-session") is None
child_close_index = max(
index
for index, event in enumerate(fake.events)
if event[0] == "scope.pop" and event[1] == runtime.sessions["child-session"].handle
)
plugin.on_subagent_stop(
parent_session_id="parent-session",
child_session_id="child-session",
child_status="completed",
)
assert "child-session" not in runtime.sessions
assert runtime.host.get_session("child-session") is None
stop_mark_index = next(
index
for index, event in enumerate(fake.events)
if event[0] == "scope.event" and event[1] == "hermes.subagent.stop"
)
assert child_close_index < stop_mark_index
def test_nemo_relay_plugin_skips_embedded_child_atif_file_by_default(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_ENABLED", "1")
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY", str(tmp_path / "atif"))
plugin.on_session_start(session_id="parent-session")
plugin.on_subagent_start(
parent_session_id="parent-session",
child_session_id="child-session",
child_subagent_id="child-sa",
)
plugin.on_session_start(session_id="child-session")
plugin.on_session_end(session_id="child-session")
plugin.on_session_finalize(session_id="child-session")
plugin.on_session_end(session_id="parent-session")
plugin.on_session_finalize(session_id="parent-session")
assert (tmp_path / "atif" / "hermes-atif-parent-session.json").exists()
assert not (tmp_path / "atif" / "hermes-atif-child-session.json").exists()
def test_nemo_relay_plugin_can_write_embedded_child_atif_file_in_all_mode(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_ENABLED", "1")
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY", str(tmp_path / "atif"))
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_SUBAGENT_EXPORT_MODE", "all")
plugin.on_session_start(session_id="parent-session")
plugin.on_subagent_start(
parent_session_id="parent-session",
child_session_id="child-session",
child_subagent_id="child-sa",
)
plugin.on_session_start(session_id="child-session")
plugin.on_session_end(session_id="child-session")
plugin.on_session_finalize(session_id="child-session")
plugin.on_session_end(session_id="parent-session")
plugin.on_session_finalize(session_id="parent-session")
assert (tmp_path / "atif" / "hermes-atif-parent-session.json").exists()
assert (tmp_path / "atif" / "hermes-atif-child-session.json").exists()
def test_nemo_relay_plugin_can_initialize_plugins_toml(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
atof_dir = tmp_path / "exports" / "events"
atif_dir = tmp_path / "exports" / "trajectories"
plugins_toml.write_text(
f"""
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config.atof]
enabled = true
output_directory = "{atof_dir}"
[components.config.atif]
enabled = true
output_directory = "{atif_dir}"
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
plugin.on_session_start(session_id="s1")
assert any(event[0] == "plugin.initialize" for event in fake.events)
assert not any(event[0] == "atof.register" for event in fake.events)
assert atof_dir.is_dir()
assert atif_dir.is_dir()
def test_nemo_relay_plugin_clears_plugins_toml_on_final_session_finalize_and_reinitializes(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
"""
version = 1
[[components]]
kind = "observability"
enabled = true
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
plugin.on_session_start(session_id="s1")
plugin.on_session_finalize(session_id="s1", reason="shutdown")
plugin.on_session_start(session_id="s2")
event_names = [event[0] for event in fake.events]
assert event_names.count("plugin.initialize") == 2
assert event_names.count("plugin.clear") == 1
def test_nemo_relay_plugin_activates_and_owns_dynamic_plugins(tmp_path, monkeypatch):
from agent import relay_llm, relay_tools
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
_enable_dynamic_plugin(tmp_path, monkeypatch)
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_ENABLED", "1")
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY", str(tmp_path / "atif"))
plugin.on_session_start(session_id="s1")
runtime = plugin._get_runtime()
assert runtime is not None
assert runtime._plugin_activation is not None
coordinator = relay_runtime.SESSION_COORDINATOR
lease = coordinator.acquire_conversation(
profile_key=relay_runtime.current_profile_key(),
session_id="s1",
platform="cli",
)
turn = coordinator.begin_turn(lease, turn_id="turn-1", task_id="task-1")
llm_result = relay_llm.execute(
{"messages": []},
lambda request: {"request": request},
session_id="s1",
name="openai",
model_name="fixture",
metadata={"api_mode": "custom", "api_request_id": "api-1"},
)
tool_args = relay_runtime.apply_tool_request_intercepts(
session_id="s1",
tool_name="fixture-tool",
args={"value": 1},
)
tool_result, final_args = relay_tools.execute(
"fixture-tool",
tool_args,
lambda args: {"args": args},
session_id="s1",
metadata={"tool_call_id": "tool-1"},
)
coordinator.end_turn(turn, outcome="success")
coordinator.release_conversation(lease)
assert llm_result["request"]["intercepted"] is True
assert tool_result["args"]["intercepted"] is True
assert final_args["intercepted"] is True
relay_runtime.SESSION_COORDINATOR.finalize_conversation(
profile_key=relay_runtime.current_profile_key(),
session_id="s1",
)
plugin.on_session_finalize(session_id="s1", reason="shutdown")
assert runtime._plugin_activation is not None
assert not any(event[0] == "plugin.activation.close" for event in fake.events)
plugin.on_session_start(session_id="s2")
plugin.on_session_finalize(session_id="s2", reason="shutdown")
assert sum(event[0] == "plugin.activate_dynamic" for event in fake.events) == 1
activation = next(event for event in fake.events if event[0] == "plugin.activate_dynamic")
assert "dynamic_plugins" not in activation[1]
assert activation[2] == [
{
"plugin_id": "fixture",
"kind": "rust_dynamic",
"manifest_ref": str(tmp_path / "fixture" / "relay-plugin.toml"),
"config": {"mode": "test"},
}
]
event_names = [event[0] for event in fake.events]
assert "plugin.clear" not in event_names
assert event_names.index("subscribers.flush") < event_names.index("atif.export")
assert event_names.index("atif.export") < event_names.index("atif.deregister")
runtime.shutdown()
event_names = [event[0] for event in fake.events]
assert event_names.index("atif.deregister") < event_names.index("plugin.activation.close")
def test_nemo_relay_plugin_uses_real_0_6_dynamic_activation_api(
tmp_path,
monkeypatch,
):
relay = pytest.importorskip("nemo_relay")
if getattr(relay, "_native", None) is None:
pytest.skip("NeMo Relay native binding is unavailable on this platform")
plugin = _fresh_plugin(monkeypatch, relay)
_enable_dynamic_plugin(tmp_path, monkeypatch)
calls = []
class _NativeActivation:
def __init__(self):
self.report = {"diagnostics": []}
self.is_active = True
async def close(self):
self.is_active = False
async def _initialize_with_dynamic_plugins(config, dynamic_plugins):
calls.append((config, dynamic_plugins))
return _NativeActivation()
monkeypatch.setattr(
relay.plugin,
"_initialize_with_dynamic_plugins",
_initialize_with_dynamic_plugins,
)
plugin.on_session_start(session_id="s1")
runtime = plugin._get_runtime()
assert runtime is not None
assert isinstance(runtime._plugin_activation, relay.plugin.PluginHostActivation)
assert calls == [
(
{"version": 1},
[
{
"plugin_id": "fixture",
"kind": "rust_dynamic",
"manifest_ref": str(tmp_path / "fixture" / "relay-plugin.toml"),
"config": {"mode": "test"},
}
],
)
]
activation = runtime._plugin_activation
runtime.shutdown()
assert activation.is_active is False
def test_real_binding_shares_plugin_configuration_across_two_profiles(
tmp_path,
monkeypatch,
):
relay = pytest.importorskip("nemo_relay")
if getattr(relay, "_native", None) is None:
pytest.skip("NeMo Relay native binding is unavailable on this platform")
plugin = _fresh_plugin(monkeypatch, relay)
original_initialize = relay.plugin.initialize
original_clear = relay.plugin.clear
original_clear()
initialize_calls = []
clear_calls = 0
async def _initialize(config):
initialize_calls.append(config)
return await original_initialize(config)
def _clear():
nonlocal clear_calls
clear_calls += 1
return original_clear()
monkeypatch.setattr(relay.plugin, "initialize", _initialize)
monkeypatch.setattr(relay.plugin, "clear", _clear)
monkeypatch.setattr(
plugin,
"_load_settings",
lambda: plugin._Settings(plugins_config={"version": 1}),
)
profile_a = str(tmp_path / "profile-a")
profile_b = str(tmp_path / "profile-b")
host_a = relay_runtime.RelayRuntime(relay=relay, profile_key=profile_a)
host_b = relay_runtime.RelayRuntime(relay=relay, profile_key=profile_b)
try:
runtime_a = plugin._get_runtime(profile_key=profile_a, host=host_a)
runtime_b = plugin._get_runtime(profile_key=profile_b, host=host_b)
assert runtime_a is not None
assert runtime_b is not None
runtime_a.ensure_session({"session_id": "session-a"})
runtime_b.ensure_session({"session_id": "session-b"})
assert initialize_calls == [{"version": 1}]
assert relay.plugin.report() is not None
runtime_a.close_session({"session_id": "session-a"})
assert clear_calls == 0
assert relay.plugin.report() is not None
assert runtime_b.host.get_session("session-b") is not None
runtime_b.close_session({"session_id": "session-b"})
assert clear_calls == 1
assert relay.plugin.report() is None
finally:
plugin.reset_for_tests()
host_a.shutdown()
host_b.shutdown()
original_clear()
def test_real_binding_does_not_replace_another_profiles_plugin_configuration(
tmp_path,
monkeypatch,
):
relay = pytest.importorskip("nemo_relay")
if getattr(relay, "_native", None) is None:
pytest.skip("NeMo Relay native binding is unavailable on this platform")
plugin = _fresh_plugin(monkeypatch, relay)
original_initialize = relay.plugin.initialize
original_clear = relay.plugin.clear
original_clear()
initialize_calls = []
clear_calls = 0
async def _initialize(config):
initialize_calls.append(config)
return await original_initialize(config)
def _clear():
nonlocal clear_calls
clear_calls += 1
return original_clear()
settings = iter((
plugin._Settings(
plugins_config={"version": 1, "policy": {"unsupported": "warn"}}
),
plugin._Settings(
plugins_config={"version": 1, "policy": {"unsupported": "ignore"}}
),
))
monkeypatch.setattr(relay.plugin, "initialize", _initialize)
monkeypatch.setattr(relay.plugin, "clear", _clear)
monkeypatch.setattr(plugin, "_load_settings", lambda: next(settings))
profile_a = str(tmp_path / "profile-a")
profile_b = str(tmp_path / "profile-b")
host_a = relay_runtime.RelayRuntime(relay=relay, profile_key=profile_a)
host_b = relay_runtime.RelayRuntime(relay=relay, profile_key=profile_b)
try:
runtime_a = plugin._get_runtime(profile_key=profile_a, host=host_a)
runtime_b = plugin._get_runtime(profile_key=profile_b, host=host_b)
assert runtime_a is not None
assert runtime_b is not None
assert initialize_calls == [
{"version": 1, "policy": {"unsupported": "warn"}}
]
assert runtime_a._plugin_config_initialized is True
assert runtime_b._plugin_config_initialized is False
assert relay.plugin.report() is not None
runtime_b.shutdown()
assert clear_calls == 0
assert relay.plugin.report() is not None
runtime_a.shutdown()
assert clear_calls == 1
assert relay.plugin.report() is None
finally:
plugin.reset_for_tests()
host_a.shutdown()
host_b.shutdown()
original_clear()
def test_nemo_relay_rejects_gateway_dynamic_config_with_actionable_diagnostic(
tmp_path, monkeypatch, caplog
):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
"""
version = 1
[[plugins.dynamic]]
manifest = "plugins/fixture/relay-plugin.toml"
[plugins.dynamic.config]
mode = "test"
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
with caplog.at_level("ERROR"):
plugin.on_session_start(session_id="s1")
assert not any(event[0] == "plugin.activate_dynamic" for event in fake.events)
initialize = next(event for event in fake.events if event[0] == "plugin.initialize")
assert initialize[1] == {"version": 1}
assert "does not expose the CLI lifecycle resolver" in caplog.text
assert "Use Hermes-owned [[dynamic_plugins]]" in caplog.text
def test_nemo_relay_explicit_dynamic_paths_resolve_from_plugins_toml(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
config_dir = tmp_path / "config"
config_dir.mkdir()
plugins_toml = config_dir / "plugins.toml"
plugins_toml.write_text(
"""
version = 1
[[dynamic_plugins]]
plugin_id = "worker-fixture"
kind = "worker"
manifest_ref = "../plugins/worker/relay-plugin.toml"
environment_ref = "../environments/worker-fixture"
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
plugin.on_session_start(session_id="s1")
activation = next(event for event in fake.events if event[0] == "plugin.activate_dynamic")
assert activation[2] == [
{
"plugin_id": "worker-fixture",
"kind": "worker",
"manifest_ref": str(tmp_path / "plugins" / "worker" / "relay-plugin.toml"),
"environment_ref": str(tmp_path / "environments" / "worker-fixture"),
"config": {},
}
]
runtime = plugin._get_runtime()
assert runtime is not None
runtime.shutdown()
def test_relay_tool_request_rewrite_precedes_hermes_authorization_boundary(
tmp_path,
monkeypatch,
):
from hermes_cli.middleware import apply_tool_request_middleware
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
_enable_dynamic_plugin(tmp_path, monkeypatch)
plugin.on_session_start(session_id="s1")
result = apply_tool_request_middleware(
"fixture-tool",
{"value": 1},
session_id="s1",
tool_call_id="tool-1",
)
assert result.payload == {"intercepted": True, "value": 1}
assert result.trace[0] == {"source": "nemo_relay"}
def test_nemo_relay_plugin_activates_without_duplicate_execution_hooks(
tmp_path, monkeypatch
):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
_enable_dynamic_plugin(tmp_path, monkeypatch)
registered_hooks = []
class _Context:
def register_hook(self, name, callback):
del callback
registered_hooks.append(name)
def register_middleware(self, name, callback):
del callback
fake.events.append(("hermes.register_middleware", name))
plugin.register(_Context())
event_names = [event[0] for event in fake.events]
assert "plugin.activate_dynamic" in event_names
assert "hermes.register_middleware" not in event_names
assert not {
"pre_api_request",
"post_api_request",
"api_request_error",
"pre_tool_call",
"post_tool_call",
}.intersection(registered_hooks)
runtime = plugin._get_runtime()
assert runtime is not None
runtime.shutdown()
def test_nemo_relay_plugin_degrades_to_static_config_on_relay_0_5(
tmp_path, monkeypatch, caplog
):
fake = _FakeNemoRelay()
delattr(fake.plugin, "initialize_with_dynamic_plugins")
plugin = _fresh_plugin(monkeypatch, fake)
_enable_dynamic_plugin(tmp_path, monkeypatch)
with caplog.at_level("WARNING"):
plugin.on_session_start(session_id="s1")
initialize = next(event for event in fake.events if event[0] == "plugin.initialize")
assert "dynamic_plugins" not in initialize[1]
assert not any(event[0] == "plugin.activate_dynamic" for event in fake.events)
assert "available in NeMo Relay 0.6+" in caplog.text
def test_nemo_relay_plugin_rejects_invalid_dynamic_specs(tmp_path, monkeypatch, caplog):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
"""
version = 1
dynamic_plugins = [{ kind = "rust_dynamic", manifest_ref = "missing-id" }]
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
with caplog.at_level("WARNING"):
plugin.on_session_start(session_id="s1")
assert not any(event[0] == "plugin.activate_dynamic" for event in fake.events)
assert "plugin_id is required" in caplog.text
def test_nemo_relay_plugin_rejects_entire_mixed_valid_invalid_dynamic_request(
tmp_path, monkeypatch, caplog
):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
f"""
version = 1
[[dynamic_plugins]]
plugin_id = "valid-fixture"
kind = "rust_dynamic"
manifest_ref = "{(tmp_path / "valid" / "relay-plugin.toml").as_posix()}"
[[dynamic_plugins]]
kind = "worker"
manifest_ref = "{(tmp_path / "invalid" / "relay-plugin.toml").as_posix()}"
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
with caplog.at_level("WARNING"):
plugin.on_session_start(session_id="s1")
assert not any(event[0] == "plugin.activate_dynamic" for event in fake.events)
initialize = next(event for event in fake.events if event[0] == "plugin.initialize")
assert "dynamic_plugins" not in initialize[1]
assert "no dynamic plugins will be activated" in caplog.text
def test_nemo_relay_plugin_registers_shutdown_after_dynamic_retry(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
activation_attempts = 0
async def _flaky_activate(config, dynamic_plugins):
nonlocal activation_attempts
activation_attempts += 1
fake.events.append(
("plugin.activate_dynamic.attempt", activation_attempts, config, dynamic_plugins)
)
if activation_attempts == 1:
raise RuntimeError("temporary activation failure")
return _FakePluginActivation(fake.events)
fake.plugin.initialize_with_dynamic_plugins = _flaky_activate
plugin = _fresh_plugin(monkeypatch, fake)
_enable_dynamic_plugin(tmp_path, monkeypatch)
plugin.on_session_start(session_id="s1")
plugin.on_session_finalize(session_id="s1")
plugin.on_session_start(session_id="s2")
runtime = plugin._get_runtime()
assert runtime is not None
assert runtime._plugin_activation is not None
assert runtime._shutdown_registered is True
assert activation_attempts == 2
runtime.shutdown()
assert any(event[0] == "plugin.activation.close" for event in fake.events)
def test_nemo_relay_plugin_attempts_activation_close_after_subscriber_flush_failure(
tmp_path, monkeypatch, caplog
):
fake = _FakeNemoRelay()
def _failing_flush():
fake.events.append(("subscribers.flush.failed",))
raise RuntimeError("flush boom")
fake.subscribers.flush = _failing_flush
plugin = _fresh_plugin(monkeypatch, fake)
_enable_dynamic_plugin(tmp_path, monkeypatch)
plugin.on_session_start(session_id="s1")
runtime = plugin._get_runtime()
assert runtime is not None
with caplog.at_level("WARNING"):
runtime.shutdown()
event_names = [event[0] for event in fake.events]
assert event_names.count("subscribers.flush.failed") == 2
flush_indices = [
index for index, name in enumerate(event_names) if name == "subscribers.flush.failed"
]
assert max(flush_indices) < event_names.index("plugin.activation.close")
assert runtime._plugin_activation is None
assert "subscriber flush failed: flush boom" in caplog.text
def test_nemo_relay_plugin_continues_shutdown_after_atif_export_failure(
tmp_path, monkeypatch, caplog
):
fake = _FakeNemoRelay()
class _FailingAtifExporter(_FakeAtifExporter):
def export_json(self):
self.events.append(("atif.export.failed", self.session_id))
raise OSError("disk full")
fake.AtifExporter = lambda session_id, agent_name, agent_version, **kwargs: (
_FailingAtifExporter(fake.events, session_id, agent_name, agent_version, kwargs)
)
plugin = _fresh_plugin(monkeypatch, fake)
_enable_dynamic_plugin(tmp_path, monkeypatch)
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_ENABLED", "1")
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY", str(tmp_path / "atif"))
plugin.on_session_start(session_id="s1")
runtime = plugin._get_runtime()
assert runtime is not None
with caplog.at_level("WARNING"):
runtime.shutdown()
event_names = [event[0] for event in fake.events]
assert event_names.index("atif.export.failed") < event_names.index("atif.deregister")
assert event_names.index("atif.deregister") < event_names.index("plugin.activation.close")
assert runtime._plugin_activation is None
assert "ATIF export failed: disk full" in caplog.text
def test_nemo_relay_plugin_keeps_plugins_toml_active_while_other_sessions_remain(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
"""
version = 1
[[components]]
kind = "observability"
enabled = true
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
plugin.on_session_start(session_id="parent")
plugin.on_session_start(session_id="child")
plugin.on_session_finalize(session_id="child", reason="shutdown")
plugin.on_session_finalize(session_id="parent", reason="shutdown")
event_names = [event[0] for event in fake.events]
assert event_names.count("plugin.initialize") == 1
assert event_names.count("plugin.clear") == 1
def test_nemo_relay_plugin_reinitializes_plugins_toml_inside_active_event_loop(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
"""
version = 1
[[components]]
kind = "observability"
enabled = true
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
async def _drive() -> None:
plugin.on_session_start(session_id="s1")
plugin.on_session_finalize(session_id="s1", reason="shutdown")
plugin.on_session_start(session_id="s2")
await asyncio.sleep(0)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
asyncio.run(_drive())
gc.collect()
assert not any("was never awaited" in str(w.message) for w in caught)
runtime = plugin._get_runtime()
assert runtime is not None
assert runtime._plugin_config_initialized is True
scope_push_names = [event[1] for event in fake.events if event[0] == "scope.push"]
assert relay_runtime.SESSION_SCOPE in scope_push_names
def test_nemo_relay_plugin_retries_plugins_toml_after_clear_failure(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
initialize_calls = 0
async def _counting_initialize(config):
nonlocal initialize_calls
initialize_calls += 1
fake.events.append(("plugin.initialize.attempt", initialize_calls, config))
return {"diagnostics": []}
async def _failing_clear():
fake.events.append(("plugin.clear.failed",))
raise RuntimeError("boom")
fake.plugin.initialize = _counting_initialize
fake.plugin.clear = _failing_clear
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
"""
version = 1
[[components]]
kind = "observability"
enabled = true
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
plugin.on_session_start(session_id="s1")
plugin.on_session_finalize(session_id="s1", reason="shutdown")
plugin.on_session_start(session_id="s2")
event_names = [event[0] for event in fake.events]
assert event_names.count("plugin.initialize.attempt") == 2
assert event_names.count("plugin.clear.failed") == 1
scope_push_names = [event[1] for event in fake.events if event[0] == "scope.push"]
assert relay_runtime.SESSION_SCOPE in scope_push_names
def test_nemo_relay_plugin_disables_direct_atif_when_plugins_toml_owns_atif(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
f"""
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config.atif]
enabled = true
output_directory = "{(tmp_path / "managed-atif").as_posix()}"
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_ENABLED", "1")
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY", str(tmp_path / "direct-atif"))
plugin.on_session_start(session_id="s1")
plugin.on_session_finalize(session_id="s1", reason="shutdown")
event_names = [event[0] for event in fake.events]
assert "plugin.initialize" in event_names
assert "plugin.clear" in event_names
assert "atif.register" not in event_names
assert not (tmp_path / "direct-atif" / "hermes-atif-s1.json").exists()
def test_nemo_relay_plugin_keeps_direct_atif_when_plugins_toml_init_fails(tmp_path, monkeypatch):
fake = _FakeNemoRelay()
async def _failing_initialize(config):
fake.events.append(("plugin.initialize.failed", config))
raise RuntimeError("boom")
fake.plugin.initialize = _failing_initialize
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
f"""
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config.atif]
enabled = true
output_directory = "{(tmp_path / "managed-atif").as_posix()}"
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_ENABLED", "1")
monkeypatch.setenv("HERMES_NEMO_RELAY_ATIF_OUTPUT_DIRECTORY", str(tmp_path / "direct-atif"))
plugin.on_session_start(session_id="s1")
plugin.on_session_finalize(session_id="s1", reason="shutdown")
event_names = [event[0] for event in fake.events]
assert "plugin.initialize.failed" in event_names
assert "plugin.clear" not in event_names
assert "atif.register" in event_names
assert (tmp_path / "direct-atif" / "hermes-atif-s1.json").exists()
def test_nemo_relay_plugin_retries_plugins_toml_after_fallback_only_session_and_clears_direct_atof(
tmp_path,
monkeypatch,
):
fake = _FakeNemoRelay()
initialize_calls = 0
async def _flaky_initialize(config):
nonlocal initialize_calls
initialize_calls += 1
fake.events.append(("plugin.initialize.attempt", initialize_calls, config))
if initialize_calls == 1:
raise RuntimeError("boom")
return {"diagnostics": []}
fake.plugin.initialize = _flaky_initialize
plugin = _fresh_plugin(monkeypatch, fake)
plugins_toml = tmp_path / "plugins.toml"
plugins_toml.write_text(
f"""
version = 1
[[components]]
kind = "observability"
enabled = true
[components.config.atof]
enabled = true
output_directory = "{(tmp_path / "managed-atof").as_posix()}"
""",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_NEMO_RELAY_PLUGINS_TOML", str(plugins_toml))
monkeypatch.setenv("HERMES_NEMO_RELAY_ATOF_ENABLED", "1")
monkeypatch.setenv("HERMES_NEMO_RELAY_ATOF_OUTPUT_DIRECTORY", str(tmp_path / "direct-atof"))
plugin.on_session_start(session_id="s1")
plugin.on_session_finalize(session_id="s1", reason="shutdown")
plugin.on_session_start(session_id="s2")
runtime = plugin._get_runtime()
assert runtime is not None
assert runtime._plugin_config_initialized is True
event_names = [event[0] for event in fake.events]
assert event_names.count("plugin.initialize.attempt") == 2
assert event_names.count("atof.register") == 1
assert event_names.count("atof.deregister") == 1