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
45 lines
1.8 KiB
Python
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
|