Merge pull request #2208 from Molecule-AI/fix/peer-discovery-fall-back-to-db-fields
fix(workspace): keep peers visible when agent_card is null
This commit is contained in:
commit
5990f7a876
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
111
workspace/tests/test_shared_runtime_peer_summary.py
Normal file
111
workspace/tests/test_shared_runtime_peer_summary.py
Normal 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([]) == ""
|
||||
Loading…
Reference in New Issue
Block a user