From 3eb599bbb61375cf4df9f835466c0d79584a3e48 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Mon, 27 Apr 2026 16:43:32 -0700 Subject: [PATCH] fix(workspace): use SDK constant for agent-card readiness probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial-prompt readiness probe in workspace/main.py hardcoded the pre-1.x well-known path. After the a2a-sdk 1.x bump the SDK started mounting the agent card at the new canonical path (the value of `a2a.utils.constants.AGENT_CARD_WELL_KNOWN_PATH`), so the probe returned 404 every attempt and silently fell through to "server not ready after 30s, skipping". Net effect: every workspace silently dropped its `initial_prompt` from config.yaml — the agent never sent the kickoff self-message, and users hit a fresh chat with no context. Reported by an external user as "/.well-known/agent.json 404 — the a2a-sdk agent card route was not being mounted at the expected path". The route IS mounted; the probe was looking at the wrong place. Fix imports `AGENT_CARD_WELL_KNOWN_PATH` from `a2a.utils.constants` and uses it directly in the probe URL — the SDK constant is now the single source of truth, so any future rename travels through automatically. Adds two static regression tests pinning the invariant: 1. No hardcoded `/.well-known/agent.json` literal anywhere in main.py. 2. The probe URL fstring interpolates AGENT_CARD_WELL_KNOWN_PATH (catches a "fix" that imports the constant for show but reverts to a literal in the actual GET). Verified manually inside ghcr.io/molecule-ai/workspace-template-langgraph that AGENT_CARD_WELL_KNOWN_PATH == '/.well-known/agent-card.json' and that `create_agent_card_routes(card)` mounts at exactly that path — constant + mount are aligned in the runtime image, so the probe will now find the server. Full workspace test suite: 1209 passed, 2 xfailed. Co-Authored-By: Claude Opus 4.7 (1M context) --- workspace/main.py | 14 +++- .../tests/test_agent_card_well_known_path.py | 84 +++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 workspace/tests/test_agent_card_well_known_path.py diff --git a/workspace/main.py b/workspace/main.py index 5c09b134..85e891e2 100644 --- a/workspace/main.py +++ b/workspace/main.py @@ -437,13 +437,23 @@ async def main(): # pragma: no cover ) async def _send_initial_prompt(): """Wait for server to be ready, then send initial_prompt as self-message.""" - # Wait for the A2A server to accept connections + # Wait for the A2A server to accept connections. + # Use the SDK's own constant for the well-known path so this + # probe and the route mounted by create_agent_card_routes() + # never drift apart. Pre-fix this hardcoded the pre-1.x + # well-known path string; a2a-sdk 1.x renamed it (the + # canonical value lives in a2a.utils.constants now), so + # the probe got 404 every attempt and fell through to + # "server not ready after 30s, skipping" even though the + # server was actually serving fine. Net effect: every + # workspace silently dropped its `initial_prompt`. + from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH ready = False for attempt in range(30): await asyncio.sleep(1) try: async with httpx.AsyncClient(timeout=5.0) as client: - resp = await client.get(f"http://127.0.0.1:{port}/.well-known/agent.json") + resp = await client.get(f"http://127.0.0.1:{port}{AGENT_CARD_WELL_KNOWN_PATH}") if resp.status_code == 200: ready = True break diff --git a/workspace/tests/test_agent_card_well_known_path.py b/workspace/tests/test_agent_card_well_known_path.py new file mode 100644 index 00000000..fe06c9fd --- /dev/null +++ b/workspace/tests/test_agent_card_well_known_path.py @@ -0,0 +1,84 @@ +"""Pin the agent-card readiness probe to the SDK's canonical path. + +main.py's _send_initial_prompt() polls the local A2A server's +well-known agent-card URL to know when it's safe to send the initial +prompt as a self-message. Pre-fix the URL was hardcoded to the pre-1.x +literal; a2a-sdk 1.x renamed the well-known path (the canonical value +lives in `a2a.utils.constants.AGENT_CARD_WELL_KNOWN_PATH`), so the +probe got 404 every attempt and silently fell through to "server not +ready after 30s, skipping" — dropping every workspace's +`initial_prompt` from config.yaml. + +The fix is to import the SDK's `AGENT_CARD_WELL_KNOWN_PATH` constant +and use it directly in the probe URL. These tests pin the static +invariants of that fix: + + 1. No hardcoded `/.well-known/agent.json` literal anywhere in + main.py (catches a future contributor reverting to a literal). + 2. The probe URL fstring interpolates `AGENT_CARD_WELL_KNOWN_PATH` + (catches a "fix" that imports the constant for show but still + uses a literal in the actual GET). + +Note: we deliberately do not assert the constant's value or compare +it against `create_agent_card_routes()` here. The runtime SDK is +mocked in this directory's conftest for the executor-test path, so +any test that imports the real `a2a.utils.constants` would either +collide with the mock or require running in a separate pytest session. +The two static invariants are sufficient: by always following whatever +the SDK constant says, we travel through any rename automatically. The +SDK's own contract that `create_agent_card_routes` mounts at the +constant's value is the SDK's responsibility, not ours. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +WORKSPACE_ROOT = Path(__file__).resolve().parents[1] + + +def test_main_uses_sdk_constant_for_agent_card_probe(): + """No hardcoded `/.well-known/agent.json` literal anywhere in main.py. + + The SDK constant (AGENT_CARD_WELL_KNOWN_PATH) is the single source + of truth — string-literal probes drift the moment the SDK renames. + """ + main = (WORKSPACE_ROOT / "main.py").read_text() + + bad_literal = "/.well-known/agent.json" + offenders = [ + (lineno, line) + for lineno, line in enumerate(main.splitlines(), 1) + if bad_literal in line + ] + assert not offenders, ( + f"Found pre-1.x literal {bad_literal!r} in main.py — must use " + f"the SDK's AGENT_CARD_WELL_KNOWN_PATH constant instead. " + f"Offending lines: {offenders}" + ) + + assert ( + "AGENT_CARD_WELL_KNOWN_PATH" in main + ), "main.py must import a2a.utils.constants.AGENT_CARD_WELL_KNOWN_PATH" + + +def test_probe_loop_uses_constant_in_url_format(): + """Spot-check that the URL fstring in main.py interpolates the + constant, not a literal. Catches a future "fix" that imports the + constant for show but still uses a literal in the actual GET.""" + main = (WORKSPACE_ROOT / "main.py").read_text() + + # The probe pattern: `client.get(f"http://127.0.0.1:{port}{...}")` + # where `{...}` must be `{AGENT_CARD_WELL_KNOWN_PATH}`, not a + # hardcoded path. + pattern = re.compile( + r'client\.get\(f"http://127\.0\.0\.1:\{port\}\{(?P[^}]+)\}"\)' + ) + matches = pattern.findall(main) + assert matches, "no readiness probe pattern found in main.py" + for expr in matches: + assert "AGENT_CARD_WELL_KNOWN_PATH" in expr, ( + f"readiness probe URL uses {expr!r} instead of " + f"AGENT_CARD_WELL_KNOWN_PATH" + )