Why architecture matters here

Conflating results with conversation is the original sin of naive agent integrations. If a remote agent returns its spreadsheet as a giant base64 blob glued into a chat message, three things break at once. The message history balloons — every subsequent turn re-sends the blob through the model's context, so cost and latency explode. The output loses its type — the consumer has to guess from prose whether it received CSV or XLSX. And streaming becomes impossible, because a message is atomic; a report that takes ninety seconds to generate leaves the caller staring at a spinner with no partial progress and no way to cancel cheaply. Artifacts exist to make the deliverable a separate, addressable, streamable object with its own type and its own lifecycle.

Architecturally, three properties fall out of getting this right. Results are addressable: an artifact has a stable id within its task, so a client can reference it, re-fetch it, deduplicate it, and hand its id to a downstream agent instead of copying megabytes around. Results are typed: every part carries a media type, so the consumer dispatches on application/pdf versus text/markdown without heuristics, and a validating boundary can reject a payload whose bytes do not match its declared type. Results are incremental: the same artifact can be built from many streamed chunks, so a long generation shows progress, supports early cancellation, and never has to fit in a single message frame.

The trade-off you operate around is that artifacts introduce a second data plane. A message says 'here is the summary'; the artifact it references lives somewhere — inline in the event, or behind a URI in a blob store. That indirection buys scale but demands discipline: lifetimes, access control, and content-type honesty now matter, because the agent producing the artifact and the agent consuming it may belong to different teams, clouds, and trust domains. The rest of this article is about running that second data plane without letting it leak, rot, or lie.

Advertisement

The architecture: every piece explained

Top row: the object model. A remote agent executes a task — A2A's unit of work, which carries a status (submitted, working, input-required, completed, failed, canceled) and accumulates outputs. Those outputs are artifacts. Each artifact has an artifactId (stable for the task), an optional human name and description, and an ordered list of parts. A part is one of three shapes: a TextPart (a string with an optional media type like text/markdown), a FilePart (a file with a name and mime type, whose bytes are either inline base64 or a URI), or a DataPart (structured JSON — a table, a set of metrics, a form result). One artifact can mix parts: a report artifact might carry a markdown summary part and a data part of the underlying numbers.

Middle row: delivery and bytes. Artifacts reach the client through TaskArtifactUpdate events on the task's stream. Each event names the artifact and carries a slice of it, with two assembly flags: append (add these parts to the artifact already being built rather than replacing it) and lastChunk (this is the final piece — the artifact is now complete). This is how a 30MB file streams as a sequence of FileParts that the client concatenates. The crucial engineering choice is inline versus reference: small payloads travel as inline bytes (base64 in the event) for one-round-trip simplicity; large ones travel by reference — the FilePart carries a uri into a blob store, and the client fetches the bytes out-of-band, usually via a signed, expiring URL. The threshold between the two is a policy every A2A deployment must set.

Bottom rows: the consumer side. The client assembler collects TaskArtifactUpdate events keyed by artifactId, appends parts in order, resolves any URIs, validates that bytes match declared types, and persists the finished artifact. The consumer then does whatever the deliverable is for — renders the PDF, downloads the file, or feeds a DataPart into the next agent as input. The ops strip lists the controls that make this safe across trust boundaries: a content-type allow-list and validation, hard size limits, retention windows on the blob store, signed URLs with short TTLs, and content-hash deduplication so identical artifacts are not stored or re-transferred twice.

A2A artifacts — the durable outputs of a task, separate from the chatlarge, typed, streamable results referenced by idRemote agentproduces artifactsTaskunit of work + statusArtifactid + name + partsPartstext / file / dataTaskArtifactUpdatestreamed incrementsappend / lastChunkassembly flagsBlob storebytes by reference (uri)Inline bytessmall payloads (base64)Client assemblerreassemble + validate + persistConsumerrender / download / feed downstreamOps — content-type policy + size limits + retention + signed URLs + dedupemitstatuschunkinlinestreamassembleresolve urioperateoperate
A2A artifacts: a task emits named, typed artifacts as streamed part updates; small payloads go inline, large ones by reference, and the client reassembles and persists them.
Advertisement

End-to-end flow

Trace a concrete exchange: an orchestrator agent asks a specialist 'financial-analysis' agent to produce a quarterly report. The orchestrator sends a message on a new task; the specialist accepts, moves the task to working, and begins generating. Because the report is large and slow, the specialist streams.

First it emits a TaskArtifactUpdate for artifact report-1: a TextPart with the executive summary in markdown, append=false (this begins the artifact), lastChunk=false. The orchestrator's assembler creates a buffer for report-1 and shows the summary immediately — the user sees progress at second three, not second ninety. Over the next minute the specialist streams more TextParts (section by section), each with append=true. Then it produces the underlying spreadsheet: an 18MB XLSX. That exceeds the inline threshold, so the specialist writes the bytes to its blob store, obtains a signed URL valid for fifteen minutes, and emits a FilePart carrying the URI, mime type application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, a content hash, and a byte length — append=true. Finally it emits a DataPart with headline metrics and sets lastChunk=true and moves the task to completed.

The orchestrator's assembler now holds a complete report-1: concatenated markdown, a file reference, and a data block. It resolves the URI — fetching the XLSX out-of-band and verifying the content hash and length before trusting a byte — and persists the artifact under the task. Crucially it references the artifact by id when it hands the result to the next agent, rather than copying 18MB into a chat message. If the downstream agent only needs the metrics, it consumes the DataPart and never fetches the file at all.

Now the stress path. Suppose the stream drops after the summary but before lastChunk. The assembler holds a partial artifact and, critically, knows it — no lastChunk arrived — so it does not present it as final. On reconnect the client resubscribes to the task; because artifact updates are keyed by id and idempotent under the append protocol, the specialist can resend from the last acknowledged chunk (or the client can re-fetch the completed task snapshot) and the artifact assembles correctly. Suppose instead the signed URL has expired by the time a slow consumer tries to download: the fetch fails cleanly with a 403, the consumer requests a fresh URL by artifact id, and the retention policy — not a broken link — governs whether the bytes still exist. Every failure resolves to 'ask again by id', which is exactly the property addressability was supposed to buy.