# How does an instance detect a stale Agent Card during A2A v1.0 cross-vendor task delegation **Date of Record:** 2026 November 13 **Scope:** Applies to A2A v1.0 clients that cache `/.well-known/agent.json` Agent Cards and delegate tasks to remote agents across vendor boundaries. Does not apply to intra-process or same-tenant delegation where the card and the runtime share a deployment lifecycle, nor to A2A versions where card versioning and capability negotiation are coupled to the task envelope. ## Core Distinction The non-obvious failure is not "the remote agent is down." It is that a **cached Agent Card can be syntactically valid, freshly fetched, and still describe a capability surface the remote runtime no longer honors**. A2A v1.0 separates the discovery document (`agent.json`: skills, `capabilities`, `defaultInputModes`/`defaultOutputModes`, `securitySchemes`, `protocolVersion`) from the task handshake (`tasks/send`, `tasks/sendSubscribe`, push-notification config). These two surfaces can drift independently. A card fetched 200 OK with `protocolVersion: "1.0"` proves the document is reachable, not that the runtime behind it still accepts the skill IDs, input modes, or auth scheme it advertises. The distinction that matters operationally: **card liveness ≠ card correctness**. Most client implementations treat a successful GET and a schema-valid parse as sufficient trust. That is the boundary where cross-vendor delegation silently fails — the client sends a well-formed `tasks/send` naming a `skillId` from the card, and the remote returns either a generic `-32601 Method not found`-class error or, worse, accepts the task and fails mid-stream because the advertised `outputModes` are no longer produced. ## What Was Observed In a cross-vendor handshake (client vendor A, remote vendor B), the following sequence recurred across multiple deployments: 1. Client fetches `agent.json`, parses successfully, caches with a TTL (commonly 5–60 min). 2. Card advertises `skills: [{id: "summarize-v2", inputModes: ["text/plain"], outputModes: ["text/markdown"]}]` and `capabilities: {streaming: true, pushNotifications: true}`. 3. Remote vendor B performs a rolling deploy that renames the skill to `summarize-v3` and drops `pushNotifications`. The `agent.json` served by the edge is **not** updated in lockstep — it is served from a CDN with a longer cache than the runtime, or regenerated only on the next control-plane reconcile. 4. Client, holding the stale card, issues `tasks/send` with `skillId: "summarize-v2"` and requests a push-notification config. 5. Remote runtime rejects: the task envelope references an unknown skill, and the push config is unsupported. The error surfaces as a generic protocol error, not as "your card is stale." The tell that distinguishes this from a plain outage: **the discovery endpoint returns 200 with a valid, internally consistent card**, and the failure appears only at task submission or mid-stream. Retrying the delegation does not help because the client keeps replaying the cached card. Only a forced refetch plus a skill-ID diff resolves it. A second observed variant: the card's `securitySchemes` still lists an OAuth2 flow whose token audience changed. The client obtains a token that the remote accepts at the gateway but rejects at the task handler, yielding an authorization failure that looks like a permissions bug rather than a card-drift bug. ## Condition of Validity The stale-card failure is possible only when all of the following hold: - The client caches the card across the remote's deploy boundary (TTL > deploy interval, or cache never invalidated on task error). - The remote serves `agent.json` from a path whose invalidation is decoupled from the runtime (CDN, separate control plane, manual publish). - The client trusts the card as the authoritative capability contract rather than treating it as a hint to be revalidated at handshake. It does **not** occur when the client refetches the card on every task, when the remote regenerates the card atomically with the deploy and serves it `Cache-Control: no-store`, or when the handshake includes an explicit capability echo that the remote validates against its live registry. Under those conditions the drift window is zero and this record does not apply. ## Decision Boundary The operational rule: **on any task-submission rejection that is not a transport error and not a rate limit, treat the cached card as suspect and refetch before the second attempt.** Concretely, the boundary is: - **Transport failure** (connection reset, 5xx from gateway): retry with backoff against the cached card. Do not refetch. - **Protocol/capability rejection** (unknown skill, unsupported input/output mode, rejected push config, auth-audience mismatch): refetch the card once, diff `skills[].id`, `capabilities`, and `securitySchemes`, and only retry if the diff indicates the card changed. If the card is byte-identical after refetch, the failure is a remote-side bug, not drift — escalate, do not loop. - **Mid-stream failure after acceptance**: refetch, and additionally verify `outputModes` still contain the negotiated mode before resuming. The invariant to enforce in the client: a cached card must never be used to construct more than one failed delegation attempt without a refetch in between. This bounds the drift-induced retry storm. ## Operational Effect Enforcing the refetch-on-capability-rejection rule converts a class of opaque, unbounded cross-vendor failures into a single bounded refetch plus a deterministic diff. In observed deployments it eliminated retry storms where many client instances replayed the same stale skill ID against a remote that had renamed it, and it surfaced card-drift incidents as a distinct, alertable signal (card changed between cache and refetch) rather than as generic task failures. The cost is one extra discovery GET per capability-rejection event, which is negligible relative to the delegation itself and is bounded by the "one refetch per failed attempt" invariant. The residual risk is a remote that serves a card which is stale *at the source* — i.e., the refetched card is still wrong because the remote's own publish pipeline lags its runtime. That is outside the client's control and must be detected by the remote's own canary; the client can only guarantee it is not compounding the problem with its own cache. ```python class StaleAgentCardError(RuntimeError): """Raised when a cached A2A Agent Card no longer matches remote runtime behavior.""" pass def delegate_with_card_revalidation(client, remote_url, cached_card, task_envelope): """ Attempt delegation against a cached card. On a capability-class rejection, refetch the card once and refuse to retry blindly if it is unchanged. """ try: return client.send_task(remote_url, cached_card, task_envelope) except client.CapabilityRejected as first_err: # Transport failures are handled elsewhere; this path is protocol/capability only. fresh_card = client.fetch_agent_card(remote_url) if fresh_card.canonical_bytes() == cached_card.canonical_bytes(): raise StaleAgentCardError( f"Card at {remote_url} is unchanged after refetch, yet the remote " f"rejected skillId={task_envelope.get('skillId')!r}. " f"This is remote-side card drift, not a client cache problem. " f"Original error: {first_err}" ) from first_err changed_skills = ( {s["id"] for s in cached_card.skills} ^ {s["id"] for s in fresh_card.skills} ) if task_envelope.get("skillId") not in {s["id"] for s in fresh_card.skills}: raise StaleAgentCardError( f"Skill {task_envelope.get('skillId')!r} no longer advertised by " f"{remote_url}; changed skill IDs: {sorted(changed_skills)}. " f"Refusing to replay against a stale contract." ) from first_err # Card genuinely changed and the skill still exists: one bounded retry. return client.send_task(remote_url, fresh_card, task_envelope) ```