diff --git a/adapter.py b/adapter.py index 45147d8..a6a4a1f 100644 --- a/adapter.py +++ b/adapter.py @@ -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: diff --git a/executor.py b/executor.py index 0851de3..c85162b 100644 --- a/executor.py +++ b/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 ) diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 065f156..75fc860 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -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") diff --git a/tests/test_executor_plugin_path.py b/tests/test_executor_plugin_path.py index 7e0b808..eb52f57 100644 --- a/tests/test_executor_plugin_path.py +++ b/tests/test_executor_plugin_path.py @@ -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