Files
hermes-agent/tests/plugins/test_context_engine_user_dir.py
teknium1 913c045c53 fix: resolve user-installed context engines from $HERMES_HOME/plugins/<name>
plugins/context_engine.load_context_engine scanned only the bundled directory. An engine
dropped into $HERMES_HOME/plugins/<name> with `context.engine: <name>` was reachable only
through the general plugin system, which skips any user plugin not listed in
plugins.enabled — so every agent init logged "Context engine '<name>' not found — falling
back to built-in compressor" although the engine was installed and named in config.

Live probe on base (fake HOME, plugins/ctx_demo with register(ctx), context.engine:
ctx_demo): the warning fired on EVERY init, not only the first; adding the plugin to
plugins.enabled made it load through the general fallback. `context.engine` is the
activation signal (as memory.provider / cron.provider are for their kinds), so the engine
loader now resolves bundled then user dirs the way plugins/cron_providers does: same
`user_plugins_dir()` seam, cheap source heuristic (register_context_engine / ContextEngine),
user engines imported under a synthetic namespace, bundled wins on collision, and
discover_context_engines() lists them for `hermes plugins` / the dashboard.

Fixes #61839
credit: @giggling-ginger #61995
2026-09-21 23:38:16 -07:00

45 lines
1.8 KiB
Python

"""``context.engine: <name>`` names the active engine, so an engine dropped into
``$HERMES_HOME/plugins/<name>/`` loads without a ``plugins.enabled`` entry and never trips the
"Context engine 'X' not found — falling back to built-in compressor" warning (#61839)."""
import logging
from pathlib import Path
from textwrap import dedent
_ENGINE_SRC = dedent('''
from agent.context_engine import ContextEngine
class Demo(ContextEngine):
@property
def name(self):
return "ctx_demo"
def update_from_response(self, usage):
pass
def should_compress(self, prompt_tokens=None):
return False
def compress(self, messages, current_tokens=None):
return messages
def register(ctx):
ctx.register_context_engine(Demo())
''')
def test_user_installed_engine_is_selected_by_name(tmp_path: Path, monkeypatch, caplog):
engine_dir = tmp_path / "plugins" / "ctx_demo"
engine_dir.mkdir(parents=True)
(engine_dir / "__init__.py").write_text(_ENGINE_SRC)
(tmp_path / "plugins" / "notes").mkdir() # unrelated user plugin: never treated as an engine
(tmp_path / "plugins" / "notes" / "__init__.py").write_text("def register(ctx): pass\n")
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
from agent.agent_init import _select_context_engine
from plugins.context_engine import discover_context_engines, load_context_engine
assert load_context_engine("notes") is None
assert "ctx_demo" in {name for name, _, _ in discover_context_engines()}
with caplog.at_level(logging.WARNING, logger="run_agent"):
engine = _select_context_engine({"context": {"engine": "ctx_demo"}})
assert engine is not None and engine.name == "ctx_demo"
assert "not found" not in caplog.text