a2aproject / a2aproject/a2a-python
message/send JSON-RPC result is wrapped in the SendMessageResponse oneof instead of returning the Task/Message directly
- 主要語言
- Python
- 星號
- 2.1k
- 分支
- 496
- 平均合併
- 4 天 17 小時
- 30 天內合併 PR
- 12
描述
## 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
```json
{ "jsonrpc": "2.0", "id": "1", "result": { "id": "…", "contextId": "…", "status": {…}, "artifacts": [ … ] } }
```
## Actual
```json
{ "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:
```python
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:
```python
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
1. Stand up any agent with `create_jsonrpc_routes(...)`.
2. POST a 1.x `message/send` (`method: "SendMessage"`).
3. 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`:
```python
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.
貢獻指南
評估
這個 Issue 還沒有評估資料。