Environment
- a2a-sdk 1.0.3
- Python 3.13
- Transport: JSON-RPC (
create_jsonrpc_routes, DefaultRequestHandlerV2), enable_v0_3_compat=True
Summary
For a 1.x message/send call, the JSON-RPC result comes back as {"task": {…}} (or {"message": {…}}) rather than the Task or Message object itself. A client that reads result.status and result.artifacts, which the A2A 1.0 spec describes for the unary send, finds nothing, because the payload sits one level deeper under result.task.
The task/message oneof belongs to the streaming StreamResponse. The unary message/send should return the Task or Message directly, the way tasks/get and tasks/cancel already do.
Expected
{ "jsonrpc": "2.0", "id": "1", "result": { "id": "…", "contextId": "…", "status": {…}, "artifacts": [ … ] } }
Actual
{ "jsonrpc": "2.0", "id": "1", "result": { "task": { "id": "…", "contextId": "…", "status": {…}, "artifacts": [ … ] } } }
Root cause
In a2a/server/routes/jsonrpc_dispatcher.py, _handle_send_message wraps the return value in a SendMessageResponse before serializing:
async def _handle_send_message(self, request_obj, context):
task_or_message = await self.request_handler.on_message_send(request_obj, context)
if isinstance(task_or_message, Task):
return MessageToDict(SendMessageResponse(task=task_or_message))
return MessageToDict(SendMessageResponse(message=task_or_message))
MessageToDict on a SendMessageResponse emits the oneof field name (task or message) as a key, which becomes the JSON-RPC result. The sibling handlers serialize the object directly and do not show this wrapper:
async def _handle_get_task(self, request_obj, context):
task = await self.request_handler.on_get_task(request_obj, context)
return MessageToDict(task, preserving_proto_field_name=False)
Impact
A2A 1.0 clients that follow the spec read the Task at result, not at result.task. Against this SDK they see an empty result and report the call as incomplete. The two shapes also disagree within one SDK: tasks/get returns the Task un-nested, message/send nests it, so a client cannot use one code path for both.
Reproduce
- Stand up any agent with
create_jsonrpc_routes(...).
- POST a 1.x
message/send (method: "SendMessage").
- Read the response:
result has a single key, task.
Suggested fix
Return the Task or Message directly from _handle_send_message, matching _handle_get_task and _handle_cancel_task:
async def _handle_send_message(self, request_obj, context):
task_or_message = await self.request_handler.on_message_send(request_obj, context)
return MessageToDict(task_or_message, preserving_proto_field_name=False)
The v0.3 compat path is unaffected. It already returns the Task/Message directly through the v0.3 adapter.
Environment
create_jsonrpc_routes,DefaultRequestHandlerV2),enable_v0_3_compat=TrueSummary
For a 1.x
message/sendcall, the JSON-RPCresultcomes back as{"task": {…}}(or{"message": {…}}) rather than the Task or Message object itself. A client that readsresult.statusandresult.artifacts, which the A2A 1.0 spec describes for the unary send, finds nothing, because the payload sits one level deeper underresult.task.The
task/messageoneof belongs to the streamingStreamResponse. The unarymessage/sendshould return the Task or Message directly, the waytasks/getandtasks/cancelalready do.Expected
{ "jsonrpc": "2.0", "id": "1", "result": { "id": "…", "contextId": "…", "status": {…}, "artifacts": [ … ] } }Actual
{ "jsonrpc": "2.0", "id": "1", "result": { "task": { "id": "…", "contextId": "…", "status": {…}, "artifacts": [ … ] } } }Root cause
In
a2a/server/routes/jsonrpc_dispatcher.py,_handle_send_messagewraps the return value in aSendMessageResponsebefore serializing:MessageToDicton aSendMessageResponseemits the oneof field name (taskormessage) as a key, which becomes the JSON-RPCresult. The sibling handlers serialize the object directly and do not show this wrapper:Impact
A2A 1.0 clients that follow the spec read the Task at
result, not atresult.task. Against this SDK they see an empty result and report the call as incomplete. The two shapes also disagree within one SDK:tasks/getreturns the Task un-nested,message/sendnests it, so a client cannot use one code path for both.Reproduce
create_jsonrpc_routes(...).message/send(method: "SendMessage").resulthas a single key,task.Suggested fix
Return the Task or Message directly from
_handle_send_message, matching_handle_get_taskand_handle_cancel_task:The v0.3 compat path is unaffected. It already returns the Task/Message directly through the v0.3 adapter.