fix(workspace): keep peers visible in coordinator prompt when agent_card is null

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.
This commit is contained in:
Hongming Wang 2026-04-28 14:10:29 -07:00
parent 7eeac153de
commit 8ff0748ab9
2 changed files with 141 additions and 14 deletions

View File

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

View File

@ -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([]) == ""