Skip to content

feat: Add 0.3 compatibility layer#797

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

feat: Add 0.3 compatibility layer#797
kabir wants to merge 56 commits intoa2aproject:mainfrom
kabir:add-0.3-compat-reference

Conversation

@kabir
Copy link
Copy Markdown
Collaborator

@kabir kabir commented Apr 16, 2026

This passes the reference/* tests

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 v0.3 compatibility layer for the A2A SDK, including new client transport implementations (gRPC, JSON-RPC, REST) and a server-side conversion layer to map v0.3 requests to v1.0. The review identified several issues: the SSE implementation in JdkA2AHttpClient is fragile regarding message boundaries, the A2A.getAgentCard method inefficiently creates new HTTP clients, the GrpcTransport.close() method fails to shut down the channel, the gRPC getAgentCard implementation lacks support for extended cards, and the JSONRPCTransport has a potential race condition in its SSE event listener setup. Additionally, an improvement was suggested to include response bodies in HTTP error messages.

Comment on lines +111 to +123
public void onNext(String item) {
// SSE messages sometimes start with "data:". Strip that off
if (item != null && item.startsWith("data:")) {
item = item.substring(5).trim();
if (!item.isEmpty()) {
messageConsumer.accept(item);
}
}
if (subscription != null) {
subscription.request(1);
}
}

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 SSE implementation in onNext is fragile. It assumes that each line starting with data: is a complete and independent message. However, the SSE specification allows a single message to span multiple data: lines, and messages are only considered complete when an empty line (end of event) is encountered. Additionally, this implementation ignores other SSE fields like event:, which might be important for certain transports.

Consider accumulating data: lines in a buffer and only triggering the messageConsumer when an empty line (end of event) is encountered.

* @throws A2AClientJSONError If the response body cannot be decoded as JSON or validated against the AgentCard schema
*/
public static AgentCard getAgentCard(String agentUrl) throws A2AClientError, A2AClientJSONError {
return getAgentCard(new JdkA2AHttpClient(), agentUrl);
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

Creating a new JdkA2AHttpClient instance for every call to getAgentCard is inefficient. HttpClient (which is wrapped by JdkA2AHttpClient) is designed to be long-lived and reused across multiple requests to benefit from connection pooling and shared resources.

Consider using a shared static instance of JdkA2AHttpClient or allowing one to be passed in.


@Override
public void close() {
}
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 close() method is currently a no-op. If the gRPC channel was created specifically for this transport (e.g., via the factory in GrpcTransportProvider), it should be shut down here to avoid resource leaks.

Check if the channel is an instance of ManagedChannel and call shutdown() if it is.

    @Override
    public void close() {
        if (asyncStub.getChannel() instanceof io.grpc.ManagedChannel managedChannel) {
            managedChannel.shutdown();
        }
    }

Comment on lines +264 to +267
public AgentCard getAgentCard(@Nullable ClientCallContext context) throws A2AClientException {
// 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 implementation for gRPC currently ignores the ClientCallContext and returns the cached agentCard. This prevents the client from retrieving an 'authenticated extended card' even if the server supports it and the context contains the necessary credentials.

Unlike the JSON-RPC and REST transports, this implementation lacks the logic to fetch the extended card when requested.

Comment on lines +132 to +138
AtomicReference<CompletableFuture<Void>> ref = new AtomicReference<>();
SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);

try {
A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders);
ref.set(builder.postAsyncSSE(
msg -> sseEventListener.onMessage(msg, ref.get()),
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

There is a potential race condition here. postAsyncSSE might trigger the messageConsumer (and thus sseEventListener.onMessage) before the ref.set() call completes. If this happens, ref.get() inside the lambda will return null, leading to a NullPointerException in SSEEventListener when it tries to cancel the future.

Consider ensuring the future is available to the listener before any data can be processed.

A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders);
A2AHttpResponse response = builder.post();
if (!response.success()) {
throw new IOException("Request failed " + response.status());
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

When an HTTP request fails with a non-success status code, the error message only includes the status code. It would be much more helpful for debugging if the response body (which often contains error details in JSON format) was included in the exception message.

Suggested change
throw new IOException("Request failed " + response.status());
throw new IOException("Request failed " + response.status() + ": " + response.body());

kabir and others added 29 commits April 16, 2026 11:22
Comment out usage of DefaultRequestHandler until we have the payload conversion.
Disable tests since the code is not runnable yet
…handlers

Add fully functional Quarkus reference implementations and transport handlers
for JSON-RPC and gRPC protocols with v0.3 compatibility layer.

Architecture:
- Quarkus Reference Layer (reference/*/quarkus/) routes to transport handlers
- Transport Handlers (transport/jsonrpc, grpc) contain stubbed translation layer
- Translation from v0.3 types → current SDK types will happen in handlers
- Reuses main codebase classes (ServerCallContext, TransportMetadata, etc.)

Changes:
- Add A2ACompat03Headers class with X-A2A-Extensions header constant
- Implement JSONRPCHandler with all methods stubbed for translation layer
- Implement GrpcHandler with all gRPC service methods stubbed
- Add compat-0.3 spec dependencies to transport and reference modules
- Update BOM verifiers to exclude compat-0.3 from extras/reference BOMs
- Fix TCK package names (io.a2a.tck.server → org.a2aproject.sdk.compat03.tck.server)
- Add all 16 compat-0.3 modules to SDK BOM

Modules now building successfully:
- compat-0.3/spec, spec-grpc
- compat-0.3/client/* (base + 4 transports)
- compat-0.3/transport/* (jsonrpc, grpc, rest - with translation stubs)
- compat-0.3/reference/* (common, jsonrpc, grpc, rest - fully functional)
- All BOMs (sdk, extras, reference) pass verification

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
kabir and others added 27 commits April 16, 2026 11:22
Add test-scope dependencies for:
- a2a-java-sdk-compat-0.3-client (client base)
- a2a-java-sdk-compat-0.3-client-transport-jsonrpc (client transport)

These dependencies enable shared test infrastructure for
AbstractCompat03ServerTest and AgentExecutorProducer that will be
used to verify the conversion layer works correctly across transports.
…ersion

Convert the 0.3.x AgentExecutorProducer to v1.0 while preserving all test scenarios:
- Update method signatures: EventQueue -> AgentEmitter, JSONRPCError -> A2AError
- Replace TaskUpdater calls with direct AgentEmitter API calls
- Convert error handling: eventQueue.enqueueEvent(error) -> agentEmitter.fail(error)
- Convert message handling: eventQueue.enqueueEvent(message) -> agentEmitter.sendMessage(message)
- Update record accessor: getId() -> id()
- Update TextPart constructor: new TextPart(text, null) -> new TextPart(text)
- Preserve all test scenario logic (multi-event-test, task-not-supported-123, etc.)

This shared test infrastructure can now be used by all three transports (JSONRPC, REST, gRPC).
Change line 45 to throw UnsupportedOperationError instead of using
agentEmitter.fail() to match the spec requirements and maintain
consistency with the cancel() method's error handling pattern.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Port all ~37 test methods from v0.3 AbstractA2AServerTest to v1.0
compatibility layer. Tests use v0.3 client types with v1.0 server types,
converting at boundaries using mappers.

Key features:
- Created V10GsonObjectMapper for REST-Assured v1.0 JSON serialization
- All test utilities convert v0.3 ↔ v1.0 at HTTP boundaries
- saveTaskInTaskStore/getTaskFromTaskStore use TaskMapper
- enqueueEventOnServer uses event-specific mappers
- savePushNotificationConfigInStore uses v0.3 JSON directly
- All 37 test methods ported including complex scenarios:
  * testResubscribeExistingTaskSuccess
  * testMainQueueReferenceCountingWithMultipleConsumers
  * testNonBlockingWithMultipleMessages
  * testMainQueueStaysOpenForNonFinalTasks
  * testMainQueueClosesForFinalizedTasks
  * Push notification CRUD operations
  * JSONRPC-specific error handling tests

Type conversion pattern:
- Client uses v0.3 types (org.a2aproject.sdk.compat03.spec.*)
- Server utilities use v1.0 types (org.a2aproject.sdk.spec.*)
- Conversion via A03Mappers (TaskMapper, MessageMapper, etc.)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…port

Add REST-assured and Jakarta WS-RS dependencies to server-conversion module
for test utilities. Fix V10GsonObjectMapper import to use correct package
for JsonProcessingException (jsonrpc.common.json not json).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…ionConfigInStore

The method was incorrectly using v0.3 JsonUtil to serialize a v0.3
PushNotificationConfig and sending it to the v1.0 server. This fix wraps
the config in a TaskPushNotificationConfig, converts to v1.0 using the
mapper, and serializes with v1.0 JsonUtil.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Use fully qualified v1.0 JsonUtil (org.a2aproject.sdk.jsonrpc.common.json.JsonUtil)
instead of unqualified JsonUtil which resolves to v0.3 import. This matches
the pattern used in other test utility methods.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…module

Enables reusing shared test infrastructure (AbstractCompat03ServerTest and
AgentExecutorProducer) in QuarkusA2AJSONRPCTest and other reference server
tests. Adds test-jar dependency to compat-0.3 parent POM dependencyManagement
and reference/jsonrpc module dependencies.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Port JSONRPC-specific test routes from 0.3.x that expose TestUtilsBean endpoints via Quarkus REST.
The ported class uses v1.0 types (Task, TaskStatusUpdateEvent, TaskPushNotificationConfig) and
v1.0 JsonUtil for serialization while maintaining the same REST endpoint patterns.

Key changes:
- Package: io.a2a.server.apps.quarkus → org.a2aproject.sdk.compat03.server.apps.quarkus
- Import v1.0 spec types: org.a2aproject.sdk.spec.*
- Import v1.0 JsonUtil: org.a2aproject.sdk.jsonrpc.common.json.JsonUtil
- Use v1.0 TestUtilsBean from tests/server-common
- PushNotificationConfig → TaskPushNotificationConfig
- Inject A2AServerRoutes for streaming subscription tracking

Endpoints exposed (10 total):
- POST /test/task - save task
- GET /test/task/:taskId - get task
- DELETE /test/task/:taskId - delete task
- POST /test/queue/ensure/:taskId - ensure queue exists
- POST /test/queue/enqueueTaskStatusUpdateEvent/:taskId - enqueue status event
- POST /test/queue/enqueueTaskArtifactUpdateEvent/:taskId - enqueue artifact event
- GET /test/streamingSubscribedCount - get subscription count
- GET /test/queue/childCount/:taskId - get child queue count
- DELETE /test/task/:taskId/config/:configId - delete push notification config
- POST /test/task/:taskId - save push notification config

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implement QuarkusA2AJSONRPCTest to extend AbstractCompat03ServerTest with all
inherited test methods. Remove @disabled annotation to enable test execution.
Add required client test dependencies to pom.xml for ClientBuilder and JSONRPC
transport classes.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add META-INF/beans.xml files to enable CDI bean discovery in:
- compat-0.3/transport/jsonrpc (for JSONRPCHandler)
- compat-0.3/server-conversion (for Convert03To10RequestHandler)
- compat-0.3/reference/jsonrpc (for reference server beans)

Add test configuration:
- TestBeanProducers: Provides default AgentCard bean for testing
- application.properties: Configures Quarkus to index server-conversion
  dependency for CDI bean discovery

This fixes unsatisfied dependency errors during test startup. The server
now starts successfully and tests can run.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…tests

Fixed test server startup failures by addressing CDI bean discovery issues:

1. TestBeanProducers.createTestAgentCard(): Added required fields (url,
   description, defaultInputModes, defaultOutputModes, skills) that were
   missing from the v0.3 AgentCard builder

2. application.properties: Added classifier=tests to quarkus.index-dependency
   configuration to properly index the server-conversion test-jar containing
   AgentExecutorProducer

3. application.properties: Excluded v1.0 test producers
   (AgentExecutorProducer, AgentCardProducer) that conflict with v0.3
   compatibility layer implementations

Test results improved from 27 failures + 8 errors to 13 failures + 6 errors.
Remaining issues are push notification tests (capabilities disabled) and
JSON error serialization (Gson reflection).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changed pushNotifications from false to true in TestBeanProducers.
This fixes 9 push notification test failures.

Test results improved from 13 failures + 6 errors to 3 failures + 6 errors.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added surefire argLine to open java.base/java.lang module to allow Gson
to access Throwable fields when deserializing error responses.

This fixes 6 test errors related to JSON error serialization:
- testInvalidJSONRPCRequestInvalidId
- testInvalidJSONRPCRequestMissingJsonrpc
- testInvalidJSONRPCRequestMissingMethod
- testInvalidJSONRPCRequestNonExistentMethod
- testInvalidParamsJSONRPCRequest
- testMalformedJSONRPCRequest

Test results improved from 3 failures + 6 errors to 3 failures + 0 errors.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
v1.0 introduced a feature where resubscribing to a task sends the current
task state as the first event (a TaskEvent). This is valid behavior that
v0.3 clients should accept.

Updated test consumers to accept TaskEvent as the first event only:
- testResubscribeExistingTaskSuccess
- testNonBlockingWithMultipleMessages
- testMainQueueReferenceCountingWithMultipleConsumers

Added validation using AtomicBoolean.compareAndSet() to ensure TaskEvent
only appears as the very first event, marking it as unexpected if it
appears later in the stream.

Added TODO comment to check if this initial snapshot is part of the v0.3
spec or a v1.0 enhancement.

Test results: All 37 tests now pass (was 3 failures + 0 errors).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Port v0.3 REST reference server integration tests to verify
v0.3 client → v0.3 transport → v1.0 backend conversion layer.

Changes:
- Add server-conversion test-jar and client dependencies to pom.xml
- Add Gson reflection JVM args (--add-opens java.lang)
- Create TestBeanProducers with v0.3 AgentCard (streaming + push notifications)
- Create application.properties with Quarkus CDI indexing and port 8081
- Implement QuarkusA2ARestTest extending AbstractCompat03ServerTest
- Port A2ATestRoutes to expose v1.0 TestUtilsBean via REST endpoints
- Add beans.xml to REST transport and reference modules for CDI discovery

Results: 37/37 tests run (1 failure, 9 skipped - same pattern as JSONRPC)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…tificationConfiguration

When getTaskPushNotificationConfiguration is called without a pushNotificationConfigId
(i.e., GetTaskPushNotificationConfigParams constructed with only taskId), the REST
client transport was constructing a URL with literal "null" in the path:
/v1/tasks/{taskId}/pushNotificationConfigs/null

This caused the test testGetPushNotificationSuccess to fail with "Task not found" error.

The fix uses the taskId as the default configId when pushNotificationConfigId is null,
following the convention established in the v0.3 spec where the taskId serves as the
default/primary push notification config ID.

This aligns with the server-side behavior and existing test patterns which use
taskId as configId when no specific configId is provided.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Port the testMethodNotFound test from v0.3.x QuarkusA2ARestTest. This test
verifies that REST endpoints properly reject unsupported HTTP methods with
405 (Method Not Allowed) status code.

The test sends PUT and DELETE requests to /v1/message:send (which only
accepts POST) and asserts that both return 405 status.

This brings the REST reference server test coverage to parity with the
v0.3.x implementation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Complete the v0.3 to v1.0 compatibility layer reference server test suite by porting the gRPC tests, following the same pattern as JSONRPC and REST.

Created test infrastructure:
- QuarkusA2AGrpcTest: Main test class with gRPC transport configuration
- A2ATestResource: JAX-RS endpoints exposing TestUtilsBean for test utilities
- TestBeanProducers: CDI producers for v0.3 AgentCard
- application.properties: Test configuration with gRPC settings and CDI indexing
- META-INF/beans.xml: CDI beans configuration
- META-INF/services: TransportMetadata service registration

Key implementation details:
- Uses JAX-RS for test utilities (like v0.3.x) instead of Reactive Routes
- Added quarkus.index-dependency.transport-grpc to enable gRPC service discovery
- Excluded v1.0 spec-grpc to prevent conflicts with v0.3 spec-grpc
- Added streaming subscription hook to GrpcHandler for test synchronization
- Configured maven-surefire-plugin with JVM args for Gson reflection

Test results: 37 tests passing, 0 failures, 9 skipped

All three transports now complete:
- JSONRPC: 50 tests passing
- REST: 38 tests passing
- gRPC: 37 tests passing

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Remove empty placeholder test files that were awaiting server-common port.
These unit tests verify routing infrastructure (METHOD_NAME_KEY propagation)
which is already covered by the integration test suite (AbstractCompat03ServerTest).

The integration tests provide better end-to-end coverage:
- JSONRPC: 50 tests passing
- REST: 38 tests passing
- gRPC: 37 tests passing (never had routing unit tests)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace 4 TODO comments questioning whether TaskEvent as first event is
valid in v0.3 with spec-referencing comments confirming this behavior.

The v0.3 specification at section 7.2.1 (SendStreamingMessageResponse)
confirms that sending the current task snapshot as the first event on
resubscribe is the intended behavior.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…test-jar

Phase 2 of v0.3 compatibility layer cleanup - eliminate dependency on v1.0 test infrastructure.

Created unified test infrastructure in compat-0.3/server-conversion test-jar:
- AgentCardProducer: Produces v0.3 AgentCard with transport-specific configuration
- TestUtilsBean: Server-side test utilities (duplicated from v1.0 to avoid dependency)
- TestHttpClient: Test HTTP client implementation (duplicated from v1.0)
- beans.xml: CDI configuration for test bean discovery

Removed v1.0 test infrastructure dependency:
- Deleted a2a-java-sdk-tests-server-common dependencies from reference module POMs
- Removed duplicate TestBeanProducers from jsonrpc/rest/grpc reference modules
- Updated application.properties to use v0.3 compat test infrastructure

Transport-specific configuration:
- Created compat-0.3-requesthandler-test.properties for each transport
- JSONRPC: preferred-transport=jsonrpc
- REST: preferred-transport=rest
- gRPC: preferred-transport=grpc

Test results: 112 tests passing (37 JSONRPC + 37 gRPC + 38 REST, 18 skipped)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Use registerTypeAdapter for Throwable instead of registerTypeHierarchyAdapter
to prevent adapter from matching JSONRPCError subclasses. This allows proper
adapter selection without requiring --add-opens JVM flag.

Changes:
- JsonUtil: Use registerTypeAdapter(Throwable.class) for exact type matching
- Remove --add-opens java.base/java.lang=ALL-UNNAMED from all reference POMs
- Add V03GsonObjectMapper for deserializing v0.3 JSONRPC error responses
- Add givenV03() helper in AbstractCompat03ServerTest for error tests

All 112 reference tests pass without JVM args.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@kabir kabir force-pushed the add-0.3-compat-reference branch from b96b370 to 96797e1 Compare April 16, 2026 10:26
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.

1 participant