Thomas Piacentini
← Atelier

Chapitre

Artifact and chunks

Texte original · anglais

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: 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}}.

four of the six artifact framessseFull source
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.
Original source
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 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
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.
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.

Original SVG

Original source
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 {{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) 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 {{docs whats-new-v1}}. The proto names the wrapper artifactUpdate {{proto StreamResponse}}, and its event has no index field {{proto TaskArtifactUpdateEvent}}. 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 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 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.

Original source
- 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

Code et livrables 1
Ce que vous en retenez

Enregistré dans votre carnet sur cet appareil.

Sources et limites

Les repères de lecture sont déclaratifs. Les résultats de tests sont enregistrés séparément ; ils valent pour les cas exécutés.

AI Engineering from Scratch · 3f9715c · MIT

Télécharger ce fichier ↓