feat: dual-mode for upstream register_platform (post-#17751) + legacy register_platform_adapter
NousResearch/hermes-agent#17751 (merged 2026-04-30) shipped a
comprehensive pluggable-platform system with:
- ctx.register_platform(name, label, adapter_factory, check_fn, ...)
- Open Platform enum (Platform('molecule') creates a pseudo-member
via _missing_() when the platform_registry knows about it)
That supersedes my upstream PR #18775 (which used a narrower
register_platform_adapter shape with a closed enum + custom
PluginPlatformIdentifier). Closing #18775 as redundant.
This plugin previously coupled to my fork's API. Migration:
- __init__.py register() now prefers ctx.register_platform when
available; falls back to ctx.register_platform_adapter on legacy
forks (template-hermes' baked-in fork until it migrates).
- adapter.py constructs Platform(name) when the enum accepts
'molecule', else falls back to PluginPlatformIdentifier(name).
Same wheel installs cleanly on stock hermes-agent (post-#17751)
AND on the legacy template-hermes fork build. Removed the test
stub of PluginPlatformIdentifier; tests now stub the open-enum
Platform shape with the same _missing_() behavior the upstream
ships.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
191ee89c19
commit
754d162d99
@ -16,9 +16,41 @@ __all__ = [
|
|||||||
|
|
||||||
|
|
||||||
def register(ctx) -> None:
|
def register(ctx) -> None:
|
||||||
"""Plugin entry point — called by hermes_cli.plugins on discovery."""
|
"""Plugin entry point — called by hermes_cli.plugins on discovery.
|
||||||
ctx.register_platform_adapter(
|
|
||||||
name="molecule",
|
Dual-API shim:
|
||||||
adapter_class=MoleculeAdapter,
|
- Upstream NousResearch/hermes-agent#17751 (merged 2026-04-30)
|
||||||
requirements_check=check_molecule_requirements,
|
shipped ``ctx.register_platform(name, label, adapter_factory,
|
||||||
)
|
check_fn, ...)``. This is the canonical API on stock hermes-agent.
|
||||||
|
- Earlier forks (incl. hermes-agent before #17751 landed) expose
|
||||||
|
``ctx.register_platform_adapter(name, adapter_class,
|
||||||
|
requirements_check)`` instead — narrower signature, no factory.
|
||||||
|
|
||||||
|
Detect at runtime so the same wheel installs cleanly on both.
|
||||||
|
"""
|
||||||
|
if hasattr(ctx, "register_platform"):
|
||||||
|
ctx.register_platform(
|
||||||
|
name="molecule",
|
||||||
|
label="Molecule",
|
||||||
|
adapter_factory=lambda cfg: MoleculeAdapter(cfg),
|
||||||
|
check_fn=check_molecule_requirements,
|
||||||
|
required_env=["MOLECULE_WORKSPACE_ID", "MOLECULE_PLATFORM_URL"],
|
||||||
|
install_hint=(
|
||||||
|
"set MOLECULE_WORKSPACE_ID, MOLECULE_WORKSPACE_TOKEN, "
|
||||||
|
"MOLECULE_PLATFORM_URL, MOLECULE_ORG_ID; ensure "
|
||||||
|
"molecule-ai-workspace-runtime is on the python that "
|
||||||
|
"MOLECULE_MCP_PYTHON resolves to"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
elif hasattr(ctx, "register_platform_adapter"):
|
||||||
|
ctx.register_platform_adapter(
|
||||||
|
name="molecule",
|
||||||
|
adapter_class=MoleculeAdapter,
|
||||||
|
requirements_check=check_molecule_requirements,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"hermes-channel-molecule: this hermes-agent version exposes "
|
||||||
|
"neither register_platform (upstream #17751+) nor "
|
||||||
|
"register_platform_adapter (legacy fork) — cannot register"
|
||||||
|
)
|
||||||
|
|||||||
@ -40,7 +40,25 @@ from gateway.platforms.base import (
|
|||||||
MessageType,
|
MessageType,
|
||||||
SendResult,
|
SendResult,
|
||||||
)
|
)
|
||||||
from hermes_cli.plugins import PluginPlatformIdentifier
|
from gateway.config import Platform
|
||||||
|
|
||||||
|
|
||||||
|
def _platform_identity(name: str):
|
||||||
|
"""Pick the right Platform-shaped identity for the installed hermes.
|
||||||
|
|
||||||
|
Upstream #17751 made Platform an open enum (``Platform("molecule")``
|
||||||
|
works via ``_missing_()``). Legacy forks have a closed enum and ship
|
||||||
|
``PluginPlatformIdentifier`` for plugin-supplied platforms instead.
|
||||||
|
Detect at import time so the same plugin works on both.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return Platform(name)
|
||||||
|
except ValueError:
|
||||||
|
# Closed enum (legacy fork) — fall back to the fork's plugin
|
||||||
|
# identifier shape. Import lazily so a stock hermes-agent doesn't
|
||||||
|
# need this symbol to exist.
|
||||||
|
from hermes_cli.plugins import PluginPlatformIdentifier
|
||||||
|
return PluginPlatformIdentifier(name)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -142,7 +160,7 @@ class MoleculeAdapter(BasePlatformAdapter):
|
|||||||
"""Hermes platform adapter for the molecule platform via A2A MCP."""
|
"""Hermes platform adapter for the molecule platform via A2A MCP."""
|
||||||
|
|
||||||
def __init__(self, config) -> None:
|
def __init__(self, config) -> None:
|
||||||
super().__init__(config, PluginPlatformIdentifier(PLUGIN_NAME))
|
super().__init__(config, _platform_identity(PLUGIN_NAME))
|
||||||
|
|
||||||
self._workspace_id = os.environ.get("MOLECULE_WORKSPACE_ID", "")
|
self._workspace_id = os.environ.get("MOLECULE_WORKSPACE_ID", "")
|
||||||
self._platform_url = os.environ.get(
|
self._platform_url = os.environ.get(
|
||||||
|
|||||||
@ -32,44 +32,53 @@ _ADAPTER_PATH = _REPO_ROOT / "hermes_channel_molecule" / "adapter.py"
|
|||||||
|
|
||||||
|
|
||||||
def _load_adapter_module():
|
def _load_adapter_module():
|
||||||
"""Import adapter.py without going through hermes_cli.plugins.
|
"""Import adapter.py without going through gateway/* + hermes_cli/*.
|
||||||
|
|
||||||
The real plugin loader supplies hermes_cli.plugins; in tests we stub
|
The real plugin loader pulls in gateway.config + gateway.platforms.base;
|
||||||
the only symbol the adapter needs (PluginPlatformIdentifier) so the
|
in tests we stub them so the import doesn't require the whole
|
||||||
import doesn't pull the whole hermes-agent tree.
|
hermes-agent tree.
|
||||||
"""
|
"""
|
||||||
fake_plugins = type(sys)("hermes_cli.plugins")
|
# Stub gateway.config.Platform — the adapter constructs Platform("molecule")
|
||||||
|
# in __init__ to identify itself to the upstream platform_registry.
|
||||||
class PluginPlatformIdentifier:
|
|
||||||
__slots__ = ("value",)
|
|
||||||
|
|
||||||
def __init__(self, name: str) -> None:
|
|
||||||
self.value = name
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
return hash(("__plugin_platform__", self.value))
|
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
return (
|
|
||||||
isinstance(other, PluginPlatformIdentifier)
|
|
||||||
and self.value == other.value
|
|
||||||
)
|
|
||||||
|
|
||||||
fake_plugins.PluginPlatformIdentifier = PluginPlatformIdentifier
|
|
||||||
sys.modules.setdefault("hermes_cli", type(sys)("hermes_cli"))
|
|
||||||
sys.modules["hermes_cli.plugins"] = fake_plugins
|
|
||||||
|
|
||||||
# Stub gateway.platforms.base — the adapter only uses
|
|
||||||
# BasePlatformAdapter, MessageEvent, MessageType, SendResult.
|
|
||||||
fake_gateway = type(sys)("gateway")
|
fake_gateway = type(sys)("gateway")
|
||||||
fake_platforms = type(sys)("gateway.platforms")
|
fake_config = type(sys)("gateway.config")
|
||||||
fake_base = type(sys)("gateway.platforms.base")
|
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional, List, Dict, Any as TAny
|
from typing import Optional, List, Dict, Any as TAny
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
class Platform(Enum):
|
||||||
|
# Open enum (per upstream #17751): Platform("molecule") creates a
|
||||||
|
# pseudo-member at runtime when not in the in-tree set. Empty
|
||||||
|
# enums can't be created in Python — seed with a sentinel that
|
||||||
|
# the adapter never references.
|
||||||
|
_SENTINEL = "__test_sentinel__"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _missing_(cls, value):
|
||||||
|
if not isinstance(value, str) or not value.strip():
|
||||||
|
return None
|
||||||
|
value = value.strip().lower()
|
||||||
|
if value in cls._value2member_map_:
|
||||||
|
return cls._value2member_map_[value]
|
||||||
|
pseudo = object.__new__(cls)
|
||||||
|
pseudo._value_ = value
|
||||||
|
pseudo._name_ = value.upper().replace("-", "_")
|
||||||
|
cls._value2member_map_[value] = pseudo
|
||||||
|
cls._member_map_[pseudo._name_] = pseudo
|
||||||
|
return pseudo
|
||||||
|
|
||||||
|
fake_config.Platform = Platform
|
||||||
|
fake_gateway.config = fake_config
|
||||||
|
sys.modules["gateway"] = fake_gateway
|
||||||
|
sys.modules["gateway.config"] = fake_config
|
||||||
|
|
||||||
|
# Stub gateway.platforms.base — the adapter only uses
|
||||||
|
# BasePlatformAdapter, MessageEvent, MessageType, SendResult.
|
||||||
|
fake_platforms = type(sys)("gateway.platforms")
|
||||||
|
fake_base = type(sys)("gateway.platforms.base")
|
||||||
|
|
||||||
class MessageType(Enum):
|
class MessageType(Enum):
|
||||||
TEXT = "text"
|
TEXT = "text"
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user