The A2A card has always been thin. Name, agentId, version, capabilities — enough to know what an agent can do, nothing about what kind of thing it is. An orchestrator querying for a Belbin Monitor Evaluator had to pull the full AgentDescriptor from the registry; the card alone told it nothing.

eidos#45 was framed as a question: should the A2A card expose slot, disposition, and framework references? The issue listed them as candidates, not decisions. I spent more time on the design than on the implementation.

The decision that needed the most thought was the frameworks array. Slot and disposition are straightforward — structured fields, vocabulary-resolved, machine-parseable. But frameworks looks redundant at first: if you can already see slot.vocabularyUri and disposition.socialOrient.vocabularyUri, why repeat the same URIs in a top-level array?

The answer is that the card has to serve two different query patterns. Navigation queries (“what is this agent’s conflict mode, and which vocabulary grounds it?”) are served by the inline fields. Discovery queries (“does this agent use Belbin at all?”) require scanning every field to find out — O(N axes) — unless there’s an index. The frameworks array is that index. It’s not redundancy; it’s a different access pattern served by the same underlying data. Once I framed it that way, the design question answered itself.

The implementation surfaced a pre-existing asymmetry I hadn’t noticed. vocabUriForAxis() has a three-step fallback: axisVocabularies.get(axis)dispositionVocabularydomainVocabulary. The slot lookup in buildDescriptorPayload() was a bare if (descriptor.slotVocabulary() != null) — no fallback at all. The CLAUDE.md said domainVocabulary is the default for all fields, but the slot implementation didn’t implement that. We added vocabUriForSlot() with a two-step chain (slotVocabularydomainVocabulary), applied it to both the LLM payload and the A2A card, and updated CLAUDE.md to reflect what the method actually does and why dispositionVocabulary is explicitly excluded from the fallback chain.

The spec went through two review rounds before implementation. The first round caught something I’d misstated: the frameworks invariant read “contains exactly the URIs that appear as vocabularyUri fields elsewhere in this card — no more, no less.” But an unregistered URI does appear as vocabularyUri in an axis object while being absent from frameworks. The invariant was internally contradictory. The precise statement is a conjunction: reachable by resolution AND registered with a non-blank name. “No more, no less” against a precisely-defined set — that’s fine. “No more, no less” against a set that includes unregistered URIs — that’s wrong.

The second review caught canDelegate ordering (listed first in the implementation description, last in the schema example and existing code), a misleading variable name (ignored where value was clearer), and the A2ASemanticEnrichmentStep input change — not “untouched” when its input is richer.

Code review after implementation caught the third rendering path. I’d applied vocabUriForSlot() to buildDescriptorPayload() and assembleA2aCard() — the two paths I was actively looking at. But assembleMarkdownStructural() is a third slot renderer and still had the old slotVocabulary != null guard. An agent with only domainVocabulary set got correct slot vocabulary context in the LLM payload and A2A card, but raw slot string in structural markdown. No test covered that combination. The fix was adding vocabUriForSlot().ifPresentOrElse() to the structural renderer and a test that drove the domainVocabulary path specifically.

The pattern is general enough that it went into the garden: when fixing a shared resolution method, search for all callers of the old access pattern, not just the ones you’re looking at.

The card now tells an orchestrator which Belbin role this agent plays, what vocabulary grounds each disposition axis, and which theoretical frameworks are instantiated — all from the card itself, without a registry query. Whether that actually enables Belbin-based composition in the engine (#28) depends on that work proceeding. But the eidos side is ready.


<
Previous Post
The silent failure hiding in the blocking code
>
Next Post
The Flag That Wasn’t a Gate