Compare commits
1 Commits
claude/iss
...
fix/959
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31195b2f7f |
@@ -250,12 +250,16 @@ _PROVIDER_MODELS: dict[str, list[str]] = {
|
||||
"big-pickle",
|
||||
],
|
||||
"opencode-go": [
|
||||
"glm-5",
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.5",
|
||||
"glm-5.1",
|
||||
"glm-5",
|
||||
"mimo-v2-pro",
|
||||
"mimo-v2-omni",
|
||||
"minimax-m2.7",
|
||||
"minimax-m2.5",
|
||||
"qwen3.6-plus",
|
||||
"qwen3.5-plus",
|
||||
],
|
||||
"ai-gateway": [
|
||||
"anthropic/claude-opus-4.6",
|
||||
|
||||
@@ -211,43 +211,6 @@ class PluginContext:
|
||||
}
|
||||
logger.debug("Plugin %s registered CLI command: %s", self.manifest.name, name)
|
||||
|
||||
# -- memory provider registration ----------------------------------------
|
||||
|
||||
def register_memory_provider(self, provider) -> None:
|
||||
"""Register a memory provider supplied by this plugin.
|
||||
|
||||
The provider must be an instance of ``agent.memory_provider.MemoryProvider``.
|
||||
Only one plugin-registered memory provider is accepted; a second
|
||||
attempt is rejected with a warning.
|
||||
|
||||
The registered provider is retrievable via
|
||||
``get_plugin_memory_provider()`` and is picked up by ``run_agent.py``
|
||||
when ``memory.provider`` in *config.yaml* matches the provider's
|
||||
``name`` property.
|
||||
"""
|
||||
from agent.memory_provider import MemoryProvider
|
||||
|
||||
if not isinstance(provider, MemoryProvider):
|
||||
logger.warning(
|
||||
"Plugin '%s' tried to register a memory provider that does not "
|
||||
"inherit from MemoryProvider. Ignoring.",
|
||||
self.manifest.name,
|
||||
)
|
||||
return
|
||||
if self._manager._plugin_memory_provider is not None:
|
||||
logger.warning(
|
||||
"Plugin '%s' tried to register a memory provider, but one is "
|
||||
"already registered by another plugin. Only one plugin-supplied "
|
||||
"memory provider is allowed at a time.",
|
||||
self.manifest.name,
|
||||
)
|
||||
return
|
||||
self._manager._plugin_memory_provider = provider
|
||||
logger.info(
|
||||
"Plugin '%s' registered memory provider: %s",
|
||||
self.manifest.name, provider.name,
|
||||
)
|
||||
|
||||
# -- context engine registration -----------------------------------------
|
||||
|
||||
def register_context_engine(self, engine) -> None:
|
||||
@@ -360,7 +323,6 @@ class PluginManager:
|
||||
self._plugin_tool_names: Set[str] = set()
|
||||
self._cli_commands: Dict[str, dict] = {}
|
||||
self._context_engine = None # Set by a plugin via register_context_engine()
|
||||
self._plugin_memory_provider = None # Set by a plugin via register_memory_provider()
|
||||
self._discovered: bool = False
|
||||
self._cli_ref = None # Set by CLI after plugin discovery
|
||||
# Plugin skill registry: qualified name → metadata dict.
|
||||
@@ -737,11 +699,6 @@ def get_plugin_context_engine():
|
||||
return get_plugin_manager()._context_engine
|
||||
|
||||
|
||||
def get_plugin_memory_provider():
|
||||
"""Return the plugin-registered memory provider, or None."""
|
||||
return get_plugin_manager()._plugin_memory_provider
|
||||
|
||||
|
||||
def get_plugin_toolsets() -> List[tuple]:
|
||||
"""Return plugin toolsets as ``(key, label, description)`` tuples.
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ _DEFAULT_PROVIDER_MODELS = {
|
||||
"ai-gateway": ["anthropic/claude-opus-4.6", "anthropic/claude-sonnet-4.6", "openai/gpt-5", "google/gemini-3-flash"],
|
||||
"kilocode": ["anthropic/claude-opus-4.6", "anthropic/claude-sonnet-4.6", "openai/gpt-5.4", "google/gemini-3-pro-preview", "google/gemini-3-flash-preview"],
|
||||
"opencode-zen": ["gpt-5.4", "gpt-5.3-codex", "claude-sonnet-4-6", "gemini-3-flash", "glm-5", "kimi-k2.5", "minimax-m2.7"],
|
||||
"opencode-go": ["glm-5", "kimi-k2.5", "mimo-v2-pro", "mimo-v2-omni", "minimax-m2.5", "minimax-m2.7"],
|
||||
"opencode-go": ["kimi-k2.6", "kimi-k2.5", "glm-5.1", "glm-5", "mimo-v2-pro", "mimo-v2-omni", "minimax-m2.5", "minimax-m2.7", "qwen3.6-plus", "qwen3.5-plus"],
|
||||
"huggingface": [
|
||||
"Qwen/Qwen3.5-397B-A17B", "Qwen/Qwen3-235B-A22B-Thinking-2507",
|
||||
"Qwen/Qwen3-Coder-480B-A35B-Instruct", "deepseek-ai/DeepSeek-R1-0528",
|
||||
|
||||
12
run_agent.py
12
run_agent.py
@@ -1193,18 +1193,6 @@ class AIAgent:
|
||||
from plugins.memory import load_memory_provider as _load_mem
|
||||
self._memory_manager = _MemoryManager()
|
||||
_mp = _load_mem(_mem_provider_name)
|
||||
# Fall back to a user plugin that called register_memory_provider()
|
||||
if _mp is None:
|
||||
try:
|
||||
from hermes_cli.plugins import get_plugin_memory_provider as _gpm
|
||||
_candidate = _gpm()
|
||||
if _candidate and _candidate.name == _mem_provider_name:
|
||||
_mp = _candidate
|
||||
except Exception as _gpm_err:
|
||||
logger.debug(
|
||||
"get_plugin_memory_provider() failed during fallback lookup: %s",
|
||||
_gpm_err,
|
||||
)
|
||||
if _mp and _mp.is_available():
|
||||
self._memory_manager.add_provider(_mp)
|
||||
if self._memory_manager.providers:
|
||||
|
||||
@@ -4,32 +4,62 @@ import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from hermes_cli.model_switch import list_authenticated_providers
|
||||
from hermes_cli.models import curated_models_for_provider
|
||||
|
||||
|
||||
@patch.dict(os.environ, {"OPENCODE_GO_API_KEY": "test-key"}, clear=False)
|
||||
def test_opencode_go_appears_when_api_key_set():
|
||||
"""opencode-go should appear in list_authenticated_providers when OPENCODE_GO_API_KEY is set."""
|
||||
providers = list_authenticated_providers(current_provider="openrouter")
|
||||
|
||||
|
||||
# Find opencode-go in results
|
||||
opencode_go = next((p for p in providers if p["slug"] == "opencode-go"), None)
|
||||
|
||||
|
||||
assert opencode_go is not None, "opencode-go should appear when OPENCODE_GO_API_KEY is set"
|
||||
assert opencode_go["models"] == ["glm-5", "kimi-k2.5", "mimo-v2-pro", "mimo-v2-omni", "minimax-m2.7", "minimax-m2.5"]
|
||||
assert opencode_go["models"] == [
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.5",
|
||||
"glm-5.1",
|
||||
"glm-5",
|
||||
"mimo-v2-pro",
|
||||
"mimo-v2-omni",
|
||||
"minimax-m2.7",
|
||||
"minimax-m2.5",
|
||||
]
|
||||
assert opencode_go["total_models"] == 10
|
||||
# opencode-go can appear as "built-in" (from PROVIDER_TO_MODELS_DEV when
|
||||
# models.dev is reachable) or "hermes" (from HERMES_OVERLAYS fallback when
|
||||
# the API is unavailable, e.g. in CI).
|
||||
assert opencode_go["source"] in ("built-in", "hermes")
|
||||
|
||||
|
||||
@patch("hermes_cli.models.provider_model_ids", return_value=[])
|
||||
def test_opencode_go_curated_fallback_includes_new_models(_mock_provider_model_ids):
|
||||
"""Fallback catalog should include Kimi K2.6 and both Qwen Plus models."""
|
||||
model_ids = [model_id for model_id, _ in curated_models_for_provider("opencode-go")]
|
||||
|
||||
assert model_ids == [
|
||||
"kimi-k2.6",
|
||||
"kimi-k2.5",
|
||||
"glm-5.1",
|
||||
"glm-5",
|
||||
"mimo-v2-pro",
|
||||
"mimo-v2-omni",
|
||||
"minimax-m2.7",
|
||||
"minimax-m2.5",
|
||||
"qwen3.6-plus",
|
||||
"qwen3.5-plus",
|
||||
]
|
||||
|
||||
|
||||
def test_opencode_go_not_appears_when_no_creds():
|
||||
"""opencode-go should NOT appear when no credentials are set."""
|
||||
# Ensure OPENCODE_GO_API_KEY is not set
|
||||
env_without_key = {k: v for k, v in os.environ.items() if k != "OPENCODE_GO_API_KEY"}
|
||||
|
||||
|
||||
with patch.dict(os.environ, env_without_key, clear=True):
|
||||
providers = list_authenticated_providers(current_provider="openrouter")
|
||||
|
||||
|
||||
# opencode-go should not be in results
|
||||
opencode_go = next((p for p in providers if p["slug"] == "opencode-go"), None)
|
||||
assert opencode_go is None, "opencode-go should not appear without credentials"
|
||||
|
||||
@@ -19,7 +19,6 @@ from hermes_cli.plugins import (
|
||||
PluginManifest,
|
||||
get_plugin_manager,
|
||||
get_pre_tool_call_block_message,
|
||||
get_plugin_memory_provider,
|
||||
discover_plugins,
|
||||
invoke_hook,
|
||||
)
|
||||
@@ -610,105 +609,3 @@ class TestPreLlmCallTargetRouting:
|
||||
# in PluginContext (hermes_cli/plugins.py). The tests referenced _plugin_commands,
|
||||
# commands_registered, get_plugin_command_handler, and GATEWAY_KNOWN_COMMANDS
|
||||
# integration — all of which are unimplemented features.
|
||||
|
||||
|
||||
# ── TestRegisterMemoryProvider ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestRegisterMemoryProvider:
|
||||
"""Regression tests for PluginContext.register_memory_provider() — issue #990.
|
||||
|
||||
The MemPalace plugin (and any user plugin following the developer guide)
|
||||
calls ``ctx.register_memory_provider(provider)`` inside ``register(ctx)``.
|
||||
Before the fix, PluginContext had no such method and the plugin failed to
|
||||
load with: 'PluginContext' object has no attribute 'register_memory_provider'.
|
||||
"""
|
||||
|
||||
def _make_memory_plugin(self, plugins_dir: "Path", name: str) -> None:
|
||||
"""Write a minimal user plugin that registers a stub MemoryProvider."""
|
||||
from agent.memory_provider import MemoryProvider
|
||||
|
||||
plugin_dir = plugins_dir / name
|
||||
plugin_dir.mkdir(parents=True, exist_ok=True)
|
||||
(plugin_dir / "plugin.yaml").write_text(
|
||||
f"name: {name}\nversion: 0.1.0\ndescription: Stub memory plugin\n"
|
||||
)
|
||||
# The register() body imports and calls register_memory_provider — this
|
||||
# is the exact pattern documented in memory-provider-plugin.md and used
|
||||
# by third-party plugins such as MemPalace.
|
||||
(plugin_dir / "__init__.py").write_text(
|
||||
"from agent.memory_provider import MemoryProvider\n"
|
||||
"\n"
|
||||
"class _StubProvider(MemoryProvider):\n"
|
||||
" @property\n"
|
||||
f" def name(self): return '{name}'\n"
|
||||
" def is_available(self): return True\n"
|
||||
" def initialize(self, session_id, **kw): pass\n"
|
||||
" def get_tool_schemas(self): return []\n"
|
||||
"\n"
|
||||
"def register(ctx):\n"
|
||||
" ctx.register_memory_provider(_StubProvider())\n"
|
||||
)
|
||||
|
||||
def test_register_memory_provider_succeeds(self, tmp_path, monkeypatch):
|
||||
"""A user plugin calling register_memory_provider() loads without error."""
|
||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||
self._make_memory_plugin(plugins_dir, "mempalace")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test"))
|
||||
|
||||
mgr = PluginManager()
|
||||
mgr.discover_and_load()
|
||||
|
||||
assert "mempalace" in mgr._plugins
|
||||
assert mgr._plugins["mempalace"].enabled, (
|
||||
mgr._plugins["mempalace"].error
|
||||
)
|
||||
|
||||
def test_plugin_memory_provider_stored(self, tmp_path, monkeypatch):
|
||||
"""The provider instance is accessible via get_plugin_memory_provider()."""
|
||||
import hermes_cli.plugins as plugins_mod
|
||||
|
||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||
self._make_memory_plugin(plugins_dir, "mempalace")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test"))
|
||||
|
||||
mgr = PluginManager()
|
||||
# Swap the singleton so get_plugin_memory_provider() sees our manager
|
||||
monkeypatch.setattr(plugins_mod, "_plugin_manager", mgr)
|
||||
mgr.discover_and_load()
|
||||
|
||||
provider = get_plugin_memory_provider()
|
||||
assert provider is not None
|
||||
assert provider.name == "mempalace"
|
||||
|
||||
def test_second_registration_rejected(self, tmp_path, monkeypatch):
|
||||
"""Only one plugin-registered memory provider is accepted."""
|
||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||
self._make_memory_plugin(plugins_dir, "first_provider")
|
||||
self._make_memory_plugin(plugins_dir, "second_provider")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test"))
|
||||
|
||||
mgr = PluginManager()
|
||||
mgr.discover_and_load()
|
||||
|
||||
# The manager should hold exactly one provider
|
||||
assert mgr._plugin_memory_provider is not None
|
||||
assert mgr._plugin_memory_provider.name in {"first_provider", "second_provider"}
|
||||
|
||||
def test_non_provider_rejected(self, tmp_path, monkeypatch):
|
||||
"""Passing a non-MemoryProvider object logs a warning and is ignored."""
|
||||
plugins_dir = tmp_path / "hermes_test" / "plugins"
|
||||
plugin_dir = plugins_dir / "bad_provider"
|
||||
plugin_dir.mkdir(parents=True, exist_ok=True)
|
||||
(plugin_dir / "plugin.yaml").write_text("name: bad_provider\n")
|
||||
(plugin_dir / "__init__.py").write_text(
|
||||
"def register(ctx):\n"
|
||||
" ctx.register_memory_provider('not-a-provider')\n"
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test"))
|
||||
|
||||
mgr = PluginManager()
|
||||
mgr.discover_and_load()
|
||||
|
||||
# Plugin still loads (warning only), but no provider is stored
|
||||
assert mgr._plugin_memory_provider is None
|
||||
|
||||
Reference in New Issue
Block a user