mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-20 10:11:58 +00:00
Merge pull request #29724 from bbednarski9/bbednarski/nmf-41B-nemoflow-plugin
feat(middleware): add adaptive middleware to hermes-agent, consumed by NeMo-Relay
This commit is contained in:
commit
d4a7bfd3aa
14 changed files with 2170 additions and 151 deletions
|
|
@ -18,8 +18,15 @@ from hermes_cli.plugins import (
|
|||
get_plugin_command_handler,
|
||||
get_plugin_commands,
|
||||
get_pre_tool_call_block_message,
|
||||
has_middleware,
|
||||
resolve_plugin_command_result,
|
||||
)
|
||||
from hermes_cli.middleware import (
|
||||
VALID_MIDDLEWARE,
|
||||
apply_llm_request_middleware,
|
||||
apply_tool_request_middleware,
|
||||
run_tool_execution_middleware,
|
||||
)
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -96,6 +103,223 @@ class TestPluginDiscovery:
|
|||
assert "hello_plugin" in mgr._plugins
|
||||
assert mgr._plugins["hello_plugin"].enabled
|
||||
|
||||
def test_plugin_can_register_and_invoke_middleware(self, tmp_path, monkeypatch):
|
||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||
_make_plugin_dir(
|
||||
plugins_dir,
|
||||
"mw_plugin",
|
||||
register_body=(
|
||||
"ctx.register_middleware('llm_request', "
|
||||
"lambda **kw: {'request': {**kw['request'], 'mw': True}})\n"
|
||||
" ctx.register_middleware('tool_request', "
|
||||
"lambda **kw: {'args': {**kw['args'], 'mw': True}})"
|
||||
),
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test"))
|
||||
|
||||
mgr = PluginManager()
|
||||
mgr.discover_and_load()
|
||||
|
||||
assert "llm_request" in VALID_MIDDLEWARE
|
||||
assert "tool_request" in VALID_MIDDLEWARE
|
||||
assert set(mgr._plugins["mw_plugin"].middleware_registered) == {"llm_request", "tool_request"}
|
||||
assert mgr.invoke_middleware("llm_request", request={"messages": []}) == [
|
||||
{"request": {"messages": [], "mw": True}}
|
||||
]
|
||||
assert mgr.invoke_middleware("tool_request", args={"path": "README.md"}) == [
|
||||
{"args": {"path": "README.md", "mw": True}}
|
||||
]
|
||||
assert mgr.has_middleware("llm_request") is True
|
||||
|
||||
def test_execution_middleware_does_not_retry_downstream_failure(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def middleware(**kwargs):
|
||||
return kwargs["next_call"](kwargs["args"])
|
||||
|
||||
manager = types.SimpleNamespace(_middleware={"tool_execution": [middleware]})
|
||||
monkeypatch.setattr("hermes_cli.plugins.get_plugin_manager", lambda: manager)
|
||||
|
||||
def terminal(args):
|
||||
calls.append(args)
|
||||
raise RuntimeError("tool failed")
|
||||
|
||||
with pytest.raises(RuntimeError, match="tool failed"):
|
||||
run_tool_execution_middleware("terminal", {"command": "false"}, terminal)
|
||||
|
||||
assert calls == [{"command": "false"}]
|
||||
|
||||
def test_middleware_helpers_skip_no_listener_work(self, monkeypatch):
|
||||
manager = types.SimpleNamespace(_middleware={})
|
||||
monkeypatch.setattr("hermes_cli.plugins.get_plugin_manager", lambda: manager)
|
||||
|
||||
request = {"messages": []}
|
||||
args = {"path": "README.md"}
|
||||
|
||||
llm_result = apply_llm_request_middleware(request)
|
||||
tool_result = apply_tool_request_middleware("read_file", args)
|
||||
|
||||
assert llm_result.payload is request
|
||||
assert llm_result.original_payload is request
|
||||
assert llm_result.changed is False
|
||||
assert llm_result.trace == []
|
||||
assert tool_result.payload is args
|
||||
assert tool_result.original_payload is args
|
||||
assert tool_result.changed is False
|
||||
assert tool_result.trace == []
|
||||
assert run_tool_execution_middleware("terminal", args, lambda payload: payload) is args
|
||||
assert has_middleware("tool_request") is False
|
||||
|
||||
def test_request_middleware_changed_tracks_trace_not_deep_equality(self, monkeypatch):
|
||||
def same_payload_middleware(**kwargs):
|
||||
return {"args": kwargs["args"], "source": "same-payload"}
|
||||
|
||||
manager = types.SimpleNamespace(
|
||||
_middleware={"tool_request": [same_payload_middleware]},
|
||||
invoke_middleware=lambda kind, **kwargs: [same_payload_middleware(**kwargs)],
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.plugins.get_plugin_manager", lambda: manager)
|
||||
|
||||
args = {"path": "README.md"}
|
||||
result = apply_tool_request_middleware("read_file", args)
|
||||
|
||||
assert result.payload == args
|
||||
assert result.original_payload == args
|
||||
assert result.changed is True
|
||||
assert result.trace == [{"source": "same-payload"}]
|
||||
|
||||
def test_execution_middleware_post_next_call_error_does_not_retry(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def middleware(**kwargs):
|
||||
result = kwargs["next_call"](kwargs["args"])
|
||||
raise RuntimeError(f"post-processing failed after {result}")
|
||||
|
||||
manager = types.SimpleNamespace(_middleware={"tool_execution": [middleware]})
|
||||
monkeypatch.setattr("hermes_cli.plugins.get_plugin_manager", lambda: manager)
|
||||
|
||||
def terminal(args):
|
||||
calls.append(args)
|
||||
return "terminal-result"
|
||||
|
||||
result = run_tool_execution_middleware("terminal", {"command": "printf ok"}, terminal)
|
||||
|
||||
assert result == "terminal-result"
|
||||
assert calls == [{"command": "printf ok"}]
|
||||
|
||||
def test_execution_middleware_pre_next_call_error_fails_open_to_remaining_chain(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def failing_middleware(**kwargs):
|
||||
calls.append("failing")
|
||||
raise RuntimeError("middleware setup failed")
|
||||
|
||||
def downstream_middleware(**kwargs):
|
||||
calls.append("downstream")
|
||||
return kwargs["next_call"]({**kwargs["args"], "rewritten": True})
|
||||
|
||||
manager = types.SimpleNamespace(_middleware={"tool_execution": [failing_middleware, downstream_middleware]})
|
||||
monkeypatch.setattr("hermes_cli.plugins.get_plugin_manager", lambda: manager)
|
||||
|
||||
def terminal(args):
|
||||
calls.append(("terminal", args))
|
||||
return args
|
||||
|
||||
result = run_tool_execution_middleware("terminal", {"command": "printf ok"}, terminal)
|
||||
|
||||
assert result == {"command": "printf ok", "rewritten": True}
|
||||
assert calls == ["failing", "downstream", ("terminal", {"command": "printf ok", "rewritten": True})]
|
||||
|
||||
def test_execution_middleware_translated_downstream_failure_is_not_masked(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def middleware(**kwargs):
|
||||
try:
|
||||
return kwargs["next_call"](kwargs["args"])
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"translated downstream failure: {exc}") from exc
|
||||
|
||||
manager = types.SimpleNamespace(_middleware={"tool_execution": [middleware]})
|
||||
monkeypatch.setattr("hermes_cli.plugins.get_plugin_manager", lambda: manager)
|
||||
|
||||
def terminal(args):
|
||||
calls.append(args)
|
||||
raise RuntimeError("terminal failed")
|
||||
|
||||
with pytest.raises(RuntimeError, match="translated downstream failure: terminal failed"):
|
||||
run_tool_execution_middleware("terminal", {"command": "false"}, terminal)
|
||||
|
||||
assert calls == [{"command": "false"}]
|
||||
|
||||
def test_execution_middleware_downstream_base_exception_is_not_wrapped(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def middleware(**kwargs):
|
||||
try:
|
||||
return kwargs["next_call"](kwargs["args"])
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"middleware should not catch base exception: {exc}") from exc
|
||||
|
||||
manager = types.SimpleNamespace(_middleware={"tool_execution": [middleware]})
|
||||
monkeypatch.setattr("hermes_cli.plugins.get_plugin_manager", lambda: manager)
|
||||
|
||||
def terminal(args):
|
||||
calls.append(args)
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
run_tool_execution_middleware("terminal", {"command": "interrupt"}, terminal)
|
||||
|
||||
assert calls == [{"command": "interrupt"}]
|
||||
|
||||
def test_execution_middleware_double_next_call_does_not_run_terminal_twice(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def middleware(**kwargs):
|
||||
first = kwargs["next_call"](kwargs["args"])
|
||||
# Deliberate misuse: a second next_call() must not re-run the
|
||||
# downstream tool. The chain surfaces it as an error and preserves
|
||||
# the first (successful) downstream result.
|
||||
kwargs["next_call"](kwargs["args"])
|
||||
return first
|
||||
|
||||
manager = types.SimpleNamespace(_middleware={"tool_execution": [middleware]})
|
||||
monkeypatch.setattr("hermes_cli.plugins.get_plugin_manager", lambda: manager)
|
||||
|
||||
def terminal(args):
|
||||
calls.append(args)
|
||||
return "terminal-result"
|
||||
|
||||
result = run_tool_execution_middleware("terminal", {"command": "printf ok"}, terminal)
|
||||
|
||||
assert result == "terminal-result"
|
||||
assert calls == [{"command": "printf ok"}]
|
||||
|
||||
def test_request_middleware_tolerates_non_deepcopyable_payload(self, monkeypatch):
|
||||
import threading
|
||||
|
||||
recorded = {}
|
||||
|
||||
def middleware(**kwargs):
|
||||
recorded["args"] = kwargs["args"]
|
||||
return None
|
||||
|
||||
manager = types.SimpleNamespace(
|
||||
_middleware={"tool_request": [middleware]},
|
||||
invoke_middleware=lambda kind, **kwargs: [middleware(**kwargs)],
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.plugins.get_plugin_manager", lambda: manager)
|
||||
|
||||
# threading.Lock is not deepcopyable; a hard deepcopy would raise.
|
||||
args = {"command": "noop", "lock": threading.Lock()}
|
||||
result = apply_tool_request_middleware("terminal", args)
|
||||
|
||||
# Middleware ran (payload was copied via the shallow fallback) and the
|
||||
# non-deepcopyable member is shared by reference rather than aborting.
|
||||
assert recorded["args"]["command"] == "noop"
|
||||
assert result.payload["command"] == "noop"
|
||||
assert result.payload["lock"] is args["lock"]
|
||||
|
||||
def test_discover_project_plugins(self, tmp_path, monkeypatch):
|
||||
"""Plugins in ./.hermes/plugins/ are discovered."""
|
||||
project_dir = tmp_path / "project"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue