feat: dual-mode for upstream register_platform (post-#17751) + legacy register_platform_adapter
Some checks failed
CI / test (3.11) (push) Failing after 13m37s
CI / test (3.12) (push) Failing after 13m37s

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:
Hongming Wang 2026-05-03 07:47:51 -07:00
parent 191ee89c19
commit 754d162d99
3 changed files with 96 additions and 37 deletions

View File

@ -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"
)

View File

@ -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(

View File

@ -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"