Skip to content

[Bug]: A2ARESTFastAPIApplication omits state field from task status when task is rejected #625

@benedict-khoo-sap

Description

@benedict-khoo-sap

What happened?

Steps to reproduce

  1. Install a2a-sdk[http-server] (tested with v0.3.22) and uvicorn (tested with v0.40.0) using Python 3.12:
uv init -p 3.12
uv add "a2a-sdk[http-server]" uvicorn
  1. Paste the following code in main.py
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.apps import A2ARESTFastAPIApplication, A2AStarletteApplication
from a2a.server.events import EventQueue
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore, TaskUpdater
from a2a.types import (
    AgentCapabilities,
    AgentCard,
    AgentInterface,
    AgentSkill,
    TransportProtocol,
    UnsupportedOperationError,
)
from a2a.utils import new_agent_text_message, new_task
from a2a.utils.errors import ServerError
import uvicorn

SUPPORTED_CONTENT_TYPES = ["text", "text/plain"]


class TheAgentExecutor(AgentExecutor):
    async def execute(
        self,
        context: RequestContext,
        event_queue: EventQueue,
    ) -> None:
        task = context.current_task
        if not task:
            task = new_task(context.message)  # type: ignore
            await event_queue.enqueue_event(task)

        context_id = context.context_id or task.context_id
        updater = TaskUpdater(event_queue, task.id, context_id)

        await updater.reject(message=new_agent_text_message("I don't want to work"))

    async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
        raise ServerError(error=UnsupportedOperationError())


def create_server(transport_protocol: TransportProtocol):
    capabilities = AgentCapabilities(streaming=True, push_notifications=False)
    skill = AgentSkill(
        id="convert_currency",
        name="Currency Exchange Rates Tool",
        description="Helps with exchange values between various currencies",
        tags=["currency conversion", "currency exchange"],
        examples=["What is exchange rate between USD and GBP?"],
    )
    url = "http://localhost:10000"
    agent_card = AgentCard(
        name="Currency Agent",
        description="Helps with exchange rates for currencies",
        url=url,
        version="1.0.0",
        default_input_modes=SUPPORTED_CONTENT_TYPES,
        default_output_modes=SUPPORTED_CONTENT_TYPES,
        capabilities=capabilities,
        skills=[skill],
        preferred_transport=transport_protocol,
        additional_interfaces=[AgentInterface(transport=transport_protocol, url=url)],
    )

    request_handler = DefaultRequestHandler(
        agent_executor=TheAgentExecutor(),
        task_store=InMemoryTaskStore(),
    )
    match transport_protocol:
        case TransportProtocol.http_json:
            return A2ARESTFastAPIApplication(
                agent_card=agent_card, http_handler=request_handler
            )
        case TransportProtocol.jsonrpc:
            return A2AStarletteApplication(
                agent_card=agent_card, http_handler=request_handler
            )
        case _:
            raise NotImplementedError(
                f"Unsupported server transport protocol: {transport_protocol}"
            )

server = create_server(transport_protocol=TransportProtocol.http_json)

if __name__ == "__main__":
    uvicorn.run(server.build(), host="0.0.0.0", port=10000)
  1. Run it: uv run main.py
  2. Open a terminal and send a message to the server:
curl --request POST \
  --url http://127.0.0.1:10000/v1/message:send \
  --header 'content-type: application/json' \
  --data '{
  "message": {
    "messageId": "c52d542b-06cb-4e9a-b36e-15ee6cb7eee4",
    "role": "ROLE_USER",
    "content": [
      {
        "text": "Exchange USD"
      }
    ]
  }
}'
  1. Wait a few seconds then get the task, replacing <TASK_ID> with the task ID you got inresponse to the send message request:
curl --request GET \
  --url http://127.0.0.1:10000/v1/tasks/<TASK_ID>

Actual Output

The Task response is not valid. It contains a status field with no state sub-field.

Example (IDs might be different for you, but the shape should be the same):

{
  "id": "dd8b8e5b-1831-422c-a8a0-46560ef59e59",
  "contextId": "d076a8f4-e1a2-4a25-8516-3e853433e8bb",
  "status": {
    "message": {
      "messageId": "c7e0d4aa-8a8d-4e31-bafe-e57e19585803",
      "role": "ROLE_AGENT",
      "content": [
        {
          "text": "I don't want to work"
        }
      ]
    }
  },
  "history": [
    {
      "messageId": "c52d542b-06cb-4e9a-b36e-15ee6cb7eee4",
      "contextId": "d076a8f4-e1a2-4a25-8516-3e853433e8bb",
      "taskId": "dd8b8e5b-1831-422c-a8a0-46560ef59e59",
      "role": "ROLE_USER",
      "content": [
        {
          "text": "Exchange USD"
        }
      ],
      "metadata": {}
    }
  ]
}

Expected Output

The response should be a task object that contains a status field. Within status, there should be a state field with a valid TaskState value. According to the A2A specs, state is a required field: https://a2a-protocol.org/dev/specification/#412-taskstatus

Other notes

  • If you change the server code to use TransportProtocol.jsonrpc, the state field appears as expected.
  • If you change TheAgentExecutor to complete the task instead of rejecting it, the state field appears as expected: i.e. replace
await updater.reject(message=new_agent_text_message("I don't want to work"))

with

await updater.complete(message=new_agent_text_message("I don't want to work"))

Relevant log output

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions