-
Notifications
You must be signed in to change notification settings - Fork 636
Description
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:
@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
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