From 8ff0748ab9af4948c5b9592a4f61a2b54b7b2cff Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Tue, 28 Apr 2026 14:10:29 -0700 Subject: [PATCH 1/2] fix(workspace): keep peers visible in coordinator prompt when agent_card is null MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: a Design Director coordinator with 6 freshly-created worker peers rendered an empty `## Your Peers` section in its system prompt — the hosting registry endpoint correctly returned all 6 peers, but `summarize_peer_cards()` silently dropped every entry whose `agent_card` column was null (the default until A2A discovery has run end-to-end against the worker). The coordinator then refused to delegate any task because "no peers exist". Fix: fall back to the registry row's `name` and `role` columns when `agent_card` is missing, malformed, or wrong-typed, instead of skipping the peer. The registry endpoint (`workspace-server/internal/handlers/discovery.go:queryPeerMaps`) has always returned both fields — they were just being thrown away on the consumer side. `build_peer_section()` now renders `Role: …` when the agent_card-derived skill list is empty so the coordinator's prompt still has something concrete to delegate against. Also hoists `import json` out of the per-peer loop body to module level (was previously imported once per iteration). Tests: new `test_shared_runtime_peer_summary.py` pins all four fallback cases (null / malformed string / wrong type / null + no DB name) plus the agent-card-present happy path and the mixed-list case the coordinator actually consumes. First peer-summary test coverage `shared_runtime.py` has had — no prior tests existed. Refs: 2026-04-27 Design Director discovery report from infra team. --- workspace/shared_runtime.py | 44 ++++--- .../tests/test_shared_runtime_peer_summary.py | 111 ++++++++++++++++++ 2 files changed, 141 insertions(+), 14 deletions(-) create mode 100644 workspace/tests/test_shared_runtime_peer_summary.py diff --git a/workspace/shared_runtime.py b/workspace/shared_runtime.py index dba05700..a874356a 100644 --- a/workspace/shared_runtime.py +++ b/workspace/shared_runtime.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json from typing import Any from a2a.server.agent_execution import RequestContext @@ -89,33 +90,46 @@ def append_peer_guidance( def summarize_peer_cards(peers: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Return compact peer metadata for prompt rendering.""" + """Return compact peer metadata for prompt rendering. + + Falls back to the registry row's `name` and `role` when `agent_card` is + null or unparseable so peers stay visible to delegators even before + their A2A discovery roundtrip has populated a card. Without this + fallback a coordinator-tier workspace with N freshly-created worker + peers would render an empty `## Your Peers` section and refuse to + delegate (the regression behind the 2026-04-27 Design Director + discovery bug). + """ summaries: list[dict[str, Any]] = [] for peer in peers: agent_card = peer.get("agent_card") - if not agent_card: - continue if isinstance(agent_card, str): try: - import json - agent_card = json.loads(agent_card) except Exception: - continue + agent_card = None if not isinstance(agent_card, dict): - continue + agent_card = None + + if agent_card: + skills_raw = agent_card.get("skills") or [] + skills = [ + s.get("name", s.get("id", "")) + for s in skills_raw + if isinstance(s, dict) + ] + name = agent_card.get("name") or peer.get("name") or "Unknown" + else: + skills = [] + name = peer.get("name") or "Unknown" - skills = agent_card.get("skills", []) summaries.append( { "id": peer.get("id", "unknown"), - "name": agent_card.get("name", peer.get("name", "Unknown")), + "name": name, + "role": peer.get("role") or "", "status": peer.get("status", "unknown"), - "skills": [ - s.get("name", s.get("id", "")) - for s in skills - if isinstance(s, dict) - ], + "skills": skills, } ) return summaries @@ -140,6 +154,8 @@ def build_peer_section( parts.append(f"- **{peer['name']}** (id: `{peer['id']}`, status: {peer['status']})") if peer["skills"]: parts.append(f" Skills: {', '.join(peer['skills'])}") + elif peer.get("role"): + parts.append(f" Role: {peer['role']}") parts.append("") parts.append(instruction) return "\n".join(parts) diff --git a/workspace/tests/test_shared_runtime_peer_summary.py b/workspace/tests/test_shared_runtime_peer_summary.py new file mode 100644 index 00000000..2628c279 --- /dev/null +++ b/workspace/tests/test_shared_runtime_peer_summary.py @@ -0,0 +1,111 @@ +"""Pin peer-summary fallback when agent_card is missing. + +Regresses the 2026-04-27 Design Director discovery bug: +`summarize_peer_cards()` previously skipped any peer whose `agent_card` +was null or unparseable, so a coordinator with freshly-created workers +saw an empty `## Your Peers` section in its system prompt and refused +to delegate. The registry endpoint already returns DB `name` + `role` +on every row regardless of agent_card state — falling back to those +keeps peers visible while A2A discovery catches up. +""" + +from __future__ import annotations + +from shared_runtime import build_peer_section, summarize_peer_cards + + +def _peer(**overrides): + base = { + "id": "ws-1", + "name": "DB Name", + "role": "DB Role", + "status": "active", + "agent_card": None, + } + base.update(overrides) + return base + + +def test_summarize_includes_peer_with_null_agent_card_using_db_fields(): + summaries = summarize_peer_cards([_peer()]) + assert len(summaries) == 1 + assert summaries[0]["id"] == "ws-1" + assert summaries[0]["name"] == "DB Name" + assert summaries[0]["role"] == "DB Role" + assert summaries[0]["status"] == "active" + assert summaries[0]["skills"] == [] + + +def test_summarize_prefers_agent_card_name_over_db_name(): + peer = _peer( + agent_card={"name": "Card Name", "skills": [{"name": "draft-spec"}]} + ) + summaries = summarize_peer_cards([peer]) + assert summaries[0]["name"] == "Card Name" + assert summaries[0]["skills"] == ["draft-spec"] + assert summaries[0]["role"] == "DB Role" + + +def test_summarize_handles_string_agent_card_json(): + peer = _peer(agent_card='{"name": "JSON Name", "skills": []}') + summaries = summarize_peer_cards([peer]) + assert summaries[0]["name"] == "JSON Name" + + +def test_summarize_falls_back_when_agent_card_string_is_malformed(): + peer = _peer(agent_card="not-valid-json") + summaries = summarize_peer_cards([peer]) + assert len(summaries) == 1 + assert summaries[0]["name"] == "DB Name" + assert summaries[0]["role"] == "DB Role" + assert summaries[0]["skills"] == [] + + +def test_summarize_falls_back_when_agent_card_is_wrong_type(): + peer = _peer(agent_card=42) + summaries = summarize_peer_cards([peer]) + assert len(summaries) == 1 + assert summaries[0]["name"] == "DB Name" + + +def test_summarize_handles_missing_role_and_name_with_unknown_default(): + peer = {"id": "ws-2", "status": "active", "agent_card": None} + summaries = summarize_peer_cards([peer]) + assert summaries[0]["name"] == "Unknown" + assert summaries[0]["role"] == "" + + +def test_build_peer_section_renders_role_when_skills_empty(): + section = build_peer_section([_peer()]) + assert "## Your Peers" in section + assert "**DB Name**" in section + assert "Role: DB Role" in section + assert "Skills:" not in section + + +def test_build_peer_section_prefers_skills_over_role_when_card_present(): + peer = _peer( + agent_card={"name": "Worker", "skills": [{"name": "design"}, {"name": "review"}]} + ) + section = build_peer_section([peer]) + assert "Skills: design, review" in section + assert "Role: DB Role" not in section + + +def test_build_peer_section_mixed_peers(): + peers = [ + _peer(id="ws-a"), + _peer( + id="ws-b", + agent_card={"name": "Card B", "skills": [{"name": "build"}]}, + ), + ] + section = build_peer_section(peers) + assert "id: `ws-a`" in section + assert "id: `ws-b`" in section + assert "Role: DB Role" in section + assert "Skills: build" in section + + +def test_build_peer_section_empty_when_no_peers(): + assert build_peer_section([]) == "" From 96acbd719b73dcc8b391f63662d52dac1fe593e9 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Tue, 28 Apr 2026 14:15:42 -0700 Subject: [PATCH 2/2] test: update test_peer_capabilities_format for fallback behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous assertion `'Silent Agent' not in result` was pinning the buggy behavior — peers without an agent_card were silently dropped from the prompt. With the fallback to DB name+role those peers are correctly visible. Flip the assertion so the test pins the new (correct) rendering and would catch a regression to the silent-drop behavior. --- workspace/tests/test_prompt.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/workspace/tests/test_prompt.py b/workspace/tests/test_prompt.py index 0fb4bd98..133a5d7e 100644 --- a/workspace/tests/test_prompt.py +++ b/workspace/tests/test_prompt.py @@ -203,8 +203,11 @@ def test_peer_capabilities_format(tmp_path): assert "**Echo Agent** (id: `peer-1`, status: online)" in result assert "Skills: echo, repeat" in result assert "delegate_to_workspace" in result - # peer-2 has no agent_card so it's skipped - assert "Silent Agent" not in result + # peer-2 has no agent_card but DOES have a DB name + status — must + # still render so coordinators can delegate to freshly-created peers + # whose A2A discovery hasn't populated a card yet (regression of the + # 2026-04-27 Design Director discovery bug). + assert "**Silent Agent** (id: `peer-2`, status: offline)" in result def test_peer_with_json_string_agent_card(tmp_path):