# What happens when an instance advertises an agent card whose declared capabilities diverge from its runtime task-lifecycle state machine **Date of Record:** 2026 November 8 **Scope:** Applies to A2A v1.0 peers that publish an agent card at a well-known discovery URI and accept `message/send` or `tasks/*` calls under cross-vendor delegation. Does not apply to single-vendor topologies where card and runtime are deployed atomically from the same artifact, nor to v0.x peers that predate capability negotiation fields. ## Core Distinction The agent card is a **declaration**, not a **contract**. A2A v1.0 treats the card as the discovery-time artifact used by a calling agent to decide *whether* and *how* to attempt delegation; it does not bind the callee to a specific task state machine. The runtime task lifecycle (`submitted → working → input-required → completed|failed|canceled`) is negotiated per task via `TaskStatus` transitions and, where supported, `tasks/pushNotificationConfig`. The failure mode arises when the card advertises capabilities (e.g., `capabilities.streaming: true`, `capabilities.pushNotifications: true`, `defaultInputModes` including `text/plain`) that the task lifecycle handler does not actually honor under load, cross-vendor identity, or mid-task renegotiation. Discovery succeeds; delegation succeeds; the *invariant* — that a completed task's artifacts are retrievable and a canceled task's resources are released — silently breaks. ## What Was Observed A delegating orchestrator (vendor A) discovered a peer (vendor B) whose card declared `streaming: true` and `pushNotifications: true`. The orchestrator opened a streaming `message/send`, received `TaskStatus.state = working`, then issued `tasks/cancel` after a 90s budget. The peer accepted the cancel (`canceled` returned), but the underlying task continued emitting `TaskArtifactUpdateEvent` frames on the still-open SSE stream for another 40s, and `tasks/get` on the canceled task ID returned `completed` with a partial artifact. The card had not lied about *capability support*; it had lied about the *state transition ordering invariant*: `canceled` is terminal and must suppress subsequent artifact updates. A second case: a card declared `defaultInputModes: ["text/plain", "application/json"]` but the runtime's `input-required` handler only accepted `text/plain` for follow-up messages. The orchestrator resumed with a JSON `DataPart` and received `TaskStatus.state = failed` with `error.code = -32602` (invalid params) rather than the expected `input-required` re-prompt. The card's declared input modes were a superset of the runtime's per-turn accepted modes. ## Condition of Validity The card-as-contract assumption is valid **only** when: (1) the callee's card is generated from the same build artifact as its task handler, (2) all optional capabilities in the card are exercised by the callee's conformance suite on every deploy, and (3) the delegating agent treats `TaskStatus.state` transitions as authoritative over card declarations at every step, not just at discovery. When any of these is false — most commonly in cross-vendor delegation where the card is hand-maintained or versioned independently of the runtime — the delegating agent must treat card capabilities as *hints* and re-validate them against observed `TaskStatus` transitions and error codes before committing to a long-lived task. ## Decision Boundary On discovery, if the peer's card declares `streaming` or `pushNotifications`, the delegating agent should open the task with a **probe**: send a minimal `message/send`, observe the first `TaskStatus` transition, and only then commit to the streaming path. If the first transition is `working` but no `TaskArtifactUpdateEvent` or `TaskStatusUpdateEvent` arrives within a bounded window (e.g., 2× the card's `defaultTimeout` if present, else a fixed 15s), treat the streaming capability as unverified and fall back to polling `tasks/get`. On `tasks/cancel`, do **not** trust the `canceled` response as terminal; continue to `tasks/get` until the task is absent or returns `canceled` with no artifact deltas across two consecutive polls. On `input-required` resume, validate the follow-up message's parts against the *runtime-accepted* modes observed in the prior turn, not the card's `defaultInputModes`. ## Operational Effect Treating the card as a contract produces two classes of incident: **orphaned tasks** (canceled but still consuming vendor-B compute and emitting artifacts that the orchestrator ignores, inflating cost and polluting logs) and **false failures** (valid follow-up inputs rejected because the card over-declared input modes). The mitigation is a per-peer capability cache that is *demoted* on first observed divergence and re-validated on every new task. The invariant to enforce in orchestration code: **a task is only `completed` when `tasks/get` returns `completed` and the artifact set is stable across two reads; a task is only `canceled` when `tasks/get` returns `canceled` and no further `TaskArtifactUpdateEvent` frames arrive on any open stream.** Card declarations gate *attempts*, not *outcomes*. ```python class AgentCardContractViolation(RuntimeError): """Raised when a peer's runtime task lifecycle diverges from its advertised agent card.""" pass class PeerCapabilityCache: def __init__(self): self._demoted = {} # peer_id -> set of capability names proven unreliable def demote(self, peer_id: str, capability: str) -> None: self._demoted.setdefault(peer_id, set()).add(capability) def is_trusted(self, peer_id: str, capability: str) -> bool: return capability not in self._demoted.get(peer_id, set()) def open_streaming_task(peer_id: str, card: dict, cache: PeerCapabilityCache, send_fn, poll_fn): if not card.get("capabilities", {}).get("streaming"): return poll_fn(peer_id) if not cache.is_trusted(peer_id, "streaming"): # previously observed divergence: do not trust the card, go straight to polling return poll_fn(peer_id) stream = send_fn(peer_id, stream=True) first = stream.next_event(timeout=15.0) if first is None or first.get("kind") != "TaskStatusUpdateEvent": cache.demote(peer_id, "streaming") raise AgentCardContractViolation( f"peer {peer_id} advertised streaming but emitted no TaskStatusUpdateEvent " f"within probe window; demoting capability and requiring polling fallback" ) return stream def await_terminal_state(peer_id: str, task_id: str, poll_fn, max_polls: int = 30): last_artifact_digest = None stable_reads = 0 for _ in range(max_polls): task = poll_fn(peer_id, task_id) state = task["status"]["state"] digest = hash(tuple(sorted(a["artifactId"] for a in task.get("artifacts", [])))) if state in ("completed", "canceled") and digest == last_artifact_digest: stable_reads += 1 if stable_reads >= 2: return task else: stable_reads = 0 last_artifact_digest = digest raise AgentCardContractViolation( f"task {task_id} on peer {peer_id} did not reach a stable terminal state " f"within {max_polls} polls; card-declared lifecycle invariant unverified" ) ```