Skip to content

Commit 754d162

Browse files
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>
1 parent 191ee89 commit 754d162

3 files changed

Lines changed: 95 additions & 36 deletions

File tree

hermes_channel_molecule/__init__.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,41 @@
1616

1717

1818
def register(ctx) -> None:
19-
"""Plugin entry point — called by hermes_cli.plugins on discovery."""
20-
ctx.register_platform_adapter(
21-
name="molecule",
22-
adapter_class=MoleculeAdapter,
23-
requirements_check=check_molecule_requirements,
24-
)
19+
"""Plugin entry point — called by hermes_cli.plugins on discovery.
20+
21+
Dual-API shim:
22+
- Upstream NousResearch/hermes-agent#17751 (merged 2026-04-30)
23+
shipped ``ctx.register_platform(name, label, adapter_factory,
24+
check_fn, ...)``. This is the canonical API on stock hermes-agent.
25+
- Earlier forks (incl. hermes-agent before #17751 landed) expose
26+
``ctx.register_platform_adapter(name, adapter_class,
27+
requirements_check)`` instead — narrower signature, no factory.
28+
29+
Detect at runtime so the same wheel installs cleanly on both.
30+
"""
31+
if hasattr(ctx, "register_platform"):
32+
ctx.register_platform(
33+
name="molecule",
34+
label="Molecule",
35+
adapter_factory=lambda cfg: MoleculeAdapter(cfg),
36+
check_fn=check_molecule_requirements,
37+
required_env=["MOLECULE_WORKSPACE_ID", "MOLECULE_PLATFORM_URL"],
38+
install_hint=(
39+
"set MOLECULE_WORKSPACE_ID, MOLECULE_WORKSPACE_TOKEN, "
40+
"MOLECULE_PLATFORM_URL, MOLECULE_ORG_ID; ensure "
41+
"molecule-ai-workspace-runtime is on the python that "
42+
"MOLECULE_MCP_PYTHON resolves to"
43+
),
44+
)
45+
elif hasattr(ctx, "register_platform_adapter"):
46+
ctx.register_platform_adapter(
47+
name="molecule",
48+
adapter_class=MoleculeAdapter,
49+
requirements_check=check_molecule_requirements,
50+
)
51+
else:
52+
raise RuntimeError(
53+
"hermes-channel-molecule: this hermes-agent version exposes "
54+
"neither register_platform (upstream #17751+) nor "
55+
"register_platform_adapter (legacy fork) — cannot register"
56+
)

hermes_channel_molecule/adapter.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,25 @@
4040
MessageType,
4141
SendResult,
4242
)
43-
from hermes_cli.plugins import PluginPlatformIdentifier
43+
from gateway.config import Platform
44+
45+
46+
def _platform_identity(name: str):
47+
"""Pick the right Platform-shaped identity for the installed hermes.
48+
49+
Upstream #17751 made Platform an open enum (``Platform("molecule")``
50+
works via ``_missing_()``). Legacy forks have a closed enum and ship
51+
``PluginPlatformIdentifier`` for plugin-supplied platforms instead.
52+
Detect at import time so the same plugin works on both.
53+
"""
54+
try:
55+
return Platform(name)
56+
except ValueError:
57+
# Closed enum (legacy fork) — fall back to the fork's plugin
58+
# identifier shape. Import lazily so a stock hermes-agent doesn't
59+
# need this symbol to exist.
60+
from hermes_cli.plugins import PluginPlatformIdentifier
61+
return PluginPlatformIdentifier(name)
4462

4563
logger = logging.getLogger(__name__)
4664

@@ -142,7 +160,7 @@ class MoleculeAdapter(BasePlatformAdapter):
142160
"""Hermes platform adapter for the molecule platform via A2A MCP."""
143161

144162
def __init__(self, config) -> None:
145-
super().__init__(config, PluginPlatformIdentifier(PLUGIN_NAME))
163+
super().__init__(config, _platform_identity(PLUGIN_NAME))
146164

147165
self._workspace_id = os.environ.get("MOLECULE_WORKSPACE_ID", "")
148166
self._platform_url = os.environ.get(

tests/test_adapter.py

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,44 +32,53 @@
3232

3333

3434
def _load_adapter_module():
35-
"""Import adapter.py without going through hermes_cli.plugins.
35+
"""Import adapter.py without going through gateway/* + hermes_cli/*.
3636
37-
The real plugin loader supplies hermes_cli.plugins; in tests we stub
38-
the only symbol the adapter needs (PluginPlatformIdentifier) so the
39-
import doesn't pull the whole hermes-agent tree.
37+
The real plugin loader pulls in gateway.config + gateway.platforms.base;
38+
in tests we stub them so the import doesn't require the whole
39+
hermes-agent tree.
4040
"""
41-
fake_plugins = type(sys)("hermes_cli.plugins")
42-
43-
class PluginPlatformIdentifier:
44-
__slots__ = ("value",)
45-
46-
def __init__(self, name: str) -> None:
47-
self.value = name
48-
49-
def __hash__(self) -> int:
50-
return hash(("__plugin_platform__", self.value))
41+
# Stub gateway.config.Platform — the adapter constructs Platform("molecule")
42+
# in __init__ to identify itself to the upstream platform_registry.
43+
fake_gateway = type(sys)("gateway")
44+
fake_config = type(sys)("gateway.config")
5145

52-
def __eq__(self, other: object) -> bool:
53-
return (
54-
isinstance(other, PluginPlatformIdentifier)
55-
and self.value == other.value
56-
)
46+
from dataclasses import dataclass, field
47+
from enum import Enum
48+
from typing import Optional, List, Dict, Any as TAny
49+
from datetime import datetime
5750

58-
fake_plugins.PluginPlatformIdentifier = PluginPlatformIdentifier
59-
sys.modules.setdefault("hermes_cli", type(sys)("hermes_cli"))
60-
sys.modules["hermes_cli.plugins"] = fake_plugins
51+
class Platform(Enum):
52+
# Open enum (per upstream #17751): Platform("molecule") creates a
53+
# pseudo-member at runtime when not in the in-tree set. Empty
54+
# enums can't be created in Python — seed with a sentinel that
55+
# the adapter never references.
56+
_SENTINEL = "__test_sentinel__"
57+
58+
@classmethod
59+
def _missing_(cls, value):
60+
if not isinstance(value, str) or not value.strip():
61+
return None
62+
value = value.strip().lower()
63+
if value in cls._value2member_map_:
64+
return cls._value2member_map_[value]
65+
pseudo = object.__new__(cls)
66+
pseudo._value_ = value
67+
pseudo._name_ = value.upper().replace("-", "_")
68+
cls._value2member_map_[value] = pseudo
69+
cls._member_map_[pseudo._name_] = pseudo
70+
return pseudo
71+
72+
fake_config.Platform = Platform
73+
fake_gateway.config = fake_config
74+
sys.modules["gateway"] = fake_gateway
75+
sys.modules["gateway.config"] = fake_config
6176

6277
# Stub gateway.platforms.base — the adapter only uses
6378
# BasePlatformAdapter, MessageEvent, MessageType, SendResult.
64-
fake_gateway = type(sys)("gateway")
6579
fake_platforms = type(sys)("gateway.platforms")
6680
fake_base = type(sys)("gateway.platforms.base")
6781

68-
from dataclasses import dataclass, field
69-
from enum import Enum
70-
from typing import Optional, List, Dict, Any as TAny
71-
from datetime import datetime
72-
7382
class MessageType(Enum):
7483
TEXT = "text"
7584

0 commit comments

Comments
 (0)