What happened?
Section 3.6.2 of the specification is a MUST on every request:
Agents MUST process requests using the semantics of the requested A2A-Version (matching Major.Minor). If the version is not supported by the interface, agents MUST return a VersionNotSupportedError.
Agents MUST interpret empty value as 0.3 version.
The SDK calls A2AVersionValidator.validateProtocolVersion at exactly two call sites in each transport, both of them message sends:
| Transport |
Validated |
Not validated |
transport/jsonrpc JSONRPCHandler |
onMessageSend, onMessageSendStream |
onGetTask, onListTasks, onCancelTask, onSubscribeToTask, and the four push notification config methods |
transport/rest RestHandler |
sendMessage, sendStreamingMessage |
same |
transport/grpc GrpcHandler |
sendMessage, sendStreamingMessage |
same |
So a client sending an A2A-Version the interface does not support is correctly refused when it sends a message, and served normally when it calls GetTask, ListTasks, CancelTask, SubscribeToTask or any of the push notification config operations. Whether the version is enforced depends on which operation is invoked rather than on the interface, which is the thing 3.6.2 is written to prevent: a client can conclude from a successful GetTask that its version is accepted, then have the next SendMessage refused.
The good news is that all three bindings behave identically here, so this is one behaviour to change rather than a per binding divergence.
Suggested fix
Move the check to a place every operation passes through, for instance the request handler decorator chain that already wraps RequestHandler, so it cannot be forgotten when an operation is added. A test per binding that calls GetTask with an unsupported A2A-Version and expects VersionNotSupportedError would pin it.
Happy to send a PR if you agree with the direction.
Reproducer
Added to JSONRPCHandlerTest next to the existing version tests, using the same card and the same incompatible context as testVersionNotSupportedErrorOnMessageSend, so the only difference is the operation invoked:
@Test
public void testVersionNotSupportedErrorOnGetTask() throws Exception {
AgentCard agentCard = cardWithProtocolVersion10();
JSONRPCHandler handler = new JSONRPCHandler(agentCard, requestHandler, internalExecutor);
taskStore.save(MINIMAL_TASK, false);
ServerCallContext contextWithVersion = new ServerCallContext(
UnauthenticatedUser.INSTANCE,
Map.of("foo", "bar"),
new HashSet<>(),
"2.0"); // Incompatible version
GetTaskRequest request = new GetTaskRequest("1", new TaskQueryParams(MINIMAL_TASK.id()));
GetTaskResponse response = handler.onGetTask(request, contextWithVersion);
assertInstanceOf(VersionNotSupportedError.class, response.getError());
assertNull(response.getResult());
}
Run together with the existing message-send test, the existing one passes and this one fails. I am happy to open a PR with the test alone if that is a useful place to start.
One thing I noticed and did not chase
I first wrote the same test for onCancelTask. It reproduces the same gap, but the forked JVM never exits afterwards: the cancel path taps a queue and leaves a consumer thread alive. The pre-existing testOnCancelTaskSuccess completes in 0.25s, so something about a freshly constructed handler is involved. I left that test out rather than ship something that hangs the build, but it may be worth a look on your side.
Relevant log output
$ mvn -pl transport/jsonrpc test \
-Dtest='JSONRPCHandlerTest#testVersionNotSupportedErrorOnGetTask+testVersionNotSupportedErrorOnMessageSend'
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.259 s
<<< FAILURE! -- in JSONRPCHandlerTest
[ERROR] JSONRPCHandlerTest.testVersionNotSupportedErrorOnGetTask -- Time elapsed: 0.218 s
<<< FAILURE!
org.opentest4j.AssertionFailedError: Unexpected null value,
expected: <org.a2aproject.sdk.spec.VersionNotSupportedError> but was: <null>
# the pre-existing testVersionNotSupportedErrorOnMessageSend passes in the same run
Call sites, for reference:
$ git grep -n "validateProtocolVersion" -- '*/src/main/java/*'
server-common/.../version/A2AVersionValidator.java:33: public static void validateProtocolVersion(
transport/grpc/.../handler/GrpcHandler.java:202: A2AVersionValidator.validateProtocolVersion(
transport/grpc/.../handler/GrpcHandler.java:398: A2AVersionValidator.validateProtocolVersion(
transport/jsonrpc/.../handler/JSONRPCHandler.java:233: A2AVersionValidator.validateProtocolVersion(
transport/jsonrpc/.../handler/JSONRPCHandler.java:290: A2AVersionValidator.validateProtocolVersion(
transport/rest/.../handler/RestHandler.java:230: A2AVersionValidator.validateProtocolVersion(
transport/rest/.../handler/RestHandler.java:298: A2AVersionValidator.validateProtocolVersion(
Environment
- a2a-java
main at 4f718e12 (1.2.1.Final-SNAPSHOT), behaviour identical in 1.2.0.Final
Code of Conduct
What happened?
Section 3.6.2 of the specification is a
MUSTon every request:The SDK calls
A2AVersionValidator.validateProtocolVersionat exactly two call sites in each transport, both of them message sends:transport/jsonrpcJSONRPCHandleronMessageSend,onMessageSendStreamonGetTask,onListTasks,onCancelTask,onSubscribeToTask, and the four push notification config methodstransport/restRestHandlersendMessage,sendStreamingMessagetransport/grpcGrpcHandlersendMessage,sendStreamingMessageSo a client sending an
A2A-Versionthe interface does not support is correctly refused when it sends a message, and served normally when it callsGetTask,ListTasks,CancelTask,SubscribeToTaskor any of the push notification config operations. Whether the version is enforced depends on which operation is invoked rather than on the interface, which is the thing 3.6.2 is written to prevent: a client can conclude from a successfulGetTaskthat its version is accepted, then have the nextSendMessagerefused.The good news is that all three bindings behave identically here, so this is one behaviour to change rather than a per binding divergence.
Suggested fix
Move the check to a place every operation passes through, for instance the request handler decorator chain that already wraps
RequestHandler, so it cannot be forgotten when an operation is added. A test per binding that callsGetTaskwith an unsupportedA2A-Versionand expectsVersionNotSupportedErrorwould pin it.Happy to send a PR if you agree with the direction.
Reproducer
Added to
JSONRPCHandlerTestnext to the existing version tests, using the same card and the same incompatible context astestVersionNotSupportedErrorOnMessageSend, so the only difference is the operation invoked:Run together with the existing message-send test, the existing one passes and this one fails. I am happy to open a PR with the test alone if that is a useful place to start.
One thing I noticed and did not chase
I first wrote the same test for
onCancelTask. It reproduces the same gap, but the forked JVM never exits afterwards: the cancel path taps a queue and leaves a consumer thread alive. The pre-existingtestOnCancelTaskSuccesscompletes in 0.25s, so something about a freshly constructed handler is involved. I left that test out rather than ship something that hangs the build, but it may be worth a look on your side.Relevant log output
Call sites, for reference:
Environment
mainat4f718e12(1.2.1.Final-SNAPSHOT), behaviour identical in1.2.0.FinalCode of Conduct