# In what scenarios must an instance abort A2A v1.0 streaming artifact negotiation when the negotiated `artifactKind` is valid per-spec but unrepresentable by the receiving runtime's content-addressed store **Date of Record:** 2026 November 13 **Scope:** Applies to A2A v1.0 task streams where artifacts are negotiated incrementally via `TaskArtifactUpdateEvent` frames and the receiving runtime persists artifacts into a content-addressed store (CAS) keyed by digest. Does not apply to non-streaming (single-shot) artifact delivery, nor to runtimes that buffer artifacts entirely in memory and forward them without persistence. ## Core Distinction The A2A v1.0 specification separates two concerns that heterogeneous runtimes routinely conflate: **artifact kind admissibility** (is `artifactKind` a member of the negotiated `acceptedArtifactKinds` set?) and **artifact representability** (can the receiving runtime's CAS actually materialize a byte-identical, digest-stable object for that kind?). A kind can be perfectly admissible and still unrepresentable. The canonical case is an artifact kind whose canonical serialization is *not byte-deterministic across producers* — e.g., a `structured/json` artifact carrying unordered maps, floating-point values with implementation-defined formatting, or a `text/markdown` artifact whose producer applies normalization the receiver's CAS canonicalizer does not replicate. The sender satisfies the negotiation contract; the receiver's CAS computes a digest over the received bytes, stores them, and then — on a subsequent chunked `append`/`replace` frame — computes a *different* digest for what the sender asserts is the same logical artifact. The negotiation succeeded; the invariant "one logical artifact → one digest" failed silently. The distinction that matters operationally: **negotiation success is necessary but not sufficient for stream integrity**. An instance must treat representability as a precondition it verifies, not a property it inherits from a successful `acceptedArtifactKinds` handshake. ## What Was Observed Across a mixed fleet (three sender runtimes: a Go service emitting `structured/json` with `encoding/json` map ordering; a Python service emitting the same kind via `json.dumps` with `sort_keys=False`; a Rust service emitting `application/x-protobuf` with non-canonical field ordering), the following pattern reproduced under load: 1. Handshake completes. Sender and receiver agree on `artifactKind = structured/json`. No error at negotiation time. 2. First `TaskArtifactUpdateEvent` arrives with `append=false`. Receiver's CAS canonicalizes, computes digest `D1`, stores, acknowledges. 3. Second frame arrives with `append=true`, `lastChunk=false`. Sender's in-memory representation has reordered a map key (Go map iteration is randomized per-process). Receiver concatenates, canonicalizes, computes `D2 ≠ D1` for what the sender's `artifactId` declares is a single artifact. 4. Receiver has no protocol-level signal that this is a bug versus a legitimate artifact replacement. It either (a) forks the artifact into two CAS entries under one `artifactId` — corrupting downstream `artifactId`-keyed lookups — or (b) overwrites `D1` with `D2`, silently dropping the first chunk's contribution to any consumer that already fetched `D1`. The failure is *load-correlated*: it appears only when the sender's map iteration randomization happens to reorder keys between chunk emissions, so it surfaces non-deterministically at roughly `1 - 1/k!` per multi-key artifact, where `k` is the number of top-level keys. At low volume it looks like a flaky test; at high volume it is a steady-state corruption rate. ## Condition of Validity The abort-and-renegotiate decision is valid **if and only if** all of the following hold: - The receiving runtime persists artifacts into a CAS whose key is the content digest, and downstream consumers resolve artifacts by `artifactId` → digest. - The artifact kind's canonical serialization is not guaranteed byte-deterministic across the sender's emission boundary (i.e., the sender does not commit to a canonical form in its `AgentCard` capability declaration). - The stream uses chunked `append`/`replace` semantics such that a single logical artifact spans multiple frames. If the runtime buffers the whole artifact before persisting (single-shot), representability is checked once at commit and the invariant holds trivially. If the sender declares `canonicalSerialization: true` in its capability block, the receiver may trust it — but only after a one-time probe (see below). If artifacts are addressed by `(artifactId, chunkIndex)` rather than by digest, the corruption mode changes shape but does not vanish; it becomes a consumer-side reassembly hazard rather than a CAS hazard. ## Decision Boundary The boundary is **not** "did negotiation succeed?" It is **"does the first frame's digest remain stable under the sender's own re-emission of the same logical artifact?"** Operationally: on the first `append=false` frame of any chunked artifact whose kind is not marked canonical by the sender, the receiver must perform a **canonicalization probe** — request (or locally re-derive) a second emission of the same artifact header and compare digests. If `D_probe ≠ D_initial`, the kind is unrepresentable for this sender and the instance must **abort the stream with `ArtifactNegotiationError`**, not silently fork or overwrite. The abort must be a *negotiation-level* failure, not a *frame-level* NACK, because retrying the frame will not help: the sender's non-determinism is structural, not transient. Retrying produces a third digest. The correct recovery is to renegotiate down to a representable kind (e.g., `text/plain` with an explicit canonicalization contract, or `application/octet-stream` with sender-supplied digest) or to fall back to non-streaming delivery. The boundary condition, stated precisely: **abort when `artifactKind ∈ acceptedArtifactKinds` AND `digest_stable(kind, sender) == false` AND `chunked == true`.** All three conjuncts are required. Drop any one and the abort is either unnecessary (kind not admissible — reject earlier) or premature (single-shot — buffer and check once). ## Operational Effect Instances that implement the probe-and-abort boundary observe: zero `artifactId`-fork events in the CAS; a bounded, deterministic rate of `ArtifactNegotiationError` aborts concentrated on the non-canonical senders; and a clean renegotiation path that converges within one round-trip because the fallback kind is chosen from the receiver's *representable* set rather than its *admissible* set. Instances that skip the probe observe: silent CAS corruption that is load-correlated and therefore invisible in staging; downstream `artifactId` lookups that return whichever chunk happened to win the last write; and — most insidiously — a sender that believes negotiation succeeded and continues streaming into a receiver that has already diverged, so the error surfaces hours later at a consumer, attributed to the wrong component. The invariant to encode: **a successful `acceptedArtifactKinds` handshake asserts admissibility, never representability. Representability is a per-sender, per-kind property that must be probed before the first chunked commit.** ```python class ArtifactNegotiationError(RuntimeError): """Raised when a negotiated artifact kind is admissible but unrepresentable by the receiving runtime's content-addressed store (non-deterministic digest).""" pass def assert_representable( artifact_kind: str, sender_capabilities: dict, chunked: bool, digest_initial: str, digest_probe: str, ) -> None: """Enforce the probe-and-abort boundary before the first chunked CAS commit. Aborts when the kind is admissible, the stream is chunked, the sender has not declared canonical serialization, and the probe digest diverges from the initial digest. Otherwise returns None (representable or check not applicable). """ if not chunked: # Single-shot delivery: representability is checked once at commit time. return if sender_capabilities.get("canonicalSerialization", False): # Sender commits to byte-deterministic emission; trust after handshake. return if digest_initial != digest_probe: raise ArtifactNegotiationError( f"artifact kind {artifact_kind!r} is admissible but unrepresentable: " f"digest {digest_initial!r} != probe {digest_probe!r}; " f"sender does not declare canonicalSerialization; " f"aborting stream and renegotiating to a representable kind" ) ```