Skip to content

Extending A2A Agent Cards

This guide explains the A2A (Agent-to-Agent) Agent Card data model, how FCC personas map to Agent Cards, and how to extend cards with domain-specific capabilities, custom metadata, and schema validation.


Table of Contents

  1. Agent Card Structure
  2. Data Model Reference
  3. R.I.S.C.E.A.R. to Agent Card Mapping
  4. Building Cards from Personas
  5. Adding Domain-Specific Capabilities
  6. Custom Metadata
  7. Schema Validation
  8. Round-Trip Serialization
  9. Testing
  10. Related Documentation

Agent Card Structure

An A2A Agent Card is a self-describing manifest that advertises an agent's identity, capabilities, and skills. In FCC, every persona can be represented as an Agent Card, enabling cross-project discovery and interoperability.

The card structure follows the Google A2A specification with FCC-specific extensions stored in the metadata field.

AgentCard
  +-- id            (string)    Persona ID, e.g. "RC"
  +-- name          (string)    Human-readable name
  +-- description   (string)    Derived from R.I.S.C.E.A.R. role
  +-- version       (string)    FCC package version
  +-- url           (string)    Agent endpoint URL
  +-- endpoint      (AgentEndpoint)
  |     +-- url       (string)
  |     +-- protocol  (string)  default "a2a/v1"
  +-- capabilities  (tuple[AgentCapability, ...])
  |     +-- name        (string)  Slugified capability name
  |     +-- description (string)  Full description
  |     +-- tags        (tuple[str, ...])
  +-- skills        (tuple[SkillDefinition, ...])
  |     +-- id            (string)  e.g. "scaffold_RC"
  |     +-- name          (string)  Human-readable
  |     +-- description   (string)  Action description
  |     +-- input_schema  (dict)    JSON Schema for inputs
  |     +-- output_schema (dict)    JSON Schema for outputs
  |     +-- tags          (tuple[str, ...])
  +-- metadata      (dict)
        +-- category       (string)
        +-- fcc_phase      (string)
        +-- role_title     (string)
        +-- champion_of    (string, optional)
        +-- orchestrates   (list[str], optional)

Data Model Reference

All models are frozen dataclasses defined in src/fcc/protocols/a2a/models.py.

AgentEndpoint

from fcc.protocols.a2a.models import AgentEndpoint

endpoint = AgentEndpoint(
    url="http://localhost:8080/agents/rc",
    protocol="a2a/v1",
)

AgentCapability

from fcc.protocols.a2a.models import AgentCapability

cap = AgentCapability(
    name="code_review",
    description="Systematic code review with quality assessment",
    tags=("role_skill",),
)

SkillDefinition

from fcc.protocols.a2a.models import SkillDefinition

skill = SkillDefinition(
    id="scaffold_RC",
    name="Scaffold (Research Coordinator)",
    description="Generate project scaffolding for research coordination",
    input_schema={"inputs": ["project_name", "domain"]},
    output_schema={"outputs": ["scaffold_structure"]},
    tags=("scaffold", "core"),
)

AgentCard

from fcc.protocols.a2a.models import AgentCard

card = AgentCard(
    id="RC",
    name="Research Coordinator",
    description="Coordinates cross-functional research activities",
    version="1.0.1",
    url="http://localhost:8080/agents/rc",
    endpoint=endpoint,
    capabilities=(cap,),
    skills=(skill,),
    metadata={"category": "core", "fcc_phase": "find"},
)

R.I.S.C.E.A.R. to Agent Card Mapping

The CardBuilder in src/fcc/protocols/a2a/card_builder.py maps R.I.S.C.E.A.R. persona fields to Agent Card fields as follows:

R.I.S.C.E.A.R. Field Agent Card Field Notes
persona.id card.id Direct mapping
persona.name card.name Direct mapping
persona.riscear.role card.description Role description becomes card description
persona.riscear.archetype card.capabilities[0] Primary capability tagged "archetype"
persona.riscear.role_skills card.capabilities[1..] Additional capabilities tagged "role_skill"
WorkflowAction (per persona) card.skills One skill per action type
persona.category card.metadata["category"] Stored in metadata
persona.fcc_phase card.metadata["fcc_phase"] Stored in metadata
persona.role_title card.metadata["role_title"] Stored in metadata
persona.champion_of card.metadata["champion_of"] Only if set
persona.orchestrates card.metadata["orchestrates"] Only if non-empty

Building Cards from Personas

Single Persona

from fcc.personas.registry import PersonaRegistry
from fcc.protocols.a2a.card_builder import CardBuilder

registry = PersonaRegistry.from_yaml_directory("src/fcc/data/personas")
persona = registry.get("RC")

builder = CardBuilder(base_url="https://agents.example.com")
card = builder.build_card(persona)

print(card.id)            # "RC"
print(card.description)   # The R.I.S.C.E.A.R. role text
print(len(card.capabilities))  # archetype + role_skills count

All Personas (with actions)

from fcc.workflow.actions import WorkflowActionRegistry

action_registry = WorkflowActionRegistry.from_yaml_directory(
    "src/fcc/data/personas/actions"
)

cards = builder.build_all_cards(registry, action_registry=action_registry)
print(f"Built {len(cards)} cards")  # 102 cards

Writing .well-known/agent.json

from pathlib import Path

output_path = CardBuilder.write_well_known(card, Path("output"))
# Writes to output/.well-known/agent.json

Adding Domain-Specific Capabilities

To add capabilities beyond what R.I.S.C.E.A.R. provides, build the card and then create a new card with additional capabilities. Since AgentCard is frozen, use dataclasses.replace:

from dataclasses import replace
from fcc.protocols.a2a.models import AgentCapability

custom_cap = AgentCapability(
    name="tmf_federation",
    description="Federate knowledge via TMF graph queries",
    tags=("domain", "knowledge_graph"),
)

extended_card = replace(
    card,
    capabilities=card.capabilities + (custom_cap,),
)

To add a custom skill:

from fcc.protocols.a2a.models import SkillDefinition

custom_skill = SkillDefinition(
    id="federate_query_RC",
    name="Federate Query (Research Coordinator)",
    description="Execute a federated knowledge graph query",
    input_schema={
        "type": "object",
        "properties": {
            "query": {"type": "string"},
            "sources": {"type": "array", "items": {"type": "string"}},
        },
        "required": ["query"],
    },
    output_schema={
        "type": "object",
        "properties": {
            "results": {"type": "array"},
            "source_count": {"type": "integer"},
        },
    },
    tags=("federation", "knowledge_graph"),
)

extended_card = replace(
    card,
    skills=card.skills + (custom_skill,),
)

Custom Metadata

The metadata dict on AgentCard is unstructured -- you can add any JSON-serializable key-value pairs. The CardBuilder populates standard FCC metadata automatically; add domain-specific keys after building.

extended_metadata = dict(card.metadata)
extended_metadata["deployment_region"] = "us-east-1"
extended_metadata["max_concurrency"] = 10
extended_metadata["compliance_tags"] = ["SOC2", "GDPR"]

extended_card = replace(card, metadata=extended_metadata)

When serializing to JSON, all metadata keys are preserved:

json_str = extended_card.to_json(indent=2)

Schema Validation

Every Agent Card can be validated against the JSON Schema at src/fcc/data/schemas/a2a_card.schema.json.

Programmatic Validation

errors = card.validate_against_schema()
if errors:
    for err in errors:
        print(f"Validation error: {err}")
else:
    print("Card is valid")

The validate_against_schema method uses jsonschema.Draft7Validator internally and returns a list of error message strings.

Schema Location

The schema file is resolved via fcc._resources.get_schema_path("a2a_card"). It defines the required fields (id, name, description, version, url, endpoint) and the structures for capabilities, skills, and metadata.


Round-Trip Serialization

All models support full round-trip serialization:

# PersonaSpec -> AgentCard -> dict -> JSON -> dict -> AgentCard
card = builder.build_card(persona)

card_dict = card.to_dict()
card_json = card.to_json(indent=2)

restored_dict = json.loads(card_json)
restored_card = AgentCard.from_dict(restored_dict)

assert restored_card.id == card.id
assert restored_card.name == card.name
assert len(restored_card.capabilities) == len(card.capabilities)
assert len(restored_card.skills) == len(card.skills)

Nested objects (AgentEndpoint, AgentCapability, SkillDefinition) are recursively serialized and deserialized.


Testing

Validate All 102 Cards

def test_all_cards_valid():
    registry = PersonaRegistry.from_yaml_directory("src/fcc/data/personas")
    builder = CardBuilder()
    cards = builder.build_all_cards(registry)

    assert len(cards) == 102

    for card in cards:
        errors = card.validate_against_schema()
        assert errors == [], f"Card {card.id} has errors: {errors}"

Check Skill Counts

def test_cards_with_actions_have_skills():
    registry = PersonaRegistry.from_yaml_directory("src/fcc/data/personas")
    action_registry = WorkflowActionRegistry.from_yaml_directory(
        "src/fcc/data/personas/actions"
    )
    builder = CardBuilder()
    cards = builder.build_all_cards(registry, action_registry=action_registry)

    cards_with_skills = [c for c in cards if c.skills]
    assert len(cards_with_skills) > 0

Round-Trip Consistency

def test_card_round_trip():
    builder = CardBuilder()
    card = builder.build_card(persona)

    restored = AgentCard.from_dict(card.to_dict())
    assert restored.id == card.id
    assert restored.endpoint.url == card.endpoint.url
    assert len(restored.capabilities) == len(card.capabilities)