3.3

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.

four of the six artifact framessseFull log
data: {…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "name": "test-log.txt", "parts": [{"text": "collected 12 items\n"}]}}}}

data: {…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "parts": [{"text": "tests/test_charges.py ........  [ 66%]\n"}]}, "append": true}}}

…

data: {…"artifactUpdate": {…"artifact": {"artifactId": "test-log", "parts": [{"text": "1 failed, 11 passed in 4.21s\n"}]}, "append": true, "lastChunk": true}}}

data: {…"artifactUpdate": {…"artifact": {"artifactId": "summary", "name": "summary.json", "parts": [{"data": {"passed": 11, "failed": 1, …}, "mediaType": "application/json"}]}, "lastChunk": true}}}
Frames 3, 4, 7, and 8 of the stream. Each frame loses its JSON-RPC envelope, taskId, and contextId. Frames 5 and 6 are cut whole, and so is the failure list in summary.json.

Figure 3.3 lines up all nine frames of the run with what each artifact frame did to the stored task.

Fig. 3.3five chunks become one stored artifactstructure
Five chunks become one stored artifact Top: the nine stream frames of the streamed run on the kit clock. Frames 1, 2 and 9 are status frames stamped 09:00:01.750Z, 09:00:02.000Z and 09:00:02.250Z, 250 ms apart. Frames 3 to 8 are artifact updates that carry no timestamp and fall between the second and third stamps. Middle: a table of the artifactId and flags each of frames 3 to 8 carries and what it does to the stored task: frame 3 creates artifact test-log with its first part, frames 4 to 7 append one part each, frame 7 sets lastChunk, and frame 8 creates the summary artifact. Bottom: the stored test-log.txt artifact with its five text parts, one per chunk. THE NINE FRAMES ON THE KIT CLOCK frames 3 to 8 carry no timestamp in the stream: artifactId and flags in the stored task 09:00:01.750Z 1 09:00:02.000Z 250 ms 2 3 3 test-log, name: test-log.txt creates artifacts[0] and parts[0] artifacts[0] · artifactId test-log · name test-log.txt parts[0] collected 12 items 4 4 test-log, append: true adds parts[1] parts[1] progress line for tests/test_charges.py, at 66% 5 5 test-log, append: true adds parts[2] parts[2] progress line for tests/test_refunds.py, at 100% 6 6 test-log, append: true adds parts[3] parts[3] FAILED tests/test_refunds.py::test_partial_refund - AssertionError: 450 != 500 7 7 test-log, append: true, lastChunk: true adds parts[4], the last chunk parts[4] 1 failed, 11 passed in 4.21s 8 8 summary, name: summary.json, lastChunk: true creates artifacts[1], one data part 09:00:02.250Z 250 ms 9
The five chunks of 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:

Implementationappend: true for an unknown artifactIdReads lastChunk
a2a-pythonraises InvalidAgentResponseErrorno
a2a-goan error, and the task ends in TASK_STATE_FAILED with no causeno
a2a-javadrops the chunk, logs a warning, and keeps the task unchangedno
a2a-jsstores the chunk as a new artifactno
a2a-dotnetstores the chunk as a new artifactno
a2a-rsreturns InvalidAgentResponseError, and the task keeps its last stateno
the kitstarts 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