hotfix(adapter): default MOLECULE_A2A_PLATFORM_ENABLED=false

Staging E2E for PR #32 surfaced a workspace boot failure: the deployed
image's hermes gateway never bound :8645, so adapter.setup()'s
/a2a/health probe got httpx.ConnectError and the workspace went
status=failed at ~498s.

Root cause is image-side install/discovery of the molecule-a2a plugin,
NOT the executor wire shape. Local scripts/e2e_full_chain.py runs
against a venv where I'd already installed the plugin manually — it
didn't catch the deployment-shape divergence.

Flip the default off to restore the legacy /v1/chat/completions
fallback (no session continuity, but works). Plugin path stays
opt-in via MOLECULE_A2A_PLATFORM_ENABLED=true so debugging can
continue per-workspace without rolling the whole image again.

Re-enabling will require:
  - An image-build smoke test that verifies pip show
    hermes-platform-molecule-a2a + hermes config show inside the
    built container (filed separately)
  - Verifying the molecule-a2a config stanza actually lands in
    ~/.hermes/config.yaml inside the running container

Tests updated: 37 pass. Plugin-path tests now opt-in via the helper's
default; default-detection test asserts the new chat_completions
fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-05-02 18:51:00 -07:00
parent 226e838d5e
commit f8ee17e2e3
4 changed files with 60 additions and 26 deletions

View File

@ -128,8 +128,12 @@ class HermesAgentAdapter(BaseAdapter):
import httpx
# Default off — see executor.py module docstring. Workspace boot
# was wedging on the plugin /a2a/health probe because the plugin
# didn't bind :8645 inside the deployed image. Falls back to the
# legacy /v1/chat/completions /health probe until that's fixed.
use_plugin = os.environ.get(
"MOLECULE_A2A_PLATFORM_ENABLED", "true"
"MOLECULE_A2A_PLATFORM_ENABLED", "false"
).strip().lower() in ("1", "true", "yes", "on")
if use_plugin:

View File

@ -1,6 +1,22 @@
"""A2A → hermes-agent bridge with two transports.
Default transport (``MOLECULE_A2A_PLATFORM_ENABLED=true``)
Default transport (``MOLECULE_A2A_PLATFORM_ENABLED=false``)
==========================================================
POST to ``http://127.0.0.1:8642/v1/chat/completions`` synchronously,
parse the OpenAI-shaped response, emit. No session continuity but no
plugin dependency.
This is the safe default while the plugin install path inside the
deployed image is being debugged: the staging E2E for PR #32 surfaced
a workspace boot failure where ``hermes gateway run`` did not bind
``:8645`` inside the container (root cause TBD; local
``scripts/e2e_full_chain.py`` runs against my laptop venv where the
plugin was already installed manually, so it didn't catch the
deployment-shape divergence). Flip back to plugin path with
``MOLECULE_A2A_PLATFORM_ENABLED=true`` once the image-side install
is verified.
Plugin transport (``MOLECULE_A2A_PLATFORM_ENABLED=true``)
=========================================================
POST each A2A turn to the in-container hermes-agent platform plugin's
``/a2a/inbound`` endpoint. Hermes processes the message through its full
@ -8,19 +24,8 @@ pipeline (sessions, skills, tools, hooks) and POSTs the agent's reply
back to a callback server we run inside this executor. A correlation
table maps the inbound ``message_id`` to an ``asyncio.Future`` that the
``execute()`` call awaits so the A2A response is delivered as soon as
hermes calls ``send()``, not by polling.
This earns single-session continuity for peer agents: every turn lands
in the same hermes daemon, which keeps its in-memory conversation state
intact across messages. Replaces the previous "subprocess per A2A
message" pattern that was ``/v1/chat/completions`` with no continuity.
Fallback transport (``MOLECULE_A2A_PLATFORM_ENABLED=false``)
============================================================
The pre-plugin path: POST to ``http://127.0.0.1:8642/v1/chat/completions``
synchronously, parse the OpenAI-shaped response, emit. No session
continuity but no plugin dependency. Operators flip this off if the
plugin path misbehaves in production.
hermes calls ``send()``, not by polling. Earns single-session continuity
for peer agents.
Wire shape
==========
@ -102,7 +107,9 @@ class HermesAgentProxyExecutor(AgentExecutor):
# Plugin transport state. The reply server only boots if the
# plugin path is enabled; otherwise the executor degrades to
# the legacy proxy below.
self._use_plugin = _bool_env("MOLECULE_A2A_PLATFORM_ENABLED", True)
# Default false until the image-side plugin install is verified
# — see module docstring. Operators flip on per workspace via env.
self._use_plugin = _bool_env("MOLECULE_A2A_PLATFORM_ENABLED", False)
self._plugin_host = os.environ.get(
"MOLECULE_A2A_PLATFORM_HOST", _DEFAULT_PLUGIN_HOST
)

View File

@ -66,12 +66,14 @@ async def test_setup_skips_under_smoke_mode(monkeypatch):
@pytest.mark.asyncio
async def test_setup_probes_plugin_health_by_default(monkeypatch):
"""With MOLECULE_A2A_PLATFORM_ENABLED unset, setup() must probe
/a2a/health (NOT the legacy /v1/health)."""
async def test_setup_probes_plugin_health_when_enabled(monkeypatch):
"""When MOLECULE_A2A_PLATFORM_ENABLED=true, setup() probes
/a2a/health (NOT the legacy /v1/health). Plugin path is opt-in
while the image-side install is being verified see executor.py
module docstring."""
monkeypatch.delenv("MOLECULE_SMOKE_MODE", raising=False)
monkeypatch.delenv("MOLECULE_A2A_PLATFORM_ENABLED", raising=False)
monkeypatch.setenv("MOLECULE_A2A_PLATFORM_ENABLED", "true")
health_port = _free_port()
paths_hit: list[str] = []
@ -135,10 +137,13 @@ async def test_setup_probes_chat_completions_health_when_disabled(monkeypatch):
@pytest.mark.asyncio
async def test_create_executor_returns_started_executor(monkeypatch):
"""create_executor() must return an executor whose reply server is
already running (start() was called)."""
"""create_executor() with plugin path enabled must return an
executor whose reply server is already running (start() was
called). Plugin path is opt-in while the image-side install is
verified see executor.py module docstring."""
cb_port = _free_port()
monkeypatch.setenv("MOLECULE_A2A_PLATFORM_ENABLED", "true")
monkeypatch.setenv("MOLECULE_A2A_CALLBACK_PORT", str(cb_port))
cfg = AdapterConfig(model="hermes-test")

View File

@ -43,6 +43,10 @@ def _free_port() -> int:
def _make_executor(monkeypatch, **env: str) -> HermesAgentProxyExecutor:
"""Test helper. Defaults MOLECULE_A2A_PLATFORM_ENABLED=true so the
plugin-path tests below operate in plugin mode by default. Tests
that exercise the chat_completions fallback override this."""
env.setdefault("MOLECULE_A2A_PLATFORM_ENABLED", "true")
for k, v in env.items():
monkeypatch.setenv(k, v)
cfg = AdapterConfig(
@ -85,15 +89,29 @@ def _build_context(text: str, *, task_id: str = "task-1"):
# ---- structural -----------------------------------------------------
def test_executor_init_defaults_to_plugin_path(monkeypatch):
def test_executor_init_defaults_to_chat_completions(monkeypatch):
"""Default is the safe legacy /v1/chat/completions transport while
the image-side plugin install is being debugged. Plugin path is
opt-in via MOLECULE_A2A_PLATFORM_ENABLED=true. Port defaults still
apply for when the plugin path is enabled.
Built without _make_executor so the helper's plugin-on default
doesn't mask the prod default we want to assert here."""
monkeypatch.delenv("MOLECULE_A2A_PLATFORM_ENABLED", raising=False)
ex = _make_executor(monkeypatch)
assert ex._use_plugin is True
cfg = AdapterConfig(model="hermes-test", system_prompt="you are helpful")
ex = HermesAgentProxyExecutor(cfg)
assert ex._use_plugin is False
# Defaults still set so plugin enable just needs the one env flip.
assert ex._plugin_port == 8645
assert ex._callback_port == 8646
def test_executor_falls_back_when_plugin_disabled(monkeypatch):
def test_executor_enables_plugin_path_when_opted_in(monkeypatch):
ex = _make_executor(monkeypatch, MOLECULE_A2A_PLATFORM_ENABLED="true")
assert ex._use_plugin is True
def test_executor_disabled_explicitly(monkeypatch):
ex = _make_executor(monkeypatch, MOLECULE_A2A_PLATFORM_ENABLED="false")
assert ex._use_plugin is False