# 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 {{spec §2.2}}. Two fields are REQUIRED: `artifactId`, which must be unique within the task, and `parts`, which must contain at least one part {{proto Artifact}}. The optional fields are `name`, `description`, `metadata`, and `extensions`. Results go in artifacts and talk goes in messages, one of [the core rules](#s-what-a2a-standardizes): a message should not carry the output of a task {{spec §3.7}}. ## 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 {{proto TaskArtifactUpdateEvent}}. 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 {{proto TaskArtifactUpdateEvent}}. ```listing title: four of the six artifact frames source: capture/out/05-streaming.http lang: sse note: 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. --- 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}}} ``` [Figure](#fig-artifact-chunks) lines up all nine frames of the run with what each artifact frame did to the stored task. ```figure id: fig-artifact-chunks kind: structure title: five chunks become one stored artifact claim: 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. caption: 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 {{spec §3.5.2}}. ## 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](manuals/a2a-101/capture/a2a_ref.py) {{sdk src/a2a/server/tasks/task_manager.py}}). 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` {{sdk src/a2a/server/tasks/task_manager.py}} | no | | a2a-go | an error, and the task ends in `TASK_STATE_FAILED` with no cause {{sdk-go a2aevent/event.go at v2.6.0}} | no | | a2a-java | drops the chunk, logs a warning, and keeps the task unchanged {{sdk-java sdk/spec/util/Utils.java at v1.4.0.Final}} | no | | a2a-js | stores the chunk as a new artifact {{sdk-js src/server/result_manager.ts at v1.3.0}} | no | | a2a-dotnet | stores the chunk as a new artifact {{sdk-dotnet src/A2A/Server/TaskProjection.cs at v1.0.0-preview2}} | no | | a2a-rs | returns `InvalidAgentResponseError`, and the task keeps its last state {{sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1}} | no | | the kit | starts a new artifact ([kit notes](manuals/a2a-101/capture/README.md)) | 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](#s-ref-sources)). It wraps the event as `taskArtifactUpdate` and gives it an `index` field {{docs whats-new-v1}}. The proto names the wrapper `artifactUpdate` {{proto StreamResponse}}, and its event has no `index` field {{proto TaskArtifactUpdateEvent}}. [PR #2056](https://github.com/a2aproject/A2A/pull/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](#fig-artifact-chunks) shows, and a blocking `SendMessage` returns the same merged artifacts {{spec §3.2.2}}. After a dropped connection, the missed chunks are never replayed. `SubscribeToTask` starts instead with a `Task` snapshot that already holds them {{spec §3.1.6}}. Replace your copy of each artifact with the snapshot's before you apply new chunks, as [SendStreamingMessage and SubscribeToTask](#s-streaming-and-subscribe-to-task) 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. ```takeaways - Return results as artifacts with stable `artifactId` values, and keep status messages for people. - Rebuild by `artifactId`: replace on a chunk without `append`, and add parts on a chunk with it. - Send the first chunk of an artifact without `append`, because servers disagree about an unknown id. - After a reconnect, take each artifact from the `Task` snapshot before you apply new chunks. ``` Sources: spec §2.2, §3.1.6, §3.2.2, §3.5.2, §3.7 (research/sources/specification.md); proto Artifact, TaskArtifactUpdateEvent, StreamResponse (research/sources/a2a.proto); docs docs/whats-new-v1.md at v1.0.1; sdk src/a2a/server/tasks/task_manager.py at a2a-python 1.2.2; sdk-go a2aevent/event.go and internal/taskupdate/manager.go at v2.6.0; sdk-java spec/src/main/java/org/a2aproject/sdk/spec/util/Utils.java at v1.4.0.Final; sdk-js src/server/result_manager.ts at v1.3.0; sdk-dotnet src/A2A/Server/TaskProjection.cs at v1.0.0-preview2; sdk-rust a2a-server/src/handler.rs at a2a-server-lf-v0.5.1; capture/out/04-polling.http, 05-streaming.http, 05-streaming.events.json, 10-cancel.http, 14-resubscribe.http, 14-resubscribe.events.json; capture/a2a_ref.py, capture/planner.py, capture/README.md