fix(workspace): use SDK constant for agent-card readiness probe

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) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-27 16:43:32 -07:00
parent 3f020b8591
commit 3eb599bbb6
2 changed files with 96 additions and 2 deletions

View File

@ -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

View File

@ -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<expr>[^}]+)\}"\)'
)
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"
)