Skip to content

[BUG] parts of Artifact should not be empty in A2A #1640

@punkyoon

Description

@punkyoon

Checks

  • I have updated to the lastest minor and patch version of Strands
  • I have checked the documentation and this is not expected behavior
  • I have searched ./issues and there are no duplicates of my issue

Strands Version

1.25.0

Python Version

3.14.2

Operating System

macOS 26.2

Installation Method

pip

Steps to Reproduce

When enable_a2a_compliant_streaming=True is set, strands-agents produces the following stream:

...
data: {"id":"test-stream-1","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"826257ff-26e0-441e-a4b3-f44b5e6f1615","name":"agent_response","parts":[{"kind":"text","text":"I'll"}]},"contextId":"019c2c2d-c046-7416-aba2-2becef55fa93","kind":"artifact-update","taskId":"f0e833f2-cd8a-4910-ae47-eca136984804"}}

data: {"id":"test-stream-1","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"826257ff-26e0-441e-a4b3-f44b5e6f1615","name":"agent_response","parts":[{"kind":"text","text":" suggest"}]},"contextId":"019c2c2d-c046-7416-aba2-2becef55fa93","kind":"artifact-update","taskId":"f0e833f2-cd8a-4910-ae47-eca136984804"}}

data: {"id":"test-stream-1","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"826257ff-26e0-441e-a4b3-f44b5e6f1615","name":"agent_response","parts":[{"kind":"text","text":" a"}]},"contextId":"019c2c2d-c046-7416-aba2-2becef55fa93","kind":"artifact-update","taskId":"f0e833f2-cd8a-4910-ae47-eca136984804"}}

data: {"id":"test-stream-1","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"826257ff-26e0-441e-a4b3-f44b5e6f1615","name":"agent_response","parts":[{"kind":"text","text":" plan"}]},"contextId":"019c2c2d-c046-7416-aba2-2becef55fa93","kind":"artifact-update","taskId":"f0e833f2-cd8a-4910-ae47-eca136984804"}}

data: {"id":"test-stream-1","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"826257ff-26e0-441e-a4b3-f44b5e6f1615","name":"agent_response","parts":[{"kind":"text","text":"."}]},"contextId":"019c2c2d-c046-7416-aba2-2becef55fa93","kind":"artifact-update","taskId":"f0e833f2-cd8a-4910-ae47-eca136984804"}}

data: {"id":"test-stream-1","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"826257ff-26e0-441e-a4b3-f44b5e6f1615","name":"agent_response","parts":[]},"contextId":"019c2c2d-c046-7416-aba2-2becef55fa93","kind":"artifact-update","lastChunk":true,"taskId":"f0e833f2-cd8a-4910-ae47-eca136984804"}}

data: {"id":"test-stream-1","jsonrpc":"2.0","result":{"contextId":"019c2c2d-c046-7416-aba2-2becef55fa93","final":true,"kind":"status-update","status":{"state":"completed","timestamp":"2026-02-06T06:49:52.281537+00:00"},"taskId":"f0e833f2-cd8a-4910-ae47-eca136984804"}}

There is a problem with the final TaskArtifactUpdateEvent in the stream:

data: {"id":"test-stream-1","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"826257ff-26e0-441e-a4b3-f44b5e6f1615","name":"agent_response","parts":[]},"contextId":"019c2c2d-c046-7416-aba2-2becef55fa93","kind":"artifact-update","lastChunk":true,"taskId":"f0e833f2-cd8a-4910-ae47-eca136984804"}}

When a Java-based A2A client receives this event, it throws the following error (observed with a2a-java 0.3.3.Final):

Wrapped by: java.lang.RuntimeException: com.fasterxml.jackson.databind.exc.ValueInstantiationException: Cannot construct instance of `io.a2a.spec.Artifact`, problem: Parts cannot be empty
 at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: io.a2a.spec.TaskArtifactUpdateEvent["artifact"])
	at io.a2a.client.transport.jsonrpc.sse.SSEEventListener.handleMessage(SSEEventListener.java:79)
	Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 
Error has been observed at the following site(s):
	*_________checkpoint ⇢ sendMessageStreaming

The Artifact implementation in a2a-java validates that parts is non-empty:

https://github.com/a2aproject/a2a-java/blob/v0.3.3.Final/spec/src/main/java/io/a2a/spec/Artifact.java

@JsonInclude(JsonInclude.Include.NON_ABSENT)
@JsonIgnoreProperties(ignoreUnknown = true)
public record Artifact(String artifactId, String name, String description, List<Part<?>> parts, Map<String, Object> metadata,
                       List<String> extensions) {

    public Artifact {
        Assert.checkNotNullParam("artifactId", artifactId);
        Assert.checkNotNullParam("parts", parts);
        if (parts.isEmpty()) {
            throw new IllegalArgumentException("Parts cannot be empty"); // !!HERE!!
        }
    }
...

This validation is consistent with the A2A specification, which states that parts "Must contain at least one part":

    "parts": {
        "description": "The content of the artifact. Must contain at least one part.",
        "items": {
        "$ref": "a2a.v1.Part.jsonschema.json"
        },
        "type": "array"
    }

strands-agents should not emit a TaskArtifactUpdateEvent with an empty parts array. The final lastChunk: true event should either include at least one part or be omitted entirely.

Expected Behavior

the parts field in Artifact must not be empty. it should be structured as follows:

data: {"id":"test-stream-1","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"9b2512d3-b299-4219-9171-596e78153b14","name":"agent_response","parts":[{"kind":"text","text":""}]},"contextId":"thr_019c2c2d-c046-7416-aba2-2becef55fa93","kind":"artifact-update","lastChunk":true,"taskId":"b24ee98c-be58-479b-8632-73e1bfefde59"}}

Actual Behavior

The last event in the TaskArtifactUpdateEvent stream contains empty parts data:

data: {"id":"test-stream-1","jsonrpc":"2.0","result":{"append":true,"artifact":{"artifactId":"826257ff-26e0-441e-a4b3-f44b5e6f1615","name":"agent_response","parts":[]},"contextId":"019c2c2d-c046-7416-aba2-2becef55fa93","kind":"artifact-update","lastChunk":true,"taskId":"f0e833f2-cd8a-4910-ae47-eca136984804"}}

Additional Context

No response

Possible Solution

I propose sending a TextPart with an empty string. if you agree with this approach, I will open a PR.

as-is

https://github.com/strands-agents/sdk-python/blob/v1.25.0/src/strands/multiagent/a2a/executor.py#L177-L214

to-be

    async def _handle_agent_result(self, result: SAAgentResult | None, updater: TaskUpdater) -> None:
        """Handle the final result from the Strands Agent.

        For A2A-compliant streaming: sends the final artifact chunk marker and marks
        the task as complete. If no data chunks were previously sent, includes the
        result content.

        For legacy streaming: adds the final result as a simple artifact without
        artifact_id tracking.

        Args:
            result: The agent result object containing the final response, or None if no result.
            updater: The task updater for managing task state and adding the final artifact.
        """
        if self.enable_a2a_compliant_streaming:
            if self._is_first_chunk:
                final_content = str(result) if result else ""
-               parts = [Part(root=TextPart(text=final_content))] if final_content else []
+               parts = [Part(root=TextPart(text=final_content))]
                await updater.add_artifact(
                    parts,
                    artifact_id=self._current_artifact_id,
                    name="agent_response",
                    last_chunk=True,
                )
            else:
                await updater.add_artifact(
-                   [],
+                   [Part(root=TextPart(text=""))],
                    artifact_id=self._current_artifact_id,
                    name="agent_response",
                    append=True,
                    last_chunk=True,
                )

Related Issues

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions