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:
"""Plugin entry point — called by hermes_cli.plugins on discovery."""
"""Plugin entry point — called by hermes_cli.plugins on discovery.
Dual-API shim:
- Upstream NousResearch/hermes-agent#17751 (merged 2026-04-30)
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,
SendResult,
)
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__)
@ -142,7 +160,7 @@ class MoleculeAdapter(BasePlatformAdapter):
"""Hermes platform adapter for the molecule platform via A2A MCP."""
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._platform_url = os.environ.get(

View File

@ -32,44 +32,53 @@ _ADAPTER_PATH = _REPO_ROOT / "hermes_channel_molecule" / "adapter.py"
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 only symbol the adapter needs (PluginPlatformIdentifier) so the
import doesn't pull the whole hermes-agent tree.
The real plugin loader pulls in gateway.config + gateway.platforms.base;
in tests we stub them so the import doesn't require the whole
hermes-agent tree.
"""
fake_plugins = type(sys)("hermes_cli.plugins")
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.
# Stub gateway.config.Platform — the adapter constructs Platform("molecule")
# in __init__ to identify itself to the upstream platform_registry.
fake_gateway = type(sys)("gateway")
fake_platforms = type(sys)("gateway.platforms")
fake_base = type(sys)("gateway.platforms.base")
fake_config = type(sys)("gateway.config")
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, List, Dict, Any as TAny
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):
TEXT = "text"