fix(image-gen): force-refresh plugin providers in long-lived sessions

This commit is contained in:
Wysie 2026-04-23 15:20:58 +08:00 committed by Teknium
parent 911f57ad97
commit be99feff1f
3 changed files with 138 additions and 9 deletions

View file

@ -512,10 +512,23 @@ class PluginManager:
# Public # Public
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
def discover_and_load(self) -> None: def discover_and_load(self, force: bool = False) -> None:
"""Scan all plugin sources and load each plugin found.""" """Scan all plugin sources and load each plugin found.
if self._discovered:
When ``force`` is true, clear cached discovery state first so config
changes or newly-added bundled backends become visible in long-lived
sessions without requiring a full agent restart.
"""
if self._discovered and not force:
return return
if force:
self._plugins.clear()
self._hooks.clear()
self._plugin_tool_names.clear()
self._cli_commands.clear()
self._plugin_commands.clear()
self._plugin_skills.clear()
self._context_engine = None
self._discovered = True self._discovered = True
manifests: List[PluginManifest] = [] manifests: List[PluginManifest] = []
@ -1029,9 +1042,13 @@ def get_plugin_manager() -> PluginManager:
return _plugin_manager return _plugin_manager
def discover_plugins() -> None: def discover_plugins(force: bool = False) -> None:
"""Discover and load all plugins (idempotent).""" """Discover and load all plugins.
get_plugin_manager().discover_and_load()
Default behavior is idempotent. Pass ``force=True`` to rescan plugin
manifests and reload state in the current process.
"""
get_plugin_manager().discover_and_load(force=force)
def invoke_hook(hook_name: str, **kwargs: Any) -> List[Any]: def invoke_hook(hook_name: str, **kwargs: Any) -> List[Any]:
@ -1082,10 +1099,13 @@ def get_pre_tool_call_block_message(
return None return None
def _ensure_plugins_discovered() -> PluginManager: def _ensure_plugins_discovered(force: bool = False) -> PluginManager:
"""Return the global manager after running idempotent plugin discovery.""" """Return the global manager after ensuring plugin discovery has run.
Pass ``force=True`` to rescan in the current process.
"""
manager = get_plugin_manager() manager = get_plugin_manager()
manager.discover_and_load() manager.discover_and_load(force=force)
return manager return manager

View file

@ -0,0 +1,99 @@
from __future__ import annotations
import json
import pytest
from agent import image_gen_registry
from agent.image_gen_provider import ImageGenProvider
@pytest.fixture(autouse=True)
def _reset_registry():
image_gen_registry._reset_for_tests()
yield
image_gen_registry._reset_for_tests()
class _FakeCodexProvider(ImageGenProvider):
@property
def name(self) -> str:
return "codex"
def generate(self, prompt, aspect_ratio="landscape", **kwargs):
return {
"success": True,
"image": "/tmp/codex-test.png",
"model": "gpt-5.2-codex",
"prompt": prompt,
"aspect_ratio": aspect_ratio,
"provider": "codex",
}
class TestPluginDispatch:
def test_dispatch_routes_to_codex_provider(self, monkeypatch, tmp_path):
from tools import image_generation_tool
from agent import image_gen_registry as registry_module
from hermes_cli import plugins as plugins_module
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
(tmp_path / "config.yaml").write_text("image_gen:\n provider: codex\n")
image_gen_registry.register_provider(_FakeCodexProvider())
monkeypatch.setattr(image_generation_tool, "_read_configured_image_provider", lambda: "codex")
monkeypatch.setattr(plugins_module, "_ensure_plugins_discovered", lambda: None)
monkeypatch.setattr(registry_module, "get_provider", lambda name: _FakeCodexProvider() if name == "codex" else None)
dispatched = image_generation_tool._dispatch_to_plugin_provider("draw cat", "square")
payload = json.loads(dispatched)
assert payload["success"] is True
assert payload["provider"] == "codex"
assert payload["image"] == "/tmp/codex-test.png"
assert payload["aspect_ratio"] == "square"
def test_dispatch_reports_missing_registered_provider(self, monkeypatch, tmp_path):
from tools import image_generation_tool
from hermes_cli import plugins as plugins_module
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
(tmp_path / "config.yaml").write_text("image_gen:\n provider: missing-codex\n")
monkeypatch.setattr(image_generation_tool, "_read_configured_image_provider", lambda: "missing-codex")
monkeypatch.setattr(plugins_module, "_ensure_plugins_discovered", lambda: None)
dispatched = image_generation_tool._dispatch_to_plugin_provider("draw cat", "landscape")
payload = json.loads(dispatched)
assert payload["success"] is False
assert payload["error_type"] == "provider_not_registered"
assert "image_gen.provider='missing-codex'" in payload["error"]
def test_dispatch_force_refreshes_plugins_when_provider_initially_missing(self, monkeypatch, tmp_path):
from tools import image_generation_tool
from hermes_cli import plugins as plugins_module
from agent import image_gen_registry as registry_module
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
(tmp_path / "config.yaml").write_text("image_gen:\n provider: codex\n")
monkeypatch.setattr(image_generation_tool, "_read_configured_image_provider", lambda: "codex")
calls = []
provider_state = {"provider": None}
def fake_ensure_plugins_discovered(force=False):
calls.append(force)
if force:
provider_state["provider"] = _FakeCodexProvider()
monkeypatch.setattr(plugins_module, "_ensure_plugins_discovered", fake_ensure_plugins_discovered)
monkeypatch.setattr(registry_module, "get_provider", lambda name: provider_state["provider"])
dispatched = image_generation_tool._dispatch_to_plugin_provider("draw hammy", "portrait")
payload = json.loads(dispatched)
assert calls == [False, True]
assert payload["success"] is True
assert payload["provider"] == "codex"
assert payload["aspect_ratio"] == "portrait"

View file

@ -927,6 +927,16 @@ def _dispatch_to_plugin_provider(prompt: str, aspect_ratio: str):
logger.debug("image_gen plugin dispatch skipped: %s", exc) logger.debug("image_gen plugin dispatch skipped: %s", exc)
return None return None
if provider is None:
try:
# Long-lived sessions may have discovered plugins before a bundled
# backend was patched in or before config changed. Retry once with
# a forced refresh before surfacing a missing-provider error.
_ensure_plugins_discovered(force=True)
provider = get_provider(configured)
except Exception as exc:
logger.debug("image_gen plugin force-refresh skipped: %s", exc)
if provider is None: if provider is None:
return json.dumps({ return json.dumps({
"success": False, "success": False,