forked from molecule-ai/molecule-core
Merge pull request #2518 from Molecule-AI/docs/hermes-plugin-status-update
docs(integrations): hermes plugin path status post-PR #32 merge
This commit is contained in:
commit
ea967d5787
@ -1,25 +1,57 @@
|
||||
# Upstream PR draft: Pluggable platform adapters for hermes-agent
|
||||
# Upstream PR draft: `register_platform_adapter` for hermes-agent plugins
|
||||
|
||||
**Status:** Draft — pre-submission review
|
||||
**Status:** Draft — pre-submission review (REWRITTEN 2026-05-02 after deeper research)
|
||||
**Target repo:** `NousResearch/hermes-agent`
|
||||
**Owner:** Molecule AI (hongmingwang@moleculesai.app)
|
||||
**Date drafted:** 2026-05-02
|
||||
**Date drafted:** 2026-05-02 (rewrite of earlier draft)
|
||||
|
||||
---
|
||||
|
||||
## Why this draft exists
|
||||
## Background — what changed in this draft
|
||||
|
||||
Molecule needs to deliver A2A inbox messages to a hermes-hosted agent the same way Telegram messages reach it today — through `_handle_message`, with `set_busy_session_handler` semantics for mid-turn arrivals. Today this requires forking `gateway/run.py` because the platform adapter system is closed (`_create_adapter` is a hardcoded if/elif chain at lines 2424-2578).
|
||||
The first draft proposed adding a `plugins/platforms/` discovery
|
||||
directory + a `_create_adapter()` fallback chain. **That was wrong** —
|
||||
it duplicated infrastructure that already exists.
|
||||
|
||||
But hermes already ships a working plugin discovery system for memory backends (`plugins/memory/__init__.py`). Extending the same pattern to platforms is a small, symmetric change — not novel architecture. This draft documents the proposed upstream PR before we open it, so we can iterate locally on tone, scope, and code shape.
|
||||
Deeper research established (validated by hand-rolling a test plugin
|
||||
under `~/.hermes/plugins/`):
|
||||
|
||||
- **`hermes_cli/plugins.py` already implements full plugin discovery
|
||||
across THREE sources:**
|
||||
- User dir: `~/.hermes/plugins/<name>/`
|
||||
- Project dir: `./.hermes/plugins/<name>/`
|
||||
- Pip entry_points group: `hermes_agent.plugins`
|
||||
- The discovery loop is at `hermes_cli/plugins.py:433` and
|
||||
`_scan_entry_points()` at line 499.
|
||||
- `PluginContext` (line 124) exposes a `register(ctx)` collector with:
|
||||
- `register_tool` (line 133)
|
||||
- `register_cli_command` (line 192)
|
||||
- `register_command` (line 217) — slash command
|
||||
- `register_context_engine` (line 295)
|
||||
- `register_hook` (line 327)
|
||||
- `register_skill` (line 346)
|
||||
- **But NOT `register_platform_adapter`.** Platform adapters remain
|
||||
hardcoded in `gateway/run.py:_create_adapter()` (lines 2424-2578),
|
||||
the only major subsystem still closed to plugins.
|
||||
- Memory providers have a parallel discovery system at
|
||||
`plugins/memory/__init__.py` for legacy reasons; the modern
|
||||
`hermes_cli/plugins.py` is the way forward for new plugin types.
|
||||
- Hand-rolled test confirmed user-dir and entry_points discovery both
|
||||
work end-to-end. **Zero external plugins exist in the wild today**
|
||||
— the system is technically mature but socially unused.
|
||||
|
||||
This makes the PR much smaller and more obviously correct: extend the
|
||||
existing plugin pattern by one method, mirror how memory providers
|
||||
work, no novel infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## Proposed PR title
|
||||
|
||||
> Pluggable platform adapters via `plugins/platforms/` discovery
|
||||
> `feat(gateway): platform adapter plugins via PluginContext.register_platform_adapter`
|
||||
|
||||
(Mirrors the existing `plugins/memory/` shape so the title alone signals "this is the same pattern, just for the other subsystem.")
|
||||
Branch: `feat/platform-adapter-plugins` per
|
||||
`CONTRIBUTING.md` branch convention.
|
||||
|
||||
---
|
||||
|
||||
@ -27,136 +59,188 @@ But hermes already ships a working plugin discovery system for memory backends (
|
||||
|
||||
### Problem
|
||||
|
||||
Hermes ships 19 in-tree platform adapters (Telegram, Discord, WhatsApp, Slack, Signal, Mattermost, Matrix, Email, SMS, DingTalk, Feishu, WeCom variants, Weixin, BlueBubbles, QQBot, HomeAssistant, API server, Webhook). Each is wired by editing two files:
|
||||
Hermes ships 19 in-tree platform adapters (`gateway/run.py:2424-2578`).
|
||||
Adding a new platform requires editing two files: append a `Platform`
|
||||
enum value at `gateway/config.py:48-69`, then append an `elif platform
|
||||
== Platform.X:` branch in `_create_adapter()`. For platforms with broad
|
||||
demand (Telegram, Slack, Discord) this is fine. For narrower channels
|
||||
— enterprise-internal protocols, agent-to-agent inbox bridges, niche
|
||||
regional platforms — the only path is a fork of `gateway/run.py`.
|
||||
|
||||
- `gateway/config.py:48-69` — append a `Platform` enum value
|
||||
- `gateway/run.py:2424-2578` — append an `elif platform == Platform.X:` branch in `_create_adapter()`
|
||||
|
||||
For platforms with broad demand (Telegram, Slack, etc.) this is fine: the maintenance load lives upstream, every user benefits. For platforms with narrow but real demand — enterprise-internal channels (Rocket.Chat, RingCentral, Zulip), agent-to-agent inbox protocols (e.g. Molecule's A2A), niche regional platforms, or experimental transports — the only path today is forking `gateway/run.py`. Forks drift, defeat the purpose of an OSS gateway, and discourage contribution back upstream.
|
||||
|
||||
### Prior art (already in hermes)
|
||||
|
||||
The memory subsystem solved exactly this problem at `plugins/memory/__init__.py`:
|
||||
|
||||
1. **Two-tier discovery** — bundled providers in `plugins/memory/<name>/` plus user-installed providers in `$HERMES_HOME/plugins/<name>/`. Bundled wins on name collision.
|
||||
2. **`register(ctx)` collector pattern** (`plugins/memory/__init__.py:264-305`) — a plugin's `__init__.py` exposes a `register(ctx)` function; `ctx` already supports `register_memory_provider`, `register_tool`, `register_hook`, `register_cli_command`.
|
||||
3. **`plugin.yaml` manifest** for description and metadata.
|
||||
4. **Config-driven activation** (`memory.provider: honcho` selects which provider loads).
|
||||
|
||||
Adding `register_platform_adapter` to the same collector and a `plugins/platforms/` discovery directory extends this pattern symmetrically.
|
||||
This is the only major subsystem that's still closed. Tools, CLI
|
||||
commands, slash commands, context engines, hooks, and skills all
|
||||
already extend via `hermes_cli/plugins.py`'s `PluginContext`
|
||||
collector, with three discovery paths (user dir / project dir / pip
|
||||
entry_points). Platform adapters should follow the same pattern.
|
||||
|
||||
### Proposal
|
||||
|
||||
**Three small changes:**
|
||||
Add **one collector method** to `PluginContext` and **one fallback
|
||||
branch** to `_create_adapter()`. That's the entire change.
|
||||
|
||||
1. **New collector method** in `plugins/memory/__init__.py:_ProviderCollector` (or a new shared `plugins/_collector.py` if maintainers prefer cleaner separation):
|
||||
**1. New collector method in `hermes_cli/plugins.py`**, beside the
|
||||
existing `register_tool` / `register_hook` etc.:
|
||||
|
||||
```python
|
||||
def register_platform_adapter(self, name: str, adapter_class: type, requirements_check=None):
|
||||
"""Register a platform adapter loadable as plugin.
|
||||
```python
|
||||
class PluginContext:
|
||||
# ...existing register_* methods...
|
||||
|
||||
name: unique platform identifier (matches gateway.platforms.<name> in config)
|
||||
adapter_class: subclass of BasePlatformAdapter
|
||||
requirements_check: optional callable returning bool — same shape as
|
||||
existing check_telegram_requirements() etc.
|
||||
"""
|
||||
self.platform_adapters[name] = (adapter_class, requirements_check)
|
||||
```
|
||||
def register_platform_adapter(
|
||||
self,
|
||||
name: str,
|
||||
adapter_class: type,
|
||||
requirements_check: Callable[[], bool] | None = None,
|
||||
) -> None:
|
||||
"""Register a custom platform adapter.
|
||||
|
||||
2. **New `plugins/platforms/__init__.py`** mirroring `plugins/memory/__init__.py` — `discover_platform_adapters()`, `load_platform_adapter(name)`, two-tier (bundled + `$HERMES_HOME/plugins/`) discovery.
|
||||
name — unique platform identifier (matches
|
||||
gateway.platforms.<name> in config.yaml)
|
||||
adapter_class — subclass of BasePlatformAdapter
|
||||
requirements_check— optional, returns False if dependencies
|
||||
missing (matches existing
|
||||
check_telegram_requirements pattern).
|
||||
"""
|
||||
self._registered_platform_adapters[name] = (adapter_class, requirements_check)
|
||||
```
|
||||
|
||||
3. **`_create_adapter()` fallback** at `gateway/run.py:2578` — after the in-tree if/elif chain returns None, attempt plugin lookup:
|
||||
**2. Plugin-registered adapters in `_create_adapter()`** —
|
||||
fall through to the plugin-registered map after the in-tree if/elif
|
||||
chain returns None:
|
||||
|
||||
```python
|
||||
# Existing in-tree adapters checked first (precedence preserved).
|
||||
# If no match, fall through to plugin discovery.
|
||||
from plugins.platforms import load_platform_adapter
|
||||
plugin_entry = load_platform_adapter(platform.value)
|
||||
if plugin_entry:
|
||||
adapter_class, req_check = plugin_entry
|
||||
if req_check and not req_check():
|
||||
logger.warning(f"{platform.value}: plugin requirements not met")
|
||||
return None
|
||||
return adapter_class(config)
|
||||
return None
|
||||
```
|
||||
```python
|
||||
# at gateway/run.py:2578, AFTER the existing chain
|
||||
plugin_entry = self._plugin_manager.get_platform_adapter(platform.value)
|
||||
if plugin_entry:
|
||||
adapter_class, req_check = plugin_entry
|
||||
if req_check and not req_check():
|
||||
logger.warning(f"{platform.value}: plugin requirements not met")
|
||||
return None
|
||||
return adapter_class(config)
|
||||
|
||||
4. **`Platform` enum becomes open-set.** Today it's `Enum`; switch to a string-backed pattern that accepts unknown values (still validates against the union of in-tree + discovered plugins at config-load time):
|
||||
return None # existing return
|
||||
```
|
||||
|
||||
```python
|
||||
# gateway/config.py — replace Enum with frozen dataclass + dynamic registry.
|
||||
# Keeps the in-tree values as module-level singletons for backward compat:
|
||||
# Platform.TELEGRAM still works as today.
|
||||
```
|
||||
**3. `Platform` enum stays closed** but accepts unknown values
|
||||
through a small loosening: rather than refactor enum-vs-string,
|
||||
introduce `Platform.from_string()` that returns either an existing
|
||||
enum member OR a synthetic `Platform.PLUGIN(value)`-equivalent that
|
||||
carries the plugin name through. `_create_adapter()` then dispatches
|
||||
on the carried name. This is the smallest change preserving
|
||||
backward compatibility — every existing `Platform.TELEGRAM` reference
|
||||
keeps working unchanged.
|
||||
|
||||
This is the only "shape change" in the PR. Backward compat is straightforward: every existing `Platform.TELEGRAM` reference continues to work because the module exports the same names.
|
||||
### Why this is the right shape
|
||||
|
||||
- **Symmetric.** Mirrors `register_tool`, `register_hook`, etc. — same
|
||||
collector, same discovery, same lifecycle. No new mental model.
|
||||
- **No new infrastructure.** Reuses `hermes_cli/plugins.py`'s existing
|
||||
three-source discovery (user dir / project dir / entry_points) —
|
||||
zero new code paths to test.
|
||||
- **Backward compatible.** All 19 in-tree adapters keep their
|
||||
hardcoded path; precedence is in-tree first, plugin fallback. No
|
||||
behavior change for any existing user.
|
||||
- **Discovery cost is zero.** Plugin lookup only fires if the
|
||||
platform name doesn't match an in-tree value.
|
||||
- **Forward compatible.** When external plugins become commonplace
|
||||
(today: zero published, system technically mature but unused),
|
||||
platform adapters benefit from the same ecosystem growth as tools.
|
||||
|
||||
### What we'll ship as the first consumer
|
||||
|
||||
Molecule will publish `hermes-platform-molecule-a2a` on PyPI with the
|
||||
appropriate `[project.entry-points."hermes_agent.plugins"]` entry. It
|
||||
delivers Molecule platform A2A inbox messages into the same
|
||||
`_handle_message` dispatch Telegram uses, with
|
||||
`MessageEvent(internal=True)` to bypass user-auth (peer agents are
|
||||
authenticated at the platform layer, not the Telegram-user level).
|
||||
Implementation lives in our workspace template; this PR upstream is
|
||||
the contract change that lets us register without forking.
|
||||
|
||||
### Backward compatibility
|
||||
|
||||
- All 19 in-tree adapters keep their hardcoded path in `_create_adapter()` (precedence: in-tree wins on name collision, exactly like memory plugins).
|
||||
- Existing config files (`gateway.platforms.telegram.enabled: true`) continue to work unchanged.
|
||||
- All 19 in-tree adapters keep their hardcoded path. Precedence:
|
||||
in-tree wins on name collision (matches the memory plugin pattern).
|
||||
- `gateway.platforms.telegram.enabled: true` etc. continue to work
|
||||
unchanged.
|
||||
- No new mandatory config keys.
|
||||
- Plugin discovery only runs if the platform name doesn't match an in-tree value, so cold-start cost is zero for users who don't use plugins.
|
||||
- Fork-then-add-platform users can migrate to plugins at their own pace; the in-tree path isn't deprecated.
|
||||
- Existing `Platform.X` Python references unchanged.
|
||||
- Plugin discovery only adds latency on platforms that don't match
|
||||
an in-tree value — zero cost for existing users.
|
||||
|
||||
### Test plan
|
||||
|
||||
- **Unit**: discovery scans both bundled and user dirs, respects precedence.
|
||||
- **Unit**: `_create_adapter()` falls through to plugin lookup only when in-tree doesn't match.
|
||||
- **Integration**: ship a minimal `plugins/platforms/example/` in-tree (read-only, returns canned messages) so CI exercises the full plugin code path. Same approach `plugins/memory/holographic/` takes today.
|
||||
- **Manual**: Molecule will publish `hermes-platform-molecule-a2a` as the first external consumer once this lands.
|
||||
- **Unit:** Mock plugin registers an adapter via `register_platform_adapter`;
|
||||
`_create_adapter()` returns it for the corresponding platform name.
|
||||
- **Unit:** In-tree precedence — when plugin AND in-tree both register
|
||||
`telegram`, in-tree wins.
|
||||
- **Unit:** Duplicate plugin registration warns + skips, doesn't
|
||||
replace the original.
|
||||
- **Integration:** Add `tests/plugins/platform_example/` (matching
|
||||
the existing `tests/plugins/` shape — see how `register_tool` is
|
||||
tested today). Smoke that hermes boot loads it.
|
||||
- **Manual (already done locally):** `hermes-platform-molecule-a2a`
|
||||
scaffold validates against the patched fork end-to-end:
|
||||
- 11/11 unit tests on the adapter (lifecycle, inbound auth, outbound
|
||||
routing, plugin entry-point shape)
|
||||
- 7/7 production-path checkpoints (entry_points discovery → registry
|
||||
→ `GatewayConfig.from_dict` → `_create_plugin_adapter` → live
|
||||
HTTP listener → `MessageEvent` dispatch → callback POST)
|
||||
- 9/9 user-dir-discovery validation against the patched
|
||||
`PluginContext` / `PluginManager`
|
||||
- **Pre-existing test isolation issue (independent of this PR):**
|
||||
`tests/hermes_cli/test_plugins.py::test_discover_is_idempotent` and
|
||||
two siblings assert `len(list_plugins()) == 1` after creating one
|
||||
test plugin in a tmp_path. They fail on any dev box that has a
|
||||
hermes plugin pip-installed (entry_points discovery is global, not
|
||||
isolated by HERMES_HOME). Not caused by this patch but surfaced
|
||||
during validation. Worth fixing in a follow-up by either filtering
|
||||
entry-point plugins out of these specific tests, or adding a
|
||||
`discover_only_user_dir=True` test hook to `discover_and_load`.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Extend `CONTRIBUTING.md`'s "Should it be a Skill or a Tool?" section with "Should it be a Platform Plugin or an in-tree Platform?" — same shape, same decision tree.
|
||||
- Add `plugins/platforms/README.md` mirroring `plugins/memory/`'s convention.
|
||||
- Extend `website/docs/developer-guide/build-a-hermes-plugin.md`'s
|
||||
capability list to mention platform adapters alongside tools, hooks,
|
||||
etc.
|
||||
- One-paragraph note in `gateway/run.py` explaining the in-tree-first,
|
||||
plugin-fallback precedence.
|
||||
|
||||
### Out of scope (intentionally)
|
||||
### Out of scope
|
||||
|
||||
- **Setuptools `entry_points`** — could be added later as a third discovery tier (after bundled + `$HERMES_HOME/plugins/`). Skipping for v1 because the directory-based discovery already covers the demand and matches the memory pattern. Adding entry_points is a non-breaking extension.
|
||||
- **Hot-reload** — plugins discovered at gateway boot, no live re-scan. Matches memory plugins.
|
||||
- **Sandboxing** — plugins run with full hermes process privileges. Same trust model as memory plugins; documented in the new README.
|
||||
|
||||
### Reference consumer
|
||||
|
||||
Molecule AI will ship `hermes-platform-molecule-a2a` as the first external consumer. Use case: deliver agent-to-agent inbox messages (from peer agents authenticated at the platform layer, not the Telegram-user level) into the same `_handle_message` dispatch Telegram uses, with `internal=True` events to bypass user-auth. Expected timeline: within 2 weeks of merge.
|
||||
- Memory provider system migration (still uses
|
||||
`plugins/memory/__init__.py`'s separate discovery). Out of scope
|
||||
for this PR — orthogonal cleanup.
|
||||
- A "Plugins Hub" analogous to Skills Hub. Independently useful but
|
||||
separate proposal; ship the contract first, build the
|
||||
distribution/discovery UX later.
|
||||
|
||||
---
|
||||
|
||||
## Open questions for upstream maintainers
|
||||
## Open questions to put in the GitHub Discussion
|
||||
|
||||
Per `CONTRIBUTING.md`, the right channel for design proposals is
|
||||
**GitHub Discussions**, not Discord (Discord is for "questions,
|
||||
showcasing projects, and sharing skills" — Discussions is the
|
||||
documented channel for "design proposals and architecture discussions").
|
||||
Per `CONTRIBUTING.md`, design proposals go in **GitHub Discussions**
|
||||
at `NousResearch/hermes-agent/discussions`, not Discord. Open one
|
||||
titled "RFC: `PluginContext.register_platform_adapter`" before filing
|
||||
the PR. Questions to surface:
|
||||
|
||||
Open a Discussion at `NousResearch/hermes-agent/discussions` titled
|
||||
"RFC: pluggable platform adapters via `plugins/platforms/`" with the
|
||||
problem + proposal + open questions before filing the PR. This gives
|
||||
maintainers space to weigh in on shape before code is in flight.
|
||||
|
||||
Open questions to put in the Discussion:
|
||||
|
||||
1. **Preferred naming.** `register_platform_adapter` vs `register_platform` vs `register_channel`. Consistency with memory's `register_memory_provider` argues for the long form.
|
||||
2. **Enum vs string.** Is the maintainer team open to making `Platform` open-set? If not, fallback design: keep enum, add a single `Platform.PLUGIN` sentinel + a `plugin_name` field on `PlatformConfig`. Slightly uglier but smaller blast radius.
|
||||
3. **Testing**: `plugins/platforms/example/` checked into the repo, or test-fixtures-only? Memory plugins are real (mem0, honcho, supermemory bundled), so a real example seems consistent.
|
||||
4. **Discovery ordering**: confirm the user wants bundled-wins precedence (matches memory) vs user-can-override-bundled (would let downstream patch a buggy in-tree adapter without forking). Current memory pattern is bundled-wins; we'll match it unless told otherwise.
|
||||
|
||||
---
|
||||
|
||||
## Effort estimate
|
||||
|
||||
- **Code change**: ~150 LOC across `plugins/platforms/__init__.py` (new), `gateway/config.py` (Platform refactor), `gateway/run.py` (10-line fallback in `_create_adapter`), tests (~50 LOC).
|
||||
- **Docs**: ~80 LOC across `CONTRIBUTING.md` extension and new `plugins/platforms/README.md`.
|
||||
- **Review cycle**: depends on maintainer responsiveness. Memory plugin system shipped in v0.5–0.7 era; platform plugin system would land for v0.11 if accepted.
|
||||
|
||||
---
|
||||
|
||||
## After this PR lands (Molecule-side follow-up)
|
||||
|
||||
1. Publish `hermes-platform-molecule-a2a` (PyPI + `~/.hermes/plugins/molecule-a2a/`).
|
||||
2. Bump our hermes workspace template to declare `plugins.platforms.molecule_a2a.enabled: true`.
|
||||
3. Remove the polling shim from `molecule-ai-workspace-template-hermes/adapter.py` once the plugin path is verified end-to-end.
|
||||
1. **Naming.** `register_platform_adapter` matches existing
|
||||
`register_*` collector methods. Short forms (`register_platform`,
|
||||
`register_channel`) are also possible. Defaulting to the long form
|
||||
for consistency.
|
||||
2. **Synthetic Platform value.** Is a `Platform.from_string()` helper
|
||||
(with synthetic plugin entries) acceptable, or do maintainers
|
||||
prefer a different shape — e.g., adding a `name: str` field to
|
||||
`PlatformConfig` so callers know the plugin name without going
|
||||
through the enum?
|
||||
3. **Test fixture vs example plugin.** The `tests/plugins/`
|
||||
directory has fixture-only plugins. Should the platform adapter
|
||||
test plugin live there too, or as a real bundled adapter (matching
|
||||
how memory providers ship as real bundled implementations under
|
||||
`plugins/memory/<name>/`)?
|
||||
4. **Multi-account plugins.** Existing platforms (Telegram, Slack)
|
||||
support multi-account via the `extra` config dict. Is the
|
||||
plugin-registered adapter expected to handle the same shape, or
|
||||
is single-account a reasonable v1 constraint?
|
||||
|
||||
---
|
||||
|
||||
@ -165,27 +249,49 @@ Open questions to put in the Discussion:
|
||||
Per user's gating: "if the plugin works locally in our docker setup
|
||||
and e2e testing works, yes [submit]". Validation prerequisites:
|
||||
|
||||
- [ ] Build a working `plugins/platforms/molecule_a2a/` plugin against
|
||||
a forked hermes-agent with the proposed change applied
|
||||
- [x] Build `hermes-platform-molecule-a2a` against a forked hermes
|
||||
with the proposed `register_platform_adapter` patch applied
|
||||
→ `~/hermes-platform-molecule-a2a/`, 11/11 unit tests pass,
|
||||
7/7 production-path E2E checkpoints pass
|
||||
- [x] Patched fork at `~/.hermes/hermes-agent` branch
|
||||
`feat/platform-adapter-plugins` (4 commits):
|
||||
1. `PluginContext.register_platform_adapter` + manager registry
|
||||
+ `get_plugin_platform_adapter` accessor
|
||||
2. `GatewayConfig.plugin_platforms` + `_create_plugin_adapter`
|
||||
boot path
|
||||
3. `PluginPlatformIdentifier` helper for `BasePlatformAdapter`
|
||||
construction
|
||||
4. `resolve_platform_id` for plugin-platform-safe deserialization
|
||||
in `SessionSource.from_dict` / `SessionEntry.from_dict` /
|
||||
`HomeChannel.from_dict` (without this, daemon restart loses
|
||||
every plugin-platform session)
|
||||
- [ ] Bake the forked hermes + plugin into a local copy of our
|
||||
`molecule-ai-workspace-template-hermes` Docker image
|
||||
- [ ] E2E: boot the local image, send A2A messages from a peer agent,
|
||||
observe `_handle_message` dispatch + reply through A2A queue
|
||||
- [ ] Confirm `Platform` enum refactor doesn't break downstream — grep
|
||||
for `Platform.X` usages across hermes
|
||||
- [ ] Confirm `$HERMES_HOME` is the right user-plugin root for
|
||||
platforms (matches memory convention)
|
||||
- [ ] Open a GitHub Discussion at
|
||||
`NousResearch/hermes-agent/discussions` titled
|
||||
"RFC: pluggable platform adapters via plugins/platforms/" with
|
||||
design + open questions; wait for maintainer feedback
|
||||
- [ ] Branch name: `feat/pluggable-platform-adapters` per
|
||||
CONTRIBUTING.md branch convention
|
||||
- [ ] Commit prefix: `feat(gateway): pluggable platform adapters
|
||||
via plugins/platforms/` per Conventional Commits + scope `gateway`
|
||||
- [ ] PR description covers what/why + how-to-test + platforms tested,
|
||||
per CONTRIBUTING.md PR-description requirements
|
||||
- [ ] Open PR against `NousResearch/hermes-agent` main once Discussion
|
||||
lands consensus
|
||||
- [ ] Confirm `PluginPlatformIdentifier` doesn't break any downstream
|
||||
`isinstance(self.platform, Platform)` check — grep for those
|
||||
- [ ] Open GitHub Discussion for design validation; wait for maintainer
|
||||
feedback (≥1 week)
|
||||
- [ ] Address Discussion feedback in the PR
|
||||
- [ ] PR description: what/why + how-to-test + platforms tested per
|
||||
`CONTRIBUTING.md`
|
||||
- [ ] Open PR against `NousResearch/hermes-agent` `main` (**requires
|
||||
user confirmation** — visible-to-others action)
|
||||
- [ ] Track PR; bump cadence weekly; if stalled past 4 weeks, propose
|
||||
fork-and-bundle as fallback for our hermes template image
|
||||
bundling our adapter directly under `gateway/platforms/molecule_a2a.py`
|
||||
as a fallback (smaller upstream maintenance footprint than fork)
|
||||
|
||||
---
|
||||
|
||||
## What changed from the first draft, in one paragraph
|
||||
|
||||
First draft proposed extending the memory-provider pattern to platforms
|
||||
via a new `plugins/platforms/` directory and bespoke discovery code in
|
||||
`_create_adapter()`. Research established that hermes's MODERN plugin
|
||||
system is `hermes_cli/plugins.py` (not `plugins/memory/`), already
|
||||
supports user-dir + entry_points discovery for tools/hooks/CLI/skills,
|
||||
and just needs `register_platform_adapter` added to its collector to
|
||||
cover platforms too. The new draft is ~60 lines of upstream code change
|
||||
instead of ~200, with a tighter conceptual fit and better forward
|
||||
compatibility.
|
||||
|
||||
@ -51,21 +51,77 @@ adapter POSTs A2A messages to it; gateway dispatches through the same
|
||||
|
||||
## hermes
|
||||
|
||||
**Status:** Upstream PR drafted; short-term shim deemed unnecessary.
|
||||
**Status:** Workspace template patch PR #32 MERGED 2026-05-02; image
|
||||
rebuild succeeded; plugin baked into the workspace runtime. Plugin
|
||||
package published. Real-subprocess full-chain E2E (`scripts/e2e_full_chain.py`)
|
||||
green — proves wire shape end-to-end against a real `hermes gateway run`
|
||||
subprocess + stub OpenAI-compat LLM. Caught + fixed a real `KeyError`
|
||||
in upstream `hermes_cli/tools_config.py` (PLATFORMS dict lookup
|
||||
crashed on plugin platforms) — fix on the patched fork branch
|
||||
(`HongmingWang-Rabbit/hermes-agent` `feat/platform-adapter-plugins`,
|
||||
commit `18e4849e`). Upstream PR #18775 OPEN; CONFLICTING with main.
|
||||
Not on critical path for our platform — patched fork is what the
|
||||
workspace image installs.
|
||||
|
||||
**Path:** Open the upstream `BasePlatformAdapter` system to external
|
||||
plugins. Hermes already ships a working plugin discovery system for
|
||||
memory backends (`plugins/memory/`, `register(ctx)` collector pattern,
|
||||
`$HERMES_HOME/plugins/<name>/` user-installed tier). The PR extends
|
||||
the same shape to platforms — `register_platform_adapter(...)` on the
|
||||
existing collector, new `plugins/platforms/` discovery directory,
|
||||
3-line fallback in `_create_adapter()`. Symmetric, not novel.
|
||||
Real A2A peer traffic on staging gated only on running the harness
|
||||
(`molecule-core/scripts/test-all-runtimes-a2a-e2e.sh`) — script ready,
|
||||
needs provider keys.
|
||||
|
||||
**Path:** Hermes's MODERN plugin system is `hermes_cli/plugins.py`
|
||||
(not the older `plugins/memory/`). It already does full discovery
|
||||
across user dir + project dir + pip entry_points (group:
|
||||
`hermes_agent.plugins`) for tools / hooks / CLI commands / slash
|
||||
commands / context engines / skills. **Platform adapters are the only
|
||||
plugin type still hardcoded** (`gateway/run.py:_create_adapter`).
|
||||
|
||||
The PR adds three pieces upstream:
|
||||
1. `PluginContext.register_platform_adapter(name, adapter_class, requirements_check=None)`
|
||||
2. `GatewayConfig.plugin_platforms` populated by `from_dict` for
|
||||
plugin-claimed names
|
||||
3. `GatewayRunner._create_plugin_adapter(name, config)` boot-path
|
||||
fallback
|
||||
|
||||
Plus a `PluginPlatformIdentifier` helper class so plugin adapters can
|
||||
satisfy `BasePlatformAdapter.__init__(config, platform: Platform)`
|
||||
without extending the closed Platform enum.
|
||||
|
||||
Total: ~100 LOC upstream change. External plugin then ships as
|
||||
`hermes-platform-molecule-a2a` via `pip install` + entry_points — no
|
||||
fork needed in production.
|
||||
|
||||
**Artifacts landed:**
|
||||
- `docs/integrations/hermes-platform-plugins-upstream-pr.md` — full
|
||||
PR draft including problem, prior art, proposal, code shape,
|
||||
backward compat, test plan, and four open questions to resolve in
|
||||
Discord before submitting.
|
||||
- **Upstream PR**: [NousResearch/hermes-agent#18775](https://github.com/NousResearch/hermes-agent/pull/18775)
|
||||
— 5 commits on `feat/platform-adapter-plugins`: registration
|
||||
surface, config + boot wiring, `PluginPlatformIdentifier` helper,
|
||||
`resolve_platform_id` for plugin-platform-safe deserialization, and
|
||||
`self.adapters[adapter.platform]` keying fix (caught by real-subprocess
|
||||
test before merge — see below).
|
||||
- **Plugin package**: [Molecule-AI/hermes-platform-molecule-a2a](https://github.com/Molecule-AI/hermes-platform-molecule-a2a)
|
||||
v0.1.0 — public, MIT-licensed. 11 unit tests + 8 in-process E2E
|
||||
+ 4 real-subprocess E2E checkpoints all green.
|
||||
- **Workspace template patch**: [Molecule-AI/molecule-ai-workspace-template-hermes#32](https://github.com/Molecule-AI/molecule-ai-workspace-template-hermes/pull/32)
|
||||
— Dockerfile installs the patched fork + plugin into the hermes
|
||||
installer's venv; start.sh seeds `platforms.molecule-a2a` config
|
||||
stanza. Pre-demo deliberately install-only; adapter.py rewrite to
|
||||
USE the plugin path is a separate post-demo PR.
|
||||
- Real adapter package at `~/hermes-platform-molecule-a2a/`:
|
||||
- `pyproject.toml` with `hermes_agent.plugins` entry point
|
||||
- `hermes_platform_molecule_a2a/adapter.py` —
|
||||
`MoleculeA2APlatformAdapter(BasePlatformAdapter)` with HTTP
|
||||
listener (aiohttp), inbound `MessageEvent(internal=True)` dispatch,
|
||||
outbound `send()` POST to per-chat callback URL, optional shared
|
||||
secret enforcement
|
||||
- `tests/test_adapter.py` — **11/11 unit tests pass** covering plugin
|
||||
entry-point shape, lifecycle, inbound auth, outbound routing
|
||||
- `scripts/e2e_validate.py` — production-path validation (entry
|
||||
points → registry → GatewayConfig → boot → HTTP roundtrip), all
|
||||
7 checkpoints pass
|
||||
- `docs/integrations/hermes-platform-plugins-upstream-pr.md` — PR
|
||||
draft including problem, prior art, proposal, code shape, backward
|
||||
compat, test plan, and open questions.
|
||||
- `.hermes-validation/test_register_platform_adapter.py` — local
|
||||
9-check validation of the patched fork via the user-dir discovery
|
||||
path (complementary to the entry-points path tested by the package).
|
||||
|
||||
**Why no short-term polling shim:** earlier framing was wrong. Molecule
|
||||
runtime already polls the inbox via `wait_for_message` per turn; each
|
||||
@ -77,23 +133,31 @@ conversation across turns because chat/completions is stateless), not
|
||||
push latency. That gap is solved by the upstream PR; no
|
||||
intermediate shim earns its complexity.
|
||||
|
||||
**Remaining (task #83):**
|
||||
1. Reach out in Nous Research Discord to validate open questions
|
||||
(Platform enum-vs-string refactor, naming, example-plugin scope).
|
||||
2. Submit PR to `NousResearch/hermes-agent`. **Requires user
|
||||
confirmation** — opening an upstream PR is an action visible to
|
||||
others.
|
||||
3. Once merged: ship `hermes-platform-molecule-a2a` as the first
|
||||
external consumer, bump our hermes workspace template to enable
|
||||
it, remove any transitional code.
|
||||
**Remaining:**
|
||||
1. **Upstream PR review/merge** (NousResearch/hermes-agent#18775). On
|
||||
maintainers — typical OSS review lag.
|
||||
2. **Workspace template merge + image republish** (PR #32). Once
|
||||
merged, `publish-runtime.yml` regenerates the hermes workspace image
|
||||
with the plugin baked in. Safe to merge as-is — install-only, no
|
||||
behavior change for current workspaces.
|
||||
3. **Runtime adapter rewrite** (task #87 equivalent for hermes).
|
||||
`molecule-ai-workspace-template-hermes/adapter.py` currently proxies
|
||||
A2A → `/v1/chat/completions`. Switching to POST `/a2a/inbound` is
|
||||
what unlocks single-session continuity. **Post-demo timing**
|
||||
(touches a working live integration).
|
||||
4. **Real A2A peer traffic E2E** (task #86): boot a real workspace
|
||||
from the republished image, send peer A2A message from another
|
||||
workspace, observe single-session reply. Gated on items 2 + 3.
|
||||
|
||||
---
|
||||
|
||||
## Codex (OpenAI Codex CLI)
|
||||
|
||||
**Status:** Template structurally complete (12 files, 12/12 tests passing,
|
||||
validated against real codex-cli 0.72.0). Awaiting molecule-core
|
||||
registry integration + E2E.
|
||||
**Status:** Template SHIPPED. Repo live at
|
||||
[`Molecule-AI/molecule-ai-workspace-template-codex`](https://github.com/Molecule-AI/molecule-ai-workspace-template-codex)
|
||||
(14 files, 1411 LOC, 12/12 tests). molecule-core registration in
|
||||
[PR #2512](https://github.com/Molecule-AI/molecule-core/pull/2512).
|
||||
E2E with real A2A traffic remains.
|
||||
|
||||
**Path:** Persistent `codex app-server` stdio JSON-RPC client
|
||||
(NDJSON-framed, v2 protocol). One app-server child per workspace
|
||||
|
||||
Loading…
Reference in New Issue
Block a user