Skip to content

feat: Add 0.3 protocol version compatibility layer#805

Open
kabir wants to merge 13 commits intoa2aproject:mainfrom
kabir:add-0.3-compat-reference
Open

feat: Add 0.3 protocol version compatibility layer#805
kabir wants to merge 13 commits intoa2aproject:mainfrom
kabir:add-0.3-compat-reference

Conversation

@kabir
Copy link
Copy Markdown
Collaborator

@kabir kabir commented Apr 21, 2026

This passes the 0.3 TCK, and all unit tests.
At the moment 0.3 and 1.0 are NOT co-hosted, you need to select one

@ehsavoie
Copy link
Copy Markdown
Collaborator

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive backward compatibility layer for A2A Protocol v0.3, enabling interoperability between v1.0 SDK components and v0.3 agents. The implementation includes dedicated spec types, a conversion layer using MapStruct, and transport handlers for JSON-RPC, gRPC, and REST. Key feedback includes the need to handle authentication errors in the JDK HTTP client's GET method, fixing misleading error messages in asynchronous requests, and enhancing the SSE parser's robustness. Furthermore, the REST transport requires consistent stream completion signaling, and lazy initialization of the agent card must be made thread-safe to prevent race conditions.

Comment on lines +215 to +221
.build();
HttpResponse<String> response =
httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));
return new JdkHttpResponse(response);
}

@Override
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The get() method does not check for HTTP_UNAUTHORIZED (401) or HTTP_FORBIDDEN (403) status codes, unlike the post() method (lines 270-274). This inconsistency means authentication errors during GET requests (like fetching the agent card) might not be reported with the standard A2A error messages, violating the A2A protocol specification for error code mappings.

        @Override
        public A2AHttpResponse_v0_3 get() throws IOException, InterruptedException {
            HttpRequest request = createRequestBuilder(false)
                    .build();
            HttpResponse<String> response =
                    httpClient.send(request, BodyHandlers.ofString(StandardCharsets.UTF_8));

            if (response.statusCode() == HTTP_UNAUTHORIZED) {
                throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED);
            } else if (response.statusCode() == HTTP_FORBIDDEN) {
                throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED);
            }

            return new JdkHttpResponse(response);
        }
References
  1. Adhere to the A2A protocol specification for error code mappings, even if other mappings seem more internally consistent.

response.statusCode() != HTTP_FORBIDDEN) {
subscriber.onError(new IOException("Request failed with status " + response.statusCode() + ":" + response.body()));
}
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In asyncRequest, the bodyHandler is defined as BodyHandler<Void>, which means response.body() will always return null. Including it in the error message is misleading and provides no useful information.

                            subscriber.onError(new IOException("Request failed with status " + response.statusCode()));

Comment on lines +113 to +118
if (item != null && item.startsWith("data:")) {
item = item.substring(5).trim();
if (!item.isEmpty()) {
messageConsumer.accept(item);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The SSE line parser is overly simplistic. It assumes every line starts with data: and ignores all other SSE fields (like event:, id:, or comments). More importantly, it doesn't handle multi-line data correctly, as it processes each line independently. While this might work for current A2A implementations, it's fragile and doesn't adhere to the SSE specification.

Comment on lines +117 to +119
() -> {
// We don't need to do anything special on completion
}));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The completeRunnable passed to postAsyncSSE is empty, and RestSSEEventListener_v0_3 lacks an onComplete method. This means the client will never receive a completion signal for the stream, which is inconsistent with the JSONRPCTransport_v0_3 implementation and may cause issues for users who need to know when a stream has finished.

Comment on lines +339 to +343
resolver = new A2ACardResolver_v0_3(httpClient, agentUrl, null, getHttpHeaders(context));
agentCard = resolver.getAgentCard();
needsExtendedCard = agentCard.supportsAuthenticatedExtendedCard();
}
if (!needsExtendedCard) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getAgentCard method lazily initializes the agentCard field but is not thread-safe. Concurrent calls to this method could result in multiple redundant network requests and potential race conditions when setting the agentCard and needsExtendedCard fields.

Comment on lines +325 to +329
resolver = new A2ACardResolver_v0_3(httpClient, agentUrl, null, getHttpHeaders(context));
agentCard = resolver.getAgentCard();
needsExtendedCard = agentCard.supportsAuthenticatedExtendedCard();
}
if (!needsExtendedCard) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getAgentCard method lazily initializes the agentCard field but is not thread-safe. Concurrent calls to this method could result in multiple redundant network requests and potential race conditions when setting the agentCard and needsExtendedCard fields.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive backward compatibility layer for A2A protocol version 0.3, enabling interoperability between v1.0 SDK components and v0.3 agents. The changes include a dedicated module structure for spec types, a server-side conversion layer using MapStruct, and transport-specific client and server implementations. Feedback highlights several critical issues: a missing checked exception declaration in the REST transport, incorrect resource name patterns and stubbed methods in the gRPC transport, and inconsistent field initialization. Additionally, a potential null pointer risk was identified in the SSE event listener's error handling logic.

return payloadAndHeaders;
}

private String sendPostRequest(String url, PayloadAndHeaders_v0_3 payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException_v0_3 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The sendPostRequest method throws A2AClientException_v0_3 (via RestErrorMapper_v0_3.mapRestError), but this exception is not declared in the throws clause. Assuming A2AClientException_v0_3 is a checked exception (as suggested by its declaration in interface methods), this will cause a compilation error.

Suggested change
private String sendPostRequest(String url, PayloadAndHeaders_v0_3 payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException_v0_3 {
private String sendPostRequest(String url, PayloadAndHeaders_v0_3 payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException_v0_3, A2AClientException_v0_3 {
References
  1. Application-level errors should extend Exception or RuntimeException, not java.lang.Error.

Comment on lines +212 to +214
.setParent("tasks/" + request.id())
.build();
PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(ListTaskPushNotificationConfigRequest_v0_3.METHOD,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The parent field for ListTaskPushNotificationConfigRequest is incorrectly set. It should be the name of the task resource (tasks/{taskId}), but it is currently being set to a path that includes the collection suffix and a leading slash. This will likely cause the gRPC call to fail on the server side.

        ListTaskPushNotificationConfigRequest grpcRequest = ListTaskPushNotificationConfigRequest.newBuilder()
                .setParent("tasks/" + request.id())
                .build();

Comment on lines +71 to +77
public RestTransport_v0_3(@Nullable A2AHttpClient_v0_3 httpClient, AgentCard_v0_3 agentCard,
String agentUrl, @Nullable List<ClientCallInterceptor_v0_3> interceptors) {
this.httpClient = httpClient == null ? new JdkA2AHttpClient_v0_3() : httpClient;
this.agentCard = agentCard;
this.agentUrl = agentUrl.endsWith("/") ? agentUrl.substring(0, agentUrl.length() - 1) : agentUrl;
this.interceptors = interceptors;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The needsExtendedCard field is not initialized in the constructor based on the provided agentCard. This differs from the implementation in JSONRPCTransport_v0_3 and will result in getAgentCard never attempting to fetch the authenticated extended card if the transport was initialized with an existing card.

Suggested change
public RestTransport_v0_3(@Nullable A2AHttpClient_v0_3 httpClient, AgentCard_v0_3 agentCard,
String agentUrl, @Nullable List<ClientCallInterceptor_v0_3> interceptors) {
this.httpClient = httpClient == null ? new JdkA2AHttpClient_v0_3() : httpClient;
this.agentCard = agentCard;
this.agentUrl = agentUrl.endsWith("/") ? agentUrl.substring(0, agentUrl.length() - 1) : agentUrl;
this.interceptors = interceptors;
}
public RestTransport_v0_3(@Nullable A2AHttpClient_v0_3 httpClient, AgentCard_v0_3 agentCard,
String agentUrl, @Nullable List<ClientCallInterceptor_v0_3> interceptors) {
this.httpClient = httpClient == null ? new JdkA2AHttpClient_v0_3() : httpClient;
this.agentCard = agentCard;
this.agentUrl = agentUrl.endsWith("/") ? agentUrl.substring(0, agentUrl.length() - 1) : agentUrl;
this.interceptors = interceptors;
this.needsExtendedCard = agentCard == null || agentCard.supportsAuthenticatedExtendedCard();
}

Comment on lines +269 to +272
public AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 {
// TODO: Determine how to handle retrieving the authenticated extended agent card
return agentCard;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getAgentCard method is currently a stub that just returns the cached agentCard. Unlike the JSON-RPC and REST transports, it does not attempt to fetch the card from the server or handle the authenticated extended card logic. It should be implemented using the gRPC GetAgentCard method to ensure consistency across transports.

Comment on lines +364 to +375
name.append("tasks/");
name.append(taskId);
if (pushNotificationConfigId != null) {
name.append("/pushNotificationConfigs/");
name.append(pushNotificationConfigId);
}
//name.append("/pushNotificationConfigs/");
// Use taskId as default config ID if none provided
//name.append(pushNotificationConfigId != null ? pushNotificationConfigId : taskId);
return name.toString();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getTaskPushNotificationConfigName method returns an incorrect resource name when pushNotificationConfigId is null. According to the A2A v0.3 specification, the resource name for a task's push notification configuration should follow the pattern tasks/{task_id}/pushNotificationConfigs/{config_id}. If no specific config ID is provided, it should typically default to the task ID. Currently, it returns tasks/{taskId}, which the server will fail to recognize as a configuration resource.

    private String getTaskPushNotificationConfigName(String taskId, @Nullable String pushNotificationConfigId) {
        StringBuilder name = new StringBuilder();
        name.append("tasks/");
        name.append(taskId);
        name.append("/pushNotificationConfigs/");
        name.append(pushNotificationConfigId != null ? pushNotificationConfigId : taskId);
        return name.toString();
    }

// Signal normal stream completion (null error means successful completion)
log.fine("SSEEventListener.onComplete() called - signaling successful stream completion");
if (errorHandler != null) {
log.fine("Calling errorHandler.accept(null) to signal successful completion");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Passing null to the errorHandler to signal completion is risky. If the consumer implementation of errorHandler (which is a Consumer<Throwable>) does not explicitly check for null, it will throw a NullPointerException. It is better to use a separate completion callback or ensure the error handler handles nulls.

@kabir kabir force-pushed the add-0.3-compat-reference branch from 1f52f3c to 8be6e40 Compare April 29, 2026 13:48
@@ -224,7 +224,7 @@ public RestHandler(AgentCard agentCard, AgentCardCacheMetadata cacheMetadata,
* @see RequestHandler#onMessageSend(org.a2aproject.sdk.spec.MessageSendParams, ServerCallContext)
*/
public HTTPRestResponse sendMessage(ServerCallContext context, String tenant, String body) {

log.warning("DEBUG: Received request body: " + body);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cruft

* The v1.0 {@link RequestHandler} to which all converted requests are delegated.
*/
@Inject
public RequestHandler v10Handler;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be private

@kabir kabir force-pushed the add-0.3-compat-reference branch from 8be6e40 to 245fb86 Compare April 30, 2026 08:16
kabir and others added 13 commits May 1, 2026 17:32
This passes the 0.3 TCK, and all unit tests.
At the moment 0.3 and 1.0 are NOT co-hosted, you need to select one
…s to Vert.x Web Router

Replace Quarkus Reactive Routes (@route annotations) with Vert.x Web Router
(@observes Router) in the compat-0.3 JSONRPC reference module, matching the
pattern established in the main reference modules (commit 8be6e40).

Key changes:
- Replace quarkus-reactive-routes dependency with a2a-java-sdk-reference-common
  in both JSONRPC and REST POMs (REST POM change is prep for next commit)
- Use VertxSecurityHelper for authentication instead of @authenticated annotation
- Fix SSE race condition: subscribe synchronously instead of wrapping in
  executor.execute(), preventing 100-600ms delays that caused event loss
- Replace MultiSseSupport inner class with improved version: close handler for
  client disconnect detection, setWriteQueueMaxSize(1) for anti-buffering,
  Cache-Control/X-Accel-Buffering headers
- Use per-route BodyHandler instead of global to avoid ordering issues with
  test routes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…o Vert.x Web Router

Replace @route annotations with @observes @priority(10) Router pattern,
add VertxSecurityHelper for manual authentication, fix SSE race condition
with synchronous subscription, and add per-route BodyHandler.

Move compat-0.3 reference modules from SDK BOM to Reference BOM since
they depend on reference-common (VertxSecurityHelper), and update both
BOM verifiers accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add HTTP Basic auth support to AuthInterceptor_v0_3 (matching v1.0)
- Add @authenticated annotations to compat-0.3 JSONRPC and REST route
  handler methods for security enforcement
- Update AgentCardProducer_v0_3 to conditionally include security
  schemes when test.agent.security.enabled=true
- Create AbstractA2AServerWithAuthTest_v0_3 base class in
  server-conversion with v0.3 client types
- Add JSONRPC auth test (QuarkusA2AJSONRPC_v0_3_WithAuthTest) with
  AuthTestProfile, TestIdentityProvider, and security dependencies
- Add security config to JSONRPC application.properties

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add authentication test infrastructure for the v0.3 REST reference
module, mirroring the v1.0 auth test structure. Fix missing 401/403
error handling in v0.3 JDK HTTP client's get() and delete() methods
that prevented proper authentication failure reporting.

- Add AuthTestProfile_v0_3 with embedded user store and HTTP Basic auth
- Add TestIdentityProvider_v0_3 for auto-auth in regular tests
- Add QuarkusA2ARest_v0_3_WithAuthTest extending AbstractA2AServerWithAuthTest_v0_3
- Add quarkus-security, quarkus-elytron-security-properties-file,
  quarkus-test-security test dependencies
- Add test security configuration to application.properties
- Fix JdkA2AHttpClient_v0_3 get()/delete() to throw IOException on
  401/403 responses, matching post() and v1.0 behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add authentication test infrastructure for the v0.3 gRPC reference
module, mirroring the v1.0 gRPC auth test structure.

- Add TestAuthorizationController_v0_3 to disable @authenticated
  enforcement for regular tests via AuthorizationController
- Add AuthTestProfile_v0_3 to enable real auth for auth-specific tests
- Add QuarkusA2AGrpc_v0_3_WithAuthTest extending
  AbstractA2AServerWithAuthTest_v0_3
- Add quarkus-security, quarkus-elytron-security-properties-file,
  quarkus-test-security test dependencies
- Fix GrpcHandler_v0_3 to catch SecurityException and map to
  UNAUTHENTICATED/PERMISSION_DENIED gRPC status codes instead of
  falling through to INTERNAL error handling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…d handling

- Switch JSONRPCErrorTypeAdapter to TypeAdapterFactory for correct subclass resolution
- Migrate Convert_v0_3_To10RequestHandler to constructor injection
- Extract writeJsonRpcId() utility to deduplicate id serialization logic
- Always write JSON-RPC id field (null when unknown) per spec
- Fix spurious leading slash in listTaskPushNotificationConfigurations URL
- Add AuthenticatedExtendedCardNotConfiguredError to error code mapping
- Remove debug log statement from RestHandler

Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
…ultiplexing

Add VersionRouter in reference-common that resolves A2A protocol version
from the A2A-Version header or query param. Per spec Section 3.6.2,
missing/empty version defaults to 0.3 for backward compatibility.

Add PublicAgentCard.Literal for programmatic CDI lookup. Modify both
v0.3 A2AServerRoutes_v0_3 (JSON-RPC and REST) to skip registering
/.well-known/agent-card.json when a non-@DefaultBean v1.0 AgentCard
producer is present, so v1.0 takes precedence in dual-version mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create reference/multiversion-jsonrpc with MultiVersionJSONRPCRoutes and
reference/multiversion-rest with MultiVersionRestRoutes. These are
reusable production modules that provide version-dispatching routes for
dual v1.0/v0.3 deployments.

JSON-RPC uses Vert.x route order(-1) to intercept POST / before the
standalone v1.0 and v0.3 handlers, dispatching based on A2A-Version
header. REST intercepts /v1/... paths that overlap between v0.3 literal
routes and v1.0 regex routes, with path param bridging between naming
conventions. Both include proper VersionNotSupportedError handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pport

Add tests/reference/jsonrpc module that verifies v1.0 and v0.3 JSON-RPC
clients both work when both protocol versions are provisioned on the same
server. Version routing uses A2A-Version header per spec Section 3.6.1.

Also adds A2A-Version: 1.0 header to all v1.0 client transports (JSON-RPC,
REST, gRPC) per spec requirement that clients MUST send this header. This
is needed for multiversion routing and is a spec-compliance fix.

Other changes:
- Add META-INF/beans.xml to multiversion modules for CDI discovery
- Make setStreamingMultiSseSupportSubscribedRunnable public in both
  v1.0 and v0.3 route classes for cross-bean access

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add test modules at tests/reference/rest/ and tests/reference/grpc/ that
verify both v1.0 and v0.3 protocol clients work simultaneously against a
single server instance.

Each module includes v1.0 tests (AbstractA2AServerTest), v0.3 tests
(AbstractA2AServerServerTest_v0_3), and authentication test variants for
both versions.

The REST streaming callback method setStreamingMultiSseSupportSubscribedRunnable
is made public in both A2AServerRoutes and A2AServerRoutes_v0_3 so the
multiversion test infrastructure can register callbacks from a different
package.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The spec requires that agents interpret empty/missing A2A-Version
as 0.3 for backward compatibility. Previously defaulted to the
current protocol version (1.0). All transport handler tests updated
to explicitly pass version "1.0" in their test contexts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kabir kabir force-pushed the add-0.3-compat-reference branch from 245fb86 to f205fc7 Compare May 1, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants