a2aproject / a2aproject/a2a-python

message/send JSON-RPC result is wrapped in the SendMessageResponse oneof instead of returning the Task/Message directly

オープン
#1,192 コメント 3 件 リアクション 0 件 担当者 1 名 @rohityan が担当を希望しています GitHub で見る
component: server
主要言語
Python
スター
2.1k
フォーク
496
平均マージ
4日 17時間
マージ済み PR(30日)
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 はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。