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:
parent
226e838d5e
commit
f8ee17e2e3
@ -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:
|
||||
|
||||
37
executor.py
37
executor.py
@ -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
|
||||
)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user