Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions hermes_cli/tools_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
("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"),
Expand Down Expand Up @@ -1294,6 +1295,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 = {
Expand Down
40 changes: 40 additions & 0 deletions tests/hermes_cli/test_tools_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,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")

Expand Down
51 changes: 51 additions & 0 deletions tests/run_agent/test_plugin_context_engine_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ def compress(self, messages, current_tokens=None):
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()
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions toolsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@
"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",
Expand Down
Loading