From 8595281f3ca9c77f957bc5e6dd3ea43874a2b3b9 Mon Sep 17 00:00:00 2001 From: Stephen Schoettler Date: Sat, 23 May 2026 16:26:33 -0700 Subject: [PATCH] fix: expose context engine tools with saved toolsets --- hermes_cli/tools_config.py | 19 +++++++ tests/hermes_cli/test_tools_config.py | 40 +++++++++++++++ .../test_plugin_context_engine_init.py | 51 +++++++++++++++++++ toolsets.py | 6 +++ 4 files changed, 116 insertions(+) diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 63a60c10fbd..786da72a896 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -68,6 +68,7 @@ CONFIGURABLE_TOOLSETS = [ ("skills", "📚 Skills", "list, view, manage"), ("todo", "📋 Task Planning", "todo"), ("memory", "💾 Memory", "persistent memory across sessions"), + ("context_engine", "🧩 Context Engine", "runtime tools from the active context engine"), ("session_search", "🔎 Session Search", "search past conversations"), ("clarify", "❓ Clarifying Questions", "clarify"), ("delegation", "👥 Task Delegation", "delegate_task"), @@ -1295,6 +1296,24 @@ def _get_platform_tools( enabled_toolsets.add(pts) # else: known but not in config = user disabled it + # Context-engine tools are runtime-provided by the active engine, so they + # are not part of any static platform composite. When a non-default engine + # is selected, keep its recovery/status tools available even after a user + # saves an explicit platform toolset list. Preserve the explicit empty-list + # contract: selecting no configurable tools means no context-engine tools + # either unless the user adds ``context_engine`` manually later. + context_cfg = config.get("context") or {} + if not isinstance(context_cfg, dict): + context_cfg = {} + context_engine_name = str(context_cfg.get("engine") or "compressor").strip().lower() + explicit_empty_selection = ( + platform in platform_toolsets + and isinstance(platform_toolsets.get(platform), list) + and not toolset_names + ) + if context_engine_name and context_engine_name != "compressor" and not explicit_empty_selection: + enabled_toolsets.add("context_engine") + # Preserve any explicit non-configurable toolset entries (for example, # custom toolsets or MCP server names saved in platform_toolsets). explicit_passthrough = { diff --git a/tests/hermes_cli/test_tools_config.py b/tests/hermes_cli/test_tools_config.py index f9eff4b90a5..cfef9c3b46a 100644 --- a/tests/hermes_cli/test_tools_config.py +++ b/tests/hermes_cli/test_tools_config.py @@ -81,6 +81,46 @@ def test_get_platform_tools_uses_default_when_platform_not_configured(): def test_configurable_toolsets_include_messaging(): assert any(ts_key == "messaging" for ts_key, _, _ in CONFIGURABLE_TOOLSETS) + +def test_configurable_toolsets_include_context_engine(): + assert any(ts_key == "context_engine" for ts_key, _, _ in CONFIGURABLE_TOOLSETS) + + +def test_get_platform_tools_active_context_engine_is_enabled_for_explicit_config(): + config = { + "context": {"engine": "lcm"}, + "platform_toolsets": {"cli": ["web", "terminal"]}, + } + + enabled = _get_platform_tools(config, "cli", include_default_mcp_servers=False) + + assert "context_engine" in enabled + assert "web" in enabled + assert "terminal" in enabled + + +def test_get_platform_tools_context_engine_not_added_for_default_compressor(): + config = { + "context": {"engine": "compressor"}, + "platform_toolsets": {"cli": ["web", "terminal"]}, + } + + enabled = _get_platform_tools(config, "cli", include_default_mcp_servers=False) + + assert "context_engine" not in enabled + + +def test_get_platform_tools_context_engine_respects_explicit_empty_selection(): + config = { + "context": {"engine": "lcm"}, + "platform_toolsets": {"cli": []}, + } + + enabled = _get_platform_tools(config, "cli", include_default_mcp_servers=False) + + assert "context_engine" not in enabled + + def test_get_platform_tools_default_telegram_includes_messaging(): enabled = _get_platform_tools({}, "telegram") diff --git a/tests/run_agent/test_plugin_context_engine_init.py b/tests/run_agent/test_plugin_context_engine_init.py index 83895ac6dce..7285cb1f625 100644 --- a/tests/run_agent/test_plugin_context_engine_init.py +++ b/tests/run_agent/test_plugin_context_engine_init.py @@ -26,6 +26,17 @@ class _StubEngine(ContextEngine): return messages +class _ToolEngine(_StubEngine): + def get_tool_schemas(self): + return [ + { + "name": "stub_recover", + "description": "Recover context from the stub engine.", + "parameters": {"type": "object", "properties": {}}, + } + ] + + def test_plugin_engine_gets_context_length_on_init(): """Plugin context engine should have context_length set during AIAgent init.""" engine = _StubEngine() @@ -56,6 +67,46 @@ def test_plugin_engine_gets_context_length_on_init(): assert engine.threshold_tokens == int(204_800 * engine.threshold_percent) +def test_active_context_engine_tools_survive_explicit_platform_toolsets(): + """LCM-style recovery tools must survive saved `hermes tools` lists.""" + engine = _ToolEngine() + cfg = { + "context": {"engine": "stub"}, + "platform_toolsets": {"cli": ["web", "terminal"]}, + "agent": {}, + } + + from hermes_cli.tools_config import _get_platform_tools + + enabled_toolsets = _get_platform_tools(cfg, "cli", include_default_mcp_servers=False) + assert "context_engine" in enabled_toolsets + + with ( + patch("hermes_cli.config.load_config", return_value=cfg), + patch("plugins.context_engine.load_context_engine", return_value=engine), + patch("agent.model_metadata.get_model_context_length", return_value=204_800), + patch("run_agent.get_tool_definitions", return_value=[]), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("run_agent.OpenAI"), + ): + from run_agent import AIAgent + + agent = AIAgent( + api_key="test-key-1234567890", + base_url="https://openrouter.ai/api/v1", + enabled_toolsets=sorted(enabled_toolsets), + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + + assert "stub_recover" in getattr(agent, "valid_tool_names", set()) + assert "stub_recover" in { + tool.get("function", {}).get("name") + for tool in getattr(agent, "tools", []) + } + + def test_plugin_engine_update_model_args(): """Verify update_model() receives model, context_length, base_url, api_key, provider.""" engine = _StubEngine() diff --git a/toolsets.py b/toolsets.py index bab7677887a..10c5dbb0ca0 100644 --- a/toolsets.py +++ b/toolsets.py @@ -215,6 +215,12 @@ TOOLSETS = { "tools": ["memory"], "includes": [] }, + + "context_engine": { + "description": "Runtime tools exposed by the active context engine", + "tools": [], + "includes": [] + }, "session_search": { "description": "Search and recall past conversations with summarization",