forked from molecule-ai/molecule-core
PR #2756 added a try/except around adapter.setup() so a missing LLM key doesn't crash the workspace boot. Two paths that now run AFTER setup succeeds were not similarly isolated, leaving small but real coupling risks for future adapter authors. 1. **Skill metadata enrichment swap (main.py:248-259).** When adapter.setup() returns, main.py reads adapter.loaded_skills and replaces the static stubs in agent_card.skills with rich metadata (description, tags, examples). The list comprehension assumes each element exposes .metadata.{id,name,description,tags,examples}. A future adapter that returns a non-canonical shape would raise AttributeError, propagate to the outer except, capture as adapter_error, and silently degrade an OK boot to the not-configured state — even though setup() actually succeeded. Extract to card_helpers.enrich_card_skills(card, loaded_skills) → bool. Helper swallows enrichment failures, logs the cause, returns False, leaves the static stubs in place. setup() success path continues unchanged. 6 unit tests cover: None input, empty list, canonical happy path, missing .metadata attr, partial .metadata (missing one canonical field), atomic-failure-no-partial-swap. 2. **/transcript handler (main.py:513).** Calls await adapter.transcript_lines(...) without try/except. BaseAdapter's default returns {"supported": false} so today's 4 adapters never trigger this — but a future adapter override that assumes setup() ran would surface as a 500 from Starlette's default error handler instead of a useful 503 with the exception class + message. Inline try/except returns 503 with the reason, matching the not-configured JSON-RPC handler's pattern. Both changes match the architectural principle the PR #2756 chain established: availability (workspace reachable) is decoupled from configuration / adapter behavior. Operators see useful errors instead of silent degradation; future adapter authors can't accidentally break tenant readiness with a shape mismatch. Adds: - workspace/card_helpers.py (~50 lines, 100% covered) - workspace/tests/test_card_helpers.py (6 tests) - AgentCard/AgentSkill/AgentCapabilities/AgentInterface stubs to workspace/tests/conftest.py so future card-related tests work under the existing a2a-mock infrastructure - card_helpers in TOP_LEVEL_MODULES (drift gate would have caught it) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
58 lines
2.2 KiB
Python
58 lines
2.2 KiB
Python
"""Helpers for building / mutating the workspace ``AgentCard``.
|
|
|
|
Kept as their own module so the behavior is unit-testable without booting
|
|
the whole runtime (``main.py`` is ``# pragma: no cover``).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Iterable
|
|
|
|
from a2a.types import AgentCard, AgentSkill
|
|
|
|
|
|
def enrich_card_skills(card: AgentCard, loaded_skills: Iterable | None) -> bool:
|
|
"""Replace ``card.skills`` with rich metadata from the adapter's loaded
|
|
skills, in place. Pairs with PR #2756: the card was built up front from
|
|
static ``config.skills`` names so /.well-known/agent-card.json could
|
|
serve before ``adapter.setup()`` finishes; this swaps in the richer
|
|
descriptions/tags/examples that ``setup()``'s skill loader produces.
|
|
|
|
Returns ``True`` on swap, ``False`` when the swap was skipped or
|
|
failed. Failure cases:
|
|
* ``loaded_skills`` is None / empty — caller didn't load any.
|
|
* Any element doesn't expose ``.metadata.{id,name,description,tags,examples}``
|
|
(a future adapter that doesn't follow the canonical shape).
|
|
|
|
Failures DO NOT raise — a malformed ``loaded_skills`` shape would
|
|
otherwise propagate to ``main.py``'s outer ``except Exception``,
|
|
silently degrading an OK boot to the not-configured state. Static
|
|
stubs from ``config.skills`` stay in place; setup() already
|
|
succeeded, the agent works, only the card's skill enrichment is
|
|
degraded. Operator sees a clear log line; tests assert this
|
|
distinction.
|
|
"""
|
|
if not loaded_skills:
|
|
return False
|
|
|
|
try:
|
|
rich = [
|
|
AgentSkill(
|
|
id=skill.metadata.id,
|
|
name=skill.metadata.name,
|
|
description=skill.metadata.description,
|
|
tags=skill.metadata.tags,
|
|
examples=skill.metadata.examples,
|
|
)
|
|
for skill in loaded_skills
|
|
]
|
|
except Exception as enrich_err: # noqa: BLE001
|
|
print(
|
|
f"Warning: skill metadata enrichment failed (keeping static "
|
|
f"stubs from config.skills): {type(enrich_err).__name__}: {enrich_err}",
|
|
flush=True,
|
|
)
|
|
return False
|
|
|
|
card.skills = rich
|
|
return True
|