message/send JSON-RPC result is wrapped in the SendMessageResponse oneof instead of returning the Task/Message directly
@rohityan is already working on this.
Since Aug 19, 2026.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 70/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Domain
- api
Research direction
The issue is in a2a/server/routes/jsonrpc_dispatcher.py in the _handle_send_message function. Compare it with _handle_get_task and _handle_cancel_task to see the difference in how they serialize the return value. The fix is to remove the SendMessageResponse wrapper and return the Task or Message directly via MessageToDict. Run the existing tests for the JSON-RPC routes to verify the change produces the expected JSON shape.
Written by the indexing model from the issue text.
Description
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:
resulthas 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.
- Dominant language
- Python
- Stars
- 2.2k
- Forks
- 496
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 16
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from a2aproject/a2a-python
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
a2aproject/a2a-python#1261 ·
-
component: server status:awaiting response
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
a2aproject/a2a-python#1237 · 1 comment · 1 assignee ·
-
component: server status:awaiting response status:stale
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
a2aproject/a2a-python#1215 · 2 comments · 1 assignee ·
-
component: server status:awaiting response status:stale
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
a2aproject/a2a-python#1205 · 3 comments · 1 assignee ·
-
component: server status:awaiting response status:stale
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
a2aproject/a2a-python#1204 · 2 comments · 1 assignee ·
All issues in a2aproject/a2a-python
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100