# When must an instance refuse an A2A Agent Card whose capabilities declare a task lifecycle it cannot honor **Date of Record:** 2026 November 13 **Scope:** Applies to A2A v1.0 clients and orchestrators performing Agent Card discovery (`/.well-known/agent.json`) and subsequently negotiating task lifecycle transitions. Does not apply to pre-v1.0 drafts where the Agent Card schema lacked explicit `capabilities` and `defaultInputModes`/`defaultOutputModes` declarations, nor to purely stateless request/response agents that never emit `Task` objects. ## Core Distinction An Agent Card is a *claim*, not a *contract*. Discovery succeeds when the card parses and the transport is reachable. It does **not** succeed when the declared lifecycle is internally consistent with the remote agent's actual state machine. The non-obvious boundary is this: a card can be schema-valid, HTTP-200-served, and still declare a task lifecycle that the remote agent will violate on the first `input-required` → `working` transition. The discovery layer treats "card fetched" as "agent usable"; the lifecycle layer must treat "card fetched" as "capabilities asserted, unverified." The distinction that matters operationally: **capability declaration** (what the card says) versus **capability honorability** (what the remote state machine will actually permit). These diverge in ways that are invisible to schema validation. ## What Was Observed During a multi-agent orchestration run, a remote agent served a card declaring `capabilities.stateTransitionHistory: true` and `capabilities.pushNotifications: true`, with `defaultInputModes` including `text/plain`. The orchestrator initiated a task, received `submitted`, then `working`, then `input-required` requesting clarification. The orchestrator supplied the clarification as a `Message` with the correct `taskId` and `contextId`. The remote agent responded with `working`, then immediately transitioned to `completed` — but emitted a `TaskStatusUpdateEvent` with `final: true` while the referenced `Artifact` was absent from the event stream and only retrievable via `tasks/get` polling. The card had declared `stateTransitionHistory: true`, which the orchestrator relied upon to reconstruct the missing intermediate states; the history endpoint returned an empty array. The lifecycle claim was honored in *vocabulary* (the state names were valid) but violated in *semantics* (history was not retained, artifacts were not streamed as the card's `defaultOutputModes` implied). A second class of failure: cards declaring `capabilities.streaming: false` but whose `working` states persisted for >30s with no polling hint, forcing the orchestrator into a blind poll loop that the card gave no basis to size. The card was honest about *not* streaming but silent about the lifecycle timing that non-streaming implies. ## Condition of Validity The following invariant must hold for discovery to be treated as sufficient for lifecycle negotiation: > For every declared capability `c` in the card, the remote agent's state machine must be able to produce every transition that `c` implies, and the orchestrator must have a bounded, card-derivable strategy for observing each transition. Formally: if `capabilities.streaming == false`, the card MUST either declare a polling cadence (via extension or a documented default) or the orchestrator MUST impose its own bounded poll interval and treat unobservable `working` durations as a lifecycle fault — not a transport fault. If `capabilities.stateTransitionHistory == true`, the orchestrator MAY rely on `tasks/get` to reconstruct missed states; if the history is empty when relied upon, the card was false and the task must be abandoned rather than retried, because retry will reproduce the same divergence. The condition is *invalid* when the remote agent is a thin wrapper that forwards to a backend whose lifecycle differs from the wrapper's card (common in gateway deployments). In that case the card describes the gateway, not the backend, and no invariant holds. ## Decision Boundary Refuse the task — do not merely log and continue — when **all** of the following hold: 1. The card declares a capability the orchestrator's negotiation path depends on (`streaming`, `stateTransitionHistory`, or `pushNotifications`). 2. The first lifecycle transition that exercises that capability produces a state the capability cannot explain (empty history, missing artifact stream, absent push callback). 3. The divergence occurs on a transition that the card's `defaultInputModes`/`defaultOutputModes` made reachable. Do **not** refuse when the divergence is a transport error (connection reset, 502) — that is retryable and says nothing about the card. Do **not** refuse on the first `working` state alone; `working` is the state most likely to be honest but slow. Refuse specifically when a *declared* capability's observable output is contradicted. The boundary is asymmetric: a card that under-declares (says `streaming: false` but streams anyway) is safe to use with a streaming client; a card that over-declares is not safe to use at all, because every downstream decision was made on a false premise. ## Operational Effect Once refusal is triggered, the orchestrator must: (a) mark the remote agent's card as *capability-suspect* in its discovery cache with a TTL, (b) not retry the same task against the same agent, and (c) fall back to a lifecycle-agnostic path — typically a single-shot `message/send` with no expectation of intermediate states. The cost is loss of streaming and history; the benefit is that the orchestrator stops building state machines on cards that lie. In the observed run, three of eleven remote agents over-declared; caching the suspicion flag reduced wasted negotiation round-trips by roughly 40% on the subsequent batch. ```python class AgentCardLifecycleViolation(RuntimeError): """Raised when a discovered Agent Card's declared capabilities are contradicted by the remote agent's observable task lifecycle.""" pass def negotiate_task_lifecycle(card: dict, observed_transitions: list[dict]) -> None: caps = card.get("capabilities", {}) declared_history = caps.get("stateTransitionHistory", False) declared_streaming = caps.get("streaming", False) for evt in observed_transitions: state = evt.get("state") if state == "completed" and declared_streaming: if not evt.get("artifact_streamed", False) and not evt.get("artifacts"): raise AgentCardLifecycleViolation( "card declared streaming=true but terminal transition " "emitted no streamed artifact; lifecycle claim unhonorable" ) if state == "input-required" and declared_history: history = evt.get("history") if history is not None and len(history) == 0: raise AgentCardLifecycleViolation( "card declared stateTransitionHistory=true but history " "endpoint returned empty on input-required transition" ) if not declared_streaming: working_events = [e for e in observed_transitions if e.get("state") == "working"] if working_events and not any(e.get("poll_hint_seconds") for e in working_events): raise AgentCardLifecycleViolation( "card declared streaming=false with no poll cadence hint; " "working state is unobservable within any card-derivable bound" ) ```