Artifact and chunks
A task returns its results as artifacts, and a long artifact arrives as chunks that you join by artifactId until lastChunk marks the end.
test-runner writes a log while the suite runs. Your planner wants to show that log as it grows, and to keep the verdict once the run ends. On a stream, the log arrives as five chunks of one artifact named test-log.txt. A second artifact, summary.json, arrives in one piece and holds the counts the planner acts on.
When you finish this section, you can rebuild a chunked artifact, name what the specification leaves open, and know which copy to trust.
Results are artifacts
Artifact: an output that the agent produces as the result of a task, made of parts. It can be a document, an image, or structured data. Two fields are REQUIRED: artifactId, which must be unique within the task, and parts, which must contain at least one part. The optional fields are name, description, metadata, and extensions.
Results go in artifacts and talk goes in messages, one of the core rules: a message should not carry the output of a task.
Chunks on the stream
On a stream, artifacts travel inside artifactUpdate events. Each TaskArtifactUpdateEvent carries taskId, contextId, and artifact, all REQUIRED, and two optional flags. When append is true, the content belongs at the end of the artifact with the same id that came before. When lastChunk is true, the event carries the last chunk of the artifact.
{…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "name": "test-log.txt", "parts": [{"text": "collected 12 items\n"}]}}}}
{…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "parts": [{"text": "tests/test_charges.py ........ [ 66%]\n"}]}, "append": true}}}
…
{…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "parts": [{"text": "1 failed, 11 passed in 4.21s\n"}]}, "append": true, "lastChunk": true}}}
{…"artifactUpdate": {…"artifact": {"artifactId": "summary", "name": "summary.json", "parts": [{"data": {"passed": 11, "failed": 1, …}, "mediaType": "application/json"}]}, "lastChunk": true}}}Figure 3.3 lines up all nine frames of the run with what each artifact frame did to the stored task.
test-log.txt become five parts of one stored artifact in frame order, and lastChunk ends that artifact while the stream continues. Top, the nine frames of the streaming capture on the kit clock. Frames 1, 2, and 9 sit at their timestamps, and frames 3 to 8 are spaced evenly between. Middle, what frames 3 to 8 carry and what each does to the stored task. Bottom, the stored test-log.txt as GetTask returns it in the polling capture. Parts 1 and 2 are the progress lines of the two test files, given in full in the capture.A lastChunk closes one artifact and nothing more: two frames follow it on this stream. An artifact event has no timestamp field, so the only order among chunks is the stream's order, which every binding must keep.
Joining the chunks
The kit and the reference SDK rebuild an artifact the same way. They find the stored artifact with the same artifactId and, if append is true, add the new parts to its parts. If append is absent, they replace the whole artifact, or add it when the id is new (the kit's server code). The five log lines stay five text parts, so join their text in order to get the file.
The specification does not say what append means for an id the server has never seen, and the implementations disagree:
| Implementation | append: true for an unknown artifactId | Reads lastChunk |
|---|---|---|
| a2a-python | raises InvalidAgentResponseError | no |
| a2a-go | an error, and the task ends in TASK_STATE_FAILED with no cause | no |
| a2a-java | drops the chunk, logs a warning, and keeps the task unchanged | no |
| a2a-js | stores the chunk as a new artifact | no |
| a2a-dotnet | stores the chunk as a new artifact | no |
| a2a-rs | returns InvalidAgentResponseError, and the task keeps its last state | no |
| the kit | starts a new artifact (kit notes) | no |
So the same chunk can fail the task, vanish, or become a new artifact. Send the first chunk of every artifact without append. The specification is also silent on whether later metadata merges or replaces.
Do not take field names from the migration guide (conflicts D9 and D10). It wraps the event as taskArtifactUpdate and gives it an index field. The proto names the wrapper artifactUpdate, and its event has no index field. PR #2056 fixed the wrapper names on main and kept the index claim, and no release carries that fix yet.
Trust the stored copy
GetTask returns test-log.txt with all five parts, as the bottom of figure 3.3 shows, and a blocking SendMessage returns the same merged artifacts.
After a dropped connection, the missed chunks are never replayed. SubscribeToTask starts instead with a Task snapshot that already holds them. Replace your copy of each artifact with the snapshot's before you apply new chunks, as SendStreamingMessage and SubscribeToTask shows.
An artifact can also end without a last chunk: the canceled run in 10-cancel.http stores two parts of a log, and lastChunk never arrives. A stored Artifact has no field that says it is complete, so judge it by the task's state.
A2A protocol 1.0.1 · 3303592 · 2026-05-28 · Edition 2026.10