mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 13:26:40 +00:00
After the cutover no local surface fired pre_command where the command runs. slash.exec and command.dispatch (classic CLI forwards, Ink TUI, Desktop) never fired it, and the classic CLI fired it from its own second copy of the plugin. - The gateway fires pre_command for every recognized command a local session runs, under the session's profile: before the handler for commands it runs itself (slash.exec, command.dispatch; an accepted TUI read fires once inside the read path), and for commands the classic CLI runs in its own process through the new command.observe (plain data: the command as typed and its arguments). command.observe replies first, so the CLI never waits on a plugin. The hook arguments keep main's values: surface and platform "cli" on every local surface, session_key the session id. - It fires only for commands that run. A busy rejection or a named refusal fires nothing; during a turn it fires when the busy dispatcher accepts the command, so a refused /goal, /loop or empty /queue or /steer fires nothing either. Every command that does run mid-turn fires, as main's CLI did, except messaging's escape hatches (/stop and the approval controls). - Each delivery waits out its callback on its own thread, never the shared executor, with at most one delivery in flight per session: one session's deliveries fire in order and a slow callback never holds up another session's. The hook dispatcher's existing bounds apply (plugins.hook_callback_timeout and its abandoned-worker cap), and with plugins.isolation: host the hook runs in the plugin host. pre_command takes that bounded path everywhere, so off the gateway its callbacks also run on a worker thread. - On a gateway session the classic CLI loads no plugin code. It reads skills, plugin commands and plugin toolsets from the gateway (commands.catalog, scoped to the session's own folder, and plugins.list) for /help, skill dispatch and completion, /tools and the status and banner fallbacks. -s and skills.auto_load are rendered in the gateway, and /reload-skills and /skills install rescan there and drop the CLI's cached catalog. These reads are cached per gateway life and never served once the connection has hung up. The "Unknown toolsets" warning comes from the gateway (a new unknown_toolsets field when the session opens). An older gateway without command.observe gets one named notice, and nothing fires locally. - The canonical gateway connection serves plugins.list for its profile: loaded plugins with tool/hook/command counts, load error, toolsets and portable MCP server names (optional fields added to the contract; TS and OpenRPC regenerated). On a gateway session the CLI's /plugins reads it instead of this process's plugin manager. Off the gateway the in-process paths are unchanged. Key-less inject_message (B2-11), which the classic CLI's own plugin copy used to serve, is handled in the next commit. Refs #134889 (PLG-7) Refs #139523
50 lines
1.9 KiB
Python
50 lines
1.9 KiB
Python
"""``hermes chat -q``/``-Q`` runs leave this process's plugin manager alone.
|
|
|
|
On main they set ``PluginManager._cli_ref`` so a plugin tool dispatched from a one-shot turn got a
|
|
``parent_agent`` (#67597). The turn and its plugin tools now run in the gateway, and the CLI loads
|
|
no plugins (PLG-7), so there is nothing here to bind.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
import cli
|
|
from hermes_cli.plugins import get_plugin_manager
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _one_shot_seams(monkeypatch):
|
|
monkeypatch.delenv("HERMES_KANBAN_GOAL_MODE", raising=False)
|
|
monkeypatch.setattr(cli, "_should_seed_interactive", lambda *a, **k: False)
|
|
monkeypatch.setattr(cli, "_collect_query_images", lambda q, i: (q, []))
|
|
monkeypatch.setattr(cli, "_collect_kanban_task_images", lambda imgs: [])
|
|
monkeypatch.setattr(cli, "_finalize_single_query", lambda c: None)
|
|
monkeypatch.setattr(get_plugin_manager(), "_cli_ref", None)
|
|
|
|
|
|
def _stub(**extra):
|
|
return SimpleNamespace(
|
|
_single_query_mode=False, _claim_active_session=lambda *a, **k: True,
|
|
console=SimpleNamespace(print=lambda *a, **k: None), _show_security_advisories=lambda: None,
|
|
chat=lambda *a, **k: "response", _print_exit_summary=lambda **k: None,
|
|
_last_turn_result={"failed": False}, **extra,
|
|
)
|
|
|
|
|
|
def test_one_shot_turn_binds_nothing_on_the_local_plugin_manager():
|
|
stub = _stub()
|
|
with pytest.raises(SystemExit):
|
|
cli._run_single_query_mode(stub, "do the thing", None, False, True)
|
|
assert get_plugin_manager()._cli_ref is None
|
|
|
|
|
|
def test_quiet_turn_binds_nothing_on_the_local_plugin_manager():
|
|
stub = _stub(_ensure_runtime_credentials=lambda: False, _credentials_rate_limited=False,
|
|
session_id="s1", model="m")
|
|
with pytest.raises(SystemExit):
|
|
cli._run_single_query_mode(stub, "do the thing", None, True, True)
|
|
assert get_plugin_manager()._cli_ref is None
|