[Bug]: Serialization: Artifacts contain stringified response objects instead of TextPart content
allenday opened this issue · 2 comments
What happened?
The A2A SDK has a critical serialization bug where TextPart.text content is not properly extracted during transport. Instead of receiving the actual text content, clients receive the stringified representation of the entire response object containing artifacts.
Expected Behavior
- Agent creates JSON content:
json_content = structured_response.model_dump_json() - Agent wraps in TextPart:
TextPart(text=json_content) - Agent adds to artifact:
Part(root=TextPart(text=json_content)) - Client receives: The original json_content string
Actual Behavior
- Agent creates JSON content:
json_content = structured_response.model_dump_json()✅ - Agent wraps in TextPart:
TextPart(text=json_content)✅ - Agent adds to artifact:
Part(root=TextPart(text=json_content))✅ - Client receives:
artifacts=[Artifact(artifact_id='...', description=None, extensions=None, metadata=None, name='consensus_analysis_result', parts=[Part(root=TextPart(kind='text', metad...
Evidence
The received content shows Python's default string representation of a list containing Artifact objects, not the intended JSON content.
Debug output from client:
DEBUG: Found 1 artifacts
DEBUG: Artifact 0: has parts = True
DEBUG: Artifact 0 has 1 parts
DEBUG: Part 0: has root = True, root type = <class 'a2a.types.TextPart'>
DEBUG: Found text content: artifacts=[Artifact(artifact_id='91f810b7-a027-40d9-9ad5-36d5dbce09f1', description=None, extensions...
Environment
- A2A SDK Version:
a2a-sdk==0.3.0 - Python Version:
3.13 - Transport:
JsonRpcTransport - Setup: Docker containers with leader/follower agents
Reproduction Steps
- Create an agent that generates structured JSON content
- Wrap content in TextPart(text=json_content)
- Add to artifact via
updater.add_artifact([Part(root=TextPart(text=content))]) - Send through A2A transport to client
- Client extracts
part.root.text- receives artifact structure instead of original content
Minimal Reproduction Code
Agent (sender):
# Generate structured content
json_content = {"title": "Test", "data": "content"}
json_string = json.dumps(json_content)
# Create artifact with TextPart
await updater.add_artifact(
[Part(root=TextPart(text=json_string))],
name="test_result"
)Client (receiver):
# Extract content from artifact
for artifact in response.artifacts:
for part in artifact.parts:
if hasattr(part, 'root') and hasattr(part.root, 'text'):
content = part.root.text
print(f"Received: {content}")
# Expected: '{"title": "Test", "data": "content"}'
# Actual: 'artifacts=[Artifact(artifact_id=...'Impact
- Severity: Critical
- Scope: Any A2A system passing structured content through artifacts
- Workaround: Complex pattern matching to extract JSON from corrupted strings
- Affects: Content extraction, structured data exchange, API responses
Root Cause Analysis
The bug appears to be in the A2A transport/serialization layer where:
- The entire response object gets converted to string using Python's
str()method - Instead of extracting
TextPart.textcontent during serialization - This happens between agent artifact creation and client reception
- Likely in
JsonRpcTransportor related serialization components
Proposed Fix
The serialization layer should extract TextPart.text content rather than stringifying the entire response object. The transport mechanism needs to:
- Properly traverse the artifact structure
- Extract text content from
TextPartobjects - Preserve the original text content during transport
- Ensure clients receive the intended content, not object representations
Workaround Implemented
We've implemented client-side detection and recovery:
def extract_json_from_corrupted_artifact(corrupted_content: str) -> str:
"""Extract actual JSON from A2A serialization bug artifacts."""
# Pattern matching to recover JSON from artifact structure strings
# Multiple fallback strategies for different corruption patternsThis workaround allows systems to function despite the bug, but the SDK should be fixed to prevent the corruption from occurring.
Relevant log output
Debug output from client:
DEBUG: Found 1 artifacts
DEBUG: Artifact 0: has parts = True
DEBUG: Artifact 0 has 1 parts
DEBUG: Part 0: has root = True, root type = <class 'a2a.types.TextPart'>
DEBUG: Found text content: artifacts=[Artifact(artifact_id='91f810b7-a027-40d9-9ad5-36d5dbce09f1', description=None, extensions...Code of Conduct
- I agree to follow this project's Code of Conduct
Hi, I tried reproducing the issue, but couldn't reproduce this issue:
I have updated the HelloWorld agent as below to return stringified JSON response:
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import Part, TextPart
from a2a.utils import (
new_task,
)
from pydantic import BaseModel
class HelloResponse(BaseModel):
"""Hello Response."""
message: str
class HelloWorldAgent:
"""Hello World Agent."""
async def invoke(self) -> str:
response = HelloResponse(message="Hello World")
return response.model_dump_json()
class HelloWorldAgentExecutor(AgentExecutor):
"""Test AgentProxy Implementation."""
def __init__(self):
self.agent = HelloWorldAgent()
async def execute(
self,
context: RequestContext,
event_queue: EventQueue,
) -> None:
task = new_task(context.message) # type: ignore
await event_queue.enqueue_event(task)
result = await self.agent.invoke()
updater = TaskUpdater(event_queue, task.id, task.context_id)
await updater.add_artifact(
[Part(root=TextPart(text=result))],
name='result',
)
await updater.complete()
async def cancel(
self, context: RequestContext, event_queue: EventQueue
) -> None:
raise Exception('cancel not supported')Following is the client code:
client = A2AClient(
httpx_client=httpx_client, agent_card=final_agent_card_to_use
)
logger.info('A2AClient initialized.')
send_message_payload: dict[str, Any] = {
'message': {
'role': 'user',
'parts': [
{'kind': 'text', 'text': 'hello'}
],
'messageId': uuid4().hex,
},
}
request = SendMessageRequest(
id=str(uuid4()), params=MessageSendParams(**send_message_payload)
)
response = await client.send_message(request)
for artifact in response.root.result.artifacts:
for part in artifact.parts:
if hasattr(part, 'root') and hasattr(part.root, 'text'):
content = part.root.text
print(f"Received: {content}")
streaming_request = SendStreamingMessageRequest(
id=str(uuid4()), params=MessageSendParams(**send_message_payload)
)
stream_response = client.send_message_streaming(streaming_request)
async for chunk in stream_response:
if isinstance(chunk.root.result, TaskArtifactUpdateEvent):
artifact = chunk.root.result.artifact
if artifact:
for part in artifact.parts:
if hasattr(part, 'root') and hasattr(part.root, 'text'):
content = part.root.text
print(f"Received: {content}")Following is the output I see for both cases:
Received: {"message":"Hello World"}
Can you please try with the above test if you still see the issue?
Alternatively you can try calling the agent endpoint with curl as below
curl --request POST \
--url http://localhost:9999/ \
--header 'content-type: application/json' \
--data '{"jsonrpc": "2.0","id": 33,"method": "message/send","params": {"message": {"role": "user","parts": [{ "type": "text", "text": "hello" }],"messageId":"foo4","kind": "message"}}}'Here's the example response you should see:
{
"id": 33,
"jsonrpc": "2.0",
"result": {
"artifacts": [
{
"artifactId": "765122f0-765b-4275-809c-fe624dfa4f2a",
"name": "result",
"parts": [
{
"kind": "text",
"text": "{\"message\":\"Hello World\"}"
}
]
}
],
"contextId": "762e892e-5d55-47bc-9e3c-51db498ea018",
"history": [
{
"contextId": "762e892e-5d55-47bc-9e3c-51db498ea018",
"kind": "message",
"messageId": "foo4",
"parts": [
{
"kind": "text",
"text": "hello"
}
],
"role": "user",
"taskId": "92e1ec09-4c60-4a2f-b2f2-56ccd9c5c77d"
}
],
"id": "92e1ec09-4c60-4a2f-b2f2-56ccd9c5c77d",
"kind": "task",
"status": {
"state": "completed",
"timestamp": "2025-08-08T19:57:04.170392+00:00"
}
}
}