From f16bd279fd83e191e530f67f2ac7681e8f435d43 Mon Sep 17 00:00:00 2001 From: kuangmi-bit Date: Thu, 6 Aug 2026 12:17:45 +0800 Subject: [PATCH 1/2] fix(server): accept trailing-slash JSON-RPC endpoint; enqueue Task in TCK SUT Two changes that together let the 1.0 TCK exercise the JSON-RPC SUT: 1. create_jsonrpc_routes now registers both the exact rpc_url and its trailing-slash variant. HTTP clients (httpx in particular) normalize an empty request path to a trailing slash, so POST /a2a/jsonrpc/ was previously 404 even though /a2a/jsonrpc worked. This is a protocol compatibility fix: the spec does not mandate one spelling over the other, and a 404 on the trailing-slash form breaks any client that does not strip it. 2. tck/sut_agent.py now enqueues the Task itself (via new_task_from_user_message) before emitting TaskStatusUpdateEvents. The SDK's active-task machinery requires this ordering (InvalidAgentResponseError otherwise), and the 1.0 TCK CORE-SEND tests assert it. Verified against a2a-tck 1.0.0.alpha2 (jsonrpc, must level): 53 failed -> 6 failed before this change, with the remaining failures being SUT feature gaps (artifacts) and one SDK error-code mapping gap, not transport issues. --- src/a2a/server/routes/jsonrpc_routes.py | 7 ++++++- tck/sut_agent.py | 8 ++++++++ tests/server/routes/test_jsonrpc_routes.py | 12 +++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/a2a/server/routes/jsonrpc_routes.py b/src/a2a/server/routes/jsonrpc_routes.py index a94d513ae..f411d5eba 100644 --- a/src/a2a/server/routes/jsonrpc_routes.py +++ b/src/a2a/server/routes/jsonrpc_routes.py @@ -64,5 +64,10 @@ def create_jsonrpc_routes( path=rpc_url, endpoint=dispatcher.handle_requests, methods=['POST'], - ) + ), + Route( + path=f'{rpc_url}/', + endpoint=dispatcher.handle_requests, + methods=['POST'], + ), ] diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 0ca3a1450..176c8cfc1 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -14,6 +14,7 @@ import a2a.types.a2a_pb2_grpc as a2a_grpc from a2a.compat.v0_3.grpc_handler import CompatGrpcHandler +from a2a.helpers.proto_helpers import new_task_from_user_message from a2a.server.agent_execution.agent_executor import AgentExecutor from a2a.server.agent_execution.context import RequestContext from a2a.server.events.event_queue import EventQueue @@ -87,6 +88,13 @@ async def execute( self.running_tasks.add(task_id) + # 1.0 semantics: the Task itself must be enqueued before any + # TaskStatusUpdateEvent (the SDK enforces this ordering). + task = context.current_task + if not task: + task = new_task_from_user_message(user_message) + await event_queue.enqueue_event(task) + logger.info( '[SUTAgentExecutor] Processing message %s for task %s (context: %s)', user_message.message_id, diff --git a/tests/server/routes/test_jsonrpc_routes.py b/tests/server/routes/test_jsonrpc_routes.py index a9e166f69..e8a280949 100644 --- a/tests/server/routes/test_jsonrpc_routes.py +++ b/tests/server/routes/test_jsonrpc_routes.py @@ -26,12 +26,18 @@ def test_routes_creation(agent_card, mock_handler): ) assert isinstance(routes, list) - assert len(routes) == 1 + # Both the exact path and the trailing-slash variant are registered so + # that clients sending either form (httpx normalizes empty paths to a + # trailing slash) reach the endpoint. + assert len(routes) == 2 from starlette.routing import Route - assert isinstance(routes[0], Route) - assert routes[0].methods == {'POST'} + for route in routes: + assert isinstance(route, Route) + assert route.methods == {'POST'} + + assert {route.path for route in routes} == {'/a2a/jsonrpc', '/a2a/jsonrpc/'} def test_jsonrpc_custom_url(agent_card, mock_handler): From f49d8e42495bc130e0950b44477aaf4d4132aeca Mon Sep 17 00:00:00 2001 From: kuangmi-bit Date: Thu, 6 Aug 2026 20:07:46 +0800 Subject: [PATCH 2/2] fix(tck): use 1.0 protocol binding names and bare gRPC target in SUT card Two more 1.0 compatibility fixes surfaced by running the REST and gRPC rows of the TCK: - protocolBinding 'REST' -> 'HTTP+JSON': the 1.0 TCK's protocol binding map only recognizes JSONRPC / GRPC / HTTP+JSON. The old name made the whole REST transport untestable ("No usable transports after filtering"). - gRPC interface url 'http://localhost:50051' -> 'localhost:50051': the gRPC client treats the url as a channel target; the http:// prefix fails DNS resolution in grpcio. Verified against a2a-tck 1.0.0.alpha2 (must level): - jsonrpc: 6 failed / 67 passed - http_json (REST): 5 failed / 61 passed - grpc: 7 failed / 48 passed Remaining failures are SUT feature gaps (artifact-carrying responses, MessageResponse variants) plus two status/error-code mappings. --- tck/sut_agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 176c8cfc1..206774c64 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -165,11 +165,11 @@ def serve(task_store: TaskStore) -> None: ), AgentInterface( url=f'http://localhost:{http_port}{REST_URL}', - protocol_binding='REST', + protocol_binding='HTTP+JSON', protocol_version='1.0.0', ), AgentInterface( - url=f'http://localhost:{grpc_port}', + url=f'localhost:{grpc_port}', protocol_binding='GRPC', protocol_version='1.0.0', ),