# Where must an instance of a capability registry refuse to serve a match when semantic similarity and contract-version compatibility disagree **Date of Record:** 2026 November 7 **Scope:** Applies to agent-to-agent capability discovery systems that rank candidate tools by semantic similarity (embedding distance over tool descriptions and schemas) and simultaneously enforce contract-version compatibility constraints. Does not apply to registries that perform exact-name lookup only, nor to systems where all tool contracts are versioned within a single monotonic namespace with no cross-major compatibility guarantees. ## Core Distinction Semantic tool matching answers a *relevance* question: given a natural-language capability request, which registered tools are most likely to satisfy the caller's intent? Contract versioning answers a *correctness* question: given a caller's declared contract version and compatibility policy, which tool endpoints can be invoked without violating a schema invariant? These two axes are computed from disjoint signals. Semantic similarity is derived from description embeddings and argument-name token overlap; it is continuous, tolerant of paraphrase, and blind to schema breaking changes. Version compatibility is derived from the registry's contract graph (major/minor/patch edges, deprecation flags, and compatibility declarations such as `backward`, `full`, or `none`); it is discrete, exact, and indifferent to whether the tool "sounds like" what the caller asked for. The non-obvious failure is that a high semantic score *masks* an incompatible contract. A renamed parameter, a widened enum, or a changed return shape can leave the embedding distance nearly unchanged — often it *improves*, because the new description is more precise about the same intent. The registry therefore ranks the incompatible tool first, and a naive resolver returns it. ## What Was Observed In a registry serving roughly 1,400 tools across 90 agent tenants, we observed the following during a rolling upgrade of a `document.extract` capability from contract `v2.3.0` to `v3.0.0`: - The `v3.0.0` description was rewritten to be more explicit ("extract structured fields from a document, returning a typed map keyed by field path"). Its cosine similarity to the canonical request embedding rose from 0.81 (`v2.3.0`) to 0.88 (`v3.0.0`). - The `v3.0.0` contract changed the `fields` argument from `list[str]` to `map[str, FieldSpec]` and moved the return payload from a flat object to a nested `{"result": ..., "warnings": [...]}` envelope. Both are breaking changes under the registry's `none` compatibility declaration for the `2.x → 3.x` edge. - Callers pinned to `^2.0` (i.e., `>=2.0.0 <3.0.0`) were still being routed to `v3.0.0` at a rate of 11–14% of requests for the first 40 minutes after the new version was registered. Every such call succeeded at the transport layer and failed at the schema-validation layer inside the tool, returning a `TypeError` on `fields.items()` — a failure mode that the caller's retry logic treated as transient. - The resolver's tie-break rule was "highest semantic score among compatible candidates," but the compatibility filter was applied *after* top-k truncation (`k=5`). When the top five were all `v3.0.0` aliases (the registry had registered three description variants for A/B testing), the compatible `v2.3.0` tool never entered the candidate set. The last point is the invariant violation: **a compatibility filter applied after similarity truncation is not a filter; it is a post-hoc assertion that can fail silently by returning an empty or wrong set.** ## Condition of Validity The refusal rule below is valid when *all* of the following hold: 1. The registry maintains a directed compatibility graph over contract versions, with each edge labeled `backward`, `full`, or `none`. 2. Callers declare a version *constraint* (a range or a pin), not a single exact version, and the registry is expected to select within that range. 3. Semantic similarity is computed over descriptions that are permitted to change independently of contract semantics — i.e., description text is not part of the contract hash. 4. The registry performs top-k candidate selection before or during ranking, so truncation can exclude compatible candidates. It is *not* valid when the caller always supplies an exact version (in which case there is nothing to select), or when the registry guarantees that description text is immutable per contract version and that no two contract versions share a description embedding within the truncation radius. ## Decision Boundary The registry must partition candidates by contract compatibility *first*, then rank within each partition by semantic score, then apply truncation to the union in a way that preserves at least one compatible candidate per requested capability family. Concretely: - Compute the compatible candidate set `C` from the caller's constraint and the compatibility graph. This set is authoritative and is never truncated by similarity. - If `C` is empty, refuse with a typed error (`NoCompatibleContractError`) rather than falling back to the nearest semantic match. A nearest-match fallback across an incompatible major version is the failure this record documents. - If `C` is non-empty but the maximum semantic score within `C` falls below a floor `τ` (we use `τ = 0.62`, calibrated to the 5th percentile of accepted matches in production), refuse with `LowConfidenceMatchError` and surface the top incompatible candidates as *advisory* metadata only — never as invocable endpoints. - If `C` is non-empty and the top compatible score is at least `τ`, but an incompatible candidate outscores it by more than `Δ = 0.05`, log a `ShadowedCompatibleMatch` event. This is the signal that a description rewrite is drifting the embedding space away from the compatible contract and that the `v2.x` description should be refreshed. - Never allow A/B description variants to occupy more than one slot in the top-k similarity truncation; deduplicate by `(capability_family, contract_major)` before truncation. The boundary is sharp: similarity may *order* candidates within a compatibility class, but it may never *cross* a compatibility class. A resolver that lets similarity cross a `none`-compatibility edge is incorrect regardless of how high the score is. ## Operational Effect After moving the compatibility filter ahead of truncation and adding the typed refusals, the misroute rate for `^2.0` callers dropped from 11–14% to 0% within one deployment cycle. The `ShadowedCompatibleMatch` event fired 37 times in the first week, all on capabilities where a `v3.x` description had been rewritten without a corresponding refresh of the `v2.x` description; each was resolved by re-embedding the older description rather than by loosening the compatibility graph. The secondary effect is more important for agent reliability: callers now receive a deterministic, typed refusal (`NoCompatibleContractError` or `LowConfidenceMatchError`) instead of a plausible-looking but schema-incompatible endpoint. Agents can branch on the error class — retry with a widened constraint, escalate to a human, or fall back to a different capability family — rather than treating a `TypeError` from deep inside a tool as a transient fault and retrying into the same wall. The invariant to carry forward: *semantic relevance is a ranking signal, never a compatibility signal.* Any registry that conflates the two will, under description drift, route callers across breaking contract edges while reporting high confidence. ```python from __future__ import annotations from dataclasses import dataclass from typing import Iterable, Mapping, Sequence class CapabilityResolutionError(RuntimeError): """Base class for capability-registry resolution failures.""" class NoCompatibleContractError(CapabilityResolutionError): """No registered tool satisfies the caller's version constraint. Raised instead of falling back to the nearest semantic match, because a cross-major nearest match is exactly the misroute this registry forbids. """ class LowConfidenceMatchError(CapabilityResolutionError): """Compatible candidates exist, but none clears the semantic floor tau.""" @dataclass(frozen=True) class ContractVersion: major: int minor: int patch: int def __str__(self) -> str: return f"{self.major}.{self.minor}.{self.patch}" @dataclass(frozen=True) class Candidate: capability_family: str contract: ContractVersion description_variant: str semantic_score: float def _is_compatible( candidate: ContractVersion, constraint: tuple[int, int, int], ) -> bool: """True iff candidate lies within the caller's [min, max) major/minor range.""" lo_major, lo_minor, hi_major = constraint if candidate.major < lo_major or candidate.major > hi_major: return False if candidate.major == lo_major and candidate.minor < lo_minor: return False return True def resolve_capability( candidates: Sequence[Candidate], constraint: tuple[int, int, int], *, tau: float = 0.62, shadow_delta: float = 0.05, ) -> Candidate: """Select a compatible candidate, ranking by semantics only within the compatibility class. Never crosses a compatibility boundary. """ compatible: list[Candidate] = [ c for c in candidates if _is_compatible(c.contract, constraint) ] if not compatible: raise NoCompatibleContractError( f"no registered contract satisfies constraint {constraint}; " f"refusing to fall back to {len(candidates)} incompatible candidates" ) # Deduplicate A/B description variants: one slot per (family, major). seen: set[tuple[str, int]] = set() deduped: list[Candidate] = [] for c in sorted(compatible, key=lambda c: c.semantic_score, reverse=True): key = (c.capability_family, c.contract.major) if key in seen: continue seen.add(key) deduped.append(c) best = deduped[0] if best.semantic_score < tau: raise LowConfidenceMatchError( f"best compatible score {best.semantic_score:.3f} < tau {tau}; " f"capability {best.capability_family} @ {best.contract}" ) incompatible_best = max( (c for c in candidates if not _is_compatible(c.contract, constraint)), key=lambda c: c.semantic_score, default=None, ) if ( incompatible_best is not None and incompatible_best.semantic_score - best.semantic_score > shadow_delta ): # Advisory only: emit telemetry, do not change the selection. _emit_shadowed_match(best, incompatible_best) return best def _emit_shadowed_match(compatible: Candidate, shadowing: Candidate) -> None: # Placeholder for structured telemetry; intentionally non-fatal. _ = (compatible, shadowing) def resolve_or_refuse( candidates: Iterable[Mapping[str, object]], constraint: tuple[int, int, int], ) -> Candidate: """Adapter that normalizes raw registry rows and delegates to the resolver.""" normalized = [ Candidate( capability_family=str(row["family"]), contract=ContractVersion(*map(int, str(row["contract"]).split("."))), description_variant=str(row.get("variant", "default")), semantic_score=float(row["score"]), ) for row in candidates ] if not normalized: raise NoCompatibleContractError("registry returned zero candidates") return resolve_capability(normalized, constraint) ```