From d79ee319dc0b55fa6b9fa72d9d577664d9485411 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Mon, 2 Nov 2026 21:44:27 +0000 Subject: [PATCH 1/3] ci: enforce Spotless formatting checks Add a dedicated JDK 25 workflow and contributor commands. Exclude build outputs, generated gRPC sources, and released documentation snapshots. Disable type shortening that drops nullability annotations and Markdown reflow that corrupts lists. This fixes #139 --- .github/workflows/formatting.yml | 25 +++++++++++++++++++++++++ CONTRIBUTING.md | 18 ++++++++++++++++++ pom.xml | 21 +++++++++++---------- 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/formatting.yml diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml new file mode 100644 index 000000000..ea3187137 --- /dev/null +++ b/.github/workflows/formatting.yml @@ -0,0 +1,25 @@ +name: Check Formatting + +on: + push: + pull_request: + workflow_dispatch: + +concurrency: + group: '${{ github.workflow }} @ ${{ github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + formatting: + name: Check formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - name: Set up JDK 25 + uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6 + with: + java-version: '25' + distribution: 'temurin' + cache: maven + - name: Check formatting with Spotless + run: mvn -B -N -Dskip.spotless=false spotless:check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b92171d8..71e2159e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -71,6 +71,24 @@ To set up your development environment you need to: mvn clean install -DskipTests=true ``` +## Code Formatting + +CI checks formatting with Spotless using JDK 25. Before submitting a PR, apply the configured formatting from the +repository root: + +```bash +mvn -N -Dskip.spotless=false spotless:apply +``` + +To check formatting without changing files: + +```bash +mvn -N -Dskip.spotless=false spotless:check +``` + +Run these commands with JDK 25. The root configuration covers sources and POMs throughout the repository; `-N` avoids +repeating the same work in each module. Released documentation snapshots are excluded. + ## Contributing Guidelines When submitting a PR, please keep the following guidelines in mind: diff --git a/pom.xml b/pom.xml index 53af4bcee..67d0e80d8 100644 --- a/pom.xml +++ b/pom.xml @@ -636,6 +636,12 @@ + + **/target/** + + **/src/main/java/org/a2aproject/sdk/grpc/*.java + **/src/main/java/org/a2aproject/sdk/compat03/grpc/*.java + **/src/main/java/**/*.java **/src/test/java/**/*.java @@ -647,7 +653,6 @@ false - @@ -656,6 +661,9 @@ + + **/target/** + pom.xml **/pom.xml @@ -678,7 +686,7 @@ - docs/**/*.md + docs/content/dev/**/*.md CODE_OF_CONDUCT.md CONTRIBUTING.md CONTRIBUTING_INTEGRATIONS.md @@ -686,14 +694,7 @@ RELEASE.md SECURITY.md - - COMMONMARK - ALL,TOC - YamlFrontMatter,Emoji - - 120 - - + From cd4b53476f68c28b0323c1e4149384a3582e7232 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Mon, 2 Nov 2026 21:44:29 +0000 Subject: [PATCH 2/3] style: apply Spotless formatting Apply the configured Java and POM formatting throughout the repository. This fixes #139 --- boms/extras/pom.xml | 4 +- boms/extras/src/it/extras-usage-test/pom.xml | 6 +- .../bom/test/extras/ExtrasBomVerifier.java | 42 +- boms/reference/pom.xml | 6 +- .../src/it/reference-usage-test/pom.xml | 6 +- .../test/reference/ReferenceBomVerifier.java | 40 +- boms/sdk/pom.xml | 10 +- boms/sdk/src/it/sdk-usage-test/pom.xml | 6 +- .../sdk/bom/test/sdk/SdkBomVerifier.java | 38 +- boms/test-utils/pom.xml | 6 +- .../sdk/bom/test/DynamicBomVerifier.java | 139 +- .../a2aproject/sdk/bom/test/package-info.java | 1 - client/base/pom.xml | 8 +- .../src/main/java/org/a2aproject/sdk/A2A.java | 72 +- .../a2aproject/sdk/client/AbstractClient.java | 112 +- .../org/a2aproject/sdk/client/Client.java | 166 +- .../a2aproject/sdk/client/ClientBuilder.java | 85 +- .../a2aproject/sdk/client/ClientEvent.java | 3 +- .../sdk/client/ClientTaskManager.java | 37 +- .../a2aproject/sdk/client/MessageEvent.java | 37 +- .../org/a2aproject/sdk/client/TaskEvent.java | 2 +- .../sdk/client/TaskUpdateEvent.java | 9 +- .../sdk/client/config/ClientConfig.java | 9 +- .../sdk/client/config/package-info.java | 1 - .../a2aproject/sdk/client/package-info.java | 1 - .../test/java/org/a2aproject/sdk/A2ATest.java | 32 +- .../AuthenticationAuthorizationTest.java | 324 +- .../sdk/client/ClientBuilderTest.java | 195 +- .../sdk/client/ClientTaskManagerTest.java | 674 ++-- client/transport/grpc/pom.xml | 6 +- .../transport/grpc/EventStreamObserver.java | 15 +- .../transport/grpc/GrpcErrorMapper.java | 150 +- .../client/transport/grpc/GrpcTransport.java | 324 +- .../transport/grpc/GrpcTransportConfig.java | 5 +- .../grpc/GrpcTransportConfigBuilder.java | 8 +- .../transport/grpc/GrpcTransportProvider.java | 17 +- .../client/transport/grpc/package-info.java | 2 +- .../grpc/EventStreamObserverTest.java | 4 +- .../transport/grpc/GrpcErrorMapperTest.java | 93 +- .../transport/grpc/GrpcTransportTest.java | 114 +- client/transport/jsonrpc/pom.xml | 8 +- .../transport/jsonrpc/JSONRPCTransport.java | 347 +- .../JSONRPCTransportConfigBuilder.java | 12 +- .../jsonrpc/JSONRPCTransportProvider.java | 24 +- .../transport/jsonrpc/package-info.java | 1 - .../jsonrpc/sse/SSEEventListener.java | 12 +- .../transport/jsonrpc/sse/package-info.java | 1 - .../JSONRPCTransportStreamingTest.java | 120 +- .../jsonrpc/JSONRPCTransportTest.java | 648 ++-- .../transport/jsonrpc/JsonMessages.java | 387 +- .../jsonrpc/JsonStreamingMessages.java | 56 +- .../jsonrpc/sse/SSEEventListenerTest.java | 217 +- client/transport/rest/pom.xml | 8 +- .../transport/rest/RestErrorMapper.java | 218 +- .../client/transport/rest/RestTransport.java | 365 +- .../rest/RestTransportConfigBuilder.java | 11 +- .../transport/rest/RestTransportProvider.java | 25 +- .../client/transport/rest/package-info.java | 1 - .../transport/rest/sse/SSEEventListener.java | 37 +- .../transport/rest/sse/package-info.java | 1 - .../transport/rest/JsonRestMessages.java | 107 +- .../transport/rest/RestErrorMapperTest.java | 63 +- .../transport/rest/RestTransportTest.java | 709 ++-- .../rest/sse/SSEEventListenerTest.java | 174 +- client/transport/spi/pom.xml | 6 +- .../client/transport/spi/ClientTransport.java | 48 +- .../transport/spi/ClientTransportConfig.java | 9 +- .../spi/ClientTransportConfigBuilder.java | 4 +- .../spi/ClientTransportProvider.java | 7 +- .../interceptors/ClientCallInterceptor.java | 9 +- .../spi/interceptors/PayloadAndHeaders.java | 3 +- .../interceptors/auth/AuthInterceptor.java | 40 +- .../interceptors/auth/CredentialService.java | 3 +- .../InMemoryContextCredentialService.java | 11 +- .../spi/interceptors/auth/package-info.java | 1 - .../spi/interceptors/package-info.java | 1 - .../client/transport/spi/package-info.java | 1 - .../spi/sse/AbstractSSEEventListener.java | 9 +- .../transport/spi/sse/package-info.java | 1 - .../auth/AuthInterceptorTest.java | 427 +-- .../spi/sse/SSEEventListenerTest.java | 52 +- common/pom.xml | 8 +- .../sdk/common/A2AErrorMessages.java | 10 +- .../org/a2aproject/sdk/common/A2AHeaders.java | 4 +- .../a2aproject/sdk/common/package-info.java | 1 - .../java/org/a2aproject/sdk/util/Assert.java | 9 +- .../a2aproject/sdk/util/HtmlEscapeUtils.java | 12 +- .../a2aproject/sdk/util/HttpHeaderUtils.java | 11 +- .../java/org/a2aproject/sdk/util/NotNull.java | 11 +- .../org/a2aproject/sdk/util/package-info.java | 1 - .../sdk/util/HtmlEscapeUtilsTest.java | 13 +- compat-0.3/client/base/pom.xml | 8 +- .../org/a2aproject/sdk/compat03/A2A_v0_3.java | 64 +- .../compat03/client/AbstractClient_v0_3.java | 89 +- .../compat03/client/ClientBuilder_v0_3.java | 81 +- .../sdk/compat03/client/ClientEvent_v0_3.java | 4 +- .../client/ClientTaskManager_v0_3.java | 37 +- .../sdk/compat03/client/Client_v0_3.java | 173 +- .../compat03/client/MessageEvent_v0_3.java | 2 - .../compat03/client/TaskUpdateEvent_v0_3.java | 1 - .../client/config/ClientConfig_v0_3.java | 13 +- .../compat03/client/config/package-info.java | 1 - .../sdk/compat03/client/package-info.java | 1 - .../sdk/compat03/A2A_v0_3_Test.java | 47 +- ...AuthenticationAuthorization_v0_3_Test.java | 363 +- .../client/ClientBuilder_v0_3_Test.java | 147 +- .../client/ClientTaskManager_v0_3_Test.java | 55 +- compat-0.3/client/transport/grpc/pom.xml | 8 +- .../grpc/EventStreamObserver_v0_3.java | 20 +- .../transport/grpc/GrpcErrorMapper_v0_3.java | 80 +- .../grpc/GrpcTransportConfigBuilder_v0_3.java | 15 +- .../grpc/GrpcTransportConfig_v0_3.java | 7 +- .../grpc/GrpcTransportProvider_v0_3.java | 12 +- .../transport/grpc/GrpcTransport_v0_3.java | 317 +- .../client/transport/grpc/package-info.java | 2 +- .../grpc/EventStreamObserver_v0_3_Test.java | 7 +- .../grpc/GrpcErrorMapper_v0_3_Test.java | 120 +- compat-0.3/client/transport/jsonrpc/pom.xml | 8 +- .../JSONRPCTransportConfigBuilder_v0_3.java | 4 +- .../JSONRPCTransportProvider_v0_3.java | 16 +- .../jsonrpc/JSONRPCTransport_v0_3.java | 445 ++- .../jsonrpc/sse/SSEEventListener_v0_3.java | 27 +- .../JSONRPCTransportStreaming_v0_3_Test.java | 125 +- .../jsonrpc/JSONRPCTransport_v0_3_Test.java | 588 +-- .../transport/jsonrpc/JsonMessages_v0_3.java | 127 +- .../jsonrpc/JsonStreamingMessages_v0_3.java | 100 +- .../sse/SSEEventListener_v0_3_Test.java | 221 +- compat-0.3/client/transport/rest/pom.xml | 8 +- .../transport/rest/RestErrorMapper_v0_3.java | 60 +- .../rest/RestTransportConfigBuilder_v0_3.java | 4 +- .../rest/RestTransportProvider_v0_3.java | 15 +- .../transport/rest/RestTransport_v0_3.java | 357 +- .../client/transport/rest/package-info.java | 1 - .../rest/sse/RestSSEEventListener_v0_3.java | 35 +- .../transport/rest/sse/package-info.java | 1 - .../transport/rest/JsonRestMessages_v0_3.java | 160 +- .../rest/RestTransport_v0_3_Test.java | 376 +- compat-0.3/client/transport/spi/pom.xml | 6 +- .../client/http/A2ACardResolver_v0_3.java | 35 +- .../ClientTransportConfigBuilder_v0_3.java | 6 +- .../spi/ClientTransportConfig_v0_3.java | 5 +- .../spi/ClientTransportProvider_v0_3.java | 8 +- .../transport/spi/ClientTransport_v0_3.java | 46 +- .../ClientCallInterceptor_v0_3.java | 9 +- .../interceptors/PayloadAndHeaders_v0_3.java | 2 +- .../auth/AuthInterceptor_v0_3.java | 43 +- .../auth/CredentialService_v0_3.java | 3 +- ...InMemoryContextCredentialService_v0_3.java | 11 +- .../spi/interceptors/auth/package-info.java | 1 - .../spi/interceptors/package-info.java | 1 - .../client/transport/spi/package-info.java | 1 - .../http/A2ACardResolver_v0_3_Test.java | 29 +- .../client/http/JsonMessages_v0_3.java | 26 +- .../auth/AuthInterceptor_v0_3_Test.java | 400 +-- compat-0.3/pom.xml | 72 +- compat-0.3/reference/grpc/pom.xml | 8 +- .../A2AExtensionsInterceptor_v0_3.java | 25 +- .../BlockingOffloadInterceptor_v0_3.java | 43 +- .../QuarkusCallContextFactory_v0_3.java | 25 +- .../grpc/quarkus/QuarkusGrpcHandler_v0_3.java | 22 +- .../QuarkusGrpcTransportMetadata_v0_3.java | 2 +- .../grpc/quarkus/A2ATestResource_v0_3.java | 33 +- .../quarkus/QuarkusA2AGrpc_v0_3_Test.java | 25 +- .../QuarkusA2AGrpc_v0_3_WithAuthTest.java | 59 +- ...2AGrpc_v0_3_WithTaskAuthorizationTest.java | 62 +- .../TestAuthorizationController_v0_3.java | 4 +- compat-0.3/reference/jsonrpc/pom.xml | 6 +- .../apps/quarkus/A2AServerRoutes_v0_3.java | 138 +- .../apps/quarkus/CallContextFactory_v0_3.java | 2 +- .../QuarkusJSONRPCTransportMetadata_v0_3.java | 2 +- .../quarkus/A2AServerRoutes_v0_3_Test.java | 221 +- .../apps/quarkus/A2ATestRoutes_v0_3.java | 200 +- .../QuarkusA2AJSONRPC_v0_3_AndroidTest.java | 7 +- .../QuarkusA2AJSONRPC_v0_3_JdkTest.java | 7 +- .../QuarkusA2AJSONRPC_v0_3_VertxTest.java | 14 +- ...usA2AJSONRPC_v0_3_WithAuthAndroidTest.java | 23 +- .../QuarkusA2AJSONRPC_v0_3_WithAuthTest.java | 23 +- ...rkusA2AJSONRPC_v0_3_WithAuthVertxTest.java | 30 +- ...C_v0_3_WithTaskAuthorizationVertxTest.java | 23 +- compat-0.3/reference/rest/pom.xml | 6 +- .../rest/quarkus/A2AServerRoutes_v0_3.java | 182 +- .../rest/quarkus/CallContextFactory_v0_3.java | 2 +- .../QuarkusRestTransportMetadata_v0_3.java | 6 +- .../quarkus/A2AServerRoutes_v0_3_Test.java | 102 +- .../rest/quarkus/A2ATestRoutes_v0_3.java | 200 +- .../QuarkusA2ARest_v0_3_AndroidTest.java | 7 +- .../quarkus/QuarkusA2ARest_v0_3_JdkTest.java | 7 +- .../quarkus/QuarkusA2ARest_v0_3_Test.java | 103 +- .../QuarkusA2ARest_v0_3_VertxTest.java | 14 +- ...arkusA2ARest_v0_3_WithAuthAndroidTest.java | 26 +- .../QuarkusA2ARest_v0_3_WithAuthTest.java | 26 +- ...QuarkusA2ARest_v0_3_WithAuthVertxTest.java | 33 +- ...t_v0_3_WithTaskAuthorizationVertxTest.java | 23 +- compat-0.3/server-conversion/pom.xml | 6 +- .../compat03/conversion/A2AProtocol_v0_3.java | 3 +- .../Convert_v0_3_To10RequestHandler.java | 150 +- .../conversion/ErrorConverter_v0_3.java | 33 +- ...PushNotificationPayloadFormatter_v0_3.java | 4 +- .../mappers/config/A03ToV10MapperConfig.java | 7 +- .../mappers/config/A2AMappers_v0_3.java | 24 +- .../mappers/domain/ArtifactMapper_v0_3.java | 47 +- .../domain/AuthenticationInfoMapper_v0_3.java | 25 +- .../mappers/domain/EventKindMapper_v0_3.java | 22 +- .../domain/FileContentMapper_v0_3.java | 20 +- .../mappers/domain/MessageMapper_v0_3.java | 55 +- .../mappers/domain/PartMapper_v0_3.java | 19 +- .../mappers/domain/RoleMapper_v0_3.java | 2 +- .../domain/StreamingEventKindMapper_v0_3.java | 25 +- .../TaskArtifactUpdateEventMapper_v0_3.java | 35 +- .../mappers/domain/TaskMapper_v0_3.java | 77 +- ...TaskPushNotificationConfigMapper_v0_3.java | 47 +- .../mappers/domain/TaskStateMapper_v0_3.java | 2 +- .../mappers/domain/TaskStatusMapper_v0_3.java | 20 +- .../TaskStatusUpdateEventMapper_v0_3.java | 29 +- .../params/CancelTaskParamsMapper_v0_3.java | 13 +- .../MessageSendConfigurationMapper_v0_3.java | 37 +- .../params/MessageSendParamsMapper_v0_3.java | 26 +- .../params/TaskIdParamsMapper_v0_3.java | 2 +- .../params/TaskQueryParamsMapper_v0_3.java | 5 +- ...hNotificationConfigsResultMapper_v0_3.java | 21 +- .../AbstractA2ARequestHandlerTest_v0_3.java | 110 +- .../AbstractA2AServerServerTest_v0_3.java | 1832 ++++++---- .../AbstractA2AServerWithAuthTest_v0_3.java | 141 +- ...AServerWithTaskAuthorizationTest_v0_3.java | 121 +- .../conversion/AgentCardProducer_v0_3.java | 58 +- .../conversion/AuthTestProfile_v0_3.java | 6 +- .../conversion/GsonObjectMapper_v0_3.java | 6 +- ...otificationPayloadFormatter_v0_3_Test.java | 65 +- .../TaskAuthorizationTestProfile_v0_3.java | 2 - .../conversion/TestHttpClient_v0_3.java | 16 +- .../conversion/TestIdentityProvider_v0_3.java | 18 +- .../conversion/TestUtilsBean_v0_3.java | 27 +- .../conversion/V10GsonObjectMapper_v0_3.java | 3 +- .../domain/FileContentMapper_v0_3_Test.java | 20 +- .../mappers/domain/TaskMapper_v0_3_Test.java | 136 +- ...TaskStatusUpdateEventMapper_v0_3_Test.java | 30 +- compat-0.3/spec-grpc/pom.xml | 124 +- .../grpc/utils/ProtoJsonUtils_v0_3.java | 6 +- .../compat03/grpc/utils/ProtoUtils_v0_3.java | 642 ++-- .../sdk/compat03/grpc/utils/package-info.java | 1 - .../grpc/utils/ProtoJsonUtils_v0_3_Test.java | 101 +- .../grpc/utils/ToProto_v0_3_Test.java | 349 +- compat-0.3/spec/pom.xml | 8 +- .../json/JsonMappingException_v0_3.java | 3 +- .../sdk/compat03/json/JsonUtil_v0_3.java | 269 +- .../compat03/spec/A2AClientError_v0_3.java | 3 +- .../spec/A2AClientInvalidArgsError_v0_3.java | 3 +- .../spec/A2AClientInvalidStateError_v0_3.java | 3 +- .../spec/A2AClientJSONError_v0_3.java | 3 +- .../sdk/compat03/spec/A2AError_v0_3.java | 3 +- .../sdk/compat03/spec/A2AException_v0_3.java | 3 +- .../spec/APIKeySecurityScheme_v0_3.java | 10 +- .../compat03/spec/AgentCapabilities_v0_3.java | 10 +- .../spec/AgentCardSignature_v0_3.java | 7 +- .../sdk/compat03/spec/AgentCard_v0_3.java | 72 +- .../compat03/spec/AgentExtension_v0_3.java | 5 +- .../compat03/spec/AgentInterface_v0_3.java | 2 - .../sdk/compat03/spec/AgentSkill_v0_3.java | 16 +- .../sdk/compat03/spec/Artifact_v0_3.java | 14 +- ...edExtendedCardNotConfiguredError_v0_3.java | 6 +- .../spec/AuthenticationInfo_v0_3.java | 1 - .../spec/AuthorizationCodeOAuthFlow_v0_3.java | 6 +- .../compat03/spec/CancelTaskRequest_v0_3.java | 16 +- .../spec/CancelTaskResponse_v0_3.java | 8 +- .../spec/ClientCredentialsOAuthFlow_v0_3.java | 7 +- .../ContentTypeNotSupportedError_v0_3.java | 6 +- .../sdk/compat03/spec/DataPart_v0_3.java | 8 +- ...TaskPushNotificationConfigParams_v0_3.java | 8 +- ...askPushNotificationConfigRequest_v0_3.java | 26 +- ...skPushNotificationConfigResponse_v0_3.java | 10 +- .../sdk/compat03/spec/Event_v0_3.java | 3 +- .../sdk/compat03/spec/FilePart_v0_3.java | 6 +- .../sdk/compat03/spec/FileWithBytes_v0_3.java | 4 +- .../sdk/compat03/spec/FileWithUri_v0_3.java | 5 +- ...AuthenticatedExtendedCardRequest_v0_3.java | 21 +- ...uthenticatedExtendedCardResponse_v0_3.java | 10 +- ...TaskPushNotificationConfigParams_v0_3.java | 10 +- ...askPushNotificationConfigRequest_v0_3.java | 28 +- ...skPushNotificationConfigResponse_v0_3.java | 16 +- .../compat03/spec/GetTaskRequest_v0_3.java | 17 +- .../compat03/spec/GetTaskResponse_v0_3.java | 6 +- .../spec/HTTPAuthSecurityScheme_v0_3.java | 16 +- .../compat03/spec/ImplicitOAuthFlow_v0_3.java | 5 +- .../sdk/compat03/spec/InternalError_v0_3.java | 3 +- .../spec/InvalidAgentResponseError_v0_3.java | 5 +- .../spec/InvalidParamsError_v0_3.java | 2 +- .../spec/InvalidRequestError_v0_3.java | 3 +- .../compat03/spec/JSONErrorResponse_v0_3.java | 3 +- .../compat03/spec/JSONParseError_v0_3.java | 8 +- .../spec/JSONRPCErrorResponse_v0_3.java | 6 +- .../sdk/compat03/spec/JSONRPCError_v0_3.java | 1 - .../compat03/spec/JSONRPCMessage_v0_3.java | 2 +- .../compat03/spec/JSONRPCRequest_v0_3.java | 6 +- .../compat03/spec/JSONRPCResponse_v0_3.java | 25 +- .../sdk/compat03/spec/JsonrpcId_v0_3.java | 3 +- ...TaskPushNotificationConfigParams_v0_3.java | 1 - ...askPushNotificationConfigRequest_v0_3.java | 26 +- ...skPushNotificationConfigResponse_v0_3.java | 25 +- .../spec/MessageSendConfiguration_v0_3.java | 14 +- .../compat03/spec/MessageSendParams_v0_3.java | 7 +- .../sdk/compat03/spec/Message_v0_3.java | 55 +- .../spec/MethodNotFoundError_v0_3.java | 7 +- .../spec/MutualTLSSecurityScheme_v0_3.java | 3 +- .../spec/NonStreamingJSONRPCRequest_v0_3.java | 14 +- .../spec/OAuth2SecurityScheme_v0_3.java | 15 +- .../sdk/compat03/spec/OAuthFlows_v0_3.java | 7 +- .../OpenIdConnectSecurityScheme_v0_3.java | 6 +- .../compat03/spec/PasswordOAuthFlow_v0_3.java | 4 +- .../spec/PushNotificationConfig_v0_3.java | 12 +- ...ushNotificationNotSupportedError_v0_3.java | 7 +- .../compat03/spec/SecurityScheme_v0_3.java | 8 +- .../spec/SendMessageRequest_v0_3.java | 15 +- .../spec/SendMessageResponse_v0_3.java | 6 +- .../SendStreamingMessageRequest_v0_3.java | 69 +- .../SendStreamingMessageResponse_v0_3.java | 9 +- ...askPushNotificationConfigRequest_v0_3.java | 21 +- ...skPushNotificationConfigResponse_v0_3.java | 15 +- .../spec/StreamingEventKind_v0_3.java | 3 +- .../spec/StreamingJSONRPCRequest_v0_3.java | 7 +- .../spec/TaskArtifactUpdateEvent_v0_3.java | 31 +- .../sdk/compat03/spec/TaskIdParams_v0_3.java | 1 - .../spec/TaskNotCancelableError_v0_3.java | 8 +- .../compat03/spec/TaskNotFoundError_v0_3.java | 2 +- .../spec/TaskPushNotificationConfig_v0_3.java | 3 +- .../compat03/spec/TaskQueryParams_v0_3.java | 5 +- .../spec/TaskResubscriptionRequest_v0_3.java | 17 +- .../sdk/compat03/spec/TaskState_v0_3.java | 4 +- .../spec/TaskStatusUpdateEvent_v0_3.java | 25 +- .../sdk/compat03/spec/TaskStatus_v0_3.java | 5 +- .../sdk/compat03/spec/Task_v0_3.java | 27 +- .../sdk/compat03/spec/TextPart_v0_3.java | 6 +- .../spec/UnsupportedOperationError_v0_3.java | 2 +- .../sdk/compat03/spec/UpdateEvent_v0_3.java | 4 +- .../sdk/compat03/util/Utils_v0_3.java | 54 +- .../JSONRPCErrorSerialization_v0_3_Test.java | 81 +- .../spec/SubTypeSerialization_v0_3_Test.java | 108 +- .../spec/TaskSerialization_v0_3_Test.java | 643 ++-- compat-0.3/tck/pom.xml | 46 +- .../tck/server/AgentCardProducer_v0_3.java | 48 +- .../server/AgentExecutorProducer_v0_3.java | 21 +- .../sdk/compat03/tck/server/package-info.java | 7 +- compat-0.3/tests/server-common/pom.xml | 6 +- .../test/AgentExecutorProducer_v0_3.java | 39 +- .../TestTaskAuthorizationProvider_v0_3.java | 17 +- compat-0.3/transport/grpc/pom.xml | 8 +- .../grpc/context/GrpcContextKeys_v0_3.java | 12 +- .../grpc/handler/CallContextFactory_v0_3.java | 2 +- .../grpc/handler/GrpcHandler_v0_3.java | 261 +- .../grpc/handler/GrpcHandler_v0_3_Test.java | 225 +- .../GrpcTestTransportMetadata_v0_3.java | 3 +- compat-0.3/transport/jsonrpc/pom.xml | 8 +- .../jsonrpc/handler/JSONRPCHandler_v0_3.java | 227 +- .../handler/JSONRPCRequestDecoder_v0_3.java | 93 +- .../handler/JSONRPCHandler_v0_3_Test.java | 1208 ++++--- .../JSONRPCRequestDecoder_v0_3_Test.java | 215 +- .../JSONRPCTestTransportMetadata_v0_3.java | 3 +- compat-0.3/transport/rest/pom.xml | 8 +- .../rest/handler/RestHandler_v0_3.java | 356 +- .../rest/handler/RestHandler_v0_3_Test.java | 618 ++-- .../RestTestTransportMetadata_v0_3.java | 3 +- docs/pom.xml | 32 +- examples/cloud-deployment/server/pom.xml | 6 +- .../cloud/CloudAgentCardProducer.java | 54 +- .../cloud/CloudAgentExecutorProducer.java | 7 +- .../examples/cloud/A2ACloudExampleClient.java | 257 +- examples/helloworld/client/pom.xml | 13 +- .../helloworld/client/HelloWorldClient.java | 182 +- .../helloworld/client/HelloWorldRunner.java | 33 +- examples/helloworld/pom.xml | 20 +- examples/helloworld/server/pom.xml | 14 +- .../helloworld/server/AgentCardProducer.java | 40 +- .../server/AgentExecutorProducer.java | 1 - examples/stream-lifecycle/client/pom.xml | 6 +- .../client/StreamLifecycleClient.java | 199 +- examples/stream-lifecycle/pom.xml | 18 +- examples/stream-lifecycle/server/pom.xml | 12 +- .../server/AgentCardProducer.java | 39 +- .../server/AgentExecutorProducer.java | 1 - .../server/CloseStreamsHook.java | 20 +- .../server/StreamLifecycleTest.java | 13 +- extras/common/pom.xml | 4 +- .../common/events/TaskFinalizedEvent.java | 3 +- extras/http-client-android/pom.xml | 6 +- .../http/android/AndroidA2AHttpClient.java | 915 ++--- .../android/AndroidA2AHttpClientProvider.java | 18 +- .../sdk/client/http/android/package-info.java | 1 - .../AndroidA2AHttpClientFactoryTest.java | 21 +- .../AndroidA2AHttpClientProviderTest.java | 11 +- .../AndroidA2AHttpClientRedirectTest.java | 70 +- .../android/AndroidA2AHttpClientSSETest.java | 8 +- .../android/AndroidBoundedLineReaderTest.java | 13 +- .../http/android/OkHttpA2AHttpClient.java | 194 +- .../OkHttpPostRedirectHeaderLeakTest.java | 91 +- extras/http-client-cdi/pom.xml | 6 +- .../http/cdi/CdiA2AHttpClientProvider.java | 1 - .../http/cdi/CdiA2AHttpClientFactoryTest.java | 83 +- .../cdi/CdiA2AHttpClientProviderTest.java | 28 +- extras/http-client-vertx/pom.xml | 6 +- .../client/http/vertx/VertxA2AHttpClient.java | 381 +- .../vertx/VertxA2AHttpClientProvider.java | 35 +- .../sdk/client/http/vertx/package-info.java | 1 - .../vertx/VertxA2AHttpClientFactoryTest.java | 21 +- .../VertxA2AHttpClientIntegrationTest.java | 121 +- .../vertx/VertxA2AHttpClientRedirectTest.java | 204 +- .../http/vertx/VertxA2AHttpClientSSETest.java | 10 +- .../http/vertx/VertxA2AHttpClientTest.java | 17 +- extras/multitenancy/pom.xml | 4 +- .../multitenancy/CdiAgentCardRouter.java | 29 +- .../multitenancy/CdiAgentExecutorRouter.java | 12 +- .../sdk/extras/multitenancy/package-info.java | 1 - extras/multitenancy/tests/grpc/pom.xml | 4 +- .../tests/grpc/MultiTenantGrpcTest.java | 24 +- extras/multitenancy/tests/jsonrpc/pom.xml | 4 +- .../tests/jsonrpc/MultiTenantJSONRPCTest.java | 3 +- extras/multitenancy/tests/rest/pom.xml | 4 +- .../tests/rest/MultiTenantRestTest.java | 3 +- extras/multitenancy/tests/router-unit/pom.xml | 4 +- .../multitenancy/CdiAgentCardRouterTest.java | 44 +- .../CdiAgentExecutorRouterTest.java | 13 +- extras/multitenancy/tests/test-common/pom.xml | 4 +- .../tests/AbstractMultiTenantServerTest.java | 131 +- .../tests/MultiTenantAgentCardProducer.java | 35 +- .../MultiTenantAgentExecutorProducer.java | 6 +- .../extras/multitenancy/tests/Tenants.java | 3 +- .../tests/TestAuthorizationController.java | 4 +- .../opentelemetry/client-propagation/pom.xml | 4 +- ...penTelemetryClientPropagatorTransport.java | 97 +- ...metryClientPropagatorTransportWrapper.java | 5 +- .../client/propagation/package-info.java | 1 - extras/opentelemetry/client/pom.xml | 4 +- .../client/OpenTelemetryClientTransport.java | 255 +- .../OpenTelemetryClientTransportWrapper.java | 4 +- .../opentelemetry/client/package-info.java | 1 - .../OpenTelemetryClientTransportTest.java | 304 +- extras/opentelemetry/common/pom.xml | 4 +- .../opentelemetry/A2AObservabilityNames.java | 7 +- .../StreamingEventAttributes.java | 20 +- .../extras/opentelemetry/OtelSpanMocks.java | 10 +- .../StreamingEventAttributesTest.java | 16 +- .../opentelemetry/integration-tests/pom.xml | 22 +- .../opentelemetry/it/A2ATestRoutes.java | 271 +- .../it/CustomDispatchExecutorProducer.java | 21 +- .../it/CustomHttpClientProducer.java | 8 +- .../opentelemetry/it/SimpleAgentExecutor.java | 13 +- .../it/TestAgentCardProducer.java | 38 +- .../opentelemetry/it/TestUtilsBean.java | 16 +- .../opentelemetry/it/A2ATestRoutesTest.java | 29 +- .../sdk/extras/opentelemetry/it/BaseTest.java | 11 +- .../it/OpenTelemetryA2ABaseTest.java | 594 ++-- .../opentelemetry/it/OpenTelemetryA2AIT.java | 3 +- .../it/OpenTelemetryA2ATest.java | 3 +- .../opentelemetry/it/OpenTelemetryTest.java | 32 +- extras/opentelemetry/pom.xml | 22 +- extras/opentelemetry/server/pom.xml | 4 +- .../server/OpenTelemetryA2AHttpClient.java | 37 +- .../OpenTelemetryHttpClientProducer.java | 39 +- ...emetryPushNotificationSenderDecorator.java | 35 +- .../OpenTelemetryRequestHandlerDecorator.java | 308 +- .../opentelemetry/server/package-info.java | 1 - .../OpenTelemetryA2AHttpClientTest.java | 93 +- .../OpenTelemetryHttpClientProducerTest.java | 21 +- ...ryPushNotificationSenderDecoratorTest.java | 117 +- ...nTelemetryRequestHandlerDecoratorTest.java | 644 ++-- .../pom.xml | 6 +- ...paDatabasePushNotificationConfigStore.java | 241 +- .../jpa/JpaPushNotificationConfig.java | 36 +- .../database/jpa/TaskConfigId.java | 11 +- ...otificationConfigStoreIntegrationTest.java | 510 +-- ...ationConfigStoreTestAgentCardProducer.java | 22 +- ...ificationConfigStoreTestAgentExecutor.java | 12 +- .../JpaPushNotificationConfigStoreTest.java | 236 +- .../jpa/MockPushNotificationSender.java | 12 +- extras/queue-manager-replicated/core/pom.xml | 6 +- .../core/ReplicatedEventQueueItem.java | 29 +- .../core/ReplicatedQueueManager.java | 93 +- .../replicated/core/ReplicationStrategy.java | 2 +- .../core/EventSerializationTest.java | 216 +- .../core/ReplicatedQueueManagerTest.java | 397 ++- extras/queue-manager-replicated/pom.xml | 6 +- .../replication-mp-reactive/pom.xml | 6 +- .../ReactiveMessagingReplicationStrategy.java | 21 +- ...ctiveMessagingReplicationStrategyTest.java | 42 +- .../tests-multi-instance/pom.xml | 12 +- .../quarkus-app-1/pom.xml | 4 +- .../app1/MIRApp1AgentCardProducer.java | 1 - .../app1/MIRApp1AgentExecutorProducer.java | 1 - .../quarkus-app-2/pom.xml | 4 +- .../app2/MIRApp2AgentCardProducer.java | 1 - .../app2/MIRApp2AgentExecutorProducer.java | 1 - .../quarkus-common/pom.xml | 4 +- .../MultiInstanceReplicationAgentCards.java | 37 +- ...MultiInstanceReplicationAgentExecutor.java | 11 +- .../tests-multi-instance/tests/pom.xml | 4 +- .../MultiInstanceReplicationTest.java | 583 +-- .../tests-single-instance/pom.xml | 38 +- .../KafkaReplicationIntegrationTest.java | 398 ++- .../tests/ReplicatedQueueManagerTest.java | 10 +- .../ReplicationTestAgentCardProducer.java | 22 +- .../tests/ReplicationTestAgentExecutor.java | 8 +- .../tests/TestKafkaEventConsumer.java | 32 +- extras/task-store-database-jpa/pom.xml | 8 +- .../database/jpa/JpaDatabaseTaskStore.java | 252 +- .../taskstore/database/jpa/JpaSubjectKey.java | 12 +- .../taskstore/database/jpa/JpaTask.java | 16 +- .../jpa/JpaTaskAuthorizationProvider.java | 23 +- .../taskstore/database/jpa/JpaTaskOwner.java | 7 +- .../jpa/JpaTaskOwnershipRepository.java | 44 +- .../jpa/JpaTaskOwnershipTransactions.java | 10 +- .../database/jpa/JpaTaskReadScope.java | 6 +- ...JpaDatabaseTaskStoreAuthorizationTest.java | 178 +- .../jpa/JpaDatabaseTaskStoreConfigTest.java | 20 +- .../JpaDatabaseTaskStoreIntegrationTest.java | 102 +- .../jpa/JpaDatabaseTaskStoreTest.java | 698 ++-- ...atabaseTaskStoreTestAgentCardProducer.java | 16 +- ...JpaDatabaseTaskStoreTestAgentExecutor.java | 12 +- .../database/jpa/JpaSubjectKeyTest.java | 16 +- .../jpa/JpaTaskAuthorizationCdiTest.java | 36 +- .../JpaTaskAuthorizationDatabaseProfile.java | 19 +- .../JpaTaskAuthorizationDatabaseResource.java | 10 +- .../jpa/JpaTaskAuthorizationDatabaseTest.java | 343 +- .../jpa/JpaTaskAuthorizationMySqlTest.java | 5 +- .../jpa/JpaTaskAuthorizationPostgresTest.java | 5 +- .../jpa/JpaTaskAuthorizationQueryTest.java | 256 +- .../database/jpa/JpaTaskOwnershipTest.java | 179 +- http-client/pom.xml | 6 +- .../sdk/client/http/A2ACardResolver.java | 56 +- .../sdk/client/http/A2AHttpClient.java | 9 +- .../http/A2AHttpClientConfiguration.java | 13 +- .../sdk/client/http/A2AHttpClientFactory.java | 68 +- .../sdk/client/http/A2AHttpHeaders.java | 33 +- .../sdk/client/http/JdkA2AHttpClient.java | 424 ++- .../sdk/client/http/RedirectHeaderPolicy.java | 108 +- .../sdk/client/http/SSEParserConfig.java | 6 +- .../sdk/client/http/ServerSentEvent.java | 3 +- .../client/http/ServerSentEventParser.java | 55 +- .../sdk/client/http/package-info.java | 1 - .../sdk/client/http/A2ACardResolverTest.java | 323 +- .../client/http/A2AHttpClientFactoryTest.java | 104 +- .../A2AHttpClientFactoryUsageExample.java | 51 +- .../http/A2AHttpClientProviderTest.java | 26 +- .../AbstractA2AHttpClientIntegrationTest.java | 173 +- .../AbstractA2AHttpClientRedirectTest.java | 198 +- .../http/AbstractA2AHttpClientSSETest.java | 278 +- .../http/BoundedLineBodySubscriberTest.java | 57 +- .../http/JdkA2AHttpClientRedirectTest.java | 113 +- .../client/http/JdkA2AHttpClientSSETest.java | 5 +- .../sdk/client/http/JdkA2AHttpClientTest.java | 441 ++- .../sdk/client/http/JsonMessages.java | 8 +- .../client/http/RedirectHeaderPolicyTest.java | 292 +- .../http/ServerSentEventParserTest.java | 126 +- integrations/microprofile-config/pom.xml | 6 +- .../MicroProfileConfigProvider.java | 26 +- .../MicroProfileConfigProviderTest.java | 31 +- itk/pom.xml | 34 +- .../java/org/a2aproject/sdk/itk/ActsAuth.java | 58 +- .../sdk/itk/ActsAuthInterceptor.java | 9 +- .../org/a2aproject/sdk/itk/ActsBehaviors.java | 55 +- .../a2aproject/sdk/itk/ActsClientParse.java | 57 +- .../a2aproject/sdk/itk/AdditionalRoutes.java | 50 +- .../a2aproject/sdk/itk/AgentCardProducer.java | 51 +- .../sdk/itk/AgentExecutorProducer.java | 310 +- .../java/org/a2aproject/sdk/itk/Main.java | 5 +- jsonrpc-common/pom.xml | 6 +- .../common/json/JsonMappingException.java | 3 +- .../sdk/jsonrpc/common/json/JsonUtil.java | 328 +- .../common/wrappers/A2AErrorResponse.java | 4 +- .../jsonrpc/common/wrappers/A2AMessage.java | 1 - .../jsonrpc/common/wrappers/A2ARequest.java | 10 +- .../jsonrpc/common/wrappers/A2AResponse.java | 23 +- .../common/wrappers/CancelTaskRequest.java | 12 +- .../common/wrappers/CancelTaskResponse.java | 6 +- ...eateTaskPushNotificationConfigRequest.java | 20 +- ...ateTaskPushNotificationConfigResponse.java | 10 +- ...leteTaskPushNotificationConfigRequest.java | 20 +- ...eteTaskPushNotificationConfigResponse.java | 8 +- .../wrappers/GetExtendedAgentCardRequest.java | 16 +- .../GetExtendedAgentCardResponse.java | 8 +- .../GetTaskPushNotificationConfigRequest.java | 20 +- ...GetTaskPushNotificationConfigResponse.java | 11 +- .../common/wrappers/GetTaskRequest.java | 8 +- .../common/wrappers/GetTaskResponse.java | 4 +- ...istTaskPushNotificationConfigsRequest.java | 20 +- ...stTaskPushNotificationConfigsResponse.java | 17 +- .../common/wrappers/ListTasksRequest.java | 8 +- .../common/wrappers/ListTasksResponse.java | 4 +- .../common/wrappers/ListTasksResult.java | 17 +- .../wrappers/NonStreamingJSONRPCRequest.java | 17 +- .../common/wrappers/SendMessageRequest.java | 8 +- .../common/wrappers/SendMessageResponse.java | 4 +- .../wrappers/SendStreamingMessageRequest.java | 102 +- .../SendStreamingMessageResponse.java | 7 +- .../wrappers/StreamingJSONRPCRequest.java | 8 +- .../wrappers/SubscribeToTaskRequest.java | 14 +- .../json/A2AErrorSerializationTest.java | 51 +- .../sdk/jsonrpc/common/json/JsonUtilTest.java | 11 +- .../common/json/PartSerializationTest.java | 88 +- .../SecurityRequirementSerializationTest.java | 47 +- .../json/SecuritySchemeSerializationTest.java | 165 +- .../StreamingEventKindSerializationTest.java | 151 +- .../common/json/TaskSerializationTest.java | 635 ++-- .../GetExtendedAgentCardRequestTest.java | 7 +- pom.xml | 411 ++- reference/common/pom.xml | 8 +- .../quarkus/AsyncManagedExecutorProducer.java | 20 +- .../common/quarkus/DefaultProducers.java | 12 +- .../common/quarkus/SseResponseWriter.java | 200 +- .../server/common/quarkus/VersionRouter.java | 3 +- .../common/quarkus/VertxSecurityHelper.java | 65 +- .../AsyncManagedExecutorProducerTest.java | 59 +- .../common/quarkus/SseResponseWriterTest.java | 93 +- .../quarkus/VertxSecurityHelperTest.java | 93 +- reference/grpc/pom.xml | 8 +- .../quarkus/A2AExtensionsInterceptor.java | 45 +- .../quarkus/BlockingOffloadInterceptor.java | 43 +- .../quarkus/QuarkusCallContextFactory.java | 27 +- .../grpc/quarkus/QuarkusGrpcHandler.java | 27 +- .../server/grpc/quarkus/A2ATestResource.java | 46 +- .../grpc/quarkus/QuarkusA2AGrpcTest.java | 24 +- .../quarkus/QuarkusA2AGrpcWithAuthTest.java | 63 +- ...arkusA2AGrpcWithTaskAuthorizationTest.java | 64 +- .../QuarkusGrpcHandlerLazyResolutionTest.java | 81 +- .../quarkus/TestAuthorizationController.java | 4 +- reference/jsonrpc/pom.xml | 6 +- .../server/apps/quarkus/A2AServerRoutes.java | 258 +- .../apps/quarkus/CallContextFactory.java | 2 +- .../apps/quarkus/A2AServerRoutesTest.java | 919 +++-- .../server/apps/quarkus/A2ATestRoutes.java | 232 +- .../quarkus/QuarkusA2AJSONRPCAndroidTest.java | 6 +- .../quarkus/QuarkusA2AJSONRPCJdkTest.java | 6 +- .../quarkus/QuarkusA2AJSONRPCVertxTest.java | 13 +- .../QuarkusA2AJSONRPCWithAuthAndroidTest.java | 30 +- .../QuarkusA2AJSONRPCWithAuthJdkTest.java | 28 +- .../QuarkusA2AJSONRPCWithAuthVertxTest.java | 35 +- ...JSONRPCWithTaskAuthorizationVertxTest.java | 21 +- reference/multiversion-jsonrpc/pom.xml | 6 +- .../jsonrpc/MultiVersionJSONRPCRoutes.java | 94 +- reference/multiversion-rest/pom.xml | 6 +- .../rest/MultiVersionRestRoutes.java | 289 +- reference/rest/pom.xml | 10 +- .../server/rest/quarkus/A2AServerRoutes.java | 320 +- .../rest/quarkus/CallContextFactory.java | 2 +- .../sdk/server/rest/quarkus/package-info.java | 1 - .../rest/quarkus/A2AServerRoutesTest.java | 506 ++- .../server/rest/quarkus/A2ATestRoutes.java | 266 +- .../quarkus/QuarkusA2ARestAndroidTest.java | 6 +- .../rest/quarkus/QuarkusA2ARestJdkTest.java | 6 +- .../rest/quarkus/QuarkusA2ARestTest.java | 64 +- .../rest/quarkus/QuarkusA2ARestVertxTest.java | 13 +- .../QuarkusA2ARestWithAuthAndroidTest.java | 24 +- .../QuarkusA2ARestWithAuthJdkTest.java | 24 +- .../QuarkusA2ARestWithAuthVertxTest.java | 31 +- ...A2ARestWithTaskAuthorizationVertxTest.java | 21 +- server-common/pom.xml | 8 +- .../sdk/server/AgentCardCacheMetadata.java | 31 +- .../sdk/server/AgentCardOperation.java | 30 +- .../sdk/server/AgentCardRequestPolicy.java | 21 +- .../sdk/server/AgentCardResolver.java | 13 +- .../sdk/server/AgentCardValidator.java | 101 +- .../CachingAgentCardTransportValidator.java | 4 +- .../sdk/server/ExtendedAgentCard.java | 5 +- .../a2aproject/sdk/server/FixedInstance.java | 15 +- .../a2aproject/sdk/server/Instrumented.java | 5 +- .../sdk/server/JSONRPCException.java | 2 +- .../sdk/server/PublicAgentCard.java | 5 +- .../sdk/server/ServerCallContext.java | 43 +- .../sdk/server/TransportMetadata.java | 7 +- .../server/agentexecution/RequestContext.java | 64 +- .../SimpleRequestContextBuilder.java | 12 +- .../sdk/server/auth/AuthenticatedUser.java | 10 +- .../auth/TaskAuthorizationProvider.java | 18 +- .../sdk/server/auth/UnauthenticatedUser.java | 3 +- .../org/a2aproject/sdk/server/auth/User.java | 1 + .../config/DefaultValuesConfigProvider.java | 31 +- .../DefaultTaskStreamLifecycleHook.java | 10 +- .../sdk/server/events/EnhancedRunnable.java | 5 +- .../sdk/server/events/EventConsumer.java | 559 +-- .../sdk/server/events/EventEnqueueHook.java | 2 +- .../sdk/server/events/EventQueue.java | 261 +- .../events/EventQueueClosedException.java | 3 +- .../sdk/server/events/EventQueueFactory.java | 4 +- .../server/events/InMemoryQueueManager.java | 74 +- .../sdk/server/events/MainEventBus.java | 19 +- .../server/events/MainEventBusContext.java | 3 +- .../server/events/MainEventBusProcessor.java | 180 +- .../events/MainEventBusProcessorCallback.java | 21 +- .../MainEventBusProcessorInitializer.java | 6 +- .../server/events/NoTaskQueueException.java | 9 +- .../sdk/server/events/QueueManager.java | 8 +- .../events/TaskQueueExistsException.java | 9 +- .../sdk/server/extensions/A2AExtensions.java | 13 +- .../sdk/server/multitenancy/Tenant.java | 5 +- .../DefaultRequestHandler.java | 1168 +++--- .../requesthandlers/RequestHandler.java | 42 +- .../sdk/server/tasks/AgentEmitter.java | 101 +- .../tasks/BasePushNotificationSender.java | 175 +- .../DefaultPushNotificationUrlValidator.java | 74 +- .../InMemoryPushNotificationConfigStore.java | 109 +- .../sdk/server/tasks/InMemoryTaskStore.java | 122 +- .../tasks/PushNotificationConfigStore.java | 25 +- .../sdk/server/tasks/ResultAggregator.java | 347 +- .../sdk/server/tasks/TaskManager.java | 121 +- .../tasks/TaskPersistenceException.java | 3 +- .../tasks/TaskSerializationException.java | 3 +- .../sdk/server/tasks/TaskStore.java | 16 +- .../tasks/TaskStoreAuthorizationFallback.java | 117 +- .../sdk/server/tasks/TaskStoreException.java | 6 +- .../sdk/server/util/ArtifactUtils.java | 25 +- .../a2aproject/sdk/server/util/CdiUtils.java | 46 +- .../util/async/AsyncExecutorProducer.java | 41 +- .../sdk/server/util/async/AsyncUtils.java | 208 +- .../async/EventConsumerExecutorProducer.java | 40 +- .../sdk/server/util/async/Internal.java | 6 +- .../sdk/server/util/sse/SseFormatter.java | 18 +- .../server/version/A2AVersionValidator.java | 24 +- .../server/AgentCardRequestPolicyTest.java | 211 +- .../sdk/server/AgentCardResolverTest.java | 148 +- .../sdk/server/AgentCardValidatorTest.java | 260 +- .../sdk/server/ServerCallContextTest.java | 97 +- .../a2aproject/sdk/server/TestInstances.java | 3 +- .../agentexecution/RequestContextTest.java | 517 ++- .../SimpleRequestContextBuilderTest.java | 78 +- .../auth/SimpleTaskAuthorizationProvider.java | 4 +- .../auth/TaskAuthorizationCapabilityTest.java | 18 +- .../server/events/EnhancedRunnableTest.java | 158 +- .../sdk/server/events/EventConsumerTest.java | 419 ++- .../sdk/server/events/EventQueueTest.java | 293 +- .../events/InMemoryQueueManagerTest.java | 81 +- .../MainEventBusProcessorExceptionTest.java | 186 +- ...BusProcessorPushNotificationOrderTest.java | 242 +- .../events/TestStreamLifecycleHook.java | 1 - .../server/extensions/A2AExtensionsTest.java | 91 +- .../AbstractA2ARequestHandlerTest.java | 147 +- ...equestHandlerBuilderAuthorizationTest.java | 536 ++- ...HandlerReferenceTaskAuthorizationTest.java | 168 +- .../DefaultRequestHandlerTest.java | 2408 +++++++------ .../requesthandlers/LogCaptureAssertions.java | 16 +- .../tasks/AbstractTaskStoreExceptionTest.java | 6 +- .../tasks/AgentEmitterConcurrencyTest.java | 257 +- .../sdk/server/tasks/AgentEmitterTest.java | 168 +- ...MemoryPushNotificationConfigStoreTest.java | 477 ++- .../InMemoryTaskStoreAuthorizationTest.java | 31 +- .../server/tasks/InMemoryTaskStoreTest.java | 19 +- .../tasks/PushNotificationSenderTest.java | 381 +- .../PushNotificationUrlValidatorTest.java | 207 +- .../server/tasks/ResultAggregatorTest.java | 250 +- .../server/tasks/TaskManagerSnapshotTest.java | 41 +- .../sdk/server/tasks/TaskManagerTest.java | 862 +++-- .../tasks/TaskPersistenceExceptionTest.java | 104 +- .../tasks/TaskSerializationExceptionTest.java | 128 +- .../TaskStoreAuthorizationFallbackTest.java | 205 +- .../server/tasks/TaskStoreExceptionTest.java | 25 +- .../sdk/server/util/ArtifactUtilsTest.java | 71 +- .../sdk/server/util/async/AsyncUtilsTest.java | 859 ++--- .../version/A2AVersionValidatorTest.java | 43 +- spec-grpc/pom.xml | 9 +- .../sdk/grpc/mapper/A2ACommonFieldMapper.java | 51 +- .../sdk/grpc/mapper/A2AMappers.java | 18 +- .../sdk/grpc/mapper/A2AProtoMapperConfig.java | 78 +- .../mapper/APIKeySecuritySchemeMapper.java | 20 +- .../grpc/mapper/AgentCapabilitiesMapper.java | 9 +- .../sdk/grpc/mapper/AgentCardMapper.java | 33 +- .../grpc/mapper/AgentCardSignatureMapper.java | 15 +- .../sdk/grpc/mapper/AgentExtensionMapper.java | 9 +- .../sdk/grpc/mapper/AgentInterfaceMapper.java | 8 +- .../sdk/grpc/mapper/AgentProviderMapper.java | 3 +- .../sdk/grpc/mapper/AgentSkillMapper.java | 3 +- .../sdk/grpc/mapper/ArtifactMapper.java | 13 +- .../grpc/mapper/AuthenticationInfoMapper.java | 17 +- .../AuthorizationCodeOAuthFlowMapper.java | 12 +- .../ClientCredentialsOAuthFlowMapper.java | 12 +- ...askPushNotificationConfigParamsMapper.java | 14 +- ...askPushNotificationConfigParamsMapper.java | 14 +- .../mapper/HTTPAuthSecuritySchemeMapper.java | 25 +- ...skPushNotificationConfigsParamsMapper.java | 14 +- .../grpc/mapper/ListTasksParamsMapper.java | 65 +- .../grpc/mapper/ListTasksResultMapper.java | 13 +- .../sdk/grpc/mapper/MessageMapper.java | 28 +- .../MessageSendConfigurationMapper.java | 16 +- .../grpc/mapper/MessageSendParamsMapper.java | 18 +- .../mapper/MutualTLSSecuritySchemeMapper.java | 17 +- .../mapper/OAuth2SecuritySchemeMapper.java | 28 +- .../sdk/grpc/mapper/OAuthFlowsMapper.java | 8 +- .../OpenIdConnectSecuritySchemeMapper.java | 20 +- .../sdk/grpc/mapper/PartMapper.java | 15 +- .../sdk/grpc/mapper/ResourceNameParser.java | 18 +- .../mapper/SecurityRequirementMapper.java | 21 +- .../sdk/grpc/mapper/SecuritySchemeMapper.java | 44 +- .../sdk/grpc/mapper/StreamResponseMapper.java | 58 +- .../mapper/SubscribeToTaskRequestMapper.java | 9 +- .../mapper/TaskArtifactUpdateEventMapper.java | 17 +- .../sdk/grpc/mapper/TaskIdParamsMapper.java | 21 +- .../sdk/grpc/mapper/TaskMapper.java | 15 +- .../TaskPushNotificationConfigMapper.java | 31 +- .../grpc/mapper/TaskQueryParamsMapper.java | 5 +- .../sdk/grpc/mapper/TaskStateMapper.java | 15 +- .../sdk/grpc/mapper/TaskStatusMapper.java | 24 +- .../mapper/TaskStatusUpdateEventMapper.java | 7 +- .../sdk/grpc/utils/JSONRPCUtils.java | 363 +- .../sdk/grpc/utils/ProtoJsonUtils.java | 6 +- .../a2aproject/sdk/grpc/utils/ProtoUtils.java | 347 +- .../sdk/grpc/utils/package-info.java | 1 - .../grpc/mapper/A2ACommonFieldMapperTest.java | 135 +- .../mapper/ListTasksParamsMapperTest.java | 19 +- .../grpc/mapper/StreamResponseMapperTest.java | 228 +- .../sdk/grpc/utils/JSONRPCUtilsTest.java | 860 +++-- .../sdk/grpc/utils/PartTypeAdapterTest.java | 117 +- .../sdk/grpc/utils/ProtoJsonUtilsTest.java | 97 +- .../sdk/grpc/utils/ToProtoTest.java | 378 +- spec/pom.xml | 8 +- .../a2aproject/sdk/spec/A2AClientError.java | 3 +- .../sdk/spec/A2AClientHTTPError.java | 11 +- .../sdk/spec/A2AClientInvalidArgsError.java | 3 +- .../sdk/spec/A2AClientInvalidStateError.java | 3 +- .../sdk/spec/A2AClientJSONError.java | 3 +- .../org/a2aproject/sdk/spec/A2AError.java | 6 +- .../a2aproject/sdk/spec/A2AErrorCodes.java | 1 - .../org/a2aproject/sdk/spec/A2AException.java | 3 +- .../org/a2aproject/sdk/spec/A2AMethods.java | 13 +- .../a2aproject/sdk/spec/A2AProtocolError.java | 1 - .../sdk/spec/APIKeySecurityScheme.java | 15 +- .../sdk/spec/AgentCapabilities.java | 19 +- .../org/a2aproject/sdk/spec/AgentCard.java | 27 +- .../sdk/spec/AgentCardSignature.java | 14 +- .../a2aproject/sdk/spec/AgentExtension.java | 18 +- .../a2aproject/sdk/spec/AgentInterface.java | 5 +- .../org/a2aproject/sdk/spec/AgentSkill.java | 18 +- .../org/a2aproject/sdk/spec/Artifact.java | 23 +- .../sdk/spec/AuthenticationInfo.java | 1 - .../sdk/spec/AuthorizationCodeOAuthFlow.java | 11 +- .../a2aproject/sdk/spec/CancelTaskParams.java | 12 +- .../sdk/spec/ClientCredentialsOAuthFlow.java | 7 +- .../a2aproject/sdk/spec/Compat03Fields.java | 15 +- .../spec/ContentTypeNotSupportedError.java | 10 +- .../org/a2aproject/sdk/spec/DataPart.java | 16 +- ...eleteTaskPushNotificationConfigParams.java | 10 +- .../sdk/spec/DeviceCodeOAuthFlow.java | 9 +- .../java/org/a2aproject/sdk/spec/Event.java | 3 +- .../ExtendedAgentCardNotConfiguredError.java | 7 +- .../spec/ExtensionSupportRequiredError.java | 7 +- .../org/a2aproject/sdk/spec/FilePart.java | 13 +- .../a2aproject/sdk/spec/FileWithBytes.java | 13 +- .../org/a2aproject/sdk/spec/FileWithUri.java | 4 +- .../GetTaskPushNotificationConfigParams.java | 14 +- .../sdk/spec/HTTPAuthSecurityScheme.java | 10 +- .../a2aproject/sdk/spec/InternalError.java | 7 +- .../sdk/spec/InvalidAgentResponseError.java | 7 +- .../sdk/spec/InvalidParamsError.java | 6 +- .../sdk/spec/InvalidRequestError.java | 7 +- .../a2aproject/sdk/spec/JSONParseError.java | 6 +- ...ListTaskPushNotificationConfigsParams.java | 25 +- ...ListTaskPushNotificationConfigsResult.java | 7 +- .../a2aproject/sdk/spec/ListTasksParams.java | 35 +- .../java/org/a2aproject/sdk/spec/Message.java | 38 +- .../sdk/spec/MessageSendConfiguration.java | 14 +- .../sdk/spec/MessageSendParams.java | 23 +- .../sdk/spec/MethodNotFoundError.java | 6 +- .../sdk/spec/OAuth2SecurityScheme.java | 9 +- .../org/a2aproject/sdk/spec/OAuthFlows.java | 10 +- .../sdk/spec/OpenIdConnectSecurityScheme.java | 9 +- .../java/org/a2aproject/sdk/spec/Part.java | 2 +- .../PushNotificationNotSupportedError.java | 6 +- .../sdk/spec/SecurityRequirement.java | 6 +- .../a2aproject/sdk/spec/SecurityScheme.java | 8 +- .../sdk/spec/StreamingEventKind.java | 3 +- .../java/org/a2aproject/sdk/spec/Task.java | 17 +- .../sdk/spec/TaskArtifactUpdateEvent.java | 18 +- .../org/a2aproject/sdk/spec/TaskIdParams.java | 13 +- .../sdk/spec/TaskNotCancelableError.java | 7 +- .../sdk/spec/TaskNotFoundError.java | 1 - .../sdk/spec/TaskPushNotificationConfig.java | 14 +- .../a2aproject/sdk/spec/TaskQueryParams.java | 11 +- .../org/a2aproject/sdk/spec/TaskState.java | 4 +- .../org/a2aproject/sdk/spec/TaskStatus.java | 7 +- .../sdk/spec/TaskStatusUpdateEvent.java | 13 +- .../org/a2aproject/sdk/spec/TextPart.java | 13 +- .../sdk/spec/UnsupportedOperationError.java | 6 +- .../org/a2aproject/sdk/spec/UpdateEvent.java | 3 +- .../sdk/spec/VersionNotSupportedError.java | 7 +- .../sdk/spec/util/CollectionCopies.java | 9 +- .../a2aproject/sdk/spec/util/ErrorDetail.java | 3 +- .../a2aproject/sdk/spec/util/PageToken.java | 12 +- .../org/a2aproject/sdk/spec/util/Utils.java | 47 +- .../sdk/spec/util/package-info.java | 1 - .../sdk/spec/AgentCapabilitiesTest.java | 24 +- .../sdk/spec/AgentCardSignatureTest.java | 4 +- .../a2aproject/sdk/spec/AgentCardTest.java | 70 +- .../sdk/spec/AgentExtensionTest.java | 10 +- .../a2aproject/sdk/spec/AgentSkillTest.java | 20 +- .../org/a2aproject/sdk/spec/ArtifactTest.java | 7 +- .../spec/AuthorizationCodeOAuthFlowTest.java | 14 +- .../sdk/spec/CancelTaskParamsTest.java | 15 +- .../spec/ClientCredentialsOAuthFlowTest.java | 10 +- .../org/a2aproject/sdk/spec/DataPartTest.java | 40 +- .../sdk/spec/DeviceCodeOAuthFlowTest.java | 49 +- .../org/a2aproject/sdk/spec/FilePartTest.java | 11 +- .../sdk/spec/FileWithBytesTest.java | 24 +- ...tTaskPushNotificationConfigParamsTest.java | 8 +- .../spec/MessageSendConfigurationTest.java | 30 +- .../sdk/spec/MessageSendParamsTest.java | 38 +- .../sdk/spec/SecurityRequirementTest.java | 9 +- .../sdk/spec/TaskArtifactUpdateEventTest.java | 43 +- .../spec/TaskPushNotificationConfigTest.java | 9 +- .../sdk/spec/TaskStatusUpdateEventTest.java | 76 +- .../org/a2aproject/sdk/spec/TextPartTest.java | 4 +- .../sdk/spec/util/PageTokenTest.java | 78 +- .../a2aproject/sdk/spec/util/UtilsTest.java | 230 +- tck/pom.xml | 44 +- .../sdk/sut/TckAgentCardProducer.java | 38 +- .../sdk/sut/TckAgentExecutorProducer.java | 65 +- test-utils-docker/pom.xml | 4 +- .../testutils/docker/DockerAvailability.java | 14 +- .../docker/DockerRequiredExtension.java | 3 +- .../sdk/testutils/docker/RequiresDocker.java | 4 +- tests/multiversion/grpc/pom.xml | 6 +- .../multiversion/grpc/A2ATestResource.java | 41 +- .../multiversion/grpc/AuthTestProfile.java | 6 +- .../grpc/MultiVersionGrpcTest.java | 18 +- .../grpc/MultiVersionGrpcWithAuthTest.java | 55 +- ...iVersionGrpcWithTaskAuthorizationTest.java | 64 +- .../grpc/MultiVersion_v0_3_GrpcTest.java | 20 +- .../MultiVersion_v0_3_GrpcWithAuthTest.java | 53 +- .../grpc/TaskAuthorizationTestProfile.java | 2 - .../grpc/TestAuthorizationController.java | 4 +- tests/multiversion/jsonrpc/pom.xml | 6 +- .../multiversion/jsonrpc/A2ATestRoutes.java | 153 +- .../multiversion/jsonrpc/AuthTestProfile.java | 6 +- .../jsonrpc/MultiVersionJSONRPCTest.java | 107 +- .../MultiVersionJSONRPCWithAuthTest.java | 23 +- ...rsionJSONRPCWithTaskAuthorizationTest.java | 21 +- .../MultiVersionJSONRPC_v0_3_Test.java | 74 +- ...MultiVersionJSONRPC_v0_3_WithAuthTest.java | 23 +- .../jsonrpc/TaskAuthorizationTestProfile.java | 2 - .../jsonrpc/TestIdentityProvider.java | 18 +- tests/multiversion/rest/pom.xml | 6 +- .../multiversion/rest/A2ATestRoutes.java | 263 +- .../multiversion/rest/AuthTestProfile.java | 6 +- .../rest/MultiVersionRestTest.java | 62 +- .../rest/MultiVersionRestWithAuthTest.java | 23 +- ...iVersionRestWithTaskAuthorizationTest.java | 21 +- .../rest/MultiVersion_v0_3_RestTest.java | 55 +- .../MultiVersion_v0_3_RestWithAuthTest.java | 24 +- .../rest/TaskAuthorizationTestProfile.java | 2 - .../rest/TestIdentityProvider.java | 18 +- tests/server-common/pom.xml | 24 +- .../apps/common/A2AGsonObjectMapper.java | 36 +- .../apps/common/AbstractA2AServerTest.java | 3166 ++++++++++------- .../common/AbstractA2AServerWithAuthTest.java | 143 +- ...actA2AServerWithTaskAuthorizationTest.java | 90 +- .../server/apps/common/AgentCardProducer.java | 81 +- .../apps/common/AgentExecutorProducer.java | 129 +- .../common/AgentToAgentClientFactory.java | 55 +- .../server/apps/common/AuthTestProfile.java | 6 +- .../common/TaskAuthorizationTestProfile.java | 2 - .../server/apps/common/TestHttpClient.java | 15 +- .../apps/common/TestIdentityProvider.java | 23 +- .../common/TestTaskAuthorizationProvider.java | 17 +- .../sdk/server/apps/common/TestUtilsBean.java | 25 +- transport/grpc/pom.xml | 8 +- .../grpc/context/GrpcContextKeys.java | 49 +- .../grpc/handler/CallContextFactory.java | 4 +- .../transport/grpc/handler/GrpcHandler.java | 518 +-- .../transport/grpc/handler/package-info.java | 1 - .../grpc/handler/GrpcHandlerTest.java | 2055 ++++++----- .../handler/GrpcTestTransportMetadata.java | 1 - transport/jsonrpc/pom.xml | 8 +- .../jsonrpc/context/JSONRPCContextKeys.java | 2 +- .../jsonrpc/handler/JSONRPCHandler.java | 240 +- .../jsonrpc/handler/package-info.java | 1 - .../jsonrpc/handler/JSONRPCHandlerTest.java | 2996 +++++++++------- .../handler/JSONRPCTestTransportMetadata.java | 1 - transport/rest/pom.xml | 8 +- .../rest/context/RestContextKeys.java | 3 +- .../transport/rest/handler/RestHandler.java | 486 ++- .../transport/rest/handler/package-info.java | 1 - .../rest/handler/RestHandlerTest.java | 2367 +++++++----- .../handler/RestTestTransportMetadata.java | 1 - 976 files changed, 46651 insertions(+), 33243 deletions(-) diff --git a/boms/extras/pom.xml b/boms/extras/pom.xml index a26d0fb8f..4dd9cdaab 100644 --- a/boms/extras/pom.xml +++ b/boms/extras/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/boms/extras/src/it/extras-usage-test/pom.xml b/boms/extras/src/it/extras-usage-test/pom.xml index 6b8bab1e2..04b42fa97 100644 --- a/boms/extras/src/it/extras-usage-test/pom.xml +++ b/boms/extras/src/it/extras-usage-test/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 org.a2aproject.sdk.it @@ -161,10 +159,10 @@ verify-bom-completeness - verify java + verify org.a2aproject.sdk.bom.test.extras.ExtrasBomVerifier diff --git a/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java b/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java index dea222f66..93d1e2966 100644 --- a/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java +++ b/boms/extras/src/it/extras-usage-test/src/main/java/org/a2aproject/sdk/bom/test/extras/ExtrasBomVerifier.java @@ -1,8 +1,7 @@ package org.a2aproject.sdk.bom.test.extras; -import org.a2aproject.sdk.bom.test.DynamicBomVerifier; - import java.util.Set; +import org.a2aproject.sdk.bom.test.DynamicBomVerifier; /** * Verifies Extras BOM completeness by attempting to load all discovered classes. @@ -11,24 +10,29 @@ */ public class ExtrasBomVerifier extends DynamicBomVerifier { - private static final Set EXTRAS_EXCLUSIONS = Set.of( - "boms/", // BOM test modules themselves - "docs/", // Documentation site (not an SDK artifact) - "examples/", // Example applications - "itk/", // Integration Test Kit agent - "tck/", // TCK test suite - "tests/", // Integration tests - "test-utils-docker/", // Test utilities for Docker-based tests - "compat-0.3/", // Compat 0.3 modules (part of SDK BOM, not extras BOM) - "extras/queue-manager-replicated/tests-multi-instance/", // Test harness applications - "extras/queue-manager-replicated/tests-single-instance/", // Test harness applications - "extras/opentelemetry/integration-tests/", // Test harness applications - "extras/multitenancy/tests/" // Test support jar (uses provided junit/rest-assured deps) - ); + private static final Set EXTRAS_EXCLUSIONS = + Set.of( + "boms/", // BOM test modules themselves + "docs/", // Documentation site (not an SDK artifact) + "examples/", // Example applications + "itk/", // Integration Test Kit agent + "tck/", // TCK test suite + "tests/", // Integration tests + "test-utils-docker/", // Test utilities for Docker-based tests + "compat-0.3/", // Compat 0.3 modules (part of SDK BOM, not extras BOM) + "extras/queue-manager-replicated/tests-multi-instance/", // Test harness + // applications + "extras/queue-manager-replicated/tests-single-instance/", // Test harness + // applications + "extras/opentelemetry/integration-tests/", // Test harness applications + "extras/multitenancy/tests/" // Test support jar (uses provided + // junit/rest-assured deps) + ); - private static final Set EXTRAS_FORBIDDEN = Set.of( - "reference/" // Reference implementations (separate BOM) - must NOT be loadable - ); + private static final Set EXTRAS_FORBIDDEN = + Set.of( + "reference/" // Reference implementations (separate BOM) - must NOT be loadable + ); public ExtrasBomVerifier() { super(EXTRAS_EXCLUSIONS, EXTRAS_FORBIDDEN); diff --git a/boms/reference/pom.xml b/boms/reference/pom.xml index 5fc745b34..e7c44ccdb 100644 --- a/boms/reference/pom.xml +++ b/boms/reference/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/boms/reference/src/it/reference-usage-test/pom.xml b/boms/reference/src/it/reference-usage-test/pom.xml index c14c8beac..daa6fa36c 100644 --- a/boms/reference/src/it/reference-usage-test/pom.xml +++ b/boms/reference/src/it/reference-usage-test/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 org.a2aproject.sdk.it @@ -150,10 +148,10 @@ verify-bom-completeness - verify java + verify org.a2aproject.sdk.bom.test.reference.ReferenceBomVerifier diff --git a/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/bom/test/reference/ReferenceBomVerifier.java b/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/bom/test/reference/ReferenceBomVerifier.java index 0c7caff97..75e063605 100644 --- a/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/bom/test/reference/ReferenceBomVerifier.java +++ b/boms/reference/src/it/reference-usage-test/src/main/java/org/a2aproject/sdk/bom/test/reference/ReferenceBomVerifier.java @@ -1,8 +1,7 @@ package org.a2aproject.sdk.bom.test.reference; -import org.a2aproject.sdk.bom.test.DynamicBomVerifier; - import java.util.Set; +import org.a2aproject.sdk.bom.test.DynamicBomVerifier; /** * Verifies Reference BOM completeness by attempting to load all discovered classes. @@ -11,24 +10,27 @@ */ public class ReferenceBomVerifier extends DynamicBomVerifier { - private static final Set REFERENCE_EXCLUSIONS = Set.of( - "boms/", // BOM test modules themselves - "docs/", // Documentation site (not an SDK artifact) - "examples/", // Example applications - "itk/", // Integration Test Kit agent - "tck/", // TCK test suite - "tests/", // Integration tests - "test-utils-docker/", // Test utilities for Docker-based tests - "compat-0.3/client/", // Compat 0.3 client modules (part of SDK BOM) - "compat-0.3/http-client/", // Compat 0.3 HTTP client (part of SDK BOM) - "compat-0.3/tck/", // Compat 0.3 TCK (not yet enabled) - "compat-0.3/tests/" // Compat 0.3 test utilities - // Note: reference/ and compat-0.3/reference/ are NOT excluded - we verify those classes load - ); + private static final Set REFERENCE_EXCLUSIONS = + Set.of( + "boms/", // BOM test modules themselves + "docs/", // Documentation site (not an SDK artifact) + "examples/", // Example applications + "itk/", // Integration Test Kit agent + "tck/", // TCK test suite + "tests/", // Integration tests + "test-utils-docker/", // Test utilities for Docker-based tests + "compat-0.3/client/", // Compat 0.3 client modules (part of SDK BOM) + "compat-0.3/http-client/", // Compat 0.3 HTTP client (part of SDK BOM) + "compat-0.3/tck/", // Compat 0.3 TCK (not yet enabled) + "compat-0.3/tests/" // Compat 0.3 test utilities + // Note: reference/ and compat-0.3/reference/ are NOT excluded - we verify those + // classes load + ); - private static final Set REFERENCE_FORBIDDEN = Set.of( - "extras/" // Extras modules (separate BOM) - must NOT be loadable - ); + private static final Set REFERENCE_FORBIDDEN = + Set.of( + "extras/" // Extras modules (separate BOM) - must NOT be loadable + ); public ReferenceBomVerifier() { super(REFERENCE_EXCLUSIONS, REFERENCE_FORBIDDEN); diff --git a/boms/sdk/pom.xml b/boms/sdk/pom.xml index 7fd26cac6..edc67974a 100644 --- a/boms/sdk/pom.xml +++ b/boms/sdk/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -180,16 +178,16 @@ ${project.groupId} a2a-java-sdk-tests-server-common + ${project.version} test-jar test - ${project.version} ${project.groupId} a2a-java-sdk-server-common + ${project.version} test-jar test - ${project.version} diff --git a/boms/sdk/src/it/sdk-usage-test/pom.xml b/boms/sdk/src/it/sdk-usage-test/pom.xml index dc8a23965..b557eb620 100644 --- a/boms/sdk/src/it/sdk-usage-test/pom.xml +++ b/boms/sdk/src/it/sdk-usage-test/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 org.a2aproject.sdk.it @@ -191,10 +189,10 @@ verify-bom-completeness - verify java + verify org.a2aproject.sdk.bom.test.sdk.SdkBomVerifier diff --git a/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/bom/test/sdk/SdkBomVerifier.java b/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/bom/test/sdk/SdkBomVerifier.java index 5b79e78b4..d55c010a4 100644 --- a/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/bom/test/sdk/SdkBomVerifier.java +++ b/boms/sdk/src/it/sdk-usage-test/src/main/java/org/a2aproject/sdk/bom/test/sdk/SdkBomVerifier.java @@ -1,8 +1,7 @@ package org.a2aproject.sdk.bom.test.sdk; -import org.a2aproject.sdk.bom.test.DynamicBomVerifier; - import java.util.Set; +import org.a2aproject.sdk.bom.test.DynamicBomVerifier; /** * Verifies SDK BOM completeness by attempting to load all discovered classes. @@ -11,23 +10,26 @@ */ public class SdkBomVerifier extends DynamicBomVerifier { - private static final Set SDK_EXCLUSIONS = Set.of( - "boms/", // BOM test modules themselves - "docs/", // Documentation site (not an SDK artifact) - "examples/", // Example applications - "itk/", // Integration Test Kit agent - "tck/", // TCK test suite - "compat-0.3/tck/", // Compat 0.3 TCK (not yet enabled) - "compat-0.3/reference/", // Compat 0.3 reference implementations (in reference BOM) - "compat-0.3/tests/", // Compat 0.3 test utilities) - "tests/", // Integration tests - "test-utils-docker/" // Test utilities for Docker-based tests - ); + private static final Set SDK_EXCLUSIONS = + Set.of( + "boms/", // BOM test modules themselves + "docs/", // Documentation site (not an SDK artifact) + "examples/", // Example applications + "itk/", // Integration Test Kit agent + "tck/", // TCK test suite + "compat-0.3/tck/", // Compat 0.3 TCK (not yet enabled) + "compat-0.3/reference/", // Compat 0.3 reference implementations (in reference + // BOM) + "compat-0.3/tests/", // Compat 0.3 test utilities) + "tests/", // Integration tests + "test-utils-docker/" // Test utilities for Docker-based tests + ); - private static final Set SDK_FORBIDDEN = Set.of( - "extras/", // Extras modules (separate BOM) - must NOT be loadable - "reference/" // Reference implementations (separate BOM) - must NOT be loadable - ); + private static final Set SDK_FORBIDDEN = + Set.of( + "extras/", // Extras modules (separate BOM) - must NOT be loadable + "reference/" // Reference implementations (separate BOM) - must NOT be loadable + ); public SdkBomVerifier() { super(SDK_EXCLUSIONS, SDK_FORBIDDEN); diff --git a/boms/test-utils/pom.xml b/boms/test-utils/pom.xml index 29554a17b..1de9c2112 100644 --- a/boms/test-utils/pom.xml +++ b/boms/test-utils/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 @@ -20,8 +18,8 @@ 17 17 - UTF-8 true + UTF-8 diff --git a/boms/test-utils/src/main/java/org/a2aproject/sdk/bom/test/DynamicBomVerifier.java b/boms/test-utils/src/main/java/org/a2aproject/sdk/bom/test/DynamicBomVerifier.java index bba8ae455..2d7b00e30 100644 --- a/boms/test-utils/src/main/java/org/a2aproject/sdk/bom/test/DynamicBomVerifier.java +++ b/boms/test-utils/src/main/java/org/a2aproject/sdk/bom/test/DynamicBomVerifier.java @@ -39,7 +39,8 @@ public void verify() throws Exception { Path current = Paths.get("").toAbsolutePath(); while (current != null) { if (Files.isDirectory(current)) { - if (Files.exists(current.resolve("pom.xml")) && Files.exists(current.resolve("boms"))) { + if (Files.exists(current.resolve("pom.xml")) + && Files.exists(current.resolve("boms"))) { projectRoot = current; break; } @@ -49,14 +50,16 @@ public void verify() throws Exception { if (projectRoot == null) { throw new IllegalStateException("Could not find project root directory."); } - //Path projectRoot = Paths.get("").toAbsolutePath().getParent().getParent().getParent().getParent().getParent(); + // Path projectRoot = + // Paths.get("").toAbsolutePath().getParent().getParent().getParent().getParent().getParent(); System.out.println("Scanning project root: " + projectRoot); Set requiredClasses = discoverRequiredClasses(projectRoot); Set forbiddenClasses = discoverForbiddenClasses(projectRoot); System.out.println("Discovered " + requiredClasses.size() + " required classes to verify"); - System.out.println("Discovered " + forbiddenClasses.size() + " forbidden classes to verify"); + System.out.println( + "Discovered " + forbiddenClasses.size() + " forbidden classes to verify"); sanityCheckDiscovery(requiredClasses, forbiddenClasses); @@ -71,7 +74,8 @@ public void verify() throws Exception { } catch (ClassNotFoundException e) { failures.add("[REQUIRED] " + className + " - NOT FOUND"); } catch (NoClassDefFoundError e) { - failures.add("[REQUIRED] " + className + " - MISSING DEPENDENCY: " + e.getMessage()); + failures.add( + "[REQUIRED] " + className + " - MISSING DEPENDENCY: " + e.getMessage()); } catch (Exception e) { failures.add("[REQUIRED] " + className + " - ERROR: " + e.getMessage()); } @@ -82,7 +86,11 @@ public void verify() throws Exception { for (String className : forbiddenClasses) { try { Class.forName(className); - failures.add("[FORBIDDEN] " + className + " - INCORRECTLY LOADABLE (BOM includes dependency it shouldn't!)"); + failures.add( + "[FORBIDDEN] " + + className + + " - INCORRECTLY LOADABLE (BOM includes dependency it" + + " shouldn't!)"); } catch (ClassNotFoundException | NoClassDefFoundError e) { // Expected - class should not be loadable correctlyForbidden++; @@ -92,8 +100,16 @@ public void verify() throws Exception { } System.out.println("\n=== BOM Verification Results ==="); - System.out.println("Required classes successfully loaded: " + successful + "/" + requiredClasses.size()); - System.out.println("Forbidden classes correctly not loadable: " + correctlyForbidden + "/" + forbiddenClasses.size()); + System.out.println( + "Required classes successfully loaded: " + + successful + + "/" + + requiredClasses.size()); + System.out.println( + "Forbidden classes correctly not loadable: " + + correctlyForbidden + + "/" + + forbiddenClasses.size()); System.out.println("Total failures: " + failures.size()); if (!failures.isEmpty()) { @@ -103,32 +119,56 @@ public void verify() throws Exception { System.exit(1); } - System.out.println("\n✅ BOM is COMPLETE - all required classes loaded, all forbidden classes not loadable!"); + System.out.println( + "\n" + + "✅ BOM is COMPLETE - all required classes loaded, all forbidden classes not" + + " loadable!"); } private void sanityCheckDiscovery(Set requiredClasses, Set forbiddenClasses) { - // Do some sanity checks for some classes from both top-level and nested modules to make sure the + // Do some sanity checks for some classes from both top-level and nested modules to make + // sure the // discovery mechanism worked properly - sanityCheckDiscovery("org.a2aproject.sdk.spec.AgentCard", requiredClasses, forbiddenClasses); - sanityCheckDiscovery("org.a2aproject.sdk.server.events.EventConsumer", requiredClasses, forbiddenClasses); - sanityCheckDiscovery("org.a2aproject.sdk.client.transport.spi.ClientTransport", requiredClasses, forbiddenClasses); + sanityCheckDiscovery( + "org.a2aproject.sdk.spec.AgentCard", requiredClasses, forbiddenClasses); + sanityCheckDiscovery( + "org.a2aproject.sdk.server.events.EventConsumer", + requiredClasses, + forbiddenClasses); + sanityCheckDiscovery( + "org.a2aproject.sdk.client.transport.spi.ClientTransport", + requiredClasses, + forbiddenClasses); - sanityCheckDiscovery("org.a2aproject.sdk.server.common.quarkus.DefaultProducers", requiredClasses, forbiddenClasses); - sanityCheckDiscovery("org.a2aproject.sdk.extras.common.events.TaskFinalizedEvent", requiredClasses, forbiddenClasses); - sanityCheckDiscovery("org.a2aproject.sdk.extras.queuemanager.replicated.core.ReplicatedEventQueueItem", requiredClasses, forbiddenClasses); + sanityCheckDiscovery( + "org.a2aproject.sdk.server.common.quarkus.DefaultProducers", + requiredClasses, + forbiddenClasses); + sanityCheckDiscovery( + "org.a2aproject.sdk.extras.common.events.TaskFinalizedEvent", + requiredClasses, + forbiddenClasses); + sanityCheckDiscovery( + "org.a2aproject.sdk.extras.queuemanager.replicated.core.ReplicatedEventQueueItem", + requiredClasses, + forbiddenClasses); // Make sure that the required and forbidden sets don't contain the same classes Set intersection = new HashSet<>(requiredClasses); intersection.retainAll(forbiddenClasses); if (!intersection.isEmpty()) { - System.err.println("The following classes appear in both the required and forbidden sets: " + intersection); + System.err.println( + "The following classes appear in both the required and forbidden sets: " + + intersection); System.exit(1); } } - private void sanityCheckDiscovery(String className, Set requiredClasses, Set forbiddenClasses) { + private void sanityCheckDiscovery( + String className, Set requiredClasses, Set forbiddenClasses) { if (!requiredClasses.contains(className) && !forbiddenClasses.contains(className)) { - System.err.println("Class expected to be on the classpath was not discovered: " + className); + System.err.println( + "Class expected to be on the classpath was not discovered: " + className); System.exit(1); } } @@ -137,39 +177,53 @@ private void sanityCheckDiscovery(String className, Set requiredClasses, * Discover classes that MUST be loadable (not excluded, not forbidden) */ private Set discoverRequiredClasses(Path projectRoot) throws IOException { - return discoverClasses(projectRoot, relativePath -> !isExcluded(relativePath) && !isForbidden(relativePath)); + return discoverClasses( + projectRoot, + relativePath -> !isExcluded(relativePath) && !isForbidden(relativePath)); } /** * Discover classes that must NOT be loadable (forbidden but not excluded) */ private Set discoverForbiddenClasses(Path projectRoot) throws IOException { - return discoverClasses(projectRoot, relativePath -> !isExcluded(relativePath) && isForbidden(relativePath)); + return discoverClasses( + projectRoot, + relativePath -> !isExcluded(relativePath) && isForbidden(relativePath)); } /** * Common discovery logic with custom filter */ - private Set discoverClasses(Path projectRoot, java.util.function.Predicate pathFilter) throws IOException { + private Set discoverClasses( + Path projectRoot, java.util.function.Predicate pathFilter) throws IOException { Set classes = new TreeSet<>(); try (Stream paths = Files.walk(projectRoot)) { paths.filter(Files::isRegularFile) - .filter(p -> p.toString().endsWith(".java")) - .filter(p -> { - String relativePath = toForwardSlash(projectRoot.relativize(p).toString()); - return !relativePath.startsWith(".") && relativePath.contains("/src/main/java/") && pathFilter.test(relativePath); - }) - .forEach(javaFile -> { - try { - String className = extractClassName(javaFile); - if (className != null) { - classes.add(className); - } - } catch (IOException e) { - System.err.println("Failed to parse: " + javaFile + " - " + e.getMessage()); - } - }); + .filter(p -> p.toString().endsWith(".java")) + .filter( + p -> { + String relativePath = + toForwardSlash(projectRoot.relativize(p).toString()); + return !relativePath.startsWith(".") + && relativePath.contains("/src/main/java/") + && pathFilter.test(relativePath); + }) + .forEach( + javaFile -> { + try { + String className = extractClassName(javaFile); + if (className != null) { + classes.add(className); + } + } catch (IOException e) { + System.err.println( + "Failed to parse: " + + javaFile + + " - " + + e.getMessage()); + } + }); } return classes; @@ -198,12 +252,13 @@ private static String toForwardSlash(String path) { // Extract package name from file content String packageName = null; try (Stream lines = Files.lines(javaFile)) { - packageName = lines.map(String::trim) - .map(PACKAGE_PATTERN::matcher) - .filter(Matcher::matches) - .map(m -> m.group(1)) - .findFirst() - .orElse(null); + packageName = + lines.map(String::trim) + .map(PACKAGE_PATTERN::matcher) + .filter(Matcher::matches) + .map(m -> m.group(1)) + .findFirst() + .orElse(null); } if (packageName != null) { return packageName + "." + simpleClassName; diff --git a/boms/test-utils/src/main/java/org/a2aproject/sdk/bom/test/package-info.java b/boms/test-utils/src/main/java/org/a2aproject/sdk/bom/test/package-info.java index 4beea380a..605a89f4d 100644 --- a/boms/test-utils/src/main/java/org/a2aproject/sdk/bom/test/package-info.java +++ b/boms/test-utils/src/main/java/org/a2aproject/sdk/bom/test/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.bom.test; import org.jspecify.annotations.NullMarked; - diff --git a/client/base/pom.xml b/client/base/pom.xml index c1c920910..2a4361edf 100644 --- a/client/base/pom.xml +++ b/client/base/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -86,4 +84,4 @@ - \ No newline at end of file + diff --git a/client/base/src/main/java/org/a2aproject/sdk/A2A.java b/client/base/src/main/java/org/a2aproject/sdk/A2A.java index 7a37b93c2..633880bbb 100644 --- a/client/base/src/main/java/org/a2aproject/sdk/A2A.java +++ b/client/base/src/main/java/org/a2aproject/sdk/A2A.java @@ -3,7 +3,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.client.http.A2ACardResolver; import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; @@ -15,7 +14,6 @@ import org.a2aproject.sdk.spec.Part; import org.a2aproject.sdk.spec.TextPart; - /** * Utility class providing convenience methods for working with the A2A Protocol. *

@@ -222,7 +220,8 @@ public static Message createAgentTextMessage(String text, String contextId, Stri * @see org.a2aproject.sdk.spec.Part * @see org.a2aproject.sdk.spec.TextPart */ - public static Message createAgentPartsMessage(List> parts, String contextId, String taskId) { + public static Message createAgentPartsMessage( + List> parts, String contextId, String taskId) { if (parts == null || parts.isEmpty()) { throw new IllegalArgumentException("Parts cannot be null or empty"); } @@ -233,24 +232,28 @@ private static Message toMessage(String text, Message.Role role, String messageI return toMessage(text, role, messageId, null, null); } - private static Message toMessage(String text, Message.Role role, String messageId, String contextId, String taskId) { - Message.Builder messageBuilder = Message.builder() - .role(role) - .parts(Collections.singletonList(new TextPart(text))) - .contextId(contextId) - .taskId(taskId); + private static Message toMessage( + String text, Message.Role role, String messageId, String contextId, String taskId) { + Message.Builder messageBuilder = + Message.builder() + .role(role) + .parts(Collections.singletonList(new TextPart(text))) + .contextId(contextId) + .taskId(taskId); if (messageId != null) { messageBuilder.messageId(messageId); } return messageBuilder.build(); } - private static Message toMessage(List> parts, Message.Role role, String messageId, String contextId, String taskId) { - Message.Builder messageBuilder = Message.builder() - .role(role) - .parts(parts) - .contextId(contextId) - .taskId(taskId); + private static Message toMessage( + List> parts, + Message.Role role, + String messageId, + String contextId, + String taskId) { + Message.Builder messageBuilder = + Message.builder().role(role).parts(parts).contextId(contextId).taskId(taskId); if (messageId != null) { messageBuilder.messageId(messageId); } @@ -286,8 +289,10 @@ private static Message toMessage(List> parts, Message.Role role, String * @see #getAgentCard(String, String, java.util.Map) * @see AgentCard */ - public static AgentCard getAgentCard(String agentUrl) throws A2AClientError, A2AClientJSONError { - return getAgentCard(A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT), agentUrl); + public static AgentCard getAgentCard(String agentUrl) + throws A2AClientError, A2AClientJSONError { + return getAgentCard( + A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT), agentUrl); } /** @@ -312,7 +317,8 @@ public static AgentCard getAgentCard(String agentUrl) throws A2AClientError, A2A * @throws org.a2aproject.sdk.spec.A2AClientJSONError if the response body cannot be decoded as JSON or validated against the AgentCard schema * @see org.a2aproject.sdk.client.http.A2AHttpClient */ - public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl) throws A2AClientError, A2AClientJSONError { + public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl) + throws A2AClientError, A2AClientJSONError { return getAgentCard(httpClient, agentUrl, null, null); } @@ -357,8 +363,14 @@ public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl) * @throws org.a2aproject.sdk.spec.A2AClientError if an HTTP error occurs fetching the card * @throws org.a2aproject.sdk.spec.A2AClientJSONError if the response body cannot be decoded as JSON or validated against the AgentCard schema */ - public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError, A2AClientJSONError { - return getAgentCard(A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT), agentUrl, relativeCardPath, authHeaders); + public static AgentCard getAgentCard( + String agentUrl, String relativeCardPath, Map authHeaders) + throws A2AClientError, A2AClientJSONError { + return getAgentCard( + A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT), + agentUrl, + relativeCardPath, + authHeaders); } /** @@ -393,13 +405,19 @@ public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, M * @throws org.a2aproject.sdk.spec.A2AClientError if an HTTP error occurs fetching the card * @throws org.a2aproject.sdk.spec.A2AClientJSONError if the response body cannot be decoded as JSON or validated against the AgentCard schema */ - public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError, A2AClientJSONError { - A2ACardResolver resolver = A2ACardResolver.builder() - .httpClient(httpClient) - .baseUrl(agentUrl) - .agentCardPath(relativeCardPath) - .authHeaders(authHeaders) - .build(); + public static AgentCard getAgentCard( + A2AHttpClient httpClient, + String agentUrl, + String relativeCardPath, + Map authHeaders) + throws A2AClientError, A2AClientJSONError { + A2ACardResolver resolver = + A2ACardResolver.builder() + .httpClient(httpClient) + .baseUrl(agentUrl) + .agentCardPath(relativeCardPath) + .authHeaders(authHeaders) + .build(); return resolver.getAgentCard(); } } diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/AbstractClient.java b/client/base/src/main/java/org/a2aproject/sdk/client/AbstractClient.java index 7f3171eab..7b556b364 100644 --- a/client/base/src/main/java/org/a2aproject/sdk/client/AbstractClient.java +++ b/client/base/src/main/java/org/a2aproject/sdk/client/AbstractClient.java @@ -6,7 +6,6 @@ import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; - import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; import org.a2aproject.sdk.spec.A2AClientException; @@ -41,7 +40,9 @@ public AbstractClient(@NonNull List> consumer this(consumers, null); } - public AbstractClient(@NonNull List> consumers, @Nullable Consumer streamingErrorHandler) { + public AbstractClient( + @NonNull List> consumers, + @Nullable Consumer streamingErrorHandler) { checkNotNullParam("consumers", consumers); this.consumers = consumers; this.streamingErrorHandler = streamingErrorHandler; @@ -76,8 +77,8 @@ public void sendMessage(@NonNull Message request) throws A2AClientException { * @param context optional client call context for the request * @throws A2AClientException if sending the message fails for any reason */ - public void sendMessage(@NonNull Message request, - @Nullable ClientCallContext context) throws A2AClientException { + public void sendMessage(@NonNull Message request, @Nullable ClientCallContext context) + throws A2AClientException { sendMessage(request, consumers, streamingErrorHandler, context); } @@ -95,9 +96,11 @@ public void sendMessage(@NonNull Message request, * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs * @throws A2AClientException if sending the message fails for any reason */ - public void sendMessage(@NonNull Message request, - @NonNull List> consumers, - @Nullable Consumer streamingErrorHandler) throws A2AClientException { + public void sendMessage( + @NonNull Message request, + @NonNull List> consumers, + @Nullable Consumer streamingErrorHandler) + throws A2AClientException { sendMessage(request, consumers, streamingErrorHandler, null); } @@ -116,10 +119,12 @@ public void sendMessage(@NonNull Message request, * @param context optional client call context for the request * @throws A2AClientException if sending the message fails for any reason */ - public abstract void sendMessage(@NonNull Message request, - @NonNull List> consumers, - @Nullable Consumer streamingErrorHandler, - @Nullable ClientCallContext context) throws A2AClientException; + public abstract void sendMessage( + @NonNull Message request, + @NonNull List> consumers, + @Nullable Consumer streamingErrorHandler, + @Nullable ClientCallContext context) + throws A2AClientException; /** * Send a message to the remote agent. This method will automatically use @@ -135,9 +140,11 @@ public abstract void sendMessage(@NonNull Message request, * @param metadata the optional metadata to include when sending the message * @throws A2AClientException if sending the message fails for any reason */ - public void sendMessage(@NonNull Message request, - @Nullable TaskPushNotificationConfig pushNotificationConfiguration, - @Nullable Map metadata) throws A2AClientException { + public void sendMessage( + @NonNull Message request, + @Nullable TaskPushNotificationConfig pushNotificationConfiguration, + @Nullable Map metadata) + throws A2AClientException { sendMessage(request, pushNotificationConfiguration, metadata, null); } @@ -156,10 +163,12 @@ public void sendMessage(@NonNull Message request, * @param context optional client call context for the request * @throws A2AClientException if sending the message fails for any reason */ - public abstract void sendMessage(@NonNull Message request, - @Nullable TaskPushNotificationConfig pushNotificationConfiguration, - @Nullable Map metadata, - @Nullable ClientCallContext context) throws A2AClientException; + public abstract void sendMessage( + @NonNull Message request, + @Nullable TaskPushNotificationConfig pushNotificationConfiguration, + @Nullable Map metadata, + @Nullable ClientCallContext context) + throws A2AClientException; /** * Send a message to the remote agent. This method will automatically use @@ -176,10 +185,12 @@ public abstract void sendMessage(@NonNull Message request, * @param context optional client call context for the request * @throws A2AClientException if sending the message fails for any reason */ - public abstract void sendMessage(@NonNull MessageSendParams params, - @NonNull List> consumers, - @Nullable Consumer streamingErrorHandler, - @Nullable ClientCallContext context) throws A2AClientException; + public abstract void sendMessage( + @NonNull MessageSendParams params, + @NonNull List> consumers, + @Nullable Consumer streamingErrorHandler, + @Nullable ClientCallContext context) + throws A2AClientException; /** * Retrieve the current state and history of a specific task. @@ -200,7 +211,8 @@ public Task getTask(TaskQueryParams request) throws A2AClientException { * @return the task * @throws A2AClientException if retrieving the task fails for any reason */ - public abstract Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) throws A2AClientException; + public abstract Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) + throws A2AClientException; /** * List tasks with optional filtering and pagination. @@ -221,7 +233,8 @@ public ListTasksResult listTasks(ListTasksParams request) throws A2AClientExcept * @return the list tasks result containing tasks and pagination information * @throws A2AClientException if listing tasks fails for any reason */ - public abstract ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) throws A2AClientException; + public abstract ListTasksResult listTasks( + ListTasksParams request, @Nullable ClientCallContext context) throws A2AClientException; /** * Request the agent to cancel a specific task. @@ -242,7 +255,8 @@ public Task cancelTask(CancelTaskParams request) throws A2AClientException { * @return the cancelled task * @throws A2AClientException if cancelling the task fails for any reason */ - public abstract Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) throws A2AClientException; + public abstract Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) + throws A2AClientException; /** * Create or update the push notification configuration for a specific task. @@ -265,8 +279,8 @@ public TaskPushNotificationConfig createTaskPushNotificationConfiguration( * @throws A2AClientException if setting the task push notification configuration fails for any reason */ public abstract TaskPushNotificationConfig createTaskPushNotificationConfiguration( - TaskPushNotificationConfig request, - @Nullable ClientCallContext context) throws A2AClientException; + TaskPushNotificationConfig request, @Nullable ClientCallContext context) + throws A2AClientException; /** * Retrieve the push notification configuration for a specific task. @@ -289,8 +303,8 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration( * @throws A2AClientException if getting the task push notification config fails for any reason */ public abstract TaskPushNotificationConfig getTaskPushNotificationConfiguration( - GetTaskPushNotificationConfigParams request, - @Nullable ClientCallContext context) throws A2AClientException; + GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) + throws A2AClientException; /** * Retrieve the list of push notification configurations for a specific task with pagination support. @@ -313,8 +327,8 @@ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurati * @throws A2AClientException if getting the task push notification configs fails for any reason */ public abstract ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations( - ListTaskPushNotificationConfigsParams request, - @Nullable ClientCallContext context) throws A2AClientException; + ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) + throws A2AClientException; /** * Delete the list of push notification configurations for a specific task. @@ -335,8 +349,8 @@ public void deleteTaskPushNotificationConfigurations( * @throws A2AClientException if deleting the task push notification configs fails for any reason */ public abstract void deleteTaskPushNotificationConfigurations( - DeleteTaskPushNotificationConfigParams request, - @Nullable ClientCallContext context) throws A2AClientException; + DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) + throws A2AClientException; /** * Subscribe to a task's event stream. @@ -363,8 +377,8 @@ public void subscribeToTask(@NonNull TaskIdParams request) throws A2AClientExcep * @param context optional client call context for the request * @throws A2AClientException if resubscribing fails for any reason */ - public void subscribeToTask(@NonNull TaskIdParams request, - @Nullable ClientCallContext context) throws A2AClientException { + public void subscribeToTask(@NonNull TaskIdParams request, @Nullable ClientCallContext context) + throws A2AClientException { subscribeToTask(request, consumers, streamingErrorHandler, context); } @@ -380,9 +394,11 @@ public void subscribeToTask(@NonNull TaskIdParams request, * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs * @throws A2AClientException if resubscribing fails for any reason */ - public void subscribeToTask(@NonNull TaskIdParams request, - @NonNull List> consumers, - @Nullable Consumer streamingErrorHandler) throws A2AClientException { + public void subscribeToTask( + @NonNull TaskIdParams request, + @NonNull List> consumers, + @Nullable Consumer streamingErrorHandler) + throws A2AClientException { subscribeToTask(request, consumers, streamingErrorHandler, null); } @@ -399,10 +415,12 @@ public void subscribeToTask(@NonNull TaskIdParams request, * @param context optional client call context for the request * @throws A2AClientException if resubscribing fails for any reason */ - public abstract void subscribeToTask(@NonNull TaskIdParams request, - @NonNull List> consumers, - @Nullable Consumer streamingErrorHandler, - @Nullable ClientCallContext context) throws A2AClientException; + public abstract void subscribeToTask( + @NonNull TaskIdParams request, + @NonNull List> consumers, + @Nullable Consumer streamingErrorHandler, + @Nullable ClientCallContext context) + throws A2AClientException; /** * Retrieve the extended AgentCard. @@ -432,7 +450,8 @@ public AgentCard getExtendedAgentCard(@Nullable String tenant) throws A2AClientE * @return the extended AgentCard * @throws A2AClientException if retrieving the extended agent card fails for any reason */ - public AgentCard getExtendedAgentCard(@Nullable ClientCallContext context) throws A2AClientException { + public AgentCard getExtendedAgentCard(@Nullable ClientCallContext context) + throws A2AClientException { return getExtendedAgentCard(null, context); } @@ -444,8 +463,8 @@ public AgentCard getExtendedAgentCard(@Nullable ClientCallContext context) throw * @return the extended AgentCard * @throws A2AClientException if retrieving the extended agent card fails for any reason */ - public abstract AgentCard getExtendedAgentCard(@Nullable String tenant, - @Nullable ClientCallContext context) throws A2AClientException; + public abstract AgentCard getExtendedAgentCard( + @Nullable String tenant, @Nullable ClientCallContext context) throws A2AClientException; /** * Close the transport and release any associated resources. @@ -460,5 +479,4 @@ public abstract AgentCard getExtendedAgentCard(@Nullable String tenant, public @Nullable Consumer getStreamingErrorHandler() { return streamingErrorHandler; } - -} \ No newline at end of file +} diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/Client.java b/client/base/src/main/java/org/a2aproject/sdk/client/Client.java index 5abb3dae8..f5bc1aa9f 100644 --- a/client/base/src/main/java/org/a2aproject/sdk/client/Client.java +++ b/client/base/src/main/java/org/a2aproject/sdk/client/Client.java @@ -6,7 +6,6 @@ import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; - import org.a2aproject.sdk.client.config.ClientConfig; import org.a2aproject.sdk.client.transport.spi.ClientTransport; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; @@ -118,7 +117,7 @@ * case FAILED -> System.err.println("Task failed: " + * tue.getTask().status().message()); * } - * + * * // Check for new artifacts * if (tue.getUpdateEvent() instanceof TaskArtifactUpdateEvent update) { * Artifact artifact = update.artifact(); @@ -198,8 +197,12 @@ public class Client extends AbstractClient { * @param consumers the event consumers * @param streamingErrorHandler the error handler for streaming scenarios */ - Client(AgentCard agentCard, ClientConfig clientConfig, ClientTransport clientTransport, - List> consumers, @Nullable Consumer streamingErrorHandler) { + Client( + AgentCard agentCard, + ClientConfig clientConfig, + ClientTransport clientTransport, + List> consumers, + @Nullable Consumer streamingErrorHandler) { super(consumers, streamingErrorHandler); checkNotNullParam("agentCard", agentCard); @@ -232,17 +235,21 @@ public static ClientBuilder builder(AgentCard agentCard) { } @Override - public void sendMessage(@NonNull Message request, - @NonNull List> consumers, - @Nullable Consumer streamingErrorHandler, - @Nullable ClientCallContext context) throws A2AClientException { - MessageSendConfiguration messageSendConfiguration = createMessageSendConfiguration(clientConfig.getTaskPushNotificationConfig()); + public void sendMessage( + @NonNull Message request, + @NonNull List> consumers, + @Nullable Consumer streamingErrorHandler, + @Nullable ClientCallContext context) + throws A2AClientException { + MessageSendConfiguration messageSendConfiguration = + createMessageSendConfiguration(clientConfig.getTaskPushNotificationConfig()); - MessageSendParams messageSendParams = MessageSendParams.builder() - .message(request) - .configuration(messageSendConfiguration) - .metadata(clientConfig.getMetadata()) - .build(); + MessageSendParams messageSendParams = + MessageSendParams.builder() + .message(request) + .configuration(messageSendConfiguration) + .metadata(clientConfig.getMetadata()) + .build(); sendMessage(messageSendParams, consumers, streamingErrorHandler, context); } @@ -293,26 +300,32 @@ public void sendMessage(@NonNull Message request, * @see TaskPushNotificationConfig */ @Override - public void sendMessage(@NonNull Message request, - @Nullable TaskPushNotificationConfig pushNotificationConfiguration, - @Nullable Map metadata, - @Nullable ClientCallContext context) throws A2AClientException { - MessageSendConfiguration messageSendConfiguration = createMessageSendConfiguration(pushNotificationConfiguration); - MessageSendParams messageSendParams = MessageSendParams.builder() - .message(request) - .configuration(messageSendConfiguration) - .metadata(metadata) - .build(); + public void sendMessage( + @NonNull Message request, + @Nullable TaskPushNotificationConfig pushNotificationConfiguration, + @Nullable Map metadata, + @Nullable ClientCallContext context) + throws A2AClientException { + MessageSendConfiguration messageSendConfiguration = + createMessageSendConfiguration(pushNotificationConfiguration); + MessageSendParams messageSendParams = + MessageSendParams.builder() + .message(request) + .configuration(messageSendConfiguration) + .metadata(metadata) + .build(); sendMessage(messageSendParams, consumers, streamingErrorHandler, context); } @Override - public void sendMessage(@NonNull MessageSendParams messageSendParams, - @NonNull List> consumers, - @Nullable Consumer streamingErrorHandler, - @Nullable ClientCallContext context) throws A2AClientException { - if (! clientConfig.isStreaming() || ! agentCard.capabilities().streaming()) { + public void sendMessage( + @NonNull MessageSendParams messageSendParams, + @NonNull List> consumers, + @Nullable Consumer streamingErrorHandler, + @Nullable ClientCallContext context) + throws A2AClientException { + if (!clientConfig.isStreaming() || !agentCard.capabilities().streaming()) { EventKind eventKind = clientTransport.sendMessage(messageSendParams, context); ClientEvent clientEvent; if (eventKind instanceof Task task) { @@ -324,20 +337,22 @@ public void sendMessage(@NonNull MessageSendParams messageSendParams, consume(clientEvent, agentCard, consumers); } else { ClientTaskManager tracker = new ClientTaskManager(); - Consumer overriddenErrorHandler = getOverriddenErrorHandler(streamingErrorHandler); - Consumer eventHandler = event -> { - try { - ClientEvent clientEvent = getClientEvent(event, tracker); - consume(clientEvent, agentCard, consumers); - } catch (A2AClientError e) { - overriddenErrorHandler.accept(e); - } - }; - clientTransport.sendMessageStreaming(messageSendParams, eventHandler, overriddenErrorHandler, context); + Consumer overriddenErrorHandler = + getOverriddenErrorHandler(streamingErrorHandler); + Consumer eventHandler = + event -> { + try { + ClientEvent clientEvent = getClientEvent(event, tracker); + consume(clientEvent, agentCard, consumers); + } catch (A2AClientError e) { + overriddenErrorHandler.accept(e); + } + }; + clientTransport.sendMessageStreaming( + messageSendParams, eventHandler, overriddenErrorHandler, context); } } - /** * Retrieve a specific task by ID. *

@@ -367,7 +382,8 @@ public void sendMessage(@NonNull MessageSendParams messageSendParams, * @see Task */ @Override - public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) throws A2AClientException { + public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) + throws A2AClientException { return clientTransport.getTask(request, context); } @@ -404,7 +420,8 @@ public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context * @see ListTasksResult */ @Override - public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) throws A2AClientException { + public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) + throws A2AClientException { return clientTransport.listTasks(request, context); } @@ -447,7 +464,8 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo * @see org.a2aproject.sdk.spec.TaskNotFoundError */ @Override - public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) throws A2AClientException { + public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) + throws A2AClientException { return clientTransport.cancelTask(request, context); } @@ -477,7 +495,8 @@ public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext con */ @Override public TaskPushNotificationConfig createTaskPushNotificationConfiguration( - TaskPushNotificationConfig request, @Nullable ClientCallContext context) throws A2AClientException { + TaskPushNotificationConfig request, @Nullable ClientCallContext context) + throws A2AClientException { return clientTransport.createTaskPushNotificationConfiguration(request, context); } @@ -502,7 +521,8 @@ public TaskPushNotificationConfig createTaskPushNotificationConfiguration( */ @Override public TaskPushNotificationConfig getTaskPushNotificationConfiguration( - GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException { + GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) + throws A2AClientException { return clientTransport.getTaskPushNotificationConfiguration(request, context); } @@ -530,7 +550,8 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration( */ @Override public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations( - ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) throws A2AClientException { + ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) + throws A2AClientException { return clientTransport.listTaskPushNotificationConfigurations(request, context); } @@ -558,7 +579,8 @@ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurati */ @Override public void deleteTaskPushNotificationConfigurations( - DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException { + DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) + throws A2AClientException { clientTransport.deleteTaskPushNotificationConfigurations(request, context); } @@ -605,23 +627,27 @@ public void deleteTaskPushNotificationConfigurations( * @throws A2AClientException if subscription is not supported or if the task cannot be found */ @Override - public void subscribeToTask(@NonNull TaskIdParams request, - @NonNull List> consumers, - @Nullable Consumer streamingErrorHandler, - @Nullable ClientCallContext context) throws A2AClientException { - if (! clientConfig.isStreaming() || ! agentCard.capabilities().streaming()) { + public void subscribeToTask( + @NonNull TaskIdParams request, + @NonNull List> consumers, + @Nullable Consumer streamingErrorHandler, + @Nullable ClientCallContext context) + throws A2AClientException { + if (!clientConfig.isStreaming() || !agentCard.capabilities().streaming()) { throw new A2AClientException("Client and/or server does not support resubscription"); } ClientTaskManager tracker = new ClientTaskManager(); - Consumer overriddenErrorHandler = getOverriddenErrorHandler(streamingErrorHandler); - Consumer eventHandler = event -> { - try { - ClientEvent clientEvent = getClientEvent(event, tracker); - consume(clientEvent, agentCard, consumers); - } catch (A2AClientError e) { - overriddenErrorHandler.accept(e); - } - }; + Consumer overriddenErrorHandler = + getOverriddenErrorHandler(streamingErrorHandler); + Consumer eventHandler = + event -> { + try { + ClientEvent clientEvent = getClientEvent(event, tracker); + consume(clientEvent, agentCard, consumers); + } catch (A2AClientError e) { + overriddenErrorHandler.accept(e); + } + }; clientTransport.subscribeToTask(request, eventHandler, overriddenErrorHandler, context); } @@ -647,7 +673,9 @@ public void subscribeToTask(@NonNull TaskIdParams request, * @see AgentCard */ @Override - public AgentCard getExtendedAgentCard(@Nullable String tenant, @Nullable ClientCallContext context) throws A2AClientException { + public AgentCard getExtendedAgentCard( + @Nullable String tenant, @Nullable ClientCallContext context) + throws A2AClientException { GetExtendedAgentCardParams params = new GetExtendedAgentCardParams(tenant); agentCard = clientTransport.getExtendedAgentCard(params, context); return agentCard; @@ -676,7 +704,8 @@ public void close() { clientTransport.close(); } - private ClientEvent getClientEvent(StreamingEventKind event, ClientTaskManager taskManager) throws A2AClientError { + private ClientEvent getClientEvent(StreamingEventKind event, ClientTaskManager taskManager) + throws A2AClientError { if (event instanceof Message message) { return new MessageEvent(message); } else if (event instanceof Task task) { @@ -693,7 +722,8 @@ private ClientEvent getClientEvent(StreamingEventKind event, ClientTaskManager t } } - private MessageSendConfiguration createMessageSendConfiguration(@Nullable TaskPushNotificationConfig taskPushNotificationConfig) { + private MessageSendConfiguration createMessageSendConfiguration( + @Nullable TaskPushNotificationConfig taskPushNotificationConfig) { return MessageSendConfiguration.builder() .acceptedOutputModes(clientConfig.getAcceptedOutputModes()) .returnImmediately(clientConfig.isPolling()) @@ -702,7 +732,8 @@ private MessageSendConfiguration createMessageSendConfiguration(@Nullable TaskPu .build(); } - private @NonNull Consumer getOverriddenErrorHandler(@Nullable Consumer errorHandler) { + private @NonNull Consumer getOverriddenErrorHandler( + @Nullable Consumer errorHandler) { return e -> { if (errorHandler != null) { errorHandler.accept(e); @@ -714,7 +745,10 @@ private MessageSendConfiguration createMessageSendConfiguration(@Nullable TaskPu }; } - private void consume(ClientEvent clientEvent, AgentCard agentCard, @NonNull List> consumers) { + private void consume( + ClientEvent clientEvent, + AgentCard agentCard, + @NonNull List> consumers) { for (BiConsumer consumer : consumers) { consumer.accept(clientEvent, agentCard); } diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/ClientBuilder.java b/client/base/src/main/java/org/a2aproject/sdk/client/ClientBuilder.java index 42614c262..a0f515488 100644 --- a/client/base/src/main/java/org/a2aproject/sdk/client/ClientBuilder.java +++ b/client/base/src/main/java/org/a2aproject/sdk/client/ClientBuilder.java @@ -10,7 +10,6 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; - import org.a2aproject.sdk.client.config.ClientConfig; import org.a2aproject.sdk.client.transport.spi.ClientTransport; import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig; @@ -23,7 +22,6 @@ import org.a2aproject.sdk.spec.TransportProtocol; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,15 +98,22 @@ */ public class ClientBuilder { - private static final Map>> transportProviderRegistry = new HashMap<>(); - private static final Map, String> transportProtocolMapping = new HashMap<>(); + private static final Map< + String, + ClientTransportProvider< + ? extends ClientTransport, ? extends ClientTransportConfig>> + transportProviderRegistry = new HashMap<>(); + private static final Map, String> transportProtocolMapping = + new HashMap<>(); private static final Logger LOGGER = LoggerFactory.getLogger(ClientBuilder.class); static { - ServiceLoader loader = ServiceLoader.load(ClientTransportProvider.class); + ServiceLoader loader = + ServiceLoader.load(ClientTransportProvider.class); for (ClientTransportProvider transport : loader) { transportProviderRegistry.put(transport.getTransportProtocol(), transport); - transportProtocolMapping.put(transport.getTransportProtocolClass(), transport.getTransportProtocol()); + transportProtocolMapping.put( + transport.getTransportProtocolClass(), transport.getTransportProtocol()); } } @@ -118,7 +123,10 @@ public class ClientBuilder { private @Nullable Consumer streamErrorHandler; private ClientConfig clientConfig = new ClientConfig.Builder().build(); - private final Map, ClientTransportConfig> clientTransports = new LinkedHashMap<>(); + private final Map< + Class, + ClientTransportConfig> + clientTransports = new LinkedHashMap<>(); /** * Package-private constructor used by {@link Client#builder(AgentCard)}. @@ -148,7 +156,9 @@ public class ClientBuilder { * @param the transport type * @return this builder for method chaining */ - public ClientBuilder withTransport(Class clazz, ClientTransportConfigBuilder, ?> configBuilder) { + public ClientBuilder withTransport( + Class clazz, + ClientTransportConfigBuilder, ?> configBuilder) { return withTransport(clazz, configBuilder.build()); } @@ -169,7 +179,8 @@ public ClientBuilder withTransport(Class clazz, C * @param the transport type * @return this builder for method chaining */ - public ClientBuilder withTransport(Class clazz, ClientTransportConfig config) { + public ClientBuilder withTransport( + Class clazz, ClientTransportConfig config) { clientTransports.put(clazz, config); return this; @@ -312,20 +323,27 @@ private ClientTransport buildClientTransport() throws A2AClientException { AgentInterface agentInterface = findBestClientTransport(); // Get the transport provider associated with the protocol - ClientTransportProvider clientTransportProvider = transportProviderRegistry.get(agentInterface.protocolBinding()); + ClientTransportProvider clientTransportProvider = + transportProviderRegistry.get(agentInterface.protocolBinding()); if (clientTransportProvider == null) { - throw new A2AClientException("No client available for " + agentInterface.protocolBinding()); + throw new A2AClientException( + "No client available for " + agentInterface.protocolBinding()); } - Class transportProtocolClass = clientTransportProvider.getTransportProtocolClass(); + Class transportProtocolClass = + clientTransportProvider.getTransportProtocolClass(); // Retrieve the configuration associated with the preferred transport - ClientTransportConfig clientTransportConfig = clientTransports.get(transportProtocolClass); + ClientTransportConfig clientTransportConfig = + clientTransports.get(transportProtocolClass); if (clientTransportConfig == null) { - throw new A2AClientException("Missing required TransportConfig for " + agentInterface.protocolBinding()); + throw new A2AClientException( + "Missing required TransportConfig for " + agentInterface.protocolBinding()); } - return wrap(clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface), clientTransportConfig); + return wrap( + clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface), + clientTransportConfig); } private Map getServerInterfacesMap() throws A2AClientException { @@ -333,7 +351,8 @@ private Map getServerInterfacesMap() throws A2AClientExc if (serverInterfaces == null || serverInterfaces.isEmpty()) { throw new A2AClientException("No server interface available in the AgentCard"); } - // If there are multiple interfaces with the same protocol binding, only the first is considered + // If there are multiple interfaces with the same protocol binding, only the first is + // considered Map serverInterfacesMap = new LinkedHashMap<>(); for (AgentInterface iface : serverInterfaces) { serverInterfacesMap.putIfAbsent(iface.protocolBinding(), iface); @@ -348,7 +367,9 @@ private List getClientPreferredTransports() { // default to JSONRPC if not specified supportedClientTransports.add(TransportProtocol.JSONRPC.asString()); } else { - clientTransports.forEach((aClass, clientTransportConfig) -> supportedClientTransports.add(transportProtocolMapping.get(aClass))); + clientTransports.forEach( + (aClass, clientTransportConfig) -> + supportedClientTransports.add(transportProtocolMapping.get(aClass))); } return supportedClientTransports; } @@ -381,7 +402,8 @@ AgentInterface findBestClientTransport() throws A2AClientException { throw new A2AClientException("No compatible transport found"); } if (!transportProviderRegistry.containsKey(matchedInterface.protocolBinding())) { - throw new A2AClientException("No client available for " + matchedInterface.protocolBinding()); + throw new A2AClientException( + "No client available for " + matchedInterface.protocolBinding()); } return matchedInterface; @@ -396,18 +418,20 @@ AgentInterface findBestClientTransport() throws A2AClientException { * @param clientTransportConfig the transport configuration * @return the wrapped transport (or original if no wrappers are available/applicable) */ - private ClientTransport wrap(ClientTransport transport, ClientTransportConfig clientTransportConfig) { - ServiceLoader wrapperLoader = ServiceLoader.load(ClientTransportWrapper.class); + private ClientTransport wrap( + ClientTransport transport, + ClientTransportConfig clientTransportConfig) { + ServiceLoader wrapperLoader = + ServiceLoader.load(ClientTransportWrapper.class); // Collect all wrappers, sort by priority, then reverse for stack application - List wrappers = wrapperLoader.stream().map(Provider::get) - .sorted() - .collect(Collectors.toList()); + List wrappers = + wrapperLoader.stream().map(Provider::get).sorted().collect(Collectors.toList()); if (wrappers.isEmpty()) { LOGGER.debug("No client transport wrappers found via ServiceLoader"); return transport; - } + } LOGGER.debug(wrappers.size() + " client transport wrappers found via ServiceLoader"); // Reverse to apply lowest priority first (building stack with highest priority outermost) @@ -419,13 +443,18 @@ private ClientTransport wrap(ClientTransport transport, ClientTransportConfig newMetadata = task.metadata() != null ? new HashMap<>(task.metadata()) : new HashMap<>(); + Map newMetadata = + task.metadata() != null ? new HashMap<>(task.metadata()) : new HashMap<>(); newMetadata.putAll(taskStatusUpdateEvent.metadata()); taskBuilder.metadata(newMetadata); } @@ -95,11 +98,12 @@ synchronized Task saveTaskEvent(TaskArtifactUpdateEvent taskArtifactUpdateEvent) } Task task = currentTask; if (task == null) { - task = Task.builder() - .status(new TaskStatus(TaskState.TASK_STATE_UNSPECIFIED)) - .id(taskId) - .contextId(contextId == null ? "" : contextId) - .build(); + task = + Task.builder() + .status(new TaskStatus(TaskState.TASK_STATE_UNSPECIFIED)) + .id(taskId) + .contextId(contextId == null ? "" : contextId) + .build(); } currentTask = appendArtifactToTask(task, taskArtifactUpdateEvent, taskId); return currentTask; @@ -118,7 +122,8 @@ synchronized Task updateWithMessage(Message message, Task task) { List history = new ArrayList<>(task.history()); if (task.status().message() != null) { history.add(task.status().message()); - taskBuilder.status(new TaskStatus(task.status().state(), null, task.status().timestamp())); + taskBuilder.status( + new TaskStatus(task.status().state(), null, task.status().timestamp())); } history.add(message); taskBuilder.history(history); @@ -133,4 +138,4 @@ private void saveTask(Task task) { contextId = currentTask.contextId(); } } -} \ No newline at end of file +} diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/MessageEvent.java b/client/base/src/main/java/org/a2aproject/sdk/client/MessageEvent.java index 62669f8f9..adfd738b8 100644 --- a/client/base/src/main/java/org/a2aproject/sdk/client/MessageEvent.java +++ b/client/base/src/main/java/org/a2aproject/sdk/client/MessageEvent.java @@ -18,15 +18,15 @@ * client.addConsumer((event, agentCard) -> { * if (event instanceof MessageEvent me) { * Message msg = me.getMessage(); - * + * * // Extract text content * String text = msg.parts().stream() * .filter(p -> p instanceof TextPart) * .map(p -> ((TextPart) p).text()) * .collect(Collectors.joining()); - * + * * System.out.println("Agent response: " + text); - * + * * // Check for images * msg.parts().stream() * .filter(p -> p instanceof ImagePart) @@ -77,16 +77,27 @@ public Message getMessage() { @Override public String toString() { - String messageAsString = "{" - + "role=" + message.role() - + ", parts=" + message.parts() - + ", messageId=" + message.messageId() - + ", contextId=" + message.contextId() - + ", taskId=" + message.taskId() - + ", metadata=" + message.metadata() - + ", kind=" + message.kind() - + ", referenceTaskIds=" + message.referenceTaskIds() - + ", extensions=" + message.extensions() + '}'; + String messageAsString = + "{" + + "role=" + + message.role() + + ", parts=" + + message.parts() + + ", messageId=" + + message.messageId() + + ", contextId=" + + message.contextId() + + ", taskId=" + + message.taskId() + + ", metadata=" + + message.metadata() + + ", kind=" + + message.kind() + + ", referenceTaskIds=" + + message.referenceTaskIds() + + ", extensions=" + + message.extensions() + + '}'; return "MessageEvent{" + "message=" + messageAsString + '}'; } } diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/TaskEvent.java b/client/base/src/main/java/org/a2aproject/sdk/client/TaskEvent.java index bf103bc02..26908bfa3 100644 --- a/client/base/src/main/java/org/a2aproject/sdk/client/TaskEvent.java +++ b/client/base/src/main/java/org/a2aproject/sdk/client/TaskEvent.java @@ -25,7 +25,7 @@ * client.addConsumer((event, agentCard) -> { * if (event instanceof TaskEvent te) { * Task task = te.getTask(); - * + * * // Check task state * TaskState state = task.status().state(); * switch (state) { diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/TaskUpdateEvent.java b/client/base/src/main/java/org/a2aproject/sdk/client/TaskUpdateEvent.java index 33cf20f1e..94dadc4cf 100644 --- a/client/base/src/main/java/org/a2aproject/sdk/client/TaskUpdateEvent.java +++ b/client/base/src/main/java/org/a2aproject/sdk/client/TaskUpdateEvent.java @@ -36,12 +36,12 @@ * if (event instanceof TaskUpdateEvent tue) { * Task currentTask = tue.getTask(); * UpdateEvent update = tue.getUpdateEvent(); - * + * * // Handle status changes * if (update instanceof TaskStatusUpdateEvent statusUpdate) { * TaskState newState = currentTask.status().state(); * System.out.println("Task " + currentTask.id() + " → " + newState); - * + * * if (newState == TaskState.COMPLETED) { * System.out.println("Final result: " + * currentTask.artifact().parts()); @@ -50,12 +50,12 @@ * currentTask.status().message()); * } * } - * + * * // Handle new content * if (update instanceof TaskArtifactUpdateEvent artifactUpdate) { * Artifact newContent = artifactUpdate.artifact(); * System.out.println("New content received: " + newContent.parts()); - * + * * // For streaming text generation * newContent.parts().stream() * .filter(p -> p instanceof TextPart) @@ -150,5 +150,4 @@ public Task getTask() { public UpdateEvent getUpdateEvent() { return updateEvent; } - } diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/config/ClientConfig.java b/client/base/src/main/java/org/a2aproject/sdk/client/config/ClientConfig.java index b6c505490..34624b85b 100644 --- a/client/base/src/main/java/org/a2aproject/sdk/client/config/ClientConfig.java +++ b/client/base/src/main/java/org/a2aproject/sdk/client/config/ClientConfig.java @@ -4,7 +4,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.jspecify.annotations.Nullable; @@ -150,7 +149,8 @@ public class ClientConfig { private ClientConfig(Builder builder) { this.streaming = builder.streaming == null ? true : builder.streaming; this.polling = builder.polling == null ? false : builder.polling; - this.useClientPreference = builder.useClientPreference == null ? false : builder.useClientPreference; + this.useClientPreference = + builder.useClientPreference == null ? false : builder.useClientPreference; this.acceptedOutputModes = builder.acceptedOutputModes; this.taskPushNotificationConfig = builder.taskPushNotificationConfig; this.historyLength = builder.historyLength; @@ -352,7 +352,8 @@ public Builder setAcceptedOutputModes(List acceptedOutputModes) { * @return this builder for method chaining * @see org.a2aproject.sdk.client.Client#sendMessage(org.a2aproject.sdk.spec.Message, org.a2aproject.sdk.spec.TaskPushNotificationConfig, java.util.Map, org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext) */ - public Builder setTaskPushNotificationConfig(TaskPushNotificationConfig taskPushNotificationConfig) { + public Builder setTaskPushNotificationConfig( + TaskPushNotificationConfig taskPushNotificationConfig) { this.taskPushNotificationConfig = taskPushNotificationConfig; return this; } @@ -408,4 +409,4 @@ public ClientConfig build() { return new ClientConfig(this); } } -} \ No newline at end of file +} diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/config/package-info.java b/client/base/src/main/java/org/a2aproject/sdk/client/config/package-info.java index c3054ea3f..c9f3004cb 100644 --- a/client/base/src/main/java/org/a2aproject/sdk/client/config/package-info.java +++ b/client/base/src/main/java/org/a2aproject/sdk/client/config/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.config; import org.jspecify.annotations.NullMarked; - diff --git a/client/base/src/main/java/org/a2aproject/sdk/client/package-info.java b/client/base/src/main/java/org/a2aproject/sdk/client/package-info.java index f011fe21a..496d3ccfa 100644 --- a/client/base/src/main/java/org/a2aproject/sdk/client/package-info.java +++ b/client/base/src/main/java/org/a2aproject/sdk/client/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client; import org.jspecify.annotations.NullMarked; - diff --git a/client/base/src/test/java/org/a2aproject/sdk/A2ATest.java b/client/base/src/test/java/org/a2aproject/sdk/A2ATest.java index 5445c7a7f..dd1e9d5be 100644 --- a/client/base/src/test/java/org/a2aproject/sdk/A2ATest.java +++ b/client/base/src/test/java/org/a2aproject/sdk/A2ATest.java @@ -7,7 +7,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; - import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.Part; import org.a2aproject.sdk.spec.TextPart; @@ -19,7 +18,7 @@ public class A2ATest { public void testToUserMessage() { String text = "Hello, world!"; Message message = A2A.toUserMessage(text); - + assertEquals(Message.Role.ROLE_USER, message.role()); assertEquals(1, message.parts().size()); assertEquals(text, ((TextPart) message.parts().get(0)).text()); @@ -33,7 +32,7 @@ public void testToUserMessageWithId() { String text = "Hello, world!"; String messageId = "test-message-id"; Message message = A2A.toUserMessage(text, messageId); - + assertEquals(Message.Role.ROLE_USER, message.role()); assertEquals(messageId, message.messageId()); } @@ -42,7 +41,7 @@ public void testToUserMessageWithId() { public void testToAgentMessage() { String text = "Hello, I'm an agent!"; Message message = A2A.toAgentMessage(text); - + assertEquals(Message.Role.ROLE_AGENT, message.role()); assertEquals(1, message.parts().size()); assertEquals(text, ((TextPart) message.parts().get(0)).text()); @@ -54,7 +53,7 @@ public void testToAgentMessageWithId() { String text = "Hello, I'm an agent!"; String messageId = "agent-message-id"; Message message = A2A.toAgentMessage(text, messageId); - + assertEquals(Message.Role.ROLE_AGENT, message.role()); assertEquals(messageId, message.messageId()); } @@ -64,9 +63,9 @@ public void testCreateUserTextMessage() { String text = "User message with context"; String contextId = "context-123"; String taskId = "task-456"; - + Message message = A2A.createUserTextMessage(text, contextId, taskId); - + assertEquals(Message.Role.ROLE_USER, message.role()); assertEquals(contextId, message.contextId()); assertEquals(taskId, message.taskId()); @@ -80,9 +79,9 @@ public void testCreateUserTextMessage() { @Test public void testCreateUserTextMessageWithNullParams() { String text = "Simple user message"; - + Message message = A2A.createUserTextMessage(text, null, null); - + assertEquals(Message.Role.ROLE_USER, message.role()); assertNull(message.contextId()); assertNull(message.taskId()); @@ -95,9 +94,9 @@ public void testCreateAgentTextMessage() { String text = "Agent message with context"; String contextId = "context-789"; String taskId = "task-012"; - + Message message = A2A.createAgentTextMessage(text, contextId, taskId); - + assertEquals(Message.Role.ROLE_AGENT, message.role()); assertEquals(contextId, message.contextId()); assertEquals(taskId, message.taskId()); @@ -108,15 +107,12 @@ public void testCreateAgentTextMessage() { @Test public void testCreateAgentPartsMessage() { - List> parts = Arrays.asList( - new TextPart("Part 1"), - new TextPart("Part 2") - ); + List> parts = Arrays.asList(new TextPart("Part 1"), new TextPart("Part 2")); String contextId = "context-parts"; String taskId = "task-parts"; - + Message message = A2A.createAgentPartsMessage(parts, contextId, taskId); - + assertEquals(Message.Role.ROLE_AGENT, message.role()); assertEquals(contextId, message.contextId()); assertEquals(taskId, message.taskId()); @@ -144,4 +140,4 @@ public void testCreateAgentPartsMessageWithEmptyParts() { assertEquals("Parts cannot be null or empty", e.getMessage()); } } -} \ No newline at end of file +} diff --git a/client/base/src/test/java/org/a2aproject/sdk/client/AuthenticationAuthorizationTest.java b/client/base/src/test/java/org/a2aproject/sdk/client/AuthenticationAuthorizationTest.java index e00b886dd..30952d643 100644 --- a/client/base/src/test/java/org/a2aproject/sdk/client/AuthenticationAuthorizationTest.java +++ b/client/base/src/test/java/org/a2aproject/sdk/client/AuthenticationAuthorizationTest.java @@ -5,13 +5,18 @@ import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; import java.io.IOException; import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; - import org.a2aproject.sdk.client.config.ClientConfig; import org.a2aproject.sdk.client.transport.grpc.GrpcTransport; import org.a2aproject.sdk.client.transport.grpc.GrpcTransportConfigBuilder; @@ -31,12 +36,6 @@ import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.TextPart; import org.a2aproject.sdk.spec.TransportProtocol; -import io.grpc.ManagedChannel; -import io.grpc.Server; -import io.grpc.Status; -import io.grpc.inprocess.InProcessChannelBuilder; -import io.grpc.inprocess.InProcessServerBuilder; -import io.grpc.stub.StreamObserver; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -45,7 +44,7 @@ /** * Tests for handling HTTP 401 (Unauthorized) and 403 (Forbidden) responses * when the client sends streaming and non-streaming messages. - * + * * These tests verify that the client properly fails when the server returns * authentication or authorization errors. */ @@ -65,35 +64,44 @@ public class AuthenticationAuthorizationTest { @BeforeEach public void setUp() { server = new ClientAndServer(4001); - MESSAGE = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("test message"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - + MESSAGE = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("test message"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + grpcServerName = InProcessServerBuilder.generateName(); - agentCard = AgentCard.builder() - .name("Test Agent") - .description("Test agent for auth tests") - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) // Support streaming for all tests - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(AgentSkill.builder() - .id("test_skill") - .name("Test skill") - .description("Test skill") - .tags(Collections.singletonList("test")) - .build())) - .supportedInterfaces(java.util.Arrays.asList( - new AgentInterface(TransportProtocol.JSONRPC.asString(), AGENT_URL), - new AgentInterface(TransportProtocol.HTTP_JSON.asString(), AGENT_URL), - new AgentInterface(TransportProtocol.GRPC.asString(), grpcServerName))) - .build(); + agentCard = + AgentCard.builder() + .name("Test Agent") + .description("Test agent for auth tests") + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) // Support streaming for all tests + .build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills( + Collections.singletonList( + AgentSkill.builder() + .id("test_skill") + .name("Test skill") + .description("Test skill") + .tags(Collections.singletonList("test")) + .build())) + .supportedInterfaces( + java.util.Arrays.asList( + new AgentInterface( + TransportProtocol.JSONRPC.asString(), AGENT_URL), + new AgentInterface( + TransportProtocol.HTTP_JSON.asString(), AGENT_URL), + new AgentInterface( + TransportProtocol.GRPC.asString(), grpcServerName))) + .build(); } @AfterEach @@ -112,20 +120,17 @@ public void tearDown() { @Test public void testJsonRpcNonStreamingUnauthenticated() throws A2AClientException { // Mock server to return 401 for non-streaming message - server.when( - request() - .withMethod("POST") - .withPath("/") - ).respond( - response() - .withStatusCode(401) - ); + server.when(request().withMethod("POST").withPath("/")) + .respond(response().withStatusCode(401)); Client client = getJSONRPCClientBuilder(false).build(); - A2AClientException exception = assertThrows(A2AClientException.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException exception = + assertThrows( + A2AClientException.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE)); } @@ -133,20 +138,17 @@ public void testJsonRpcNonStreamingUnauthenticated() throws A2AClientException { @Test public void testJsonRpcNonStreamingUnauthorized() throws A2AClientException { // Mock server to return 403 for non-streaming message - server.when( - request() - .withMethod("POST") - .withPath("/") - ).respond( - response() - .withStatusCode(403) - ); + server.when(request().withMethod("POST").withPath("/")) + .respond(response().withStatusCode(403)); Client client = getJSONRPCClientBuilder(false).build(); - A2AClientException exception = assertThrows(A2AClientException.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException exception = + assertThrows( + A2AClientException.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE)); } @@ -154,35 +156,19 @@ public void testJsonRpcNonStreamingUnauthorized() throws A2AClientException { @Test public void testJsonRpcStreamingUnauthenticated() throws Exception { // Mock server to return 401 for streaming message - server.when( - request() - .withMethod("POST") - .withPath("/") - ).respond( - response() - .withStatusCode(401) - ); - - assertStreamingError( - getJSONRPCClientBuilder(true), - AUTHENTICATION_FAILED_MESSAGE); + server.when(request().withMethod("POST").withPath("/")) + .respond(response().withStatusCode(401)); + + assertStreamingError(getJSONRPCClientBuilder(true), AUTHENTICATION_FAILED_MESSAGE); } @Test public void testJsonRpcStreamingUnauthorized() throws Exception { // Mock server to return 403 for streaming message - server.when( - request() - .withMethod("POST") - .withPath("/") - ).respond( - response() - .withStatusCode(403) - ); - - assertStreamingError( - getJSONRPCClientBuilder(true), - AUTHORIZATION_FAILED_MESSAGE); + server.when(request().withMethod("POST").withPath("/")) + .respond(response().withStatusCode(403)); + + assertStreamingError(getJSONRPCClientBuilder(true), AUTHORIZATION_FAILED_MESSAGE); } // ========== REST Transport Tests ========== @@ -190,20 +176,17 @@ public void testJsonRpcStreamingUnauthorized() throws Exception { @Test public void testRestNonStreamingUnauthenticated() throws A2AClientException { // Mock server to return 401 for non-streaming message - server.when( - request() - .withMethod("POST") - .withPath("/message:send") - ).respond( - response() - .withStatusCode(401) - ); + server.when(request().withMethod("POST").withPath("/message:send")) + .respond(response().withStatusCode(401)); Client client = getRestClientBuilder(false).build(); - A2AClientException exception = assertThrows(A2AClientException.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException exception = + assertThrows( + A2AClientException.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE)); } @@ -211,20 +194,17 @@ public void testRestNonStreamingUnauthenticated() throws A2AClientException { @Test public void testRestNonStreamingUnauthorized() throws A2AClientException { // Mock server to return 403 for non-streaming message - server.when( - request() - .withMethod("POST") - .withPath("/message:send") - ).respond( - response() - .withStatusCode(403) - ); + server.when(request().withMethod("POST").withPath("/message:send")) + .respond(response().withStatusCode(403)); Client client = getRestClientBuilder(false).build(); - A2AClientException exception = assertThrows(A2AClientException.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException exception = + assertThrows( + A2AClientException.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE)); } @@ -232,35 +212,19 @@ public void testRestNonStreamingUnauthorized() throws A2AClientException { @Test public void testRestStreamingUnauthenticated() throws Exception { // Mock server to return 401 for streaming message - server.when( - request() - .withMethod("POST") - .withPath("/message:stream") - ).respond( - response() - .withStatusCode(401) - ); - - assertStreamingError( - getRestClientBuilder(true), - AUTHENTICATION_FAILED_MESSAGE); + server.when(request().withMethod("POST").withPath("/message:stream")) + .respond(response().withStatusCode(401)); + + assertStreamingError(getRestClientBuilder(true), AUTHENTICATION_FAILED_MESSAGE); } @Test public void testRestStreamingUnauthorized() throws Exception { // Mock server to return 403 for streaming message - server.when( - request() - .withMethod("POST") - .withPath("/message:stream") - ).respond( - response() - .withStatusCode(403) - ); - - assertStreamingError( - getRestClientBuilder(true), - AUTHORIZATION_FAILED_MESSAGE); + server.when(request().withMethod("POST").withPath("/message:stream")) + .respond(response().withStatusCode(403)); + + assertStreamingError(getRestClientBuilder(true), AUTHORIZATION_FAILED_MESSAGE); } // ========== gRPC Transport Tests ========== @@ -271,9 +235,12 @@ public void testGrpcNonStreamingUnauthenticated() throws Exception { Client client = getGrpcClientBuilder(false).build(); - A2AClientException exception = assertThrows(A2AClientException.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException exception = + assertThrows( + A2AClientException.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE)); } @@ -284,9 +251,12 @@ public void testGrpcNonStreamingUnauthorized() throws Exception { Client client = getGrpcClientBuilder(false).build(); - A2AClientException exception = assertThrows(A2AClientException.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException exception = + assertThrows( + A2AClientException.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE)); } @@ -295,18 +265,14 @@ public void testGrpcNonStreamingUnauthorized() throws Exception { public void testGrpcStreamingUnauthenticated() throws Exception { setupGrpcServer(Status.UNAUTHENTICATED); - assertStreamingError( - getGrpcClientBuilder(true), - AUTHENTICATION_FAILED_MESSAGE); + assertStreamingError(getGrpcClientBuilder(true), AUTHENTICATION_FAILED_MESSAGE); } @Test public void testGrpcStreamingUnauthorized() throws Exception { setupGrpcServer(Status.PERMISSION_DENIED); - assertStreamingError( - getGrpcClientBuilder(true), - AUTHORIZATION_FAILED_MESSAGE); + assertStreamingError(getGrpcClientBuilder(true), AUTHORIZATION_FAILED_MESSAGE); } private ClientBuilder getJSONRPCClientBuilder(boolean streaming) { @@ -324,55 +290,71 @@ private ClientBuilder getRestClientBuilder(boolean streaming) { private ClientBuilder getGrpcClientBuilder(boolean streaming) { return Client.builder(agentCard) .clientConfig(new ClientConfig.Builder().setStreaming(streaming).build()) - .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder() - .channelFactory(target -> grpcChannel)); + .withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder().channelFactory(target -> grpcChannel)); } - private void assertStreamingError(ClientBuilder clientBuilder, String expectedErrorMessage) throws Exception { + private void assertStreamingError(ClientBuilder clientBuilder, String expectedErrorMessage) + throws Exception { AtomicReference errorRef = new AtomicReference<>(); CountDownLatch errorLatch = new CountDownLatch(1); - Consumer errorHandler = error -> { - errorRef.set(error); - errorLatch.countDown(); - }; + Consumer errorHandler = + error -> { + errorRef.set(error); + errorLatch.countDown(); + }; Client client = clientBuilder.streamingErrorHandler(errorHandler).build(); try { client.sendMessage(MESSAGE); // If no immediate exception, wait for async error - assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called"); + assertTrue( + errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called"); Throwable error = errorRef.get(); - assertTrue(error.getMessage().contains(expectedErrorMessage), - "Expected error message to contain '" + expectedErrorMessage + "' but got: " + error.getMessage()); + assertTrue( + error.getMessage().contains(expectedErrorMessage), + "Expected error message to contain '" + + expectedErrorMessage + + "' but got: " + + error.getMessage()); } catch (Exception e) { // Immediate exception is also acceptable - assertTrue(e.getMessage().contains(expectedErrorMessage), - "Expected error message to contain '" + expectedErrorMessage + "' but got: " + e.getMessage()); + assertTrue( + e.getMessage().contains(expectedErrorMessage), + "Expected error message to contain '" + + expectedErrorMessage + + "' but got: " + + e.getMessage()); } } private void setupGrpcServer(Status status) throws IOException { grpcServerName = InProcessServerBuilder.generateName(); - grpcServer = InProcessServerBuilder.forName(grpcServerName) - .directExecutor() - .addService(new A2AServiceGrpc.A2AServiceImplBase() { - @Override - public void sendMessage(SendMessageRequest request, StreamObserver responseObserver) { - responseObserver.onError(status.asRuntimeException()); - } - - @Override - public void sendStreamingMessage(SendMessageRequest request, StreamObserver responseObserver) { - responseObserver.onError(status.asRuntimeException()); - } - }) - .build() - .start(); - - grpcChannel = InProcessChannelBuilder.forName(grpcServerName) - .directExecutor() - .build(); + grpcServer = + InProcessServerBuilder.forName(grpcServerName) + .directExecutor() + .addService( + new A2AServiceGrpc.A2AServiceImplBase() { + @Override + public void sendMessage( + SendMessageRequest request, + StreamObserver responseObserver) { + responseObserver.onError(status.asRuntimeException()); + } + + @Override + public void sendStreamingMessage( + SendMessageRequest request, + StreamObserver responseObserver) { + responseObserver.onError(status.asRuntimeException()); + } + }) + .build() + .start(); + + grpcChannel = InProcessChannelBuilder.forName(grpcServerName).directExecutor().build(); } -} \ No newline at end of file +} diff --git a/client/base/src/test/java/org/a2aproject/sdk/client/ClientBuilderTest.java b/client/base/src/test/java/org/a2aproject/sdk/client/ClientBuilderTest.java index 3f1c120cc..b4512dfba 100644 --- a/client/base/src/test/java/org/a2aproject/sdk/client/ClientBuilderTest.java +++ b/client/base/src/test/java/org/a2aproject/sdk/client/ClientBuilderTest.java @@ -2,7 +2,6 @@ import java.util.Collections; import java.util.List; - import org.a2aproject.sdk.client.config.ClientConfig; import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.client.transport.grpc.GrpcTransport; @@ -27,99 +26,142 @@ private static AgentCard buildCard(List interfaces) { .description("Just a hello world agent") .version("1.0.0") .documentationUrl("http://example.com/docs") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .build()) + .capabilities( + AgentCapabilities.builder().streaming(true).pushNotifications(true).build()) .defaultInputModes(Collections.singletonList("text")) .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(AgentSkill.builder() - .id("hello_world") - .name("Returns hello world") - .description("just returns hello world") - .tags(Collections.singletonList("hello world")) - .examples(List.of("hi", "hello world")) - .build())) + .skills( + Collections.singletonList( + AgentSkill.builder() + .id("hello_world") + .name("Returns hello world") + .description("just returns hello world") + .tags(Collections.singletonList("hello world")) + .examples(List.of("hi", "hello world")) + .build())) .supportedInterfaces(interfaces) .build(); } - private final AgentCard card = buildCard(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999"))); - - private final AgentCard cardWithTenant = buildCard(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999", "/default-tenant"))); - - private final AgentCard cardWithMultipleInterfaces = buildCard(List.of( - new AgentInterface(TransportProtocol.GRPC.asString(), "http://localhost:9998", "/grpc-tenant", "1.0"), - new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:9999", "/jsonrpc-tenant", "1.0"))); + private final AgentCard card = + buildCard( + List.of( + new AgentInterface( + TransportProtocol.JSONRPC.asString(), + "http://localhost:9999"))); + + private final AgentCard cardWithTenant = + buildCard( + List.of( + new AgentInterface( + TransportProtocol.JSONRPC.asString(), + "http://localhost:9999", + "/default-tenant"))); + + private final AgentCard cardWithMultipleInterfaces = + buildCard( + List.of( + new AgentInterface( + TransportProtocol.GRPC.asString(), + "http://localhost:9998", + "/grpc-tenant", + "1.0"), + new AgentInterface( + TransportProtocol.JSONRPC.asString(), + "http://localhost:9999", + "/jsonrpc-tenant", + "1.0"))); @Test public void shouldNotFindCompatibleTransport() throws A2AClientException { - A2AClientException exception = Assertions.assertThrows(A2AClientException.class, - () -> Client - .builder(card) - .clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build()) - .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder() - .channelFactory(s -> null)) - .build()); - - Assertions.assertTrue(exception.getMessage() != null && exception.getMessage().contains("No compatible transport found")); + A2AClientException exception = + Assertions.assertThrows( + A2AClientException.class, + () -> + Client.builder(card) + .clientConfig( + new ClientConfig.Builder() + .setUseClientPreference(true) + .build()) + .withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder() + .channelFactory(s -> null)) + .build()); + + Assertions.assertTrue( + exception.getMessage() != null + && exception.getMessage().contains("No compatible transport found")); } @Test public void shouldNotFindConfigurationTransport() throws A2AClientException { - A2AClientException exception = Assertions.assertThrows(A2AClientException.class, - () -> Client - .builder(card) - .clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build()) - .build()); - - Assertions.assertTrue(exception.getMessage() != null && exception.getMessage().startsWith("Missing required TransportConfig for")); + A2AClientException exception = + Assertions.assertThrows( + A2AClientException.class, + () -> + Client.builder(card) + .clientConfig( + new ClientConfig.Builder() + .setUseClientPreference(true) + .build()) + .build()); + + Assertions.assertTrue( + exception.getMessage() != null + && exception + .getMessage() + .startsWith("Missing required TransportConfig for")); } @Test public void shouldCreateJSONRPCClient() throws A2AClientException { - Client client = Client - .builder(card) - .clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build()) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder() - .addInterceptor(null) - .httpClient(null)) - .build(); + Client client = + Client.builder(card) + .clientConfig( + new ClientConfig.Builder().setUseClientPreference(true).build()) + .withTransport( + JSONRPCTransport.class, + new JSONRPCTransportConfigBuilder() + .addInterceptor(null) + .httpClient(null)) + .build(); Assertions.assertNotNull(client); } @Test public void shouldCreateClient_differentConfigurations() throws A2AClientException { - Client client = Client - .builder(card) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig(A2AHttpClientFactory.create())) - .build(); + Client client = + Client.builder(card) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .withTransport( + JSONRPCTransport.class, + new JSONRPCTransportConfig(A2AHttpClientFactory.create())) + .build(); Assertions.assertNotNull(client); } @Test public void shouldPreserveTenantFromAgentInterface() throws A2AClientException { - ClientBuilder builder = Client - .builder(cardWithTenant) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); + ClientBuilder builder = + Client.builder(cardWithTenant) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); AgentInterface selectedInterface = builder.findBestClientTransport(); Assertions.assertEquals("/default-tenant", selectedInterface.tenant()); Assertions.assertEquals("http://localhost:9999", selectedInterface.url()); - Assertions.assertEquals(TransportProtocol.JSONRPC.asString(), selectedInterface.protocolBinding()); + Assertions.assertEquals( + TransportProtocol.JSONRPC.asString(), selectedInterface.protocolBinding()); } @Test public void shouldPreserveProtocolVersionFromAgentInterface() throws A2AClientException { - ClientBuilder builder = Client - .builder(cardWithMultipleInterfaces) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); + ClientBuilder builder = + Client.builder(cardWithMultipleInterfaces) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); AgentInterface selectedInterface = builder.findBestClientTransport(); @@ -129,44 +171,53 @@ public void shouldPreserveProtocolVersionFromAgentInterface() throws A2AClientEx @Test public void shouldSelectCorrectInterfaceWithServerPreference() throws A2AClientException { - // Server preference (default): iterates server interfaces in order, picks first that client supports + // Server preference (default): iterates server interfaces in order, picks first that client + // supports // cardWithMultipleInterfaces has [GRPC, JSONRPC] - GRPC is first // Client supports both GRPC and JSONRPC, so GRPC should be selected (server's first choice) - ClientBuilder builder = Client - .builder(cardWithMultipleInterfaces) - .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(s -> null)) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); + ClientBuilder builder = + Client.builder(cardWithMultipleInterfaces) + .withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder().channelFactory(s -> null)) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); AgentInterface selectedInterface = builder.findBestClientTransport(); - Assertions.assertEquals(TransportProtocol.GRPC.asString(), selectedInterface.protocolBinding()); + Assertions.assertEquals( + TransportProtocol.GRPC.asString(), selectedInterface.protocolBinding()); Assertions.assertEquals("http://localhost:9998", selectedInterface.url()); Assertions.assertEquals("/grpc-tenant", selectedInterface.tenant()); } @Test public void shouldSelectCorrectInterfaceWithClientPreference() throws A2AClientException { - // Client preference: iterates client transports in registration order, picks first that server supports + // Client preference: iterates client transports in registration order, picks first that + // server supports // Client registers [JSONRPC, GRPC] - JSONRPC is first // Server supports both, so JSONRPC should be selected (client's first choice) - ClientBuilder builder = Client - .builder(cardWithMultipleInterfaces) - .clientConfig(new ClientConfig.Builder().setUseClientPreference(true).build()) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(s -> null)); + ClientBuilder builder = + Client.builder(cardWithMultipleInterfaces) + .clientConfig( + new ClientConfig.Builder().setUseClientPreference(true).build()) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder().channelFactory(s -> null)); AgentInterface selectedInterface = builder.findBestClientTransport(); - Assertions.assertEquals(TransportProtocol.JSONRPC.asString(), selectedInterface.protocolBinding()); + Assertions.assertEquals( + TransportProtocol.JSONRPC.asString(), selectedInterface.protocolBinding()); Assertions.assertEquals("http://localhost:9999", selectedInterface.url()); Assertions.assertEquals("/jsonrpc-tenant", selectedInterface.tenant()); } @Test public void shouldHaveNullTenantWhenNotSet() throws A2AClientException { - ClientBuilder builder = Client - .builder(card) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); + ClientBuilder builder = + Client.builder(card) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); AgentInterface selectedInterface = builder.findBestClientTransport(); diff --git a/client/base/src/test/java/org/a2aproject/sdk/client/ClientTaskManagerTest.java b/client/base/src/test/java/org/a2aproject/sdk/client/ClientTaskManagerTest.java index 4a22399d7..0fbc88145 100644 --- a/client/base/src/test/java/org/a2aproject/sdk/client/ClientTaskManagerTest.java +++ b/client/base/src/test/java/org/a2aproject/sdk/client/ClientTaskManagerTest.java @@ -1,326 +1,348 @@ -package org.a2aproject.sdk.client; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.a2aproject.sdk.spec.A2AClientInvalidArgsError; -import org.a2aproject.sdk.spec.A2AClientInvalidStateError; -import org.a2aproject.sdk.spec.Artifact; -import org.a2aproject.sdk.spec.Message; -import org.a2aproject.sdk.spec.Task; -import org.a2aproject.sdk.spec.TaskArtifactUpdateEvent; -import org.a2aproject.sdk.spec.TaskState; -import org.a2aproject.sdk.spec.TaskStatus; -import org.a2aproject.sdk.spec.TaskStatusUpdateEvent; -import org.a2aproject.sdk.spec.TextPart; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class ClientTaskManagerTest { - - private ClientTaskManager taskManager; - private Task sampleTask; - private Message sampleMessage; - - @BeforeEach - public void setUp() { - taskManager = new ClientTaskManager(); - - sampleTask = Task.builder() - .id("task123") - .contextId("context456") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - - sampleMessage = Message.builder() - .messageId("msg1") - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("Hello"))) - .build(); - } - - @Test - public void testGetCurrentTaskNoTaskRaisesError() { - A2AClientInvalidStateError exception = assertThrows( - A2AClientInvalidStateError.class, - () -> taskManager.getCurrentTask() - ); - assertTrue(exception.getMessage().contains("No current task")); - } - - @Test - public void testSaveTaskEventWithTask() throws Exception { - Task result = taskManager.saveTaskEvent(sampleTask); - - assertEquals(sampleTask, taskManager.getCurrentTask()); - assertEquals(sampleTask, result); - } - - @Test - public void testSaveTaskEventWithTaskAlreadySetRaisesError() throws Exception { - taskManager.saveTaskEvent(sampleTask); - - A2AClientInvalidArgsError exception = assertThrows( - A2AClientInvalidArgsError.class, - () -> taskManager.saveTaskEvent(sampleTask) - ); - assertTrue(exception.getMessage().contains("Task is already set, create new manager for new tasks.")); - } - - @Test - public void testSaveTaskEventWithStatusUpdate() throws Exception { - taskManager.saveTaskEvent(sampleTask); - - TaskStatusUpdateEvent statusUpdate = TaskStatusUpdateEvent.builder() - .taskId(sampleTask.id()) - .contextId(sampleTask.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, sampleMessage, null)) - .build(); - - Task updatedTask = taskManager.saveTaskEvent(statusUpdate); - - assertEquals(TaskState.TASK_STATE_COMPLETED, updatedTask.status().state()); - assertNotNull(updatedTask.history()); - assertEquals(0, updatedTask.history().size()); - assertEquals(sampleMessage, updatedTask.status().message()); - } - - @Test - public void testSaveTaskEventWithStatusUpdateAndMetadata() throws Exception { - taskManager.saveTaskEvent(sampleTask); - - Map metadata = new HashMap<>(); - metadata.put("key1", "value1"); - metadata.put("key2", 42); - - TaskStatusUpdateEvent statusUpdate = TaskStatusUpdateEvent.builder() - .taskId(sampleTask.id()) - .contextId(sampleTask.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .metadata(metadata) - .build(); - - Task updatedTask = taskManager.saveTaskEvent(statusUpdate); - - assertNotNull(updatedTask.metadata()); - assertEquals("value1", updatedTask.metadata().get("key1")); - assertEquals(42, updatedTask.metadata().get("key2")); - } - - @Test - public void testSaveTaskEventWithArtifactUpdate() throws Exception { - taskManager.saveTaskEvent(sampleTask); - - Artifact artifact = Artifact.builder() - .artifactId("art1") - .parts(Collections.singletonList(new TextPart("artifact content"))) - .build(); - - TaskArtifactUpdateEvent artifactUpdate = TaskArtifactUpdateEvent.builder() - .taskId(sampleTask.id()) - .contextId(sampleTask.contextId()) - .artifact(artifact) - .build(); - - Task updatedTask = taskManager.saveTaskEvent(artifactUpdate); - - assertNotNull(updatedTask); - assertNotNull(updatedTask.artifacts()); - assertEquals(1, updatedTask.artifacts().size()); - assertEquals("art1", updatedTask.artifacts().get(0).artifactId()); - } - - @Test - public void testSaveTaskEventCreatesTaskIfNotExists() throws Exception { - TaskStatusUpdateEvent statusUpdate = TaskStatusUpdateEvent.builder() - .taskId("new_task") - .contextId("new_context") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - - Task updatedTask = taskManager.saveTaskEvent(statusUpdate); - - assertNotNull(updatedTask); - assertEquals("new_task", updatedTask.id()); - assertEquals("new_context", updatedTask.contextId()); - assertEquals(TaskState.TASK_STATE_WORKING, updatedTask.status().state()); - } - - @Test - public void testSaveTaskEventCreatesTaskFromArtifactUpdateIfNotExists() { - Artifact artifact = Artifact.builder() - .artifactId("art1") - .parts(Collections.singletonList(new TextPart("artifact content"))) - .build(); - - TaskArtifactUpdateEvent artifactUpdate = TaskArtifactUpdateEvent.builder() - .taskId("new_task_id") - .contextId("new_context_id") - .artifact(artifact) - .build(); - - Task updatedTask = taskManager.saveTaskEvent(artifactUpdate); - - assertNotNull(updatedTask); - assertEquals("new_task_id", updatedTask.id()); - assertEquals("new_context_id", updatedTask.contextId()); - assertNotNull(updatedTask.artifacts()); - assertEquals(1, updatedTask.artifacts().size()); - } - - @Test - public void testUpdateWithMessage() { - // Use a task with mutable history list initialized properly - Task taskWithHistory = Task.builder() - .id("task123") - .contextId("context456") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - - Task updatedTask = taskManager.updateWithMessage(sampleMessage, taskWithHistory); - - assertNotNull(updatedTask.history()); - assertEquals(1, updatedTask.history().size()); - assertEquals(sampleMessage, updatedTask.history().get(0)); - } - - @Test - public void testUpdateWithMessageMovesStatusMessage() { - Message statusMessage = Message.builder() - .messageId("status_msg") - .role(Message.Role.ROLE_AGENT) - .parts(Collections.singletonList(new TextPart("Status"))) - .build(); - - Task taskWithStatusMessage = Task.builder() - .id("task123") - .contextId("context456") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, statusMessage, null)) - .build(); - - Task updatedTask = taskManager.updateWithMessage(sampleMessage, taskWithStatusMessage); - - assertNotNull(updatedTask.history()); - assertEquals(2, updatedTask.history().size()); - assertEquals(statusMessage, updatedTask.history().get(0)); - assertEquals(sampleMessage, updatedTask.history().get(1)); - assertNull(updatedTask.status().message()); - } - - @Test - public void testUpdateWithMessagePreservesExistingHistory() { - Message existingMessage = Message.builder() - .messageId("existing_msg") - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("Existing"))) - .build(); - - Task taskWithHistory = Task.builder() - .id("task123") - .contextId("context456") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .history(List.of(existingMessage)) - .build(); - - Task updatedTask = taskManager.updateWithMessage(sampleMessage, taskWithHistory); - - assertNotNull(updatedTask.history()); - assertEquals(2, updatedTask.history().size()); - assertEquals(existingMessage, updatedTask.history().get(0)); - assertEquals(sampleMessage, updatedTask.history().get(1)); - } - - @Test - public void testSaveTaskEventMultipleStatusUpdates() throws Exception { - taskManager.saveTaskEvent(sampleTask); - - // First status update - TaskStatusUpdateEvent statusUpdate1 = TaskStatusUpdateEvent.builder() - .taskId(sampleTask.id()) - .contextId(sampleTask.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, sampleMessage, null)) - .build(); - - Task updatedTask1 = taskManager.saveTaskEvent(statusUpdate1); - assertEquals(TaskState.TASK_STATE_WORKING, updatedTask1.status().state()); - assertEquals(0, updatedTask1.history().size()); - assertEquals(sampleMessage, updatedTask1.status().message()); - - // Second status update - Message secondMessage = Message.builder() - .messageId("msg2") - .role(Message.Role.ROLE_AGENT) - .parts(Collections.singletonList(new TextPart("Second message"))) - .build(); - - TaskStatusUpdateEvent statusUpdate2 = TaskStatusUpdateEvent.builder() - .taskId(sampleTask.id()) - .contextId(sampleTask.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, secondMessage, null)) - .build(); - - Task updatedTask2 = taskManager.saveTaskEvent(statusUpdate2); - assertEquals(TaskState.TASK_STATE_COMPLETED, updatedTask2.status().state()); - assertEquals(1, updatedTask2.history().size()); - assertEquals(sampleMessage, updatedTask2.history().get(0)); - assertEquals(secondMessage, updatedTask2.status().message()); - } - - @Test - public void testSaveTaskEventMultipleArtifactUpdates() throws Exception { - taskManager.saveTaskEvent(sampleTask); - - // First artifact update - Artifact artifact1 = Artifact.builder() - .artifactId("art1") - .parts(Collections.singletonList(new TextPart("First artifact"))) - .build(); - - TaskArtifactUpdateEvent artifactUpdate1 = TaskArtifactUpdateEvent.builder() - .taskId(sampleTask.id()) - .contextId(sampleTask.contextId()) - .artifact(artifact1) - .build(); - - Task updatedTask1 = taskManager.saveTaskEvent(artifactUpdate1); - assertEquals(1, updatedTask1.artifacts().size()); - - // Second artifact update - Artifact artifact2 = Artifact.builder() - .artifactId("art2") - .parts(Collections.singletonList(new TextPart("Second artifact"))) - .build(); - - TaskArtifactUpdateEvent artifactUpdate2 = TaskArtifactUpdateEvent.builder() - .taskId(sampleTask.id()) - .contextId(sampleTask.contextId()) - .artifact(artifact2) - .build(); - - Task updatedTask2 = taskManager.saveTaskEvent(artifactUpdate2); - assertEquals(2, updatedTask2.artifacts().size()); - } - - @Test - public void testSaveTaskEventWithEmptyContextId() throws Exception { - TaskStatusUpdateEvent statusUpdate = TaskStatusUpdateEvent.builder() - .taskId("task_with_empty_context") - .contextId("") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - - Task updatedTask = taskManager.saveTaskEvent(statusUpdate); - - assertNotNull(updatedTask); - assertEquals("task_with_empty_context", updatedTask.id()); - assertEquals("", updatedTask.contextId()); - } -} +package org.a2aproject.sdk.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.a2aproject.sdk.spec.A2AClientInvalidArgsError; +import org.a2aproject.sdk.spec.A2AClientInvalidStateError; +import org.a2aproject.sdk.spec.Artifact; +import org.a2aproject.sdk.spec.Message; +import org.a2aproject.sdk.spec.Task; +import org.a2aproject.sdk.spec.TaskArtifactUpdateEvent; +import org.a2aproject.sdk.spec.TaskState; +import org.a2aproject.sdk.spec.TaskStatus; +import org.a2aproject.sdk.spec.TaskStatusUpdateEvent; +import org.a2aproject.sdk.spec.TextPart; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ClientTaskManagerTest { + + private ClientTaskManager taskManager; + private Task sampleTask; + private Message sampleMessage; + + @BeforeEach + public void setUp() { + taskManager = new ClientTaskManager(); + + sampleTask = + Task.builder() + .id("task123") + .contextId("context456") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + + sampleMessage = + Message.builder() + .messageId("msg1") + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("Hello"))) + .build(); + } + + @Test + public void testGetCurrentTaskNoTaskRaisesError() { + A2AClientInvalidStateError exception = + assertThrows(A2AClientInvalidStateError.class, () -> taskManager.getCurrentTask()); + assertTrue(exception.getMessage().contains("No current task")); + } + + @Test + public void testSaveTaskEventWithTask() throws Exception { + Task result = taskManager.saveTaskEvent(sampleTask); + + assertEquals(sampleTask, taskManager.getCurrentTask()); + assertEquals(sampleTask, result); + } + + @Test + public void testSaveTaskEventWithTaskAlreadySetRaisesError() throws Exception { + taskManager.saveTaskEvent(sampleTask); + + A2AClientInvalidArgsError exception = + assertThrows( + A2AClientInvalidArgsError.class, + () -> taskManager.saveTaskEvent(sampleTask)); + assertTrue( + exception + .getMessage() + .contains("Task is already set, create new manager for new tasks.")); + } + + @Test + public void testSaveTaskEventWithStatusUpdate() throws Exception { + taskManager.saveTaskEvent(sampleTask); + + TaskStatusUpdateEvent statusUpdate = + TaskStatusUpdateEvent.builder() + .taskId(sampleTask.id()) + .contextId(sampleTask.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, sampleMessage, null)) + .build(); + + Task updatedTask = taskManager.saveTaskEvent(statusUpdate); + + assertEquals(TaskState.TASK_STATE_COMPLETED, updatedTask.status().state()); + assertNotNull(updatedTask.history()); + assertEquals(0, updatedTask.history().size()); + assertEquals(sampleMessage, updatedTask.status().message()); + } + + @Test + public void testSaveTaskEventWithStatusUpdateAndMetadata() throws Exception { + taskManager.saveTaskEvent(sampleTask); + + Map metadata = new HashMap<>(); + metadata.put("key1", "value1"); + metadata.put("key2", 42); + + TaskStatusUpdateEvent statusUpdate = + TaskStatusUpdateEvent.builder() + .taskId(sampleTask.id()) + .contextId(sampleTask.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .metadata(metadata) + .build(); + + Task updatedTask = taskManager.saveTaskEvent(statusUpdate); + + assertNotNull(updatedTask.metadata()); + assertEquals("value1", updatedTask.metadata().get("key1")); + assertEquals(42, updatedTask.metadata().get("key2")); + } + + @Test + public void testSaveTaskEventWithArtifactUpdate() throws Exception { + taskManager.saveTaskEvent(sampleTask); + + Artifact artifact = + Artifact.builder() + .artifactId("art1") + .parts(Collections.singletonList(new TextPart("artifact content"))) + .build(); + + TaskArtifactUpdateEvent artifactUpdate = + TaskArtifactUpdateEvent.builder() + .taskId(sampleTask.id()) + .contextId(sampleTask.contextId()) + .artifact(artifact) + .build(); + + Task updatedTask = taskManager.saveTaskEvent(artifactUpdate); + + assertNotNull(updatedTask); + assertNotNull(updatedTask.artifacts()); + assertEquals(1, updatedTask.artifacts().size()); + assertEquals("art1", updatedTask.artifacts().get(0).artifactId()); + } + + @Test + public void testSaveTaskEventCreatesTaskIfNotExists() throws Exception { + TaskStatusUpdateEvent statusUpdate = + TaskStatusUpdateEvent.builder() + .taskId("new_task") + .contextId("new_context") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + + Task updatedTask = taskManager.saveTaskEvent(statusUpdate); + + assertNotNull(updatedTask); + assertEquals("new_task", updatedTask.id()); + assertEquals("new_context", updatedTask.contextId()); + assertEquals(TaskState.TASK_STATE_WORKING, updatedTask.status().state()); + } + + @Test + public void testSaveTaskEventCreatesTaskFromArtifactUpdateIfNotExists() { + Artifact artifact = + Artifact.builder() + .artifactId("art1") + .parts(Collections.singletonList(new TextPart("artifact content"))) + .build(); + + TaskArtifactUpdateEvent artifactUpdate = + TaskArtifactUpdateEvent.builder() + .taskId("new_task_id") + .contextId("new_context_id") + .artifact(artifact) + .build(); + + Task updatedTask = taskManager.saveTaskEvent(artifactUpdate); + + assertNotNull(updatedTask); + assertEquals("new_task_id", updatedTask.id()); + assertEquals("new_context_id", updatedTask.contextId()); + assertNotNull(updatedTask.artifacts()); + assertEquals(1, updatedTask.artifacts().size()); + } + + @Test + public void testUpdateWithMessage() { + // Use a task with mutable history list initialized properly + Task taskWithHistory = + Task.builder() + .id("task123") + .contextId("context456") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + + Task updatedTask = taskManager.updateWithMessage(sampleMessage, taskWithHistory); + + assertNotNull(updatedTask.history()); + assertEquals(1, updatedTask.history().size()); + assertEquals(sampleMessage, updatedTask.history().get(0)); + } + + @Test + public void testUpdateWithMessageMovesStatusMessage() { + Message statusMessage = + Message.builder() + .messageId("status_msg") + .role(Message.Role.ROLE_AGENT) + .parts(Collections.singletonList(new TextPart("Status"))) + .build(); + + Task taskWithStatusMessage = + Task.builder() + .id("task123") + .contextId("context456") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING, statusMessage, null)) + .build(); + + Task updatedTask = taskManager.updateWithMessage(sampleMessage, taskWithStatusMessage); + + assertNotNull(updatedTask.history()); + assertEquals(2, updatedTask.history().size()); + assertEquals(statusMessage, updatedTask.history().get(0)); + assertEquals(sampleMessage, updatedTask.history().get(1)); + assertNull(updatedTask.status().message()); + } + + @Test + public void testUpdateWithMessagePreservesExistingHistory() { + Message existingMessage = + Message.builder() + .messageId("existing_msg") + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("Existing"))) + .build(); + + Task taskWithHistory = + Task.builder() + .id("task123") + .contextId("context456") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .history(List.of(existingMessage)) + .build(); + + Task updatedTask = taskManager.updateWithMessage(sampleMessage, taskWithHistory); + + assertNotNull(updatedTask.history()); + assertEquals(2, updatedTask.history().size()); + assertEquals(existingMessage, updatedTask.history().get(0)); + assertEquals(sampleMessage, updatedTask.history().get(1)); + } + + @Test + public void testSaveTaskEventMultipleStatusUpdates() throws Exception { + taskManager.saveTaskEvent(sampleTask); + + // First status update + TaskStatusUpdateEvent statusUpdate1 = + TaskStatusUpdateEvent.builder() + .taskId(sampleTask.id()) + .contextId(sampleTask.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING, sampleMessage, null)) + .build(); + + Task updatedTask1 = taskManager.saveTaskEvent(statusUpdate1); + assertEquals(TaskState.TASK_STATE_WORKING, updatedTask1.status().state()); + assertEquals(0, updatedTask1.history().size()); + assertEquals(sampleMessage, updatedTask1.status().message()); + + // Second status update + Message secondMessage = + Message.builder() + .messageId("msg2") + .role(Message.Role.ROLE_AGENT) + .parts(Collections.singletonList(new TextPart("Second message"))) + .build(); + + TaskStatusUpdateEvent statusUpdate2 = + TaskStatusUpdateEvent.builder() + .taskId(sampleTask.id()) + .contextId(sampleTask.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, secondMessage, null)) + .build(); + + Task updatedTask2 = taskManager.saveTaskEvent(statusUpdate2); + assertEquals(TaskState.TASK_STATE_COMPLETED, updatedTask2.status().state()); + assertEquals(1, updatedTask2.history().size()); + assertEquals(sampleMessage, updatedTask2.history().get(0)); + assertEquals(secondMessage, updatedTask2.status().message()); + } + + @Test + public void testSaveTaskEventMultipleArtifactUpdates() throws Exception { + taskManager.saveTaskEvent(sampleTask); + + // First artifact update + Artifact artifact1 = + Artifact.builder() + .artifactId("art1") + .parts(Collections.singletonList(new TextPart("First artifact"))) + .build(); + + TaskArtifactUpdateEvent artifactUpdate1 = + TaskArtifactUpdateEvent.builder() + .taskId(sampleTask.id()) + .contextId(sampleTask.contextId()) + .artifact(artifact1) + .build(); + + Task updatedTask1 = taskManager.saveTaskEvent(artifactUpdate1); + assertEquals(1, updatedTask1.artifacts().size()); + + // Second artifact update + Artifact artifact2 = + Artifact.builder() + .artifactId("art2") + .parts(Collections.singletonList(new TextPart("Second artifact"))) + .build(); + + TaskArtifactUpdateEvent artifactUpdate2 = + TaskArtifactUpdateEvent.builder() + .taskId(sampleTask.id()) + .contextId(sampleTask.contextId()) + .artifact(artifact2) + .build(); + + Task updatedTask2 = taskManager.saveTaskEvent(artifactUpdate2); + assertEquals(2, updatedTask2.artifacts().size()); + } + + @Test + public void testSaveTaskEventWithEmptyContextId() throws Exception { + TaskStatusUpdateEvent statusUpdate = + TaskStatusUpdateEvent.builder() + .taskId("task_with_empty_context") + .contextId("") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + + Task updatedTask = taskManager.saveTaskEvent(statusUpdate); + + assertNotNull(updatedTask); + assertEquals("task_with_empty_context", updatedTask.id()); + assertEquals("", updatedTask.contextId()); + } +} diff --git a/client/transport/grpc/pom.xml b/client/transport/grpc/pom.xml index cc4fc08e1..cfc8a0376 100644 --- a/client/transport/grpc/pom.xml +++ b/client/transport/grpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/EventStreamObserver.java b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/EventStreamObserver.java index efa28fd72..e42eeeaa0 100644 --- a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/EventStreamObserver.java +++ b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/EventStreamObserver.java @@ -1,14 +1,12 @@ package org.a2aproject.sdk.client.transport.grpc; - import static org.a2aproject.sdk.grpc.utils.ProtoUtils.FromProto; +import io.grpc.stub.StreamObserver; import java.util.function.Consumer; import java.util.logging.Logger; - import org.a2aproject.sdk.grpc.StreamResponse; import org.a2aproject.sdk.spec.StreamingEventKind; -import io.grpc.stub.StreamObserver; import org.jspecify.annotations.Nullable; public class EventStreamObserver implements StreamObserver { @@ -17,7 +15,8 @@ public class EventStreamObserver implements StreamObserver { private final Consumer eventHandler; @Nullable private final Consumer errorHandler; - public EventStreamObserver(Consumer eventHandler, @Nullable Consumer errorHandler) { + public EventStreamObserver( + Consumer eventHandler, @Nullable Consumer errorHandler) { this.eventHandler = eventHandler; this.errorHandler = errorHandler; } @@ -41,7 +40,10 @@ public void onNext(StreamResponse response) { default: LOGGER.warning("Invalid stream response " + response.getPayloadCase()); if (errorHandler != null) { - errorHandler.accept(new IllegalStateException("Invalid stream response from server: " + response.getPayloadCase())); + errorHandler.accept( + new IllegalStateException( + "Invalid stream response from server: " + + response.getPayloadCase())); } return; } @@ -53,7 +55,8 @@ public void onError(Throwable t) { if (errorHandler != null) { // Map gRPC errors to proper A2A exceptions if (t instanceof io.grpc.StatusRuntimeException) { - errorHandler.accept(GrpcErrorMapper.mapGrpcError((io.grpc.StatusRuntimeException) t)); + errorHandler.accept( + GrpcErrorMapper.mapGrpcError((io.grpc.StatusRuntimeException) t)); } else { errorHandler.accept(t); } diff --git a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcErrorMapper.java b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcErrorMapper.java index cbcb05752..400a0afdc 100644 --- a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcErrorMapper.java +++ b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcErrorMapper.java @@ -1,10 +1,10 @@ package org.a2aproject.sdk.client.transport.grpc; +import com.google.protobuf.InvalidProtocolBufferException; +import io.grpc.Status; +import io.grpc.protobuf.StatusProto; import java.util.HashMap; import java.util.Map; - -import com.google.protobuf.InvalidProtocolBufferException; -import org.jspecify.annotations.Nullable; import org.a2aproject.sdk.common.A2AErrorMessages; import org.a2aproject.sdk.spec.A2AClientException; import org.a2aproject.sdk.spec.A2AErrorCodes; @@ -21,8 +21,7 @@ import org.a2aproject.sdk.spec.TaskNotFoundError; import org.a2aproject.sdk.spec.UnsupportedOperationError; import org.a2aproject.sdk.spec.VersionNotSupportedError; -import io.grpc.Status; -import io.grpc.protobuf.StatusProto; +import org.jspecify.annotations.Nullable; /** * Utility class to map gRPC exceptions to appropriate A2A error types. @@ -32,22 +31,28 @@ */ public class GrpcErrorMapper { - private static final Map REASON_MAP = Map.ofEntries( - Map.entry("TASK_NOT_FOUND", A2AErrorCodes.TASK_NOT_FOUND), - Map.entry("TASK_NOT_CANCELABLE", A2AErrorCodes.TASK_NOT_CANCELABLE), - Map.entry("PUSH_NOTIFICATION_NOT_SUPPORTED", A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED), - Map.entry("UNSUPPORTED_OPERATION", A2AErrorCodes.UNSUPPORTED_OPERATION), - Map.entry("CONTENT_TYPE_NOT_SUPPORTED", A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED), - Map.entry("INVALID_AGENT_RESPONSE", A2AErrorCodes.INVALID_AGENT_RESPONSE), - Map.entry("EXTENDED_AGENT_CARD_NOT_CONFIGURED", A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED), - Map.entry("EXTENSION_SUPPORT_REQUIRED", A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED), - Map.entry("VERSION_NOT_SUPPORTED", A2AErrorCodes.VERSION_NOT_SUPPORTED), - Map.entry("INVALID_REQUEST", A2AErrorCodes.INVALID_REQUEST), - Map.entry("METHOD_NOT_FOUND", A2AErrorCodes.METHOD_NOT_FOUND), - Map.entry("INVALID_PARAMS", A2AErrorCodes.INVALID_PARAMS), - Map.entry("INTERNAL", A2AErrorCodes.INTERNAL), - Map.entry("JSON_PARSE", A2AErrorCodes.JSON_PARSE) - ); + private static final Map REASON_MAP = + Map.ofEntries( + Map.entry("TASK_NOT_FOUND", A2AErrorCodes.TASK_NOT_FOUND), + Map.entry("TASK_NOT_CANCELABLE", A2AErrorCodes.TASK_NOT_CANCELABLE), + Map.entry( + "PUSH_NOTIFICATION_NOT_SUPPORTED", + A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED), + Map.entry("UNSUPPORTED_OPERATION", A2AErrorCodes.UNSUPPORTED_OPERATION), + Map.entry( + "CONTENT_TYPE_NOT_SUPPORTED", A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED), + Map.entry("INVALID_AGENT_RESPONSE", A2AErrorCodes.INVALID_AGENT_RESPONSE), + Map.entry( + "EXTENDED_AGENT_CARD_NOT_CONFIGURED", + A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED), + Map.entry( + "EXTENSION_SUPPORT_REQUIRED", A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED), + Map.entry("VERSION_NOT_SUPPORTED", A2AErrorCodes.VERSION_NOT_SUPPORTED), + Map.entry("INVALID_REQUEST", A2AErrorCodes.INVALID_REQUEST), + Map.entry("METHOD_NOT_FOUND", A2AErrorCodes.METHOD_NOT_FOUND), + Map.entry("INVALID_PARAMS", A2AErrorCodes.INVALID_PARAMS), + Map.entry("INTERNAL", A2AErrorCodes.INTERNAL), + Map.entry("JSON_PARSE", A2AErrorCodes.JSON_PARSE)); public static A2AClientException mapGrpcError(Throwable e) { return mapGrpcError(e, "gRPC error: "); @@ -63,10 +68,14 @@ public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) { if (errorInfo != null) { A2AErrorCodes errorCode = REASON_MAP.get(errorInfo.getReason()); if (errorCode != null) { - String errorMessage = message != null ? message : (e.getMessage() != null ? e.getMessage() : ""); - Map metadata = errorInfo.getMetadataMap().isEmpty() ? null - : new HashMap(errorInfo.getMetadataMap()); - return mapByErrorCode(errorCode, errorPrefix + errorMessage, errorMessage, metadata); + String errorMessage = + message != null ? message : (e.getMessage() != null ? e.getMessage() : ""); + Map metadata = + errorInfo.getMetadataMap().isEmpty() + ? null + : new HashMap(errorInfo.getMetadataMap()); + return mapByErrorCode( + errorCode, errorPrefix + errorMessage, errorMessage, metadata); } } @@ -74,11 +83,18 @@ public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) { String desc = message != null ? message : e.getMessage() == null ? "" : e.getMessage(); return switch (code) { case NOT_FOUND -> new A2AClientException(errorPrefix + desc, new TaskNotFoundError()); - case UNIMPLEMENTED -> new A2AClientException(errorPrefix + desc, new UnsupportedOperationError()); - case INVALID_ARGUMENT -> new A2AClientException(errorPrefix + desc, new InvalidParamsError()); - case INTERNAL -> new A2AClientException(errorPrefix + desc, new org.a2aproject.sdk.spec.InternalError(null, desc, null)); - case UNAUTHENTICATED -> new A2AClientException(errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED); - case PERMISSION_DENIED -> new A2AClientException(errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED); + case UNIMPLEMENTED -> + new A2AClientException(errorPrefix + desc, new UnsupportedOperationError()); + case INVALID_ARGUMENT -> + new A2AClientException(errorPrefix + desc, new InvalidParamsError()); + case INTERNAL -> + new A2AClientException( + errorPrefix + desc, + new org.a2aproject.sdk.spec.InternalError(null, desc, null)); + case UNAUTHENTICATED -> + new A2AClientException(errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED); + case PERMISSION_DENIED -> + new A2AClientException(errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED); default -> new A2AClientException(errorPrefix + e.getMessage(), e); }; } @@ -89,7 +105,8 @@ public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) { if (rpcStatus != null) { for (com.google.protobuf.Any detail : rpcStatus.getDetailsList()) { if (detail.is(com.google.rpc.ErrorInfo.class)) { - com.google.rpc.ErrorInfo errorInfo = detail.unpack(com.google.rpc.ErrorInfo.class); + com.google.rpc.ErrorInfo errorInfo = + detail.unpack(com.google.rpc.ErrorInfo.class); if ("a2a-protocol.org".equals(errorInfo.getDomain())) { return errorInfo; } @@ -102,22 +119,63 @@ public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) { return null; } - private static A2AClientException mapByErrorCode(A2AErrorCodes errorCode, String fullMessage, String errorMessage, @Nullable Map metadata) { + private static A2AClientException mapByErrorCode( + A2AErrorCodes errorCode, + String fullMessage, + String errorMessage, + @Nullable Map metadata) { return switch (errorCode) { - case TASK_NOT_FOUND -> new A2AClientException(fullMessage, new TaskNotFoundError(errorMessage, metadata)); - case TASK_NOT_CANCELABLE -> new A2AClientException(fullMessage, new TaskNotCancelableError(null, errorMessage, metadata)); - case PUSH_NOTIFICATION_NOT_SUPPORTED -> new A2AClientException(fullMessage, new PushNotificationNotSupportedError(null, errorMessage, metadata)); - case UNSUPPORTED_OPERATION -> new A2AClientException(fullMessage, new UnsupportedOperationError(null, errorMessage, metadata)); - case CONTENT_TYPE_NOT_SUPPORTED -> new A2AClientException(fullMessage, new ContentTypeNotSupportedError(null, errorMessage, metadata)); - case INVALID_AGENT_RESPONSE -> new A2AClientException(fullMessage, new InvalidAgentResponseError(null, errorMessage, metadata)); - case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new A2AClientException(fullMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, metadata)); - case EXTENSION_SUPPORT_REQUIRED -> new A2AClientException(fullMessage, new ExtensionSupportRequiredError(null, errorMessage, metadata)); - case VERSION_NOT_SUPPORTED -> new A2AClientException(fullMessage, new VersionNotSupportedError(null, errorMessage, metadata)); - case INVALID_REQUEST -> new A2AClientException(fullMessage, new InvalidRequestError(null, errorMessage, metadata)); - case JSON_PARSE -> new A2AClientException(fullMessage, new JSONParseError(null, errorMessage, metadata)); - case METHOD_NOT_FOUND -> new A2AClientException(fullMessage, new MethodNotFoundError(null, errorMessage, metadata)); - case INVALID_PARAMS -> new A2AClientException(fullMessage, new InvalidParamsError(null, errorMessage, metadata)); - case INTERNAL -> new A2AClientException(fullMessage, new org.a2aproject.sdk.spec.InternalError(null, errorMessage, metadata)); + case TASK_NOT_FOUND -> + new A2AClientException( + fullMessage, new TaskNotFoundError(errorMessage, metadata)); + case TASK_NOT_CANCELABLE -> + new A2AClientException( + fullMessage, new TaskNotCancelableError(null, errorMessage, metadata)); + case PUSH_NOTIFICATION_NOT_SUPPORTED -> + new A2AClientException( + fullMessage, + new PushNotificationNotSupportedError(null, errorMessage, metadata)); + case UNSUPPORTED_OPERATION -> + new A2AClientException( + fullMessage, + new UnsupportedOperationError(null, errorMessage, metadata)); + case CONTENT_TYPE_NOT_SUPPORTED -> + new A2AClientException( + fullMessage, + new ContentTypeNotSupportedError(null, errorMessage, metadata)); + case INVALID_AGENT_RESPONSE -> + new A2AClientException( + fullMessage, + new InvalidAgentResponseError(null, errorMessage, metadata)); + case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> + new A2AClientException( + fullMessage, + new ExtendedAgentCardNotConfiguredError(null, errorMessage, metadata)); + case EXTENSION_SUPPORT_REQUIRED -> + new A2AClientException( + fullMessage, + new ExtensionSupportRequiredError(null, errorMessage, metadata)); + case VERSION_NOT_SUPPORTED -> + new A2AClientException( + fullMessage, + new VersionNotSupportedError(null, errorMessage, metadata)); + case INVALID_REQUEST -> + new A2AClientException( + fullMessage, new InvalidRequestError(null, errorMessage, metadata)); + case JSON_PARSE -> + new A2AClientException( + fullMessage, new JSONParseError(null, errorMessage, metadata)); + case METHOD_NOT_FOUND -> + new A2AClientException( + fullMessage, new MethodNotFoundError(null, errorMessage, metadata)); + case INVALID_PARAMS -> + new A2AClientException( + fullMessage, new InvalidParamsError(null, errorMessage, metadata)); + case INTERNAL -> + new A2AClientException( + fullMessage, + new org.a2aproject.sdk.spec.InternalError( + null, errorMessage, metadata)); }; } } diff --git a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransport.java b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransport.java index 7609bc9d5..c5f7a3071 100644 --- a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransport.java +++ b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransport.java @@ -13,11 +13,16 @@ import static org.a2aproject.sdk.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD; import static org.a2aproject.sdk.util.Assert.checkNotNullParam; +import io.grpc.Channel; +import io.grpc.Metadata; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.stream.Collectors; - import org.a2aproject.sdk.client.transport.spi.ClientTransport; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor; @@ -49,25 +54,17 @@ import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.a2aproject.sdk.spec.TaskQueryParams; import org.a2aproject.sdk.spec.util.Utils; -import io.grpc.Channel; -import io.grpc.Metadata; -import io.grpc.StatusException; -import io.grpc.StatusRuntimeException; -import io.grpc.stub.MetadataUtils; -import io.grpc.stub.StreamObserver; import org.jspecify.annotations.Nullable; public class GrpcTransport implements ClientTransport { - private static final Metadata.Key AUTHORIZATION_METADATA_KEY = Metadata.Key.of( - AuthInterceptor.AUTHORIZATION, - Metadata.ASCII_STRING_MARSHALLER); - private static final Metadata.Key EXTENSIONS_KEY = Metadata.Key.of( - A2AHeaders.A2A_EXTENSIONS.toLowerCase(), - Metadata.ASCII_STRING_MARSHALLER); - private static final Metadata.Key VERSION_KEY = Metadata.Key.of( - A2AHeaders.A2A_VERSION.toLowerCase(), - Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key AUTHORIZATION_METADATA_KEY = + Metadata.Key.of(AuthInterceptor.AUTHORIZATION, Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key EXTENSIONS_KEY = + Metadata.Key.of( + A2AHeaders.A2A_EXTENSIONS.toLowerCase(), Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key VERSION_KEY = + Metadata.Key.of(A2AHeaders.A2A_VERSION.toLowerCase(), Metadata.ASCII_STRING_MARSHALLER); private final A2AServiceBlockingV2Stub blockingStub; private final A2AServiceStub asyncStub; private final @Nullable List interceptors; @@ -78,7 +75,11 @@ public GrpcTransport(Channel channel, AgentCard agentCard) { this(channel, agentCard, "", null); } - public GrpcTransport(Channel channel, AgentCard agentCard, @Nullable String agentTenant, @Nullable List interceptors) { + public GrpcTransport( + Channel channel, + AgentCard agentCard, + @Nullable String agentTenant, + @Nullable List interceptors) { checkNotNullParam("channel", channel); checkNotNullParam("agentCard", agentCard); this.asyncStub = A2AServiceGrpc.newStub(channel); @@ -102,17 +103,21 @@ private String resolveTenant(@Nullable String requestTenant) { } @Override - public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) throws A2AClientException { + public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); MessageSendParams tenantRequest = createRequestWithTenant(request); - org.a2aproject.sdk.grpc.SendMessageRequest sendMessageRequest = createGrpcSendMessageRequest(tenantRequest, context); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_MESSAGE_METHOD, sendMessageRequest, - agentCard, context); + org.a2aproject.sdk.grpc.SendMessageRequest sendMessageRequest = + createGrpcSendMessageRequest(tenantRequest, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(SEND_MESSAGE_METHOD, sendMessageRequest, agentCard, context); try { - A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders); - org.a2aproject.sdk.grpc.SendMessageResponse response = stubWithMetadata.sendMessage(sendMessageRequest); + A2AServiceBlockingV2Stub stubWithMetadata = + createBlockingStubWithMetadata(context, payloadAndHeaders); + org.a2aproject.sdk.grpc.SendMessageResponse response = + stubWithMetadata.sendMessage(sendMessageRequest); if (response.hasMessage()) { return FromProto.message(response.getMessage()); } else if (response.hasTask()) { @@ -126,19 +131,26 @@ public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallCont } @Override - public void sendMessageStreaming(MessageSendParams request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException { + public void sendMessageStreaming( + MessageSendParams request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); checkNotNullParam("eventConsumer", eventConsumer); MessageSendParams tenantRequest = createRequestWithTenant(request); - org.a2aproject.sdk.grpc.SendMessageRequest grpcRequest = createGrpcSendMessageRequest(tenantRequest, context); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_STREAMING_MESSAGE_METHOD, - grpcRequest, agentCard, context); - StreamObserver streamObserver = new EventStreamObserver(eventConsumer, errorConsumer); + org.a2aproject.sdk.grpc.SendMessageRequest grpcRequest = + createGrpcSendMessageRequest(tenantRequest, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(SEND_STREAMING_MESSAGE_METHOD, grpcRequest, agentCard, context); + StreamObserver streamObserver = + new EventStreamObserver(eventConsumer, errorConsumer); try { - A2AServiceStub stubWithMetadata = createAsyncStubWithMetadata(context, payloadAndHeaders); + A2AServiceStub stubWithMetadata = + createAsyncStubWithMetadata(context, payloadAndHeaders); stubWithMetadata.sendStreamingMessage(grpcRequest, streamObserver); } catch (StatusRuntimeException e) { throw GrpcErrorMapper.mapGrpcError(e, "Failed to send streaming message request: "); @@ -146,20 +158,23 @@ public void sendMessageStreaming(MessageSendParams request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException { + public void subscribeToTask( + TaskIdParams request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); checkNotNullParam("eventConsumer", eventConsumer); - org.a2aproject.sdk.grpc.SubscribeToTaskRequest grpcRequest = org.a2aproject.sdk.grpc.SubscribeToTaskRequest.newBuilder() - .setTenant(resolveTenant(request.tenant())) - .setId(request.id()) - .build(); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SUBSCRIBE_TO_TASK_METHOD, grpcRequest, agentCard, context); + org.a2aproject.sdk.grpc.SubscribeToTaskRequest grpcRequest = + org.a2aproject.sdk.grpc.SubscribeToTaskRequest.newBuilder() + .setTenant(resolveTenant(request.tenant())) + .setId(request.id()) + .build(); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(SUBSCRIBE_TO_TASK_METHOD, grpcRequest, agentCard, context); - StreamObserver streamObserver = new EventStreamObserver(eventConsumer, errorConsumer); + StreamObserver streamObserver = + new EventStreamObserver(eventConsumer, errorConsumer); try { - A2AServiceStub stubWithMetadata = createAsyncStubWithMetadata(context, payloadAndHeaders); + A2AServiceStub stubWithMetadata = + createAsyncStubWithMetadata(context, payloadAndHeaders); stubWithMetadata.subscribeToTask(grpcRequest, streamObserver); } catch (StatusRuntimeException e) { throw GrpcErrorMapper.mapGrpcError(e, "Failed to subscribe to task: "); @@ -355,15 +415,21 @@ private MessageSendParams createRequestWithTenant(MessageSendParams request) { } @Override - public AgentCard getExtendedAgentCard(GetExtendedAgentCardParams params, @Nullable ClientCallContext context) throws A2AClientException { - GetExtendedAgentCardRequest request = GetExtendedAgentCardRequest.newBuilder() - .setTenant(resolveTenant(params.tenant())) - .build(); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_EXTENDED_AGENT_CARD_METHOD, request, agentCard, context); + public AgentCard getExtendedAgentCard( + GetExtendedAgentCardParams params, @Nullable ClientCallContext context) + throws A2AClientException { + GetExtendedAgentCardRequest request = + GetExtendedAgentCardRequest.newBuilder() + .setTenant(resolveTenant(params.tenant())) + .build(); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(GET_EXTENDED_AGENT_CARD_METHOD, request, agentCard, context); try { - A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders); - org.a2aproject.sdk.grpc.AgentCard response = stubWithMetadata.getExtendedAgentCard(request); + A2AServiceBlockingV2Stub stubWithMetadata = + createBlockingStubWithMetadata(context, payloadAndHeaders); + org.a2aproject.sdk.grpc.AgentCard response = + stubWithMetadata.getExtendedAgentCard(request); return FromProto.agentCard(response); } catch (StatusRuntimeException | StatusException e) { @@ -372,10 +438,10 @@ public AgentCard getExtendedAgentCard(GetExtendedAgentCardParams params, @Nullab } @Override - public void close() { - } + public void close() {} - private org.a2aproject.sdk.grpc.SendMessageRequest createGrpcSendMessageRequest(MessageSendParams messageSendParams, @Nullable ClientCallContext context) { + private org.a2aproject.sdk.grpc.SendMessageRequest createGrpcSendMessageRequest( + MessageSendParams messageSendParams, @Nullable ClientCallContext context) { return ToProto.sendMessageRequest(messageSendParams); } @@ -387,7 +453,8 @@ private org.a2aproject.sdk.grpc.SendMessageRequest createGrpcSendMessageRequest( * @param payloadAndHeaders the payload and headers wrapper, may be null * @return the gRPC metadata */ - private Metadata createGrpcMetadata(@Nullable ClientCallContext context, @Nullable PayloadAndHeaders payloadAndHeaders) { + private Metadata createGrpcMetadata( + @Nullable ClientCallContext context, @Nullable PayloadAndHeaders payloadAndHeaders) { Metadata metadata = new Metadata(); metadata.put(VERSION_KEY, AgentInterface.CURRENT_PROTOCOL_VERSION); @@ -409,17 +476,20 @@ private Metadata createGrpcMetadata(@Nullable ClientCallContext context, @Nullab } if (payloadAndHeaders != null && payloadAndHeaders.getHeaders() != null) { // Handle all headers from interceptors (including auth headers) - for (Map.Entry headerEntry : payloadAndHeaders.getHeaders().entrySet()) { + for (Map.Entry headerEntry : + payloadAndHeaders.getHeaders().entrySet()) { String headerName = headerEntry.getKey(); String headerValue = headerEntry.getValue(); if (headerValue != null) { - // Use static key for common Authorization header, create dynamic keys for others + // Use static key for common Authorization header, create dynamic keys for + // others if (AuthInterceptor.AUTHORIZATION.equals(headerName)) { metadata.put(AUTHORIZATION_METADATA_KEY, headerValue); } else { // Create a metadata key dynamically for API keys and other custom headers - Metadata.Key metadataKey = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key metadataKey = + Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER); metadata.put(metadataKey, headerValue); } } @@ -436,8 +506,8 @@ private Metadata createGrpcMetadata(@Nullable ClientCallContext context, @Nullab * @param payloadAndHeaders the payloadAndHeaders after applying any interceptors * @return blocking stub with metadata interceptor */ - private A2AServiceBlockingV2Stub createBlockingStubWithMetadata(@Nullable ClientCallContext context, - PayloadAndHeaders payloadAndHeaders) { + private A2AServiceBlockingV2Stub createBlockingStubWithMetadata( + @Nullable ClientCallContext context, PayloadAndHeaders payloadAndHeaders) { Metadata metadata = createGrpcMetadata(context, payloadAndHeaders); return blockingStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)); } @@ -449,23 +519,31 @@ private A2AServiceBlockingV2Stub createBlockingStubWithMetadata(@Nullable Client * @param payloadAndHeaders the payloadAndHeaders after applying any interceptors * @return async stub with metadata interceptor */ - private A2AServiceStub createAsyncStubWithMetadata(@Nullable ClientCallContext context, - PayloadAndHeaders payloadAndHeaders) { + private A2AServiceStub createAsyncStubWithMetadata( + @Nullable ClientCallContext context, PayloadAndHeaders payloadAndHeaders) { Metadata metadata = createGrpcMetadata(context, payloadAndHeaders); return asyncStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)); } - private PayloadAndHeaders applyInterceptors(String methodName, Object payload, - AgentCard agentCard, @Nullable ClientCallContext clientCallContext) { - PayloadAndHeaders payloadAndHeaders = new PayloadAndHeaders(payload, - clientCallContext != null ? clientCallContext.getHeaders() : null); + private PayloadAndHeaders applyInterceptors( + String methodName, + Object payload, + AgentCard agentCard, + @Nullable ClientCallContext clientCallContext) { + PayloadAndHeaders payloadAndHeaders = + new PayloadAndHeaders( + payload, clientCallContext != null ? clientCallContext.getHeaders() : null); if (interceptors != null && !interceptors.isEmpty()) { for (ClientCallInterceptor interceptor : interceptors) { - payloadAndHeaders = interceptor.intercept(methodName, payloadAndHeaders.getPayload(), - payloadAndHeaders.getHeaders(), agentCard, clientCallContext); + payloadAndHeaders = + interceptor.intercept( + methodName, + payloadAndHeaders.getPayload(), + payloadAndHeaders.getHeaders(), + agentCard, + clientCallContext); } } return payloadAndHeaders; } - } diff --git a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportConfig.java b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportConfig.java index 90d00efbe..5ea7912c9 100644 --- a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportConfig.java +++ b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportConfig.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.client.transport.grpc; +import io.grpc.Channel; import java.util.function.Function; - import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig; import org.a2aproject.sdk.util.Assert; -import io.grpc.Channel; /** * Configuration for the gRPC transport protocol. @@ -109,4 +108,4 @@ public GrpcTransportConfig(Function channelFactory) { public Function getChannelFactory() { return this.channelFactory; } -} \ No newline at end of file +} diff --git a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportConfigBuilder.java b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportConfigBuilder.java index 1bafe78df..ae2db510e 100644 --- a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportConfigBuilder.java +++ b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportConfigBuilder.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.client.transport.grpc; +import io.grpc.Channel; import java.util.function.Function; - import org.a2aproject.sdk.client.transport.spi.ClientTransportConfigBuilder; import org.a2aproject.sdk.util.Assert; -import io.grpc.Channel; import org.jspecify.annotations.Nullable; /** @@ -145,7 +144,8 @@ * @see io.grpc.ManagedChannelBuilder * @see io.grpc.Channel */ -public class GrpcTransportConfigBuilder extends ClientTransportConfigBuilder { +public class GrpcTransportConfigBuilder + extends ClientTransportConfigBuilder { private @Nullable Function channelFactory; @@ -206,4 +206,4 @@ public GrpcTransportConfig build() { config.setInterceptors(interceptors); return config; } -} \ No newline at end of file +} diff --git a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportProvider.java b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportProvider.java index c2e469554..5033f9cc3 100644 --- a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportProvider.java +++ b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportProvider.java @@ -1,24 +1,33 @@ package org.a2aproject.sdk.client.transport.grpc; +import io.grpc.Channel; import org.a2aproject.sdk.client.transport.spi.ClientTransportProvider; import org.a2aproject.sdk.spec.A2AClientException; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.TransportProtocol; -import io.grpc.Channel; /** * Provider for gRPC transport implementation. */ -public class GrpcTransportProvider implements ClientTransportProvider { +public class GrpcTransportProvider + implements ClientTransportProvider { @Override - public GrpcTransport create(GrpcTransportConfig grpcTransportConfig, AgentCard agentCard, AgentInterface agentInterface) throws A2AClientException { + public GrpcTransport create( + GrpcTransportConfig grpcTransportConfig, + AgentCard agentCard, + AgentInterface agentInterface) + throws A2AClientException { // not making use of the interceptors for gRPC for now Channel channel = grpcTransportConfig.getChannelFactory().apply(agentInterface.url()); if (channel != null) { - return new GrpcTransport(channel, agentCard, agentInterface.tenant(), grpcTransportConfig.getInterceptors()); + return new GrpcTransport( + channel, + agentCard, + agentInterface.tenant(), + grpcTransportConfig.getInterceptors()); } throw new A2AClientException("Missing required GrpcTransportConfig"); diff --git a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/package-info.java b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/package-info.java index fe7e81954..a9b1c6584 100644 --- a/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/package-info.java +++ b/client/transport/grpc/src/main/java/org/a2aproject/sdk/client/transport/grpc/package-info.java @@ -1,4 +1,4 @@ @NullMarked package org.a2aproject.sdk.client.transport.grpc; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import org.jspecify.annotations.NullMarked; diff --git a/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/EventStreamObserverTest.java b/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/EventStreamObserverTest.java index 5b5cfe673..e6a046548 100644 --- a/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/EventStreamObserverTest.java +++ b/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/EventStreamObserverTest.java @@ -23,7 +23,9 @@ void onCompleted_callsErrorHandlerWithNull() { observer.onCompleted(); assertEquals(1, errors.size(), "errorHandler should have been called once"); - assertNull(errors.get(0), "errorHandler should be called with null to signal clean completion"); + assertNull( + errors.get(0), + "errorHandler should be called with null to signal clean completion"); assertEquals(0, events.size(), "eventHandler should not be called"); } diff --git a/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/GrpcErrorMapperTest.java b/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/GrpcErrorMapperTest.java index 421d92cbb..556761cac 100644 --- a/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/GrpcErrorMapperTest.java +++ b/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/GrpcErrorMapperTest.java @@ -6,6 +6,9 @@ import com.google.protobuf.Any; import com.google.rpc.ErrorInfo; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.protobuf.StatusProto; import org.a2aproject.sdk.spec.A2AClientException; import org.a2aproject.sdk.spec.ContentTypeNotSupportedError; import org.a2aproject.sdk.spec.ExtendedAgentCardNotConfiguredError; @@ -14,9 +17,6 @@ import org.a2aproject.sdk.spec.TaskNotFoundError; import org.a2aproject.sdk.spec.UnsupportedOperationError; import org.a2aproject.sdk.spec.VersionNotSupportedError; -import io.grpc.Status; -import io.grpc.StatusRuntimeException; -import io.grpc.protobuf.StatusProto; import org.junit.jupiter.api.Test; /** @@ -25,17 +25,17 @@ */ public class GrpcErrorMapperTest { - private static StatusRuntimeException createA2AStatusException(int grpcCode, String message, String reason) { - ErrorInfo errorInfo = ErrorInfo.newBuilder() - .setReason(reason) - .setDomain("a2a-protocol.org") - .build(); + private static StatusRuntimeException createA2AStatusException( + int grpcCode, String message, String reason) { + ErrorInfo errorInfo = + ErrorInfo.newBuilder().setReason(reason).setDomain("a2a-protocol.org").build(); - com.google.rpc.Status rpcStatus = com.google.rpc.Status.newBuilder() - .setCode(grpcCode) - .setMessage(message) - .addDetails(Any.pack(errorInfo)) - .build(); + com.google.rpc.Status rpcStatus = + com.google.rpc.Status.newBuilder() + .setCode(grpcCode) + .setMessage(message) + .addDetails(Any.pack(errorInfo)) + .build(); return StatusProto.toStatusRuntimeException(rpcStatus); } @@ -43,8 +43,11 @@ private static StatusRuntimeException createA2AStatusException(int grpcCode, Str @Test public void testExtensionSupportRequiredErrorUnmarshalling() { String errorMessage = "Extension required: https://example.com/test-extension"; - StatusRuntimeException grpcException = createA2AStatusException( - Status.Code.FAILED_PRECONDITION.value(), errorMessage, "EXTENSION_SUPPORT_REQUIRED"); + StatusRuntimeException grpcException = + createA2AStatusException( + Status.Code.FAILED_PRECONDITION.value(), + errorMessage, + "EXTENSION_SUPPORT_REQUIRED"); A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException); @@ -52,7 +55,8 @@ public void testExtensionSupportRequiredErrorUnmarshalling() { assertNotNull(result.getCause()); assertInstanceOf(ExtensionSupportRequiredError.class, result.getCause()); - ExtensionSupportRequiredError extensionError = (ExtensionSupportRequiredError) result.getCause(); + ExtensionSupportRequiredError extensionError = + (ExtensionSupportRequiredError) result.getCause(); assertNotNull(extensionError.getMessage()); assertTrue(extensionError.getMessage().contains("https://example.com/test-extension")); assertTrue(result.getMessage().contains(errorMessage)); @@ -61,8 +65,9 @@ public void testExtensionSupportRequiredErrorUnmarshalling() { @Test public void testVersionNotSupportedErrorUnmarshalling() { String errorMessage = "Version 2.0 is not supported"; - StatusRuntimeException grpcException = createA2AStatusException( - Status.Code.UNIMPLEMENTED.value(), errorMessage, "VERSION_NOT_SUPPORTED"); + StatusRuntimeException grpcException = + createA2AStatusException( + Status.Code.UNIMPLEMENTED.value(), errorMessage, "VERSION_NOT_SUPPORTED"); A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException); @@ -78,8 +83,11 @@ public void testVersionNotSupportedErrorUnmarshalling() { @Test public void testExtendedCardNotConfiguredErrorUnmarshalling() { String errorMessage = "Extended card not configured for this agent"; - StatusRuntimeException grpcException = createA2AStatusException( - Status.Code.FAILED_PRECONDITION.value(), errorMessage, "EXTENDED_AGENT_CARD_NOT_CONFIGURED"); + StatusRuntimeException grpcException = + createA2AStatusException( + Status.Code.FAILED_PRECONDITION.value(), + errorMessage, + "EXTENDED_AGENT_CARD_NOT_CONFIGURED"); A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException); @@ -87,7 +95,8 @@ public void testExtendedCardNotConfiguredErrorUnmarshalling() { assertNotNull(result.getCause()); assertInstanceOf(ExtendedAgentCardNotConfiguredError.class, result.getCause()); - ExtendedAgentCardNotConfiguredError extendedCardError = (ExtendedAgentCardNotConfiguredError) result.getCause(); + ExtendedAgentCardNotConfiguredError extendedCardError = + (ExtendedAgentCardNotConfiguredError) result.getCause(); assertNotNull(extendedCardError.getMessage()); assertTrue(extendedCardError.getMessage().contains("Extended card not configured")); } @@ -95,8 +104,9 @@ public void testExtendedCardNotConfiguredErrorUnmarshalling() { @Test public void testTaskNotFoundErrorUnmarshalling() { String errorMessage = "Task task-123 not found"; - StatusRuntimeException grpcException = createA2AStatusException( - Status.Code.NOT_FOUND.value(), errorMessage, "TASK_NOT_FOUND"); + StatusRuntimeException grpcException = + createA2AStatusException( + Status.Code.NOT_FOUND.value(), errorMessage, "TASK_NOT_FOUND"); A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException); @@ -108,8 +118,9 @@ public void testTaskNotFoundErrorUnmarshalling() { @Test public void testUnsupportedOperationErrorUnmarshalling() { String errorMessage = "Operation not supported"; - StatusRuntimeException grpcException = createA2AStatusException( - Status.Code.UNIMPLEMENTED.value(), errorMessage, "UNSUPPORTED_OPERATION"); + StatusRuntimeException grpcException = + createA2AStatusException( + Status.Code.UNIMPLEMENTED.value(), errorMessage, "UNSUPPORTED_OPERATION"); A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException); @@ -121,8 +132,9 @@ public void testUnsupportedOperationErrorUnmarshalling() { @Test public void testInvalidParamsErrorUnmarshalling() { String errorMessage = "Invalid parameters provided"; - StatusRuntimeException grpcException = createA2AStatusException( - Status.Code.INVALID_ARGUMENT.value(), errorMessage, "INVALID_PARAMS"); + StatusRuntimeException grpcException = + createA2AStatusException( + Status.Code.INVALID_ARGUMENT.value(), errorMessage, "INVALID_PARAMS"); A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException); @@ -134,8 +146,11 @@ public void testInvalidParamsErrorUnmarshalling() { @Test public void testContentTypeNotSupportedErrorUnmarshalling() { String errorMessage = "Content type application/xml not supported"; - StatusRuntimeException grpcException = createA2AStatusException( - Status.Code.INVALID_ARGUMENT.value(), errorMessage, "CONTENT_TYPE_NOT_SUPPORTED"); + StatusRuntimeException grpcException = + createA2AStatusException( + Status.Code.INVALID_ARGUMENT.value(), + errorMessage, + "CONTENT_TYPE_NOT_SUPPORTED"); A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException); @@ -143,17 +158,20 @@ public void testContentTypeNotSupportedErrorUnmarshalling() { assertNotNull(result.getCause()); assertInstanceOf(ContentTypeNotSupportedError.class, result.getCause()); - ContentTypeNotSupportedError contentTypeError = (ContentTypeNotSupportedError) result.getCause(); + ContentTypeNotSupportedError contentTypeError = + (ContentTypeNotSupportedError) result.getCause(); assertNotNull(contentTypeError.getMessage()); - assertTrue(contentTypeError.getMessage().contains("Content type application/xml not supported")); + assertTrue( + contentTypeError + .getMessage() + .contains("Content type application/xml not supported")); } @Test public void testFallbackToStatusCodeMapping() { // Create a gRPC StatusRuntimeException without ErrorInfo details - StatusRuntimeException grpcException = Status.NOT_FOUND - .withDescription("Generic not found error") - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.NOT_FOUND.withDescription("Generic not found error").asRuntimeException(); A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException); @@ -166,8 +184,11 @@ public void testFallbackToStatusCodeMapping() { @Test public void testCustomErrorPrefix() { String errorMessage = "Extension required: https://example.com/ext"; - StatusRuntimeException grpcException = createA2AStatusException( - Status.Code.FAILED_PRECONDITION.value(), errorMessage, "EXTENSION_SUPPORT_REQUIRED"); + StatusRuntimeException grpcException = + createA2AStatusException( + Status.Code.FAILED_PRECONDITION.value(), + errorMessage, + "EXTENSION_SUPPORT_REQUIRED"); String customPrefix = "Custom Error: "; A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException, customPrefix); diff --git a/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportTest.java b/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportTest.java index 5fd8363c2..b8c10cda2 100644 --- a/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportTest.java +++ b/client/transport/grpc/src/test/java/org/a2aproject/sdk/client/transport/grpc/GrpcTransportTest.java @@ -3,15 +3,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - import io.grpc.ManagedChannel; import io.grpc.Server; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.stub.StreamObserver; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import org.a2aproject.sdk.grpc.A2AServiceGrpc; import org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest; import org.a2aproject.sdk.spec.AgentCapabilities; @@ -30,41 +29,54 @@ class GrpcTransportTest { private static final String TASK_ID = "task-1"; private static final String CALLBACK_URL = "https://example.com/callback"; - private static final AgentCard CARD = AgentCard.builder() - .name("Test Agent") - .description("Agent for gRPC transport tests") - .version("1.0.0") - .supportedInterfaces(List.of(new AgentInterface("GRPC", "http://localhost"))) - .capabilities(AgentCapabilities.builder().pushNotifications(true).build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + private static final AgentCard CARD = + AgentCard.builder() + .name("Test Agent") + .description("Agent for gRPC transport tests") + .version("1.0.0") + .supportedInterfaces(List.of(new AgentInterface("GRPC", "http://localhost"))) + .capabilities(AgentCapabilities.builder().pushNotifications(true).build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); - private final AtomicReference receivedRequest = new AtomicReference<>(); + private final AtomicReference receivedRequest = + new AtomicReference<>(); private Server server; private ManagedChannel channel; @BeforeEach void setUp() throws Exception { String serverName = InProcessServerBuilder.generateName(); - server = InProcessServerBuilder.forName(serverName) - .directExecutor() - .addService(new A2AServiceGrpc.A2AServiceImplBase() { - @Override - public void getTaskPushNotificationConfig(GetTaskPushNotificationConfigRequest request, - StreamObserver responseObserver) { - receivedRequest.set(request); - responseObserver.onNext(org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder() - .setTaskId(request.getTaskId()) - .setId(request.getId().isEmpty() ? request.getTaskId() : request.getId()) - .setUrl(CALLBACK_URL) - .build()); - responseObserver.onCompleted(); - } - }) - .build() - .start(); + server = + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService( + new A2AServiceGrpc.A2AServiceImplBase() { + @Override + public void getTaskPushNotificationConfig( + GetTaskPushNotificationConfigRequest request, + StreamObserver< + org.a2aproject.sdk.grpc + .TaskPushNotificationConfig> + responseObserver) { + receivedRequest.set(request); + responseObserver.onNext( + org.a2aproject.sdk.grpc.TaskPushNotificationConfig + .newBuilder() + .setTaskId(request.getTaskId()) + .setId( + request.getId().isEmpty() + ? request.getTaskId() + : request.getId()) + .setUrl(CALLBACK_URL) + .build()); + responseObserver.onCompleted(); + } + }) + .build() + .start(); channel = InProcessChannelBuilder.forName(serverName).directExecutor().build(); } @@ -76,36 +88,52 @@ void tearDown() throws InterruptedException { @Test void testGetPushNotificationConfigWithOmittedId() throws Exception { - assertGetPushNotificationConfig(new GetTaskPushNotificationConfigParams(TASK_ID), null, "", ""); + assertGetPushNotificationConfig( + new GetTaskPushNotificationConfigParams(TASK_ID), null, "", ""); } @Test void testGetPushNotificationConfigWithOmittedIdViaBuilder() throws Exception { - assertGetPushNotificationConfig(GetTaskPushNotificationConfigParams.builder().taskId(TASK_ID).build(), - null, "", ""); + assertGetPushNotificationConfig( + GetTaskPushNotificationConfigParams.builder().taskId(TASK_ID).build(), + null, + "", + ""); } @Test void testGetPushNotificationConfigWithEmptyId() throws Exception { - assertGetPushNotificationConfig(new GetTaskPushNotificationConfigParams(TASK_ID, ""), null, "", ""); + assertGetPushNotificationConfig( + new GetTaskPushNotificationConfigParams(TASK_ID, ""), null, "", ""); } @Test void testGetPushNotificationConfigWithExplicitIdAndDefaultTenant() throws Exception { - assertGetPushNotificationConfig(new GetTaskPushNotificationConfigParams(TASK_ID, "config-1"), - "default-tenant", "config-1", "default-tenant"); + assertGetPushNotificationConfig( + new GetTaskPushNotificationConfigParams(TASK_ID, "config-1"), + "default-tenant", + "config-1", + "default-tenant"); } @Test void testGetPushNotificationConfigWithOmittedIdAndRequestTenant() throws Exception { - assertGetPushNotificationConfig(new GetTaskPushNotificationConfigParams(TASK_ID, null, "request-tenant"), - "default-tenant", "", "request-tenant"); + assertGetPushNotificationConfig( + new GetTaskPushNotificationConfigParams(TASK_ID, null, "request-tenant"), + "default-tenant", + "", + "request-tenant"); } - private void assertGetPushNotificationConfig(GetTaskPushNotificationConfigParams params, - @Nullable String defaultTenant, String expectedId, String expectedTenant) throws Exception { + private void assertGetPushNotificationConfig( + GetTaskPushNotificationConfigParams params, + @Nullable String defaultTenant, + String expectedId, + String expectedTenant) + throws Exception { GrpcTransport transport = new GrpcTransport(channel, CARD, defaultTenant, null); - TaskPushNotificationConfig result = transport.getTaskPushNotificationConfiguration(params, null); + TaskPushNotificationConfig result = + transport.getTaskPushNotificationConfiguration(params, null); GetTaskPushNotificationConfigRequest request = receivedRequest.get(); assertNotNull(request); diff --git a/client/transport/jsonrpc/pom.xml b/client/transport/jsonrpc/pom.xml index 5a117b7e6..ba9e91c2f 100644 --- a/client/transport/jsonrpc/pom.xml +++ b/client/transport/jsonrpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -65,4 +63,4 @@ - \ No newline at end of file + diff --git a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransport.java b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransport.java index eb94d50a7..7698ff7e0 100644 --- a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransport.java +++ b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransport.java @@ -13,18 +13,15 @@ import static org.a2aproject.sdk.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD; import static org.a2aproject.sdk.util.Assert.checkNotNullParam; +import com.google.protobuf.MessageOrBuilder; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; - -import com.google.protobuf.MessageOrBuilder; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; -import org.a2aproject.sdk.common.A2AHeaders; import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.client.transport.jsonrpc.sse.SSEEventListener; @@ -32,11 +29,13 @@ import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor; import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders; +import org.a2aproject.sdk.common.A2AHeaders; import org.a2aproject.sdk.grpc.utils.JSONRPCUtils; import org.a2aproject.sdk.grpc.utils.ProtoUtils; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; import org.a2aproject.sdk.jsonrpc.common.wrappers.A2AResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.CancelTaskResponse; +import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.DeleteTaskPushNotificationConfigResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.GetExtendedAgentCardResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.GetTaskPushNotificationConfigResponse; @@ -45,7 +44,6 @@ import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; import org.a2aproject.sdk.jsonrpc.common.wrappers.SendMessageResponse; -import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigResponse; import org.a2aproject.sdk.spec.A2AClientError; import org.a2aproject.sdk.spec.A2AClientException; import org.a2aproject.sdk.spec.A2AClientHTTPError; @@ -78,22 +76,31 @@ public class JSONRPCTransport implements ClientTransport { private final boolean followRedirects; public JSONRPCTransport(String agentUrl) { - this(null, null, new AgentInterface("JSONRPC", agentUrl), null); + this(null, null, new AgentInterface("JSONRPC", agentUrl), null); } public JSONRPCTransport(AgentCard agentCard) { this(null, agentCard, Utils.getFavoriteInterface(agentCard), null); } - public JSONRPCTransport(@Nullable A2AHttpClient httpClient, @Nullable AgentCard agentCard, - AgentInterface agentInterface, @Nullable List interceptors) { + public JSONRPCTransport( + @Nullable A2AHttpClient httpClient, + @Nullable AgentCard agentCard, + AgentInterface agentInterface, + @Nullable List interceptors) { this(httpClient, agentCard, agentInterface, interceptors, false); } - public JSONRPCTransport(@Nullable A2AHttpClient httpClient, @Nullable AgentCard agentCard, - AgentInterface agentInterface, @Nullable List interceptors, - boolean followRedirects) { - this.httpClient = httpClient == null ? A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT) : httpClient; + public JSONRPCTransport( + @Nullable A2AHttpClient httpClient, + @Nullable AgentCard agentCard, + AgentInterface agentInterface, + @Nullable List interceptors, + boolean followRedirects) { + this.httpClient = + httpClient == null + ? A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT) + : httpClient; this.agentCard = agentCard; this.agentInterface = agentInterface; this.interceptors = interceptors; @@ -101,13 +108,19 @@ public JSONRPCTransport(@Nullable A2AHttpClient httpClient, @Nullable AgentCard } @Override - public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) throws A2AClientException { + public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_MESSAGE_METHOD, ProtoUtils.ToProto.sendMessageRequest(request), - agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + SEND_MESSAGE_METHOD, + ProtoUtils.ToProto.sendMessageRequest(request), + agentCard, + context); try { - String httpResponseBody = sendPostRequest(agentInterface.url(), payloadAndHeaders, SEND_MESSAGE_METHOD); + String httpResponseBody = + sendPostRequest(agentInterface.url(), payloadAndHeaders, SEND_MESSAGE_METHOD); SendMessageResponse response = unmarshalResponse(httpResponseBody, SEND_MESSAGE_METHOD); return response.getResult(); } catch (A2AClientException e) { @@ -118,43 +131,64 @@ public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallCont } @Override - public void sendMessageStreaming(MessageSendParams request, Consumer eventConsumer, - @Nullable Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException { + public void sendMessageStreaming( + MessageSendParams request, + Consumer eventConsumer, + @Nullable Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); checkNotNullParam("eventConsumer", eventConsumer); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_STREAMING_MESSAGE_METHOD, - ProtoUtils.ToProto.sendMessageRequest(request), agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + SEND_STREAMING_MESSAGE_METHOD, + ProtoUtils.ToProto.sendMessageRequest(request), + agentCard, + context); final AtomicReference> ref = new AtomicReference<>(); SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer); try { - A2AHttpClient.PostBuilder builder = createPostBuilder( - agentInterface.url(), payloadAndHeaders, SEND_STREAMING_MESSAGE_METHOD, false); - ref.set(builder.postAsyncSSE( - event -> sseEventListener.onMessage(event, ref.get()), - throwable -> sseEventListener.onError(throwable, ref.get()), - () -> { - // Signal normal stream completion to error handler (null error means success) - sseEventListener.onComplete(); - })); + A2AHttpClient.PostBuilder builder = + createPostBuilder( + agentInterface.url(), + payloadAndHeaders, + SEND_STREAMING_MESSAGE_METHOD, + false); + ref.set( + builder.postAsyncSSE( + event -> sseEventListener.onMessage(event, ref.get()), + throwable -> sseEventListener.onError(throwable, ref.get()), + () -> { + // Signal normal stream completion to error handler (null error + // means success) + sseEventListener.onComplete(); + })); } catch (IOException e) { throw new A2AClientException("Failed to send streaming message request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException("Send streaming message request timed out: " + e, e); } catch (JsonProcessingException e) { - throw new A2AClientException("Failed to process JSON for streaming message request: " + e, e); + throw new A2AClientException( + "Failed to process JSON for streaming message request: " + e, e); } } @Override - public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) throws A2AClientException { + public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_TASK_METHOD, ProtoUtils.ToProto.getTaskRequest(request), - agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + GET_TASK_METHOD, + ProtoUtils.ToProto.getTaskRequest(request), + agentCard, + context); try { - String httpResponseBody = sendPostRequest(agentInterface.url(), payloadAndHeaders, GET_TASK_METHOD); + String httpResponseBody = + sendPostRequest(agentInterface.url(), payloadAndHeaders, GET_TASK_METHOD); GetTaskResponse response = unmarshalResponse(httpResponseBody, GET_TASK_METHOD); return response.getResult(); } catch (A2AClientException e) { @@ -165,13 +199,19 @@ public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context } @Override - public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) throws A2AClientException { + public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(CANCEL_TASK_METHOD, ProtoUtils.ToProto.cancelTaskRequest(request), - agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + CANCEL_TASK_METHOD, + ProtoUtils.ToProto.cancelTaskRequest(request), + agentCard, + context); try { - String httpResponseBody = sendPostRequest(agentInterface.url(), payloadAndHeaders, CANCEL_TASK_METHOD); + String httpResponseBody = + sendPostRequest(agentInterface.url(), payloadAndHeaders, CANCEL_TASK_METHOD); CancelTaskResponse response = unmarshalResponse(httpResponseBody, CANCEL_TASK_METHOD); return response.getResult(); } catch (A2AClientException e) { @@ -182,12 +222,18 @@ public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext con } @Override - public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) throws A2AClientException { + public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_METHOD, ProtoUtils.ToProto.listTasksParams(request), - agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + LIST_TASK_METHOD, + ProtoUtils.ToProto.listTasksParams(request), + agentCard, + context); try { - String httpResponseBody = sendPostRequest(agentInterface.url(), payloadAndHeaders, LIST_TASK_METHOD); + String httpResponseBody = + sendPostRequest(agentInterface.url(), payloadAndHeaders, LIST_TASK_METHOD); ListTasksResponse response = unmarshalResponse(httpResponseBody, LIST_TASK_METHOD); return response.getResult(); } catch (IOException | InterruptedException | JsonProcessingException e) { @@ -196,15 +242,25 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo } @Override - public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request, - @Nullable ClientCallContext context) throws A2AClientException { + public TaskPushNotificationConfig createTaskPushNotificationConfiguration( + TaskPushNotificationConfig request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, - ProtoUtils.ToProto.createTaskPushNotificationConfigRequest(request), agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + ProtoUtils.ToProto.createTaskPushNotificationConfigRequest(request), + agentCard, + context); try { - String httpResponseBody = sendPostRequest(agentInterface.url(), payloadAndHeaders, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); - CreateTaskPushNotificationConfigResponse response = unmarshalResponse(httpResponseBody, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + String httpResponseBody = + sendPostRequest( + agentInterface.url(), + payloadAndHeaders, + SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + CreateTaskPushNotificationConfigResponse response = + unmarshalResponse(httpResponseBody, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); return response.getResult(); } catch (A2AClientException e) { throw e; @@ -214,15 +270,25 @@ public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPu } @Override - public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request, - @Nullable ClientCallContext context) throws A2AClientException { + public TaskPushNotificationConfig getTaskPushNotificationConfiguration( + GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, - ProtoUtils.ToProto.getTaskPushNotificationConfigRequest(request), agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + ProtoUtils.ToProto.getTaskPushNotificationConfigRequest(request), + agentCard, + context); try { - String httpResponseBody = sendPostRequest(agentInterface.url(), payloadAndHeaders, GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); - GetTaskPushNotificationConfigResponse response = unmarshalResponse(httpResponseBody, GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + String httpResponseBody = + sendPostRequest( + agentInterface.url(), + payloadAndHeaders, + GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + GetTaskPushNotificationConfigResponse response = + unmarshalResponse(httpResponseBody, GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); return response.getResult(); } catch (A2AClientException e) { throw e; @@ -233,15 +299,24 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPu @Override public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations( - ListTaskPushNotificationConfigsParams request, - @Nullable ClientCallContext context) throws A2AClientException { + ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, - ProtoUtils.ToProto.listTaskPushNotificationConfigsRequest(request), agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + ProtoUtils.ToProto.listTaskPushNotificationConfigsRequest(request), + agentCard, + context); try { - String httpResponseBody = sendPostRequest(agentInterface.url(), payloadAndHeaders, LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); - ListTaskPushNotificationConfigsResponse response = unmarshalResponse(httpResponseBody, LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + String httpResponseBody = + sendPostRequest( + agentInterface.url(), + payloadAndHeaders, + LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + ListTaskPushNotificationConfigsResponse response = + unmarshalResponse(httpResponseBody, LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); return response.getResult(); } catch (A2AClientException e) { throw e; @@ -251,67 +326,107 @@ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurati } @Override - public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request, - @Nullable ClientCallContext context) throws A2AClientException { + public void deleteTaskPushNotificationConfigurations( + DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, - ProtoUtils.ToProto.deleteTaskPushNotificationConfigRequest(request), agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + ProtoUtils.ToProto.deleteTaskPushNotificationConfigRequest(request), + agentCard, + context); try { - String httpResponseBody = sendPostRequest(agentInterface.url(), payloadAndHeaders, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); - DeleteTaskPushNotificationConfigResponse response = unmarshalResponse(httpResponseBody, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + String httpResponseBody = + sendPostRequest( + agentInterface.url(), + payloadAndHeaders, + DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + DeleteTaskPushNotificationConfigResponse response = + unmarshalResponse( + httpResponseBody, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); // Response validated (no error), but no result to return } catch (A2AClientException e) { throw e; } catch (IOException | InterruptedException | JsonProcessingException e) { - throw new A2AClientException("Failed to delete task push notification configs: " + e, e); + throw new A2AClientException( + "Failed to delete task push notification configs: " + e, e); } } @Override - public void subscribeToTask(TaskIdParams request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException { + public void subscribeToTask( + TaskIdParams request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); checkNotNullParam("eventConsumer", eventConsumer); checkNotNullParam("errorConsumer", errorConsumer); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SUBSCRIBE_TO_TASK_METHOD, ProtoUtils.ToProto.subscribeToTaskRequest(request), agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + SUBSCRIBE_TO_TASK_METHOD, + ProtoUtils.ToProto.subscribeToTaskRequest(request), + agentCard, + context); AtomicReference> ref = new AtomicReference<>(); SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer); try { - A2AHttpClient.PostBuilder builder = createPostBuilder( - agentInterface.url(), payloadAndHeaders, SUBSCRIBE_TO_TASK_METHOD, false); - ref.set(builder.postAsyncSSE( - event -> sseEventListener.onMessage(event, ref.get()), - throwable -> sseEventListener.onError(throwable, ref.get()), - () -> { - // Signal normal stream completion to error handler (null error means success) - sseEventListener.onComplete(); - })); + A2AHttpClient.PostBuilder builder = + createPostBuilder( + agentInterface.url(), + payloadAndHeaders, + SUBSCRIBE_TO_TASK_METHOD, + false); + ref.set( + builder.postAsyncSSE( + event -> sseEventListener.onMessage(event, ref.get()), + throwable -> sseEventListener.onError(throwable, ref.get()), + () -> { + // Signal normal stream completion to error handler (null error + // means success) + sseEventListener.onComplete(); + })); } catch (IOException e) { throw new A2AClientException("Failed to send task resubscription request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException("Task resubscription request timed out: " + e, e); } catch (JsonProcessingException e) { - throw new A2AClientException("Failed to process JSON for task resubscription request: " + e, e); + throw new A2AClientException( + "Failed to process JSON for task resubscription request: " + e, e); } } @Override - public AgentCard getExtendedAgentCard(GetExtendedAgentCardParams params, @Nullable ClientCallContext context) throws A2AClientException { + public AgentCard getExtendedAgentCard( + GetExtendedAgentCardParams params, @Nullable ClientCallContext context) + throws A2AClientException { try { - PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_EXTENDED_AGENT_CARD_METHOD, - ProtoUtils.ToProto.extendedAgentCard(params), agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + GET_EXTENDED_AGENT_CARD_METHOD, + ProtoUtils.ToProto.extendedAgentCard(params), + agentCard, + context); try { - String httpResponseBody = sendPostRequest(agentInterface.url(), payloadAndHeaders, GET_EXTENDED_AGENT_CARD_METHOD); - GetExtendedAgentCardResponse response = unmarshalResponse(httpResponseBody, GET_EXTENDED_AGENT_CARD_METHOD); + String httpResponseBody = + sendPostRequest( + agentInterface.url(), + payloadAndHeaders, + GET_EXTENDED_AGENT_CARD_METHOD); + GetExtendedAgentCardResponse response = + unmarshalResponse(httpResponseBody, GET_EXTENDED_AGENT_CARD_METHOD); return response.getResult(); } catch (IOException | InterruptedException | JsonProcessingException e) { - throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e); + throw new A2AClientException( + "Failed to get authenticated extended agent card: " + e, e); } - } catch(A2AClientError e){ + } catch (A2AClientError e) { throw new A2AClientException("Failed to get agent card: " + e, e); } } @@ -321,25 +436,39 @@ public void close() { // no-op } - private PayloadAndHeaders applyInterceptors(String methodName, @Nullable Object payload, - @Nullable AgentCard agentCard, @Nullable ClientCallContext clientCallContext) { - PayloadAndHeaders payloadAndHeaders = new PayloadAndHeaders(payload, getHttpHeaders(clientCallContext)); - if (interceptors != null && ! interceptors.isEmpty()) { + private PayloadAndHeaders applyInterceptors( + String methodName, + @Nullable Object payload, + @Nullable AgentCard agentCard, + @Nullable ClientCallContext clientCallContext) { + PayloadAndHeaders payloadAndHeaders = + new PayloadAndHeaders(payload, getHttpHeaders(clientCallContext)); + if (interceptors != null && !interceptors.isEmpty()) { for (ClientCallInterceptor interceptor : interceptors) { - payloadAndHeaders = interceptor.intercept(methodName, payloadAndHeaders.getPayload(), - payloadAndHeaders.getHeaders(), agentCard, clientCallContext); + payloadAndHeaders = + interceptor.intercept( + methodName, + payloadAndHeaders.getPayload(), + payloadAndHeaders.getHeaders(), + agentCard, + clientCallContext); } } return payloadAndHeaders; } - private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders, String method) throws IOException, InterruptedException, JsonProcessingException { - A2AHttpClient.PostBuilder builder = createPostBuilder(url, payloadAndHeaders, method, followRedirects); + private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders, String method) + throws IOException, InterruptedException, JsonProcessingException { + A2AHttpClient.PostBuilder builder = + createPostBuilder(url, payloadAndHeaders, method, followRedirects); A2AHttpResponse response = builder.post(); if (!response.success()) { int status = response.status(); String message = "Request failed with HTTP " + status; - throw new A2AClientException(message, new A2AClientHTTPError(status, message, response.body(), response.headers().toMap())); + throw new A2AClientException( + message, + new A2AClientHTTPError( + status, message, response.body(), response.headers().toMap())); } return response.body(); } @@ -347,12 +476,18 @@ private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders, private A2AHttpClient.PostBuilder createPostBuilder( String url, PayloadAndHeaders payloadAndHeaders, String method, boolean followRedirects) throws JsonProcessingException { - A2AHttpClient.PostBuilder postBuilder = httpClient.createPost() - .url(url) - .addHeader("Content-Type", "application/json") - .addHeader(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION) - .followRedirects(followRedirects) - .body(JSONRPCUtils.toJsonRPCRequest(null, method, (MessageOrBuilder) payloadAndHeaders.getPayload())); + A2AHttpClient.PostBuilder postBuilder = + httpClient + .createPost() + .url(url) + .addHeader("Content-Type", "application/json") + .addHeader(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION) + .followRedirects(followRedirects) + .body( + JSONRPCUtils.toJsonRPCRequest( + null, + method, + (MessageOrBuilder) payloadAndHeaders.getPayload())); if (payloadAndHeaders.getHeaders() != null) { for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { @@ -383,9 +518,13 @@ private > T unmarshalResponse(String response, String m A2AResponse value = JSONRPCUtils.parseResponseBody(response, method); A2AError error = value.getError(); if (error != null) { - throw new A2AClientException(error.getMessage() + (!error.getDetails().isEmpty() ? ": " + error.getDetails() : ""), error); + throw new A2AClientException( + error.getMessage() + + (!error.getDetails().isEmpty() ? ": " + error.getDetails() : ""), + error); } - // Safe cast: JSONRPCUtils.parseResponseBody returns the correct concrete type based on method + // Safe cast: JSONRPCUtils.parseResponseBody returns the correct concrete type based on + // method return (T) value; } diff --git a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java index 5c28fc9fe..ca6a3d63d 100644 --- a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java +++ b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportConfigBuilder.java @@ -63,7 +63,9 @@ * @see A2AHttpClient * @see org.a2aproject.sdk.client.http.JdkA2AHttpClient */ -public class JSONRPCTransportConfigBuilder extends ClientTransportConfigBuilder { +public class JSONRPCTransportConfigBuilder + extends ClientTransportConfigBuilder< + JSONRPCTransportConfig, JSONRPCTransportConfigBuilder> { private @Nullable A2AHttpClient httpClient; private A2AHttpClientConfiguration httpClientConfiguration = A2AHttpClientConfiguration.DEFAULT; @@ -102,9 +104,11 @@ public JSONRPCTransportConfigBuilder httpClient(A2AHttpClient httpClient) { } /** Sets the configuration used when this builder creates the HTTP client. */ - public JSONRPCTransportConfigBuilder httpClientConfiguration(A2AHttpClientConfiguration httpClientConfiguration) { - this.httpClientConfiguration = org.a2aproject.sdk.util.Assert.checkNotNullParam( - "httpClientConfiguration", httpClientConfiguration); + public JSONRPCTransportConfigBuilder httpClientConfiguration( + A2AHttpClientConfiguration httpClientConfiguration) { + this.httpClientConfiguration = + org.a2aproject.sdk.util.Assert.checkNotNullParam( + "httpClientConfiguration", httpClientConfiguration); return this; } diff --git a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportProvider.java b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportProvider.java index 2b05a2c70..a6ca30a60 100644 --- a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportProvider.java +++ b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportProvider.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.client.transport.jsonrpc; -import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; +import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.client.transport.spi.ClientTransportProvider; import org.a2aproject.sdk.spec.A2AClientException; import org.a2aproject.sdk.spec.AgentCard; @@ -9,17 +9,27 @@ import org.a2aproject.sdk.spec.TransportProtocol; import org.jspecify.annotations.Nullable; -public class JSONRPCTransportProvider implements ClientTransportProvider { +public class JSONRPCTransportProvider + implements ClientTransportProvider { @Override - public JSONRPCTransport create(@Nullable JSONRPCTransportConfig clientTransportConfig, AgentCard agentCard, AgentInterface agentInterface) throws A2AClientException { + public JSONRPCTransport create( + @Nullable JSONRPCTransportConfig clientTransportConfig, + AgentCard agentCard, + AgentInterface agentInterface) + throws A2AClientException { JSONRPCTransportConfig currentClientTransportConfig = clientTransportConfig; if (currentClientTransportConfig == null) { - currentClientTransportConfig = new JSONRPCTransportConfig( - A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT)); + currentClientTransportConfig = + new JSONRPCTransportConfig( + A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT)); } - return new JSONRPCTransport(currentClientTransportConfig.getHttpClient(), agentCard, agentInterface, - currentClientTransportConfig.getInterceptors(), currentClientTransportConfig.isFollowRedirects()); + return new JSONRPCTransport( + currentClientTransportConfig.getHttpClient(), + agentCard, + agentInterface, + currentClientTransportConfig.getInterceptors(), + currentClientTransportConfig.isFollowRedirects()); } @Override diff --git a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/package-info.java b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/package-info.java index 7c5ca35e2..066af487d 100644 --- a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/package-info.java +++ b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.transport.jsonrpc; import org.jspecify.annotations.NullMarked; - diff --git a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/SSEEventListener.java b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/SSEEventListener.java index 978fcaf70..66eaf12ee 100644 --- a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/SSEEventListener.java +++ b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/SSEEventListener.java @@ -3,7 +3,6 @@ import java.util.concurrent.Future; import java.util.function.Consumer; import java.util.logging.Logger; - import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.client.transport.spi.sse.AbstractSSEEventListener; import org.a2aproject.sdk.grpc.StreamResponse; @@ -21,8 +20,9 @@ public class SSEEventListener extends AbstractSSEEventListener { private static final Logger LOGGER = Logger.getLogger(SSEEventListener.class.getName()); - public SSEEventListener(Consumer eventHandler, - @Nullable Consumer errorHandler) { + + public SSEEventListener( + Consumer eventHandler, @Nullable Consumer errorHandler) { super(eventHandler, errorHandler); } @@ -32,7 +32,8 @@ public void onMessage(ServerSentEvent event, @Nullable Future completableF } public void onComplete() { - LOGGER.fine("SSEEventListener.onComplete() called - signaling successful stream completion"); + LOGGER.fine( + "SSEEventListener.onComplete() called - signaling successful stream completion"); signalTerminal(null); } @@ -46,7 +47,7 @@ private void parseAndHandleMessage(String message, @Nullable Future future try { StreamResponse response = JSONRPCUtils.parseResponseEvent(message); StreamingEventKind event = ProtoUtils.FromProto.streamingEventKind(response); - + // Delegate to base class for common event handling and auto-close logic handleEvent(event, future); } catch (A2AError error) { @@ -55,5 +56,4 @@ private void parseAndHandleMessage(String message, @Nullable Future future throw new RuntimeException(e); } } - } diff --git a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/package-info.java b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/package-info.java index 8943e1130..698fbdeea 100644 --- a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/package-info.java +++ b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.transport.jsonrpc.sse; import org.jspecify.annotations.NullMarked; - diff --git a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportStreamingTest.java b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportStreamingTest.java index 368647ae1..1116f601a 100644 --- a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportStreamingTest.java +++ b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportStreamingTest.java @@ -2,8 +2,8 @@ import static org.a2aproject.sdk.client.transport.jsonrpc.JsonStreamingMessages.SEND_MESSAGE_STREAMING_TEST_REQUEST; import static org.a2aproject.sdk.client.transport.jsonrpc.JsonStreamingMessages.SEND_MESSAGE_STREAMING_TEST_RESPONSE; -import static org.a2aproject.sdk.client.transport.jsonrpc.JsonStreamingMessages.TASK_SUBSCRIPTION_TEST_REQUEST; import static org.a2aproject.sdk.client.transport.jsonrpc.JsonStreamingMessages.TASK_SUBSCRIPTION_REQUEST_TEST_RESPONSE; +import static org.a2aproject.sdk.client.transport.jsonrpc.JsonStreamingMessages.TASK_SUBSCRIPTION_TEST_REQUEST; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -17,7 +17,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; - import org.a2aproject.sdk.spec.Artifact; import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.MessageSendConfiguration; @@ -53,22 +52,22 @@ public void tearDown() { public void testSendStreamingMessageParams() { // The goal here is just to verify the correct parameters are being used // This is a unit test of the parameter construction, not the streaming itself - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("test message"))) - .contextId("context-test") - .messageId("message-test") - .build(); - - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(true) - .build(); - - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("test message"))) + .contextId("context-test") + .messageId("message-test") + .build(); + + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(true) + .build(); + + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); assertNotNull(params); assertEquals(message, params.message()); @@ -79,42 +78,44 @@ public void testSendStreamingMessageParams() { @Test public void testA2AClientSendStreamingMessage() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_STREAMING_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SEND_MESSAGE_STREAMING_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("tell me some jokes"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(true) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("tell me some jokes"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(true) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); AtomicReference receivedEvent = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - Consumer eventHandler = event -> { - receivedEvent.set(event); - latch.countDown(); - }; + Consumer eventHandler = + event -> { + receivedEvent.set(event); + latch.countDown(); + }; Consumer errorHandler = error -> {}; client.sendMessageStreaming(params, eventHandler, errorHandler, null); @@ -125,34 +126,39 @@ public void testA2AClientSendStreamingMessage() throws Exception { @Test public void testA2AClientSubscribeToTask() throws Exception { - this.server.when(request() + this.server + .when( + request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(TASK_SUBSCRIPTION_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) - .respond(response() + .withBody( + JsonBody.json( + TASK_SUBSCRIPTION_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond( + response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(TASK_SUBSCRIPTION_REQUEST_TEST_RESPONSE) - ); + .withBody(TASK_SUBSCRIPTION_REQUEST_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); TaskIdParams taskIdParams = new TaskIdParams("task-1234"); AtomicReference receivedEvent = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - Consumer eventHandler = event -> { - receivedEvent.set(event); - latch.countDown(); - }; + Consumer eventHandler = + event -> { + receivedEvent.set(event); + latch.countDown(); + }; Consumer errorHandler = error -> {}; client.subscribeToTask(taskIdParams, eventHandler, errorHandler, null); boolean eventReceived = latch.await(10, TimeUnit.SECONDS); assertTrue(eventReceived); - StreamingEventKind eventKind = receivedEvent.get();; + StreamingEventKind eventKind = receivedEvent.get(); + ; assertNotNull(eventKind); assertInstanceOf(Task.class, eventKind); Task task = (Task) eventKind; @@ -166,6 +172,8 @@ public void testA2AClientSubscribeToTask() throws Exception { assertEquals("joke", artifact.name()); Part part = artifact.parts().get(0); assertTrue(part instanceof TextPart); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart) part).text()); } -} \ No newline at end of file +} diff --git a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportTest.java b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportTest.java index 3c8029e5a..f3e8d0c62 100644 --- a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportTest.java +++ b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportTest.java @@ -32,26 +32,23 @@ import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; import org.a2aproject.sdk.spec.A2AClientException; import org.a2aproject.sdk.spec.A2AClientHTTPError; import org.a2aproject.sdk.spec.AgentCard; -import org.a2aproject.sdk.spec.ExtensionSupportRequiredError; -import org.a2aproject.sdk.spec.VersionNotSupportedError; import org.a2aproject.sdk.spec.AgentSkill; import org.a2aproject.sdk.spec.Artifact; import org.a2aproject.sdk.spec.AuthenticationInfo; import org.a2aproject.sdk.spec.CancelTaskParams; import org.a2aproject.sdk.spec.DataPart; import org.a2aproject.sdk.spec.EventKind; +import org.a2aproject.sdk.spec.ExtensionSupportRequiredError; import org.a2aproject.sdk.spec.FileContent; import org.a2aproject.sdk.spec.FilePart; import org.a2aproject.sdk.spec.FileWithBytes; @@ -71,6 +68,7 @@ import org.a2aproject.sdk.spec.TaskQueryParams; import org.a2aproject.sdk.spec.TaskState; import org.a2aproject.sdk.spec.TextPart; +import org.a2aproject.sdk.spec.VersionNotSupportedError; import org.a2aproject.sdk.spec.util.Utils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -109,13 +107,16 @@ public void testListTasksWithDefaultConstructor() throws Exception { @Test public void testListTasksWithTenant() throws Exception { - JsonObject params = listTasksAndGetRequestParams(ListTasksParams.builder().tenant("tenant-123").build()); + JsonObject params = + listTasksAndGetRequestParams( + ListTasksParams.builder().tenant("tenant-123").build()); assertEquals("tenant-123", params.get("tenant").getAsString()); } private JsonObject listTasksAndGetRequestParams(ListTasksParams params) throws Exception { - String body = """ + String body = + """ { "jsonrpc": "2.0", "id": "list-tasks", @@ -125,7 +126,8 @@ private JsonObject listTasksAndGetRequestParams(ListTasksParams params) throws E server.when(request().withMethod("POST").withPath("/")) .respond(response().withStatusCode(200).withBody(body)); - ListTasksResult result = new JSONRPCTransport("http://localhost:4001").listTasks(params, null); + ListTasksResult result = + new JSONRPCTransport("http://localhost:4001").listTasks(params, null); assertTrue(result.tasks().isEmpty()); assertEquals(0, result.totalSize()); @@ -144,46 +146,48 @@ public void configBuilderDisablesRedirectFollowingByDefault() { @Test public void configBuilderEnablesRedirectFollowing() { - assertTrue(new JSONRPCTransportConfigBuilder().followRedirects(true).build().isFollowRedirects()); + assertTrue( + new JSONRPCTransportConfigBuilder() + .followRedirects(true) + .build() + .isFollowRedirects()); } @Test public void testA2AClientSendMessage() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) - .respond( - response() - .withStatusCode(200) - .withBody(SEND_MESSAGE_TEST_RESPONSE) - ); + .withBody( + JsonBody.json( + SEND_MESSAGE_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(SEND_MESSAGE_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("tell me a joke"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(false) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("tell me a joke"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(false) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); EventKind result = client.sendMessage(params, null); assertInstanceOf(Task.class, result); Task task = (Task) result; assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id()); assertNotNull(task.contextId()); - assertEquals(TaskState.TASK_STATE_COMPLETED,task.status().state()); + assertEquals(TaskState.TASK_STATE_COMPLETED, task.status().state()); assertEquals(1, task.artifacts().size()); Artifact artifact = task.artifacts().get(0); assertEquals("artifact-1", artifact.artifactId()); @@ -191,40 +195,43 @@ public void testA2AClientSendMessage() throws Exception { assertEquals(1, artifact.parts().size()); Part part = artifact.parts().get(0); assertTrue(part instanceof TextPart); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart) part).text()); assertTrue(task.metadata().isEmpty()); } @Test public void testA2AClientSendMessageWithMessageResponse() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE) - ); + .withBody(SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("tell me a joke"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(false) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("tell me a joke"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(false) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); EventKind result = client.sendMessage(params, null); assertInstanceOf(Message.class, result); @@ -232,68 +239,68 @@ public void testA2AClientSendMessageWithMessageResponse() throws Exception { assertEquals(Message.Role.ROLE_AGENT, agentMessage.role()); Part part = agentMessage.parts().get(0); assertTrue(part instanceof TextPart); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart) part).text()); assertEquals("msg-456", agentMessage.messageId()); } - @Test public void testA2AClientSendMessageWithError() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_WITH_ERROR_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) - .respond( - response() - .withStatusCode(200) - .withBody(SEND_MESSAGE_ERROR_TEST_RESPONSE) - ); + .withBody( + JsonBody.json( + SEND_MESSAGE_WITH_ERROR_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(SEND_MESSAGE_ERROR_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("tell me a joke"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(false) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("tell me a joke"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(false) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); try { client.sendMessage(params, null); fail(); // should not reach here } catch (A2AClientException e) { - assertTrue(e.getMessage().contains("Invalid parameters: {info=Hello world}"),e.getMessage()); + assertTrue( + e.getMessage().contains("Invalid parameters: {info=Hello world}"), + e.getMessage()); } } @Test public void testA2AClientGetTask() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(GET_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) - .respond( - response() - .withStatusCode(200) - .withBody(GET_TASK_TEST_RESPONSE) - ); + .withBody( + JsonBody.json( + GET_TASK_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(GET_TASK_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - Task task = client.getTask(new TaskQueryParams("de38c76d-d54c-436c-8b9f-4c2703648d64", - 10), null); + Task task = + client.getTask( + new TaskQueryParams("de38c76d-d54c-436c-8b9f-4c2703648d64", 10), null); assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id()); assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", task.contextId()); assertEquals(TaskState.TASK_STATE_COMPLETED, task.status().state()); @@ -303,7 +310,9 @@ public void testA2AClientGetTask() throws Exception { assertEquals("artifact-1", artifact.artifactId()); Part part = artifact.parts().get(0); assertTrue(part instanceof TextPart); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart) part).text()); assertTrue(task.metadata().isEmpty()); List history = task.history(); assertNotNull(history); @@ -315,7 +324,7 @@ public void testA2AClientGetTask() throws Exception { assertEquals(3, parts.size()); part = parts.get(0); assertTrue(part instanceof TextPart); - assertEquals("tell me a joke", ((TextPart)part).text()); + assertEquals("tell me a joke", ((TextPart) part).text()); part = parts.get(1); assertTrue(part instanceof FilePart); FileContent filePart = ((FilePart) part).file(); @@ -331,21 +340,21 @@ public void testA2AClientGetTask() throws Exception { @Test public void testA2AClientCancelTask() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(CANCEL_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) - .respond( - response() - .withStatusCode(200) - .withBody(CANCEL_TASK_TEST_RESPONSE) - ); + .withBody( + JsonBody.json( + CANCEL_TASK_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(CANCEL_TASK_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - Task task = client.cancelTask(new CancelTaskParams("de38c76d-d54c-436c-8b9f-4c2703648d64"), null); + Task task = + client.cancelTask( + new CancelTaskParams("de38c76d-d54c-436c-8b9f-4c2703648d64"), null); assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id()); assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", task.contextId()); assertEquals(TaskState.TASK_STATE_CANCELED, task.status().state()); @@ -354,22 +363,27 @@ public void testA2AClientCancelTask() throws Exception { @Test public void testA2AClientGetTaskPushNotificationConfig() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - TaskPushNotificationConfig taskPushNotificationConfig = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams("de38c76d-d54c-436c-8b9f-4c2703648d64", "c295ea44-7543-4f78-b524-7a38915ad6e4"), null); + TaskPushNotificationConfig taskPushNotificationConfig = + client.getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams( + "de38c76d-d54c-436c-8b9f-4c2703648d64", + "c295ea44-7543-4f78-b524-7a38915ad6e4"), + null); assertNotNull(taskPushNotificationConfig); assertEquals("https://example.com/callback", taskPushNotificationConfig.url()); AuthenticationInfo authenticationInfo = taskPushNotificationConfig.authentication(); @@ -378,28 +392,31 @@ public void testA2AClientGetTaskPushNotificationConfig() throws Exception { @Test public void testA2AClientCreateTaskPushNotificationConfig() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - TaskPushNotificationConfig taskPushNotificationConfig = client.createTaskPushNotificationConfiguration( - TaskPushNotificationConfig.builder() - .id("c295ea44-7543-4f78-b524-7a38915ad6e4") - .taskId("de38c76d-d54c-436c-8b9f-4c2703648d64") - .url("https://example.com/callback") - .authentication(new AuthenticationInfo("jwt", null)) - .tenant("") - .build(), null); + TaskPushNotificationConfig taskPushNotificationConfig = + client.createTaskPushNotificationConfiguration( + TaskPushNotificationConfig.builder() + .id("c295ea44-7543-4f78-b524-7a38915ad6e4") + .taskId("de38c76d-d54c-436c-8b9f-4c2703648d64") + .url("https://example.com/callback") + .authentication(new AuthenticationInfo("jwt", null)) + .tenant("") + .build(), + null); assertNotNull(taskPushNotificationConfig); assertEquals("https://example.com/callback", taskPushNotificationConfig.url()); AuthenticationInfo authenticationInfo = taskPushNotificationConfig.authentication(); @@ -408,35 +425,44 @@ public void testA2AClientCreateTaskPushNotificationConfig() throws Exception { @Test public void testA2AClientGetExtendedAgentCard() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) + .withBody( + JsonBody.json( + GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE) - ); + .withBody(GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); GetExtendedAgentCardParams params = new GetExtendedAgentCardParams(null); AgentCard agentCard = client.getExtendedAgentCard(params, null); assertEquals("GeoSpatial Route Planner Agent Extended", agentCard.name()); assertEquals("Extended description", agentCard.description()); - assertEquals("https://georoute-agent.example.com/a2a/v1", Utils.getFavoriteInterface(agentCard).url()); + assertEquals( + "https://georoute-agent.example.com/a2a/v1", + Utils.getFavoriteInterface(agentCard).url()); assertEquals("Example Geo Services Inc.", agentCard.provider().organization()); assertEquals("https://www.examplegeoservices.com", agentCard.provider().url()); assertEquals("1.2.0", agentCard.version()); - assertEquals("https://docs.examplegeoservices.com/georoute-agent/api", agentCard.documentationUrl()); + assertEquals( + "https://docs.examplegeoservices.com/georoute-agent/api", + agentCard.documentationUrl()); assertTrue(agentCard.capabilities().streaming()); assertTrue(agentCard.capabilities().pushNotifications()); assertTrue(agentCard.capabilities().extendedAgentCard()); Map securitySchemes = agentCard.securitySchemes(); assertNotNull(securitySchemes); - OpenIdConnectSecurityScheme google = (OpenIdConnectSecurityScheme) securitySchemes.get("google"); - assertEquals("https://accounts.google.com/.well-known/openid-configuration", google.openIdConnectUrl()); + OpenIdConnectSecurityScheme google = + (OpenIdConnectSecurityScheme) securitySchemes.get("google"); + assertEquals( + "https://accounts.google.com/.well-known/openid-configuration", + google.openIdConnectUrl()); List security = agentCard.securityRequirements(); assertEquals(1, security.size()); Map> securityMap = security.get(0).schemes(); @@ -450,22 +476,39 @@ public void testA2AClientGetExtendedAgentCard() throws Exception { List skills = agentCard.skills(); assertEquals("route-optimizer-traffic", skills.get(0).id()); assertEquals("Traffic-Aware Route Optimizer", skills.get(0).name()); - assertEquals("Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).", skills.get(0).description()); + assertEquals( + "Calculates the optimal driving route between two or more locations, taking into" + + " account real-time traffic conditions, road closures, and user preferences" + + " (e.g., avoid tolls, prefer highways).", + skills.get(0).description()); List tags = List.of("maps", "routing", "navigation", "directions", "traffic"); assertEquals(tags, skills.get(0).tags()); - List examples = List.of("Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.", - "{\"origin\": {\"lat\": 37.422, \"lng\": -122.084}, \"destination\": {\"lat\": 37.7749, \"lng\": -122.4194}, \"preferences\": [\"avoid_ferries\"]}"); + List examples = + List.of( + "Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San" + + " Francisco International Airport' avoiding tolls.", + "{\"origin\": {\"lat\": 37.422, \"lng\": -122.084}, \"destination\":" + + " {\"lat\": 37.7749, \"lng\": -122.4194}, \"preferences\":" + + " [\"avoid_ferries\"]}"); assertEquals(examples, skills.get(0).examples()); assertEquals(defaultInputModes, skills.get(0).inputModes()); - List outputModes = List.of("application/json", "application/vnd.geo+json", "text/html"); + List outputModes = + List.of("application/json", "application/vnd.geo+json", "text/html"); assertEquals(outputModes, skills.get(0).outputModes()); assertEquals("custom-map-generator", skills.get(1).id()); assertEquals("Personalized Map Generator", skills.get(1).name()); - assertEquals("Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.", skills.get(1).description()); + assertEquals( + "Creates custom map images or interactive map views based on user-defined points of" + + " interest, routes, and style preferences. Can overlay data layers.", + skills.get(1).description()); tags = List.of("maps", "customization", "visualization", "cartography"); assertEquals(tags, skills.get(1).tags()); - examples = List.of("Generate a map of my upcoming road trip with all planned stops highlighted.", - "Show me a map visualizing all coffee shops within a 1-mile radius of my current location."); + examples = + List.of( + "Generate a map of my upcoming road trip with all planned stops" + + " highlighted.", + "Show me a map visualizing all coffee shops within a 1-mile radius of my" + + " current location."); assertEquals(examples, skills.get(1).examples()); List inputModes = List.of("application/json"); assertEquals(inputModes, skills.get(1).inputModes()); @@ -476,42 +519,48 @@ public void testA2AClientGetExtendedAgentCard() throws Exception { assertEquals("This is an extended skill.", skills.get(2).description()); assertEquals(List.of("extended"), skills.get(2).tags()); assertEquals("https://georoute-agent.example.com/icon.png", agentCard.iconUrl()); - assertEquals(CURRENT_PROTOCOL_VERSION, agentCard.supportedInterfaces().get(0).protocolVersion()); + assertEquals( + CURRENT_PROTOCOL_VERSION, agentCard.supportedInterfaces().get(0).protocolVersion()); } @Test public void testA2AClientSendMessageWithFilePart() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of( - new TextPart("analyze this image"), - new FilePart(new FileWithUri("image/jpeg", null, "file:///path/to/image.jpg")) - )) - .contextId("context-1234") - .messageId("message-1234-with-file") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(false) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts( + List.of( + new TextPart("analyze this image"), + new FilePart( + new FileWithUri( + "image/jpeg", + null, + "file:///path/to/image.jpg")))) + .contextId("context-1234") + .messageId("message-1234-with-file") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(false) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); EventKind result = client.sendMessage(params, null); assertInstanceOf(Task.class, result); @@ -526,7 +575,8 @@ public void testA2AClientSendMessageWithFilePart() throws Exception { assertEquals(1, artifact.parts().size()); Part part = artifact.parts().get(0); assertTrue(part instanceof TextPart); - assertEquals("This is an image of a cat sitting on a windowsill.", ((TextPart) part).text()); + assertEquals( + "This is an image of a cat sitting on a windowsill.", ((TextPart) part).text()); assertFalse(task.metadata().isEmpty()); assertEquals(1, task.metadata().size()); assertEquals("metadata-test", task.metadata().get("test")); @@ -534,18 +584,19 @@ public void testA2AClientSendMessageWithFilePart() throws Exception { @Test public void testA2AClientSendMessageWithDataPart() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); @@ -555,23 +606,20 @@ public void testA2AClientSendMessageWithDataPart() throws Exception { data.put("location", "San Francisco"); data.put("timestamp", "2024-01-15T10:30:00Z"); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of( - new TextPart("process this data"), - new DataPart(data) - )) - .contextId("context-1234") - .messageId("message-1234-with-data") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(false) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("process this data"), new DataPart(data))) + .contextId("context-1234") + .messageId("message-1234-with-data") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(false) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); EventKind result = client.sendMessage(params, null); assertInstanceOf(Task.class, result); @@ -586,24 +634,28 @@ public void testA2AClientSendMessageWithDataPart() throws Exception { assertEquals(1, artifact.parts().size()); Part part = artifact.parts().get(0); assertTrue(part instanceof TextPart); - assertEquals("Processed weather data: Temperature is 25.5°C, humidity is 60.2% in San Francisco.", ((TextPart) part).text()); + assertEquals( + "Processed weather data: Temperature is 25.5°C, humidity is 60.2% in San" + + " Francisco.", + ((TextPart) part).text()); assertTrue(task.metadata().isEmpty()); } @Test public void testA2AClientSendMessageWithMixedParts() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); @@ -612,24 +664,26 @@ public void testA2AClientSendMessageWithMixedParts() throws Exception { data.put("dataPoints", List.of(10, 20, 30, 40)); data.put("labels", List.of("Q1", "Q2", "Q3", "Q4")); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of( - new TextPart("analyze this data and image"), - new FilePart(new FileWithBytes("image/png", "chart.png", "aGVsbG8=")), - new DataPart(data) - )) - .contextId("context-1234") - .messageId("message-1234-with-mixed") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(false) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts( + List.of( + new TextPart("analyze this data and image"), + new FilePart( + new FileWithBytes( + "image/png", "chart.png", "aGVsbG8=")), + new DataPart(data))) + .contextId("context-1234") + .messageId("message-1234-with-mixed") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(false) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); EventKind result = client.sendMessage(params, null); assertInstanceOf(Task.class, result); @@ -644,7 +698,10 @@ public void testA2AClientSendMessageWithMixedParts() throws Exception { assertEquals(1, artifact.parts().size()); Part part = artifact.parts().get(0); assertTrue(part instanceof TextPart); - assertEquals("Analyzed chart image and data: Bar chart showing quarterly data with values [10, 20, 30, 40].", ((TextPart) part).text()); + assertEquals( + "Analyzed chart image and data: Bar chart showing quarterly data with values [10," + + " 20, 30, 40].", + ((TextPart) part).text()); assertTrue(task.metadata().isEmpty()); } @@ -654,7 +711,8 @@ public void testA2AClientSendMessageWithMixedParts() throws Exception { @Test public void testExtensionSupportRequiredErrorUnmarshalling() throws Exception { // Mock server returns JSON-RPC error with code -32008 (EXTENSION_SUPPORT_REQUIRED_ERROR) - String errorResponseBody = """ + String errorResponseBody = + """ { "jsonrpc": "2.0", "id": 1, @@ -665,32 +723,25 @@ public void testExtensionSupportRequiredErrorUnmarshalling() throws Exception { } """; - this.server.when( - request() - .withMethod("POST") - .withPath("/") - ) - .respond( - response() - .withStatusCode(200) - .withBody(errorResponseBody) - ); + this.server + .when(request().withMethod("POST").withPath("/")) + .respond(response().withStatusCode(200).withBody(errorResponseBody)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("test message"))) - .contextId("context-test") - .messageId("message-test") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(false) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("test message"))) + .contextId("context-test") + .messageId("message-test") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(false) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); // Should throw A2AClientException with ExtensionSupportRequiredError as cause try { @@ -699,7 +750,8 @@ public void testExtensionSupportRequiredErrorUnmarshalling() throws Exception { } catch (A2AClientException e) { // Verify the cause is ExtensionSupportRequiredError assertInstanceOf(ExtensionSupportRequiredError.class, e.getCause()); - ExtensionSupportRequiredError extensionError = (ExtensionSupportRequiredError) e.getCause(); + ExtensionSupportRequiredError extensionError = + (ExtensionSupportRequiredError) e.getCause(); assertTrue(extensionError.getMessage().contains("https://example.com/test-extension")); } } @@ -710,39 +762,42 @@ public void testExtensionSupportRequiredErrorUnmarshalling() throws Exception { */ @Test public void testHttpErrorExposeStatusCode() throws Exception { - this.server.when( - request() - .withMethod("POST") - .withPath("/") - ) + this.server + .when(request().withMethod("POST").withPath("/")) .respond( response() .withStatusCode(503) - .withBody("{\"error\": \"Service Unavailable\"}") - ); + .withBody("{\"error\": \"Service Unavailable\"}")); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - MessageSendParams params = MessageSendParams.builder() - .message(Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("hello"))) - .contextId("ctx") - .messageId("msg") - .build()) - .build(); + MessageSendParams params = + MessageSendParams.builder() + .message( + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("hello"))) + .contextId("ctx") + .messageId("msg") + .build()) + .build(); try { client.sendMessage(params, null); fail("Expected A2AClientException to be thrown"); } catch (A2AClientException e) { // Backward compatible: still throws A2AClientException - assertTrue(e.getMessage().contains("503"), "Expected message to contain '503' but was: " + e.getMessage()); + assertTrue( + e.getMessage().contains("503"), + "Expected message to contain '503' but was: " + e.getMessage()); // New: cause carries structured HTTP status assertInstanceOf(A2AClientHTTPError.class, e.getCause()); A2AClientHTTPError httpError = (A2AClientHTTPError) e.getCause(); assertEquals(503, httpError.getCode()); assertNotNull(httpError.getResponseBody()); - assertTrue(httpError.getResponseBody().contains("Service Unavailable"), "Expected response body to contain 'Service Unavailable' but was: " + httpError.getResponseBody()); + assertTrue( + httpError.getResponseBody().contains("Service Unavailable"), + "Expected response body to contain 'Service Unavailable' but was: " + + httpError.getResponseBody()); } } @@ -752,28 +807,26 @@ public void testHttpErrorExposeStatusCode() throws Exception { */ @Test public void testHttpErrorExposeResponseHeaders() throws Exception { - this.server.when( - request() - .withMethod("POST") - .withPath("/") - ) + this.server + .when(request().withMethod("POST").withPath("/")) .respond( response() .withStatusCode(429) .withHeader("Retry-After", "120") .withHeader("X-RateLimit-Remaining", "0") - .withBody("{\"error\": \"Too Many Requests\"}") - ); + .withBody("{\"error\": \"Too Many Requests\"}")); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - MessageSendParams params = MessageSendParams.builder() - .message(Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("hello"))) - .contextId("ctx") - .messageId("msg") - .build()) - .build(); + MessageSendParams params = + MessageSendParams.builder() + .message( + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("hello"))) + .contextId("ctx") + .messageId("msg") + .build()) + .build(); try { client.sendMessage(params, null); @@ -791,7 +844,8 @@ public void testHttpErrorExposeResponseHeaders() throws Exception { assertFalse(retryAfter.isEmpty(), "Expected Retry-After header"); assertEquals("120", retryAfter.get(0)); - List rateLimitRemaining = headers.getOrDefault("X-RateLimit-Remaining", List.of()); + List rateLimitRemaining = + headers.getOrDefault("X-RateLimit-Remaining", List.of()); assertFalse(rateLimitRemaining.isEmpty(), "Expected X-RateLimit-Remaining header"); assertEquals("0", rateLimitRemaining.get(0)); } @@ -803,7 +857,8 @@ public void testHttpErrorExposeResponseHeaders() throws Exception { @Test public void testVersionNotSupportedErrorUnmarshalling() throws Exception { // Mock server returns JSON-RPC error with code -32009 (VERSION_NOT_SUPPORTED_ERROR) - String errorResponseBody = """ + String errorResponseBody = + """ { "jsonrpc": "2.0", "id": 1, @@ -814,32 +869,25 @@ public void testVersionNotSupportedErrorUnmarshalling() throws Exception { } """; - this.server.when( - request() - .withMethod("POST") - .withPath("/") - ) - .respond( - response() - .withStatusCode(200) - .withBody(errorResponseBody) - ); + this.server + .when(request().withMethod("POST").withPath("/")) + .respond(response().withStatusCode(200).withBody(errorResponseBody)); JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("test message"))) - .contextId("context-test") - .messageId("message-test") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(false) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("test message"))) + .contextId("context-test") + .messageId("message-test") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(false) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); // Should throw A2AClientException with VersionNotSupportedError as cause try { diff --git a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JsonMessages.java b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JsonMessages.java index f73e9d8ed..dac0f449a 100644 --- a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JsonMessages.java +++ b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JsonMessages.java @@ -6,7 +6,8 @@ */ public class JsonMessages { - static final String AGENT_CARD = """ + static final String AGENT_CARD = + """ { "name": "GeoSpatial Route Planner Agent", "description": "Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", @@ -78,10 +79,66 @@ public class JsonMessages { "signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ" } ] - }"""; + }\ + """; - static final String SEND_MESSAGE_TEST_REQUEST = """ - { + static final String SEND_MESSAGE_TEST_REQUEST = + """ + { + "jsonrpc":"2.0", + "method":"SendMessage", + "params":{ + "message":{ + "messageId":"message-1234", + "contextId":"context-1234", + "role":"ROLE_USER", + "parts":[ + { + "text":"tell me a joke" + } + ], + "metadata":{ + + } + } + } + }\ + """; + + static final String SEND_MESSAGE_TEST_RESPONSE = + """ + { + "jsonrpc":"2.0", + "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", + "result":{ + "task":{ + "id":"de38c76d-d54c-436c-8b9f-4c2703648d64", + "contextId":"c295ea44-7543-4f78-b524-7a38915ad6e4", + "status":{ + "state":"TASK_STATE_COMPLETED" + }, + "artifacts":[ + { + "artifactId":"artifact-1", + "name":"joke", + "parts":[ + { + "text":"Why did the chicken cross the road? To get to the other side!" + } + ] + } + ], + "metadata":{ + + } + } + } + }\ + """; + + static final String SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE = + """ + { "jsonrpc":"2.0", "method":"SendMessage", "params":{ @@ -95,71 +152,23 @@ public class JsonMessages { } ], "metadata":{ - } - } - } - }"""; - - static final String SEND_MESSAGE_TEST_RESPONSE = """ - { - "jsonrpc":"2.0", - "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", - "result":{ - "task":{ - "id":"de38c76d-d54c-436c-8b9f-4c2703648d64", - "contextId":"c295ea44-7543-4f78-b524-7a38915ad6e4", - "status":{ - "state":"TASK_STATE_COMPLETED" - }, - "artifacts":[ - { - "artifactId":"artifact-1", - "name":"joke", - "parts":[ - { - "text":"Why did the chicken cross the road? To get to the other side!" - } - ] - } + }, + "configuration":{ + "acceptedOutputModes":[ + "text" ], - "metadata":{ - - } - } - } - }"""; - - static final String SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE = """ - { - "jsonrpc":"2.0", - "method":"SendMessage", - "params":{ - "message":{ - "messageId":"message-1234", - "contextId":"context-1234", - "role":"ROLE_USER", - "parts":[ - { - "text":"tell me a joke" + "returnImmediately":false + }, + "metadata":{ + } - ], - "metadata":{ } - }, - "configuration":{ - "acceptedOutputModes":[ - "text" - ], - "returnImmediately":false - }, - "metadata":{ - - } - } - }"""; + }\ + """; - static final String SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE = """ + static final String SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE = + """ { "jsonrpc":"2.0", "id":1, @@ -177,9 +186,11 @@ public class JsonMessages { } } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_ERROR_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_ERROR_TEST_REQUEST = + """ { "jsonrpc":"2.0", "method":"SendMessage", @@ -194,7 +205,7 @@ public class JsonMessages { } ], "metadata":{ - + } }, "configuration":{ @@ -204,12 +215,14 @@ public class JsonMessages { "returnImmediately":false }, "metadata":{ - + } } - }"""; + }\ + """; - static final String SEND_MESSAGE_ERROR_TEST_RESPONSE = """ + static final String SEND_MESSAGE_ERROR_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", @@ -218,9 +231,11 @@ public class JsonMessages { "message": "Invalid parameters", "data": {"info": "Hello world"} } - }"""; + }\ + """; - static final String GET_TASK_TEST_REQUEST = """ + static final String GET_TASK_TEST_REQUEST = + """ { "jsonrpc":"2.0", "method":"GetTask", @@ -231,7 +246,8 @@ public class JsonMessages { } """; - static final String GET_TASK_TEST_RESPONSE = """ + static final String GET_TASK_TEST_RESPONSE = + """ { "jsonrpc":"2.0", "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", @@ -272,13 +288,14 @@ public class JsonMessages { } ], "metadata":{ - + } } } """; - static final String CANCEL_TASK_TEST_REQUEST = """ + static final String CANCEL_TASK_TEST_REQUEST = + """ { "jsonrpc":"2.0", "method":"CancelTask", @@ -288,7 +305,8 @@ public class JsonMessages { } """; - static final String CANCEL_TASK_TEST_RESPONSE = """ + static final String CANCEL_TASK_TEST_RESPONSE = + """ { "jsonrpc":"2.0", "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", @@ -299,13 +317,14 @@ public class JsonMessages { "state":"TASK_STATE_CANCELED" }, "metadata":{ - + } } } """; - static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = """ + static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = + """ { "jsonrpc":"2.0", "method":"GetTaskPushNotificationConfig", @@ -313,9 +332,11 @@ public class JsonMessages { "taskId":"de38c76d-d54c-436c-8b9f-4c2703648d64", "id":"c295ea44-7543-4f78-b524-7a38915ad6e4" } - }"""; + }\ + """; - static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """ + static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", @@ -330,21 +351,24 @@ public class JsonMessages { } """; - static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = """ - { - "jsonrpc":"2.0", - "method":"CreateTaskPushNotificationConfig", - "params":{ - "id":"c295ea44-7543-4f78-b524-7a38915ad6e4", - "taskId":"de38c76d-d54c-436c-8b9f-4c2703648d64", - "url":"https://example.com/callback", - "authentication":{ - "scheme":"jwt" - } - } - }"""; + static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = + """ + { + "jsonrpc":"2.0", + "method":"CreateTaskPushNotificationConfig", + "params":{ + "id":"c295ea44-7543-4f78-b524-7a38915ad6e4", + "taskId":"de38c76d-d54c-436c-8b9f-4c2703648d64", + "url":"https://example.com/callback", + "authentication":{ + "scheme":"jwt" + } + } + }\ + """; - static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """ + static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", @@ -359,41 +383,44 @@ public class JsonMessages { } """; - static final String SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST = """ - { - "jsonrpc":"2.0", - "method":"SendMessage", - "params":{ - "message":{ - "messageId":"message-1234-with-file", - "contextId":"context-1234", - "role":"ROLE_USER", - "parts":[ - { - "text":"analyze this image" - }, - { - "url":"file:///path/to/image.jpg", - "mediaType":"image/jpeg" - } - ], - "metadata":{ - - } - }, - "configuration":{ - "acceptedOutputModes":[ - "text" - ], - "returnImmediately":false - }, - "metadata":{ - - } - } - }"""; + static final String SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST = + """ + { + "jsonrpc":"2.0", + "method":"SendMessage", + "params":{ + "message":{ + "messageId":"message-1234-with-file", + "contextId":"context-1234", + "role":"ROLE_USER", + "parts":[ + { + "text":"analyze this image" + }, + { + "url":"file:///path/to/image.jpg", + "mediaType":"image/jpeg" + } + ], + "metadata":{ + + } + }, + "configuration":{ + "acceptedOutputModes":[ + "text" + ], + "returnImmediately":false + }, + "metadata":{ - static final String SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE = """ + } + } + }\ + """; + + static final String SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE = + """ { "jsonrpc":"2.0", "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", @@ -420,9 +447,11 @@ public class JsonMessages { } } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST = + """ { "jsonrpc":"2.0", "method":"SendMessage", @@ -445,7 +474,7 @@ public class JsonMessages { } ], "metadata":{ - + } }, "configuration":{ @@ -455,12 +484,14 @@ public class JsonMessages { "returnImmediately":false }, "metadata":{ - + } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE = """ + static final String SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE = + """ { "jsonrpc":"2.0", "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", @@ -483,13 +514,15 @@ public class JsonMessages { } ], "metadata":{ - + } } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST = + """ { "jsonrpc":"2.0", "method":"SendMessage", @@ -516,7 +549,7 @@ public class JsonMessages { } ], "metadata":{ - + } }, "configuration":{ @@ -526,48 +559,53 @@ public class JsonMessages { "returnImmediately":false }, "metadata":{ - - } - } - }"""; - static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE = """ - { - "jsonrpc":"2.0", - "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", - "result":{ - "task":{ - "id":"de38c76d-d54c-436c-8b9f-4c2703648d64", - "contextId":"c295ea44-7543-4f78-b524-7a38915ad6e4", - "status":{ - "state":"TASK_STATE_COMPLETED" - }, - "artifacts":[ - { - "artifactId":"artifact-1", - "name":"mixed-analysis", - "parts":[ - { - "text":"Analyzed chart image and data: Bar chart showing quarterly data with values [10, 20, 30, 40]." - } - ] - } - ], - "metadata":{ - - } } } - }"""; + }\ + """; + + static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE = + """ + { + "jsonrpc":"2.0", + "id": "cd4c76de-d54c-436c-8b9f-4c2703648d64", + "result":{ + "task":{ + "id":"de38c76d-d54c-436c-8b9f-4c2703648d64", + "contextId":"c295ea44-7543-4f78-b524-7a38915ad6e4", + "status":{ + "state":"TASK_STATE_COMPLETED" + }, + "artifacts":[ + { + "artifactId":"artifact-1", + "name":"mixed-analysis", + "parts":[ + { + "text":"Analyzed chart image and data: Bar chart showing quarterly data with values [10, 20, 30, 40]." + } + ] + } + ], + "metadata":{ - static final String GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST = """ + } + } + } + }\ + """; + + static final String GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST = + """ { "jsonrpc": "2.0", "method": "GetExtendedAgentCard" } """; - static final String GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE = """ + static final String GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE = + """ { "jsonrpc": "2.0", "id": "1", @@ -647,9 +685,11 @@ public class JsonMessages { } ] } - }"""; + }\ + """; - static final String AGENT_CARD_SUPPORTS_EXTENDED = """ + static final String AGENT_CARD_SUPPORTS_EXTENDED = + """ { "name": "GeoSpatial Route Planner Agent", "description": "Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", @@ -713,5 +753,6 @@ public class JsonMessages { ] } ] - }"""; + }\ + """; } diff --git a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JsonStreamingMessages.java b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JsonStreamingMessages.java index f9c006b2c..af26272ad 100644 --- a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JsonStreamingMessages.java +++ b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JsonStreamingMessages.java @@ -5,7 +5,8 @@ */ public class JsonStreamingMessages { - public static final String STREAMING_TASK_EVENT = """ + public static final String STREAMING_TASK_EVENT = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -21,8 +22,8 @@ public class JsonStreamingMessages { } """; - - public static final String STREAMING_MESSAGE_EVENT = """ + public static final String STREAMING_MESSAGE_EVENT = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -38,9 +39,11 @@ public class JsonStreamingMessages { ] } } - }"""; + }\ + """; - public static final String STREAMING_STATUS_UPDATE_EVENT = """ + public static final String STREAMING_STATUS_UPDATE_EVENT = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -53,9 +56,11 @@ public class JsonStreamingMessages { } } } - }"""; + }\ + """; - public static final String STREAMING_STATUS_UPDATE_EVENT_FINAL = """ + public static final String STREAMING_STATUS_UPDATE_EVENT_FINAL = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -68,9 +73,11 @@ public class JsonStreamingMessages { } } } - }"""; + }\ + """; - public static final String STREAMING_ARTIFACT_UPDATE_EVENT = """ + public static final String STREAMING_ARTIFACT_UPDATE_EVENT = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -90,9 +97,11 @@ public class JsonStreamingMessages { } } } - }"""; + }\ + """; - public static final String STREAMING_ERROR_EVENT = """ + public static final String STREAMING_ERROR_EVENT = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -101,9 +110,11 @@ public class JsonStreamingMessages { "message": "Invalid parameters", "data": {"info": "Missing required field"} } - }"""; + }\ + """; - public static final String SEND_MESSAGE_STREAMING_TEST_REQUEST = """ + public static final String SEND_MESSAGE_STREAMING_TEST_REQUEST = + """ { "jsonrpc":"2.0", "method":"SendStreamingMessage", @@ -118,7 +129,7 @@ public class JsonStreamingMessages { } ], "metadata":{ - + } }, "configuration":{ @@ -127,31 +138,34 @@ public class JsonStreamingMessages { ] }, "metadata":{ - + } } - }"""; + }\ + """; static final String SEND_MESSAGE_STREAMING_TEST_RESPONSE = """ event: message data: {"jsonrpc":"2.0","id":1,"result":{"task":{"id":"2","contextId":"context-1234","status":{"state":"TASK_STATE_COMPLETED"},"artifacts":[{"artifactId":"artifact-1","name":"joke","parts":[{"text":"Why did the chicken cross the road? To get to the other side!"}]}],"metadata":{}}}} - + """; static final String TASK_SUBSCRIPTION_REQUEST_TEST_RESPONSE = """ event: message data: {"jsonrpc":"2.0","id":1,"result":{"task":{"id":"2","contextId":"context-1234","status":{"state":"TASK_STATE_COMPLETED"},"artifacts":[{"artifactId":"artifact-1","name":"joke","parts":[{"text":"Why did the chicken cross the road? To get to the other side!"}]}],"metadata":{}}}} - + """; - public static final String TASK_SUBSCRIPTION_TEST_REQUEST = """ + public static final String TASK_SUBSCRIPTION_TEST_REQUEST = + """ { "jsonrpc":"2.0", "method":"SubscribeToTask", "params":{ "id":"task-1234" } - }"""; -} \ No newline at end of file + }\ + """; +} diff --git a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/SSEEventListenerTest.java b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/SSEEventListenerTest.java index 3efc51fcd..6076a1268 100644 --- a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/SSEEventListenerTest.java +++ b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/sse/SSEEventListenerTest.java @@ -9,16 +9,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; - import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.client.transport.jsonrpc.JsonStreamingMessages; import org.a2aproject.sdk.spec.A2AError; @@ -39,15 +38,14 @@ public class SSEEventListenerTest { public void testOnEventWithTaskResult() throws Exception { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // Parse the task event JSON - String eventData = JsonStreamingMessages.STREAMING_TASK_EVENT.substring( - JsonStreamingMessages.STREAMING_TASK_EVENT.indexOf("{")); - + String eventData = + JsonStreamingMessages.STREAMING_TASK_EVENT.substring( + JsonStreamingMessages.STREAMING_TASK_EVENT.indexOf("{")); + // Call the onEvent method directly listener.onMessage(new ServerSentEvent(eventData), null); @@ -64,15 +62,14 @@ public void testOnEventWithTaskResult() throws Exception { public void testOnEventWithMessageResult() throws Exception { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // Parse the message event JSON - String eventData = JsonStreamingMessages.STREAMING_MESSAGE_EVENT.substring( - JsonStreamingMessages.STREAMING_MESSAGE_EVENT.indexOf("{")); - + String eventData = + JsonStreamingMessages.STREAMING_MESSAGE_EVENT.substring( + JsonStreamingMessages.STREAMING_MESSAGE_EVENT.indexOf("{")); + // Call onEvent method listener.onMessage(new ServerSentEvent(eventData), null); @@ -92,14 +89,13 @@ public void testOnEventWithMessageResult() throws Exception { public void testOnEventWithTaskStatusUpdateEventEvent() throws Exception { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // Parse the message event JSON - String eventData = JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT.substring( - JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT.indexOf("{")); + String eventData = + JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT.substring( + JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT.indexOf("{")); // Call onEvent method listener.onMessage(new ServerSentEvent(eventData), null); @@ -118,14 +114,13 @@ public void testOnEventWithTaskStatusUpdateEventEvent() throws Exception { public void testOnEventWithTaskArtifactUpdateEventEvent() throws Exception { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // Parse the message event JSON - String eventData = JsonStreamingMessages.STREAMING_ARTIFACT_UPDATE_EVENT.substring( - JsonStreamingMessages.STREAMING_ARTIFACT_UPDATE_EVENT.indexOf("{")); + String eventData = + JsonStreamingMessages.STREAMING_ARTIFACT_UPDATE_EVENT.substring( + JsonStreamingMessages.STREAMING_ARTIFACT_UPDATE_EVENT.indexOf("{")); // Call onEvent method listener.onMessage(new ServerSentEvent(eventData), null); @@ -134,7 +129,8 @@ public void testOnEventWithTaskArtifactUpdateEventEvent() throws Exception { assertNotNull(receivedEvent.get()); assertTrue(receivedEvent.get() instanceof TaskArtifactUpdateEvent); - TaskArtifactUpdateEvent taskArtifactUpdateEvent = (TaskArtifactUpdateEvent) receivedEvent.get(); + TaskArtifactUpdateEvent taskArtifactUpdateEvent = + (TaskArtifactUpdateEvent) receivedEvent.get(); assertEquals("1", taskArtifactUpdateEvent.taskId()); assertEquals("2", taskArtifactUpdateEvent.contextId()); assertFalse(taskArtifactUpdateEvent.append()); @@ -143,22 +139,23 @@ public void testOnEventWithTaskArtifactUpdateEventEvent() throws Exception { assertEquals("artifact-1", artifact.artifactId()); assertEquals(1, artifact.parts().size()); assertTrue(artifact.parts().get(0) instanceof TextPart); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) artifact.parts().get(0)).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart) artifact.parts().get(0)).text()); } @Test public void testOnEventWithError() throws Exception { // Set up event handler AtomicReference receivedError = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> receivedError.set(error) - ); + SSEEventListener listener = + new SSEEventListener(event -> {}, error -> receivedError.set(error)); // Parse the error event JSON - String eventData = JsonStreamingMessages.STREAMING_ERROR_EVENT.substring( - JsonStreamingMessages.STREAMING_ERROR_EVENT.indexOf("{")); - + String eventData = + JsonStreamingMessages.STREAMING_ERROR_EVENT.substring( + JsonStreamingMessages.STREAMING_ERROR_EVENT.indexOf("{")); + // Call onEvent method listener.onMessage(new ServerSentEvent(eventData), null); @@ -174,10 +171,8 @@ public void testOnEventWithError() throws Exception { @Test public void testOnFailure() { AtomicBoolean failureHandlerCalled = new AtomicBoolean(false); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> failureHandlerCalled.set(true) - ); + SSEEventListener listener = + new SSEEventListener(event -> {}, error -> failureHandlerCalled.set(true)); // Simulate a failure CancelCapturingFuture future = new CancelCapturingFuture(); @@ -192,23 +187,18 @@ public void testOnFailure() { @Test public void testFinalTaskStatusUpdateEventCancels() { TaskStatus completedStatus = new TaskStatus(TaskState.TASK_STATE_COMPLETED); - TaskStatusUpdateEvent tsue = new TaskStatusUpdateEvent( - "1234", - completedStatus, - "xyz", - null - ); + TaskStatusUpdateEvent tsue = + new TaskStatusUpdateEvent("1234", completedStatus, "xyz", null); // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // Parse the message event JSON - String eventData = JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( - JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{")); + String eventData = + JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( + JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{")); // Call onMessage with a cancellable future CancelCapturingFuture future = new CancelCapturingFuture(); @@ -228,14 +218,13 @@ public void testFinalTaskStatusUpdateEventCancels() { public void testOnEventWithFinalTaskStatusUpdateEventEventCancels() throws Exception { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // Parse the message event JSON - String eventData = JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( - JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{")); + String eventData = + JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( + JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{")); // Call onEvent method CancelCapturingFuture future = new CancelCapturingFuture(); @@ -253,15 +242,17 @@ public void testOnEventWithFinalTaskStatusUpdateEventEventCancels() throws Excep assertTrue(future.cancelHandlerCalled); } - - @Test public void testOnCompleteThenOnErrorDeliversSingleTerminalCallback() { AtomicInteger terminalCount = new AtomicInteger(0); AtomicReference lastArg = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> { terminalCount.incrementAndGet(); lastArg.set(error); }); + SSEEventListener listener = + new SSEEventListener( + event -> {}, + error -> { + terminalCount.incrementAndGet(); + lastArg.set(error); + }); listener.onComplete(); listener.onError(new RuntimeException("late error"), new CancelCapturingFuture()); @@ -274,9 +265,13 @@ public void testOnCompleteThenOnErrorDeliversSingleTerminalCallback() { public void testOnErrorThenOnCompleteDeliversSingleTerminalCallback() { AtomicInteger terminalCount = new AtomicInteger(0); AtomicReference lastArg = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> { terminalCount.incrementAndGet(); lastArg.set(error); }); + SSEEventListener listener = + new SSEEventListener( + event -> {}, + error -> { + terminalCount.incrementAndGet(); + lastArg.set(error); + }); RuntimeException boom = new RuntimeException("first error"); listener.onError(boom, new CancelCapturingFuture()); @@ -290,9 +285,13 @@ public void testOnErrorThenOnCompleteDeliversSingleTerminalCallback() { public void testRepeatedOnCompleteDeliversSingleTerminalCallback() { AtomicInteger terminalCount = new AtomicInteger(0); AtomicReference lastArg = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> { terminalCount.incrementAndGet(); lastArg.set(error); }); + SSEEventListener listener = + new SSEEventListener( + event -> {}, + error -> { + terminalCount.incrementAndGet(); + lastArg.set(error); + }); listener.onComplete(); listener.onComplete(); @@ -305,9 +304,13 @@ public void testRepeatedOnCompleteDeliversSingleTerminalCallback() { public void testConcurrentTerminalSignalsDeliverExactlyOneCallback() throws Exception { AtomicInteger terminalCount = new AtomicInteger(0); AtomicReference lastArg = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> { terminalCount.incrementAndGet(); lastArg.set(error); }); + SSEEventListener listener = + new SSEEventListener( + event -> {}, + error -> { + terminalCount.incrementAndGet(); + lastArg.set(error); + }); int threads = 32; ExecutorService pool = Executors.newFixedThreadPool(threads); @@ -315,20 +318,22 @@ public void testConcurrentTerminalSignalsDeliverExactlyOneCallback() throws Exce CountDownLatch done = new CountDownLatch(threads); for (int i = 0; i < threads; i++) { final boolean complete = (i % 2 == 0); - pool.submit(() -> { - try { - start.await(); - if (complete) { - listener.onComplete(); - } else { - listener.onError(new RuntimeException("race"), new CancelCapturingFuture()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } finally { - done.countDown(); - } - }); + pool.submit( + () -> { + try { + start.await(); + if (complete) { + listener.onComplete(); + } else { + listener.onError( + new RuntimeException("race"), new CancelCapturingFuture()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + done.countDown(); + } + }); } start.countDown(); assertTrue(done.await(10, TimeUnit.SECONDS)); @@ -340,12 +345,12 @@ public void testConcurrentTerminalSignalsDeliverExactlyOneCallback() throws Exce @Test public void testFinalEventThenOnCompleteDeliversSingleTerminalCallback() throws Exception { AtomicInteger terminalCount = new AtomicInteger(0); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> terminalCount.incrementAndGet()); + SSEEventListener listener = + new SSEEventListener(event -> {}, error -> terminalCount.incrementAndGet()); - String eventData = JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( - JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{")); + String eventData = + JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( + JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{")); CancelCapturingFuture future = new CancelCapturingFuture(); listener.onMessage(new ServerSentEvent(eventData), future); listener.onComplete(); @@ -358,15 +363,17 @@ public void testFinalEventThenOnCompleteDeliversSingleTerminalCallback() throws public void testFinalEventThenOnErrorDeliversNormalCompletionOnly() { AtomicInteger terminalCount = new AtomicInteger(0); AtomicReference terminalError = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> { - terminalCount.incrementAndGet(); - terminalError.set(error); - }); - - String eventData = JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( - JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{")); + SSEEventListener listener = + new SSEEventListener( + event -> {}, + error -> { + terminalCount.incrementAndGet(); + terminalError.set(error); + }); + + String eventData = + JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( + JsonStreamingMessages.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{")); CancelCapturingFuture future = new CancelCapturingFuture(); listener.onMessage(new ServerSentEvent(eventData), future); listener.onError(new RuntimeException("cancelled after final event"), future); @@ -379,8 +386,7 @@ public void testFinalEventThenOnErrorDeliversNormalCompletionOnly() { private static class CancelCapturingFuture implements Future { private boolean cancelHandlerCalled; - public CancelCapturingFuture() { - } + public CancelCapturingFuture() {} @Override public boolean cancel(boolean mayInterruptIfRunning) { @@ -404,7 +410,8 @@ public Void get() throws InterruptedException, ExecutionException { } @Override - public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + public Void get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { return null; } } diff --git a/client/transport/rest/pom.xml b/client/transport/rest/pom.xml index 0cb5ab24e..10352cfd2 100644 --- a/client/transport/rest/pom.xml +++ b/client/transport/rest/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -64,4 +62,4 @@ - \ No newline at end of file + diff --git a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestErrorMapper.java b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestErrorMapper.java index 66be96616..c92974a3d 100644 --- a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestErrorMapper.java +++ b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestErrorMapper.java @@ -1,12 +1,11 @@ package org.a2aproject.sdk.client.transport.rest; +import com.google.gson.JsonObject; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; - -import com.google.gson.JsonObject; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; @@ -33,81 +32,106 @@ */ public class RestErrorMapper { - private record ReasonAndMetadata(String reason, @org.jspecify.annotations.Nullable Map metadata) {} + private record ReasonAndMetadata( + String reason, @org.jspecify.annotations.Nullable Map metadata) {} - private static final Map REASON_MAP = Map.ofEntries( - Map.entry("TASK_NOT_FOUND", A2AErrorCodes.TASK_NOT_FOUND), - Map.entry("TASK_NOT_CANCELABLE", A2AErrorCodes.TASK_NOT_CANCELABLE), - Map.entry("PUSH_NOTIFICATION_NOT_SUPPORTED", A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED), - Map.entry("UNSUPPORTED_OPERATION", A2AErrorCodes.UNSUPPORTED_OPERATION), - Map.entry("CONTENT_TYPE_NOT_SUPPORTED", A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED), - Map.entry("INVALID_AGENT_RESPONSE", A2AErrorCodes.INVALID_AGENT_RESPONSE), - Map.entry("EXTENDED_AGENT_CARD_NOT_CONFIGURED", A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED), - Map.entry("EXTENSION_SUPPORT_REQUIRED", A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED), - Map.entry("VERSION_NOT_SUPPORTED", A2AErrorCodes.VERSION_NOT_SUPPORTED), - Map.entry("INVALID_REQUEST", A2AErrorCodes.INVALID_REQUEST), - Map.entry("METHOD_NOT_FOUND", A2AErrorCodes.METHOD_NOT_FOUND), - Map.entry("INVALID_PARAMS", A2AErrorCodes.INVALID_PARAMS), - Map.entry("INTERNAL", A2AErrorCodes.INTERNAL), - Map.entry("JSON_PARSE", A2AErrorCodes.JSON_PARSE) - ); + private static final Map REASON_MAP = + Map.ofEntries( + Map.entry("TASK_NOT_FOUND", A2AErrorCodes.TASK_NOT_FOUND), + Map.entry("TASK_NOT_CANCELABLE", A2AErrorCodes.TASK_NOT_CANCELABLE), + Map.entry( + "PUSH_NOTIFICATION_NOT_SUPPORTED", + A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED), + Map.entry("UNSUPPORTED_OPERATION", A2AErrorCodes.UNSUPPORTED_OPERATION), + Map.entry( + "CONTENT_TYPE_NOT_SUPPORTED", A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED), + Map.entry("INVALID_AGENT_RESPONSE", A2AErrorCodes.INVALID_AGENT_RESPONSE), + Map.entry( + "EXTENDED_AGENT_CARD_NOT_CONFIGURED", + A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED), + Map.entry( + "EXTENSION_SUPPORT_REQUIRED", A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED), + Map.entry("VERSION_NOT_SUPPORTED", A2AErrorCodes.VERSION_NOT_SUPPORTED), + Map.entry("INVALID_REQUEST", A2AErrorCodes.INVALID_REQUEST), + Map.entry("METHOD_NOT_FOUND", A2AErrorCodes.METHOD_NOT_FOUND), + Map.entry("INVALID_PARAMS", A2AErrorCodes.INVALID_PARAMS), + Map.entry("INTERNAL", A2AErrorCodes.INTERNAL), + Map.entry("JSON_PARSE", A2AErrorCodes.JSON_PARSE)); public static A2AClientException mapRestError(A2AHttpResponse response) { - return RestErrorMapper.mapRestError(response.body(), response.status(), response.headers().toMap()); + return RestErrorMapper.mapRestError( + response.body(), response.status(), response.headers().toMap()); } public static A2AClientException mapRestError(String body, int code) { return mapRestError(body, code, Map.of()); } - public static A2AClientException mapRestError(String body, int code, Map> headers) { + public static A2AClientException mapRestError( + String body, int code, Map> headers) { try { if (body != null && !body.isBlank()) { JsonObject node = JsonUtil.fromJson(body, JsonObject.class); - // Google Cloud API error format: { "error": { "code", "status", "message", "details" } } + // Google Cloud API error format: { "error": { "code", "status", "message", + // "details" } } if (node.has("error") && node.get("error").isJsonObject()) { JsonObject errorObj = node.getAsJsonObject("error"); - String errorMessage = errorObj.has("message") ? errorObj.get("message").getAsString() : ""; + String errorMessage = + errorObj.has("message") ? errorObj.get("message").getAsString() : ""; ReasonAndMetadata reasonAndMetadata = extractReasonAndMetadata(errorObj); if (reasonAndMetadata != null) { - A2AClientException known = mapRestErrorByReason(reasonAndMetadata.reason(), errorMessage, reasonAndMetadata.metadata()); + A2AClientException known = + mapRestErrorByReason( + reasonAndMetadata.reason(), + errorMessage, + reasonAndMetadata.metadata()); if (known.getCause() != null) { return known; } // Unrecognized reason — include HTTP status and headers - String msg = errorMessage.isEmpty() ? "Request failed with HTTP " + code : errorMessage; - return new A2AClientException(msg, new A2AClientHTTPError(code, msg, body, headers)); + String msg = + errorMessage.isEmpty() + ? "Request failed with HTTP " + code + : errorMessage; + return new A2AClientException( + msg, new A2AClientHTTPError(code, msg, body, headers)); } - return new A2AClientException(errorMessage, + return new A2AClientException( + errorMessage, new A2AClientHTTPError(code, errorMessage, body, headers)); } // Legacy format (error class name, message) String className = node.has("error") ? node.get("error").getAsString() : ""; String errorMessage = node.has("message") ? node.get("message").getAsString() : ""; if (!className.isEmpty()) { - A2AClientException known = mapRestErrorByClassName(className, errorMessage, code); + A2AClientException known = + mapRestErrorByClassName(className, errorMessage, code); if (known.getCause() != null) { return known; } } // Unknown or empty class name — include HTTP status and headers - String msg = errorMessage.isEmpty() ? "Request failed with HTTP " + code : errorMessage; - return new A2AClientException(msg, new A2AClientHTTPError(code, msg, body, headers)); + String msg = + errorMessage.isEmpty() ? "Request failed with HTTP " + code : errorMessage; + return new A2AClientException( + msg, new A2AClientHTTPError(code, msg, body, headers)); } String message = "Request failed with HTTP " + code; - return new A2AClientException(message, - new A2AClientHTTPError(code, message, body, headers)); + return new A2AClientException( + message, new A2AClientHTTPError(code, message, body, headers)); } catch (JsonProcessingException ex) { Logger.getLogger(RestErrorMapper.class.getName()).log(Level.SEVERE, null, ex); String message = "Failed to parse error response: " + ex.getMessage(); - return new A2AClientException(message, new A2AClientHTTPError(code, message, body, headers)); + return new A2AClientException( + message, new A2AClientHTTPError(code, message, body, headers)); } } /** * Extracts the "reason" and "metadata" fields from the first entry in the "details" array. */ - private static @org.jspecify.annotations.Nullable ReasonAndMetadata extractReasonAndMetadata(JsonObject errorObj) { + private static @org.jspecify.annotations.Nullable ReasonAndMetadata extractReasonAndMetadata( + JsonObject errorObj) { if (errorObj.has("details") && errorObj.get("details").isJsonArray()) { var details = errorObj.getAsJsonArray("details"); if (!details.isEmpty() && details.get(0).isJsonObject()) { @@ -119,7 +143,10 @@ public static A2AClientException mapRestError(String body, int code, Map metaMap = new HashMap<>(); for (var entry : metaObj.entrySet()) { - metaMap.put(entry.getKey(), JsonUtil.OBJECT_MAPPER.fromJson(entry.getValue(), Object.class)); + metaMap.put( + entry.getKey(), + JsonUtil.OBJECT_MAPPER.fromJson( + entry.getValue(), Object.class)); } metadata = metaMap; } @@ -138,45 +165,106 @@ public static A2AClientException mapRestError(String body, int code, Map metadata) { + private static A2AClientException mapRestErrorByReason( + String reason, + String errorMessage, + @org.jspecify.annotations.Nullable Map metadata) { A2AErrorCodes errorCode = REASON_MAP.get(reason); if (errorCode == null) { return new A2AClientException(errorMessage); } return switch (errorCode) { - case TASK_NOT_FOUND -> new A2AClientException(errorMessage, new TaskNotFoundError(errorMessage, metadata)); - case TASK_NOT_CANCELABLE -> new A2AClientException(errorMessage, new TaskNotCancelableError(null, errorMessage, metadata)); - case PUSH_NOTIFICATION_NOT_SUPPORTED -> new A2AClientException(errorMessage, new PushNotificationNotSupportedError(null, errorMessage, metadata)); - case UNSUPPORTED_OPERATION -> new A2AClientException(errorMessage, new UnsupportedOperationError(null, errorMessage, metadata)); - case CONTENT_TYPE_NOT_SUPPORTED -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, errorMessage, metadata)); - case INVALID_AGENT_RESPONSE -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, errorMessage, metadata)); - case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new A2AClientException(errorMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, metadata)); - case EXTENSION_SUPPORT_REQUIRED -> new A2AClientException(errorMessage, new ExtensionSupportRequiredError(null, errorMessage, metadata)); - case VERSION_NOT_SUPPORTED -> new A2AClientException(errorMessage, new VersionNotSupportedError(null, errorMessage, metadata)); - case INVALID_REQUEST -> new A2AClientException(errorMessage, new InvalidRequestError(null, errorMessage, metadata)); - case JSON_PARSE -> new A2AClientException(errorMessage, new JSONParseError(null, errorMessage, metadata)); - case METHOD_NOT_FOUND -> new A2AClientException(errorMessage, new MethodNotFoundError(null, errorMessage, metadata)); - case INVALID_PARAMS -> new A2AClientException(errorMessage, new InvalidParamsError(null, errorMessage, metadata)); - case INTERNAL -> new A2AClientException(errorMessage, new InternalError(null, errorMessage, metadata)); + case TASK_NOT_FOUND -> + new A2AClientException( + errorMessage, new TaskNotFoundError(errorMessage, metadata)); + case TASK_NOT_CANCELABLE -> + new A2AClientException( + errorMessage, new TaskNotCancelableError(null, errorMessage, metadata)); + case PUSH_NOTIFICATION_NOT_SUPPORTED -> + new A2AClientException( + errorMessage, + new PushNotificationNotSupportedError(null, errorMessage, metadata)); + case UNSUPPORTED_OPERATION -> + new A2AClientException( + errorMessage, + new UnsupportedOperationError(null, errorMessage, metadata)); + case CONTENT_TYPE_NOT_SUPPORTED -> + new A2AClientException( + errorMessage, + new ContentTypeNotSupportedError(null, errorMessage, metadata)); + case INVALID_AGENT_RESPONSE -> + new A2AClientException( + errorMessage, + new InvalidAgentResponseError(null, errorMessage, metadata)); + case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> + new A2AClientException( + errorMessage, + new ExtendedAgentCardNotConfiguredError(null, errorMessage, metadata)); + case EXTENSION_SUPPORT_REQUIRED -> + new A2AClientException( + errorMessage, + new ExtensionSupportRequiredError(null, errorMessage, metadata)); + case VERSION_NOT_SUPPORTED -> + new A2AClientException( + errorMessage, + new VersionNotSupportedError(null, errorMessage, metadata)); + case INVALID_REQUEST -> + new A2AClientException( + errorMessage, new InvalidRequestError(null, errorMessage, metadata)); + case JSON_PARSE -> + new A2AClientException( + errorMessage, new JSONParseError(null, errorMessage, metadata)); + case METHOD_NOT_FOUND -> + new A2AClientException( + errorMessage, new MethodNotFoundError(null, errorMessage, metadata)); + case INVALID_PARAMS -> + new A2AClientException( + errorMessage, new InvalidParamsError(null, errorMessage, metadata)); + case INTERNAL -> + new A2AClientException( + errorMessage, new InternalError(null, errorMessage, metadata)); }; } - private static A2AClientException mapRestErrorByClassName(String className, String errorMessage, int code) { + private static A2AClientException mapRestErrorByClassName( + String className, String errorMessage, int code) { return switch (className) { - case "org.a2aproject.sdk.spec.TaskNotFoundError" -> new A2AClientException(errorMessage, new TaskNotFoundError()); - case "org.a2aproject.sdk.spec.ExtendedCardNotConfiguredError" -> new A2AClientException(errorMessage, new ExtendedAgentCardNotConfiguredError(null, errorMessage, null)); - case "org.a2aproject.sdk.spec.ContentTypeNotSupportedError" -> new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, errorMessage, null)); - case "org.a2aproject.sdk.spec.InternalError" -> new A2AClientException(errorMessage, new InternalError(errorMessage)); - case "org.a2aproject.sdk.spec.InvalidAgentResponseError" -> new A2AClientException(errorMessage, new InvalidAgentResponseError(null, errorMessage, null)); - case "org.a2aproject.sdk.spec.InvalidParamsError" -> new A2AClientException(errorMessage, new InvalidParamsError()); - case "org.a2aproject.sdk.spec.InvalidRequestError" -> new A2AClientException(errorMessage, new InvalidRequestError()); - case "org.a2aproject.sdk.spec.JSONParseError" -> new A2AClientException(errorMessage, new JSONParseError()); - case "org.a2aproject.sdk.spec.MethodNotFoundError" -> new A2AClientException(errorMessage, new MethodNotFoundError()); - case "org.a2aproject.sdk.spec.PushNotificationNotSupportedError" -> new A2AClientException(errorMessage, new PushNotificationNotSupportedError()); - case "org.a2aproject.sdk.spec.TaskNotCancelableError" -> new A2AClientException(errorMessage, new TaskNotCancelableError()); - case "org.a2aproject.sdk.spec.UnsupportedOperationError" -> new A2AClientException(errorMessage, new UnsupportedOperationError()); - case "org.a2aproject.sdk.spec.ExtensionSupportRequiredError" -> new A2AClientException(errorMessage, new ExtensionSupportRequiredError(null, errorMessage, null)); - case "org.a2aproject.sdk.spec.VersionNotSupportedError" -> new A2AClientException(errorMessage, new VersionNotSupportedError(null, errorMessage, null)); + case "org.a2aproject.sdk.spec.TaskNotFoundError" -> + new A2AClientException(errorMessage, new TaskNotFoundError()); + case "org.a2aproject.sdk.spec.ExtendedCardNotConfiguredError" -> + new A2AClientException( + errorMessage, + new ExtendedAgentCardNotConfiguredError(null, errorMessage, null)); + case "org.a2aproject.sdk.spec.ContentTypeNotSupportedError" -> + new A2AClientException( + errorMessage, + new ContentTypeNotSupportedError(null, errorMessage, null)); + case "org.a2aproject.sdk.spec.InternalError" -> + new A2AClientException(errorMessage, new InternalError(errorMessage)); + case "org.a2aproject.sdk.spec.InvalidAgentResponseError" -> + new A2AClientException( + errorMessage, new InvalidAgentResponseError(null, errorMessage, null)); + case "org.a2aproject.sdk.spec.InvalidParamsError" -> + new A2AClientException(errorMessage, new InvalidParamsError()); + case "org.a2aproject.sdk.spec.InvalidRequestError" -> + new A2AClientException(errorMessage, new InvalidRequestError()); + case "org.a2aproject.sdk.spec.JSONParseError" -> + new A2AClientException(errorMessage, new JSONParseError()); + case "org.a2aproject.sdk.spec.MethodNotFoundError" -> + new A2AClientException(errorMessage, new MethodNotFoundError()); + case "org.a2aproject.sdk.spec.PushNotificationNotSupportedError" -> + new A2AClientException(errorMessage, new PushNotificationNotSupportedError()); + case "org.a2aproject.sdk.spec.TaskNotCancelableError" -> + new A2AClientException(errorMessage, new TaskNotCancelableError()); + case "org.a2aproject.sdk.spec.UnsupportedOperationError" -> + new A2AClientException(errorMessage, new UnsupportedOperationError()); + case "org.a2aproject.sdk.spec.ExtensionSupportRequiredError" -> + new A2AClientException( + errorMessage, + new ExtensionSupportRequiredError(null, errorMessage, null)); + case "org.a2aproject.sdk.spec.VersionNotSupportedError" -> + new A2AClientException( + errorMessage, new VersionNotSupportedError(null, errorMessage, null)); default -> new A2AClientException(errorMessage); }; } diff --git a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java index d51549c70..12540064b 100644 --- a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java +++ b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java @@ -13,6 +13,9 @@ import static org.a2aproject.sdk.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD; import static org.a2aproject.sdk.util.Assert.checkNotNullParam; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.util.JsonFormat; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -24,20 +27,16 @@ import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; - -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.util.JsonFormat; import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.client.http.A2AHttpResponse; -import org.a2aproject.sdk.common.A2AHeaders; import org.a2aproject.sdk.client.transport.rest.sse.SSEEventListener; import org.a2aproject.sdk.client.transport.spi.ClientTransport; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor; import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders; +import org.a2aproject.sdk.common.A2AHeaders; import org.a2aproject.sdk.grpc.utils.ProtoJsonUtils; import org.a2aproject.sdk.grpc.utils.ProtoUtils; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; @@ -77,15 +76,24 @@ public RestTransport(AgentCard agentCard) { this(null, agentCard, Utils.getFavoriteInterface(agentCard), null); } - public RestTransport(@Nullable A2AHttpClient httpClient, AgentCard agentCard, - AgentInterface agentInterface, @Nullable List interceptors) { + public RestTransport( + @Nullable A2AHttpClient httpClient, + AgentCard agentCard, + AgentInterface agentInterface, + @Nullable List interceptors) { this(httpClient, agentCard, agentInterface, interceptors, false); } - public RestTransport(@Nullable A2AHttpClient httpClient, AgentCard agentCard, - AgentInterface agentInterface, @Nullable List interceptors, + public RestTransport( + @Nullable A2AHttpClient httpClient, + AgentCard agentCard, + AgentInterface agentInterface, + @Nullable List interceptors, boolean followRedirects) { - this.httpClient = httpClient == null ? A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT) : httpClient; + this.httpClient = + httpClient == null + ? A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT) + : httpClient; this.agentCard = agentCard; this.agentInterface = agentInterface; this.interceptors = interceptors; @@ -93,13 +101,23 @@ public RestTransport(@Nullable A2AHttpClient httpClient, AgentCard agentCard, } @Override - public EventKind sendMessage(MessageSendParams messageSendParams, @Nullable ClientCallContext context) throws A2AClientException { + public EventKind sendMessage( + MessageSendParams messageSendParams, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("messageSendParams", messageSendParams); - org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(ProtoUtils.ToProto.sendMessageRequest(messageSendParams)); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_MESSAGE_METHOD, builder, agentCard, context); + org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = + org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder( + ProtoUtils.ToProto.sendMessageRequest(messageSendParams)); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(SEND_MESSAGE_METHOD, builder, agentCard, context); try { - String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, messageSendParams.tenant()) + "/message:send", payloadAndHeaders); - org.a2aproject.sdk.grpc.SendMessageResponse.Builder responseBuilder = org.a2aproject.sdk.grpc.SendMessageResponse.newBuilder(); + String httpResponseBody = + sendPostRequest( + Utils.buildBaseUrl(agentInterface, messageSendParams.tenant()) + + "/message:send", + payloadAndHeaders); + org.a2aproject.sdk.grpc.SendMessageResponse.Builder responseBuilder = + org.a2aproject.sdk.grpc.SendMessageResponse.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); if (responseBuilder.hasMessage()) { return ProtoUtils.FromProto.message(responseBuilder.getMessage()); @@ -107,7 +125,8 @@ public EventKind sendMessage(MessageSendParams messageSendParams, @Nullable Clie if (responseBuilder.hasTask()) { return ProtoUtils.FromProto.task(responseBuilder.getTask()); } - throw new A2AClientException("Failed to send message, wrong response:" + httpResponseBody); + throw new A2AClientException( + "Failed to send message, wrong response:" + httpResponseBody); } catch (A2AClientException e) { throw e; } catch (IOException | InterruptedException | JsonProcessingException e) { @@ -116,45 +135,65 @@ public EventKind sendMessage(MessageSendParams messageSendParams, @Nullable Clie } @Override - public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer eventConsumer, Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException { + public void sendMessageStreaming( + MessageSendParams messageSendParams, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", messageSendParams); checkNotNullParam("eventConsumer", eventConsumer); checkNotNullParam("messageSendParams", messageSendParams); - org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(ProtoUtils.ToProto.sendMessageRequest(messageSendParams)); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SEND_STREAMING_MESSAGE_METHOD, builder, agentCard, context); + org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = + org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder( + ProtoUtils.ToProto.sendMessageRequest(messageSendParams)); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(SEND_STREAMING_MESSAGE_METHOD, builder, agentCard, context); AtomicReference> ref = new AtomicReference<>(); SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer); try { - A2AHttpClient.PostBuilder postBuilder = createPostBuilder( - Utils.buildBaseUrl(agentInterface, messageSendParams.tenant()) + "/message:stream", - payloadAndHeaders, - false); - ref.set(postBuilder.postAsyncSSE( - event -> sseEventListener.onMessage(event, ref.get()), - throwable -> sseEventListener.onError(throwable, ref.get()), - () -> { - // Signal normal stream completion to error handler (null error means success) - sseEventListener.onComplete(); - })); + A2AHttpClient.PostBuilder postBuilder = + createPostBuilder( + Utils.buildBaseUrl(agentInterface, messageSendParams.tenant()) + + "/message:stream", + payloadAndHeaders, + false); + ref.set( + postBuilder.postAsyncSSE( + event -> sseEventListener.onMessage(event, ref.get()), + throwable -> sseEventListener.onError(throwable, ref.get()), + () -> { + // Signal normal stream completion to error handler (null error + // means success) + sseEventListener.onComplete(); + })); } catch (IOException e) { throw new A2AClientException("Failed to send streaming message request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException("Send streaming message request timed out: " + e, e); } catch (JsonProcessingException e) { - throw new A2AClientException("Failed to process JSON for streaming message request: " + e, e); + throw new A2AClientException( + "Failed to process JSON for streaming message request: " + e, e); } } @Override - public Task getTask(TaskQueryParams taskQueryParams, @Nullable ClientCallContext context) throws A2AClientException { + public Task getTask(TaskQueryParams taskQueryParams, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("taskQueryParams", taskQueryParams); - org.a2aproject.sdk.grpc.GetTaskRequest.Builder builder = org.a2aproject.sdk.grpc.GetTaskRequest.newBuilder(); + org.a2aproject.sdk.grpc.GetTaskRequest.Builder builder = + org.a2aproject.sdk.grpc.GetTaskRequest.newBuilder(); builder.setId(taskQueryParams.id()); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_TASK_METHOD, builder, agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(GET_TASK_METHOD, builder, agentCard, context); try { - StringBuilder url = new StringBuilder(Utils.buildBaseUrl(agentInterface, taskQueryParams.tenant())); + StringBuilder url = + new StringBuilder(Utils.buildBaseUrl(agentInterface, taskQueryParams.tenant())); if (taskQueryParams.historyLength() != null && taskQueryParams.historyLength() > 0) { - url.append(String.format("/tasks/%s?historyLength=%d", taskQueryParams.id(), taskQueryParams.historyLength())); + url.append( + String.format( + "/tasks/%s?historyLength=%d", + taskQueryParams.id(), taskQueryParams.historyLength())); } else { url.append(String.format("/tasks/%1s", taskQueryParams.id())); } @@ -165,7 +204,8 @@ public Task getTask(TaskQueryParams taskQueryParams, @Nullable ClientCallContext throw RestErrorMapper.mapRestError(response); } String httpResponseBody = response.body(); - org.a2aproject.sdk.grpc.Task.Builder responseBuilder = org.a2aproject.sdk.grpc.Task.newBuilder(); + org.a2aproject.sdk.grpc.Task.Builder responseBuilder = + org.a2aproject.sdk.grpc.Task.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return ProtoUtils.FromProto.task(responseBuilder); } catch (A2AClientException e) { @@ -176,13 +216,22 @@ public Task getTask(TaskQueryParams taskQueryParams, @Nullable ClientCallContext } @Override - public Task cancelTask(CancelTaskParams taskIdParams, @Nullable ClientCallContext context) throws A2AClientException { + public Task cancelTask(CancelTaskParams taskIdParams, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("taskIdParams", taskIdParams); - org.a2aproject.sdk.grpc.CancelTaskRequest.Builder builder = org.a2aproject.sdk.grpc.CancelTaskRequest.newBuilder(ProtoUtils.ToProto.cancelTaskRequest(taskIdParams)); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(CANCEL_TASK_METHOD, builder, agentCard, context); + org.a2aproject.sdk.grpc.CancelTaskRequest.Builder builder = + org.a2aproject.sdk.grpc.CancelTaskRequest.newBuilder( + ProtoUtils.ToProto.cancelTaskRequest(taskIdParams)); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(CANCEL_TASK_METHOD, builder, agentCard, context); try { - String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, taskIdParams.tenant()) + String.format("/tasks/%1s:cancel", taskIdParams.id()), payloadAndHeaders); - org.a2aproject.sdk.grpc.Task.Builder responseBuilder = org.a2aproject.sdk.grpc.Task.newBuilder(); + String httpResponseBody = + sendPostRequest( + Utils.buildBaseUrl(agentInterface, taskIdParams.tenant()) + + String.format("/tasks/%1s:cancel", taskIdParams.id()), + payloadAndHeaders); + org.a2aproject.sdk.grpc.Task.Builder responseBuilder = + org.a2aproject.sdk.grpc.Task.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return ProtoUtils.FromProto.task(responseBuilder); } catch (A2AClientException e) { @@ -193,9 +242,11 @@ public Task cancelTask(CancelTaskParams taskIdParams, @Nullable ClientCallContex } @Override - public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) throws A2AClientException { + public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - org.a2aproject.sdk.grpc.ListTasksRequest.Builder builder = org.a2aproject.sdk.grpc.ListTasksRequest.newBuilder(); + org.a2aproject.sdk.grpc.ListTasksRequest.Builder builder = + org.a2aproject.sdk.grpc.ListTasksRequest.newBuilder(); if (request.contextId() != null) { builder.setContextId(request.contextId()); } @@ -218,17 +269,20 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo builder.setIncludeArtifacts(true); } if (request.statusTimestampAfter() != null) { - builder.setStatusTimestampAfter(com.google.protobuf.Timestamp.newBuilder() - .setSeconds(request.statusTimestampAfter().getEpochSecond()) - .setNanos(request.statusTimestampAfter().getNano()) - .build()); + builder.setStatusTimestampAfter( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(request.statusTimestampAfter().getEpochSecond()) + .setNanos(request.statusTimestampAfter().getNano()) + .build()); } - PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_METHOD, builder, agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(LIST_TASK_METHOD, builder, agentCard, context); try { // Build query string - StringBuilder urlBuilder = new StringBuilder(Utils.buildBaseUrl(agentInterface, request.tenant())); + StringBuilder urlBuilder = + new StringBuilder(Utils.buildBaseUrl(agentInterface, request.tenant())); urlBuilder.append("/tasks"); String queryParams = buildListTasksQueryString(request); if (!queryParams.isEmpty()) { @@ -242,7 +296,8 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo throw RestErrorMapper.mapRestError(response); } String httpResponseBody = response.body(); - org.a2aproject.sdk.grpc.ListTasksResponse.Builder responseBuilder = org.a2aproject.sdk.grpc.ListTasksResponse.newBuilder(); + org.a2aproject.sdk.grpc.ListTasksResponse.Builder responseBuilder = + org.a2aproject.sdk.grpc.ListTasksResponse.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return new ListTasksResult( @@ -251,34 +306,42 @@ public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallCo .toList(), responseBuilder.getTotalSize(), responseBuilder.getTasksCount(), - responseBuilder.getNextPageToken().isEmpty() ? null : responseBuilder.getNextPageToken() - ); + responseBuilder.getNextPageToken().isEmpty() + ? null + : responseBuilder.getNextPageToken()); } catch (IOException | InterruptedException e) { throw new A2AClientException("Failed to list tasks: " + e, e); } } - private String buildListTasksQueryString(ListTasksParams request) { java.util.List queryParts = new java.util.ArrayList<>(); if (request.contextId() != null) { - queryParts.add("contextId=" + URLEncoder.encode(request.contextId(), StandardCharsets.UTF_8)); + queryParts.add( + "contextId=" + URLEncoder.encode(request.contextId(), StandardCharsets.UTF_8)); } if (request.status() != null) { - queryParts.add("status=" + URLEncoder.encode(request.status().toString(), StandardCharsets.UTF_8)); + queryParts.add( + "status=" + + URLEncoder.encode( + request.status().toString(), StandardCharsets.UTF_8)); } if (request.pageSize() != null) { queryParts.add("pageSize=" + request.pageSize()); } if (request.pageToken() != null) { - queryParts.add("pageToken=" + URLEncoder.encode(request.pageToken(), StandardCharsets.UTF_8)); + queryParts.add( + "pageToken=" + URLEncoder.encode(request.pageToken(), StandardCharsets.UTF_8)); } if (request.historyLength() != null) { queryParts.add("historyLength=" + request.historyLength()); } if (request.statusTimestampAfter() != null) { - queryParts.add("statusTimestampAfter=" + URLEncoder.encode( - request.statusTimestampAfter().toString(), StandardCharsets.UTF_8)); + queryParts.add( + "statusTimestampAfter=" + + URLEncoder.encode( + request.statusTimestampAfter().toString(), + StandardCharsets.UTF_8)); } if (request.includeArtifacts() != null && request.includeArtifacts()) { queryParts.add("includeArtifacts=true"); @@ -286,26 +349,38 @@ private String buildListTasksQueryString(ListTasksParams request) { return String.join("&", queryParts); } - private String buildListPushNotificationConfigsQueryString(ListTaskPushNotificationConfigsParams request) { + private String buildListPushNotificationConfigsQueryString( + ListTaskPushNotificationConfigsParams request) { java.util.List queryParts = new java.util.ArrayList<>(); if (request.pageSize() > 0) { queryParts.add("pageSize=" + request.pageSize()); } if (request.pageToken() != null && !request.pageToken().isEmpty()) { - queryParts.add("pageToken=" + URLEncoder.encode(request.pageToken(), StandardCharsets.UTF_8)); + queryParts.add( + "pageToken=" + URLEncoder.encode(request.pageToken(), StandardCharsets.UTF_8)); } return String.join("&", queryParts); } @Override - public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request, @Nullable ClientCallContext context) throws A2AClientException { + public TaskPushNotificationConfig createTaskPushNotificationConfiguration( + TaskPushNotificationConfig request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder - = ProtoUtils.ToProto.taskPushNotificationConfig(request).toBuilder(); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, agentCard, context); + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = + ProtoUtils.ToProto.taskPushNotificationConfig(request).toBuilder(); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, agentCard, context); try { - String httpResponseBody = sendPostRequest(Utils.buildBaseUrl(agentInterface, request.tenant()) + String.format("/tasks/%1s/pushNotificationConfigs", request.taskId()), payloadAndHeaders); - org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder responseBuilder = org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); + String httpResponseBody = + sendPostRequest( + Utils.buildBaseUrl(agentInterface, request.tenant()) + + String.format( + "/tasks/%1s/pushNotificationConfigs", request.taskId()), + payloadAndHeaders); + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder responseBuilder = + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return ProtoUtils.FromProto.taskPushNotificationConfig(responseBuilder); } catch (A2AClientException e) { @@ -316,22 +391,27 @@ public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPu } @Override - public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException { + public TaskPushNotificationConfig getTaskPushNotificationConfiguration( + GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.Builder builder - = org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.newBuilder(); + org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.Builder builder = + org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.newBuilder(); StringBuilder url = new StringBuilder(Utils.buildBaseUrl(agentInterface, request.tenant())); String configId = request.id(); if (configId != null && !configId.isEmpty()) { builder.setId(configId).setTaskId(request.taskId()); - url.append(String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.taskId(), configId)); + url.append( + String.format( + "/tasks/%1s/pushNotificationConfigs/%2s", request.taskId(), configId)); } else { // Use trailing slash to distinguish GET from LIST builder.setTaskId(request.taskId()); url.append(String.format("/tasks/%1s/pushNotificationConfigs/", request.taskId())); } - PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, - agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, agentCard, context); try { A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url.toString()); addStandardHeaders(getBuilder, payloadAndHeaders); @@ -340,7 +420,8 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPu throw RestErrorMapper.mapRestError(response); } String httpResponseBody = response.body(); - org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder responseBuilder = org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder responseBuilder = + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return ProtoUtils.FromProto.taskPushNotificationConfig(responseBuilder); } catch (A2AClientException e) { @@ -351,10 +432,12 @@ public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPu } @Override - public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations(ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) throws A2AClientException { + public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations( + ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder builder - = org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.newBuilder(); + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder builder = + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.newBuilder(); builder.setTaskId(request.id()); if (request.pageSize() > 0) { builder.setPageSize(request.pageSize()); @@ -362,10 +445,12 @@ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurati if (request.pageToken() != null && !request.pageToken().isEmpty()) { builder.setPageToken(request.pageToken()); } - PayloadAndHeaders payloadAndHeaders = applyInterceptors(LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, - agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, agentCard, context); try { - StringBuilder urlBuilder = new StringBuilder(Utils.buildBaseUrl(agentInterface, request.tenant())); + StringBuilder urlBuilder = + new StringBuilder(Utils.buildBaseUrl(agentInterface, request.tenant())); urlBuilder.append(String.format("/tasks/%1s/pushNotificationConfigs", request.id())); String queryString = buildListPushNotificationConfigsQueryString(request); if (!queryString.isEmpty()) { @@ -378,7 +463,10 @@ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurati throw RestErrorMapper.mapRestError(response); } String httpResponseBody = response.body(); - org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.Builder responseBuilder = org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.newBuilder(); + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.Builder + responseBuilder = + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse + .newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return ProtoUtils.FromProto.listTaskPushNotificationConfigsResult(responseBuilder); } catch (A2AClientException e) { @@ -389,13 +477,21 @@ public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurati } @Override - public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) throws A2AClientException { + public void deleteTaskPushNotificationConfigurations( + DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequestOrBuilder builder = org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder(); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, - agentCard, context); + org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequestOrBuilder builder = + org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder(); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors( + DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, builder, agentCard, context); try { - String url = Utils.buildBaseUrl(agentInterface, request.tenant()) + String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.taskId(), request.id()); + String url = + Utils.buildBaseUrl(agentInterface, request.tenant()) + + String.format( + "/tasks/%1s/pushNotificationConfigs/%2s", + request.taskId(), request.id()); A2AHttpClient.DeleteBuilder deleteBuilder = httpClient.createDelete().url(url); addStandardHeaders(deleteBuilder, payloadAndHeaders); A2AHttpResponse response = deleteBuilder.delete(); @@ -410,38 +506,52 @@ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationC } @Override - public void subscribeToTask(TaskIdParams request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException { + public void subscribeToTask( + TaskIdParams request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException { checkNotNullParam("request", request); - org.a2aproject.sdk.grpc.SubscribeToTaskRequest.Builder builder = org.a2aproject.sdk.grpc.SubscribeToTaskRequest.newBuilder(); + org.a2aproject.sdk.grpc.SubscribeToTaskRequest.Builder builder = + org.a2aproject.sdk.grpc.SubscribeToTaskRequest.newBuilder(); builder.setId(request.id()); - PayloadAndHeaders payloadAndHeaders = applyInterceptors(SUBSCRIBE_TO_TASK_METHOD, builder, - agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(SUBSCRIBE_TO_TASK_METHOD, builder, agentCard, context); AtomicReference> ref = new AtomicReference<>(); SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer); try { - String url = Utils.buildBaseUrl(agentInterface, request.tenant()) + String.format("/tasks/%1s:subscribe", request.id()); - A2AHttpClient.PostBuilder postBuilder = createPostBuilder(url, payloadAndHeaders, false); - ref.set(postBuilder.postAsyncSSE( - event -> sseEventListener.onMessage(event, ref.get()), - throwable -> sseEventListener.onError(throwable, ref.get()), - () -> { - // Signal normal stream completion to error handler (null error means success) - sseEventListener.onComplete(); - })); + String url = + Utils.buildBaseUrl(agentInterface, request.tenant()) + + String.format("/tasks/%1s:subscribe", request.id()); + A2AHttpClient.PostBuilder postBuilder = + createPostBuilder(url, payloadAndHeaders, false); + ref.set( + postBuilder.postAsyncSSE( + event -> sseEventListener.onMessage(event, ref.get()), + throwable -> sseEventListener.onError(throwable, ref.get()), + () -> { + // Signal normal stream completion to error handler (null error + // means success) + sseEventListener.onComplete(); + })); } catch (IOException e) { throw new A2AClientException("Failed to send streaming message request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException("Send streaming message request timed out: " + e, e); } catch (JsonProcessingException e) { - throw new A2AClientException("Failed to process JSON for streaming message request: " + e, e); + throw new A2AClientException( + "Failed to process JSON for streaming message request: " + e, e); } } @Override - public AgentCard getExtendedAgentCard(GetExtendedAgentCardParams params, @Nullable ClientCallContext context) throws A2AClientException { + public AgentCard getExtendedAgentCard( + GetExtendedAgentCardParams params, @Nullable ClientCallContext context) + throws A2AClientException { try { - PayloadAndHeaders payloadAndHeaders = applyInterceptors(GET_EXTENDED_AGENT_CARD_METHOD, null, agentCard, context); + PayloadAndHeaders payloadAndHeaders = + applyInterceptors(GET_EXTENDED_AGENT_CARD_METHOD, null, agentCard, context); String url = Utils.buildBaseUrl(agentInterface, params.tenant()) + "/extendedAgentCard"; A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url); addStandardHeaders(getBuilder, payloadAndHeaders); @@ -452,7 +562,8 @@ public AgentCard getExtendedAgentCard(GetExtendedAgentCardParams params, @Nullab String httpResponseBody = response.body(); return JsonUtil.fromJson(httpResponseBody, AgentCard.class); } catch (IOException | InterruptedException | JsonProcessingException e) { - throw new A2AClientException("Failed to get authenticated extended agent card: " + e, e); + throw new A2AClientException( + "Failed to get authenticated extended agent card: " + e, e); } catch (A2AClientError e) { throw new A2AClientException("Failed to get agent card: " + e, e); } @@ -463,25 +574,36 @@ public void close() { // no-op } - private PayloadAndHeaders applyInterceptors(String methodName, @Nullable MessageOrBuilder payload, - AgentCard agentCard, @Nullable ClientCallContext clientCallContext) { - PayloadAndHeaders payloadAndHeaders = new PayloadAndHeaders(payload, getHttpHeaders(clientCallContext)); + private PayloadAndHeaders applyInterceptors( + String methodName, + @Nullable MessageOrBuilder payload, + AgentCard agentCard, + @Nullable ClientCallContext clientCallContext) { + PayloadAndHeaders payloadAndHeaders = + new PayloadAndHeaders(payload, getHttpHeaders(clientCallContext)); if (interceptors != null && !interceptors.isEmpty()) { for (ClientCallInterceptor interceptor : interceptors) { - payloadAndHeaders = interceptor.intercept(methodName, payloadAndHeaders.getPayload(), - payloadAndHeaders.getHeaders(), agentCard, clientCallContext); + payloadAndHeaders = + interceptor.intercept( + methodName, + payloadAndHeaders.getPayload(), + payloadAndHeaders.getHeaders(), + agentCard, + clientCallContext); } } return payloadAndHeaders; } - private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException { + private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders) + throws IOException, InterruptedException, JsonProcessingException { MessageOrBuilder payload = (MessageOrBuilder) payloadAndHeaders.getPayload(); String body = payload != null ? ProtoJsonUtils.toJson(JsonFormat.printer(), payload) : ""; if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine(body); } - A2AHttpClient.PostBuilder builder = buildPost(url, payloadAndHeaders, body, followRedirects); + A2AHttpClient.PostBuilder builder = + buildPost(url, payloadAndHeaders, body, followRedirects); A2AHttpResponse response = builder.post(); if (!response.success()) { if (LOGGER.isLoggable(Level.FINE)) { @@ -495,7 +617,9 @@ private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders) private A2AHttpClient.PostBuilder createPostBuilder( String url, PayloadAndHeaders payloadAndHeaders, boolean followRedirects) throws JsonProcessingException, InvalidProtocolBufferException { - String body = ProtoJsonUtils.toJson(JsonFormat.printer(), (MessageOrBuilder) payloadAndHeaders.getPayload()); + String body = + ProtoJsonUtils.toJson( + JsonFormat.printer(), (MessageOrBuilder) payloadAndHeaders.getPayload()); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine(body); } @@ -505,12 +629,14 @@ private A2AHttpClient.PostBuilder createPostBuilder( private A2AHttpClient.PostBuilder buildPost( String url, PayloadAndHeaders payloadAndHeaders, String body, boolean followRedirects) throws JsonProcessingException { - A2AHttpClient.PostBuilder postBuilder = httpClient.createPost() - .url(url) - .addHeader("Content-Type", "application/json") - .addHeader(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION) - .followRedirects(followRedirects) - .body(body); + A2AHttpClient.PostBuilder postBuilder = + httpClient + .createPost() + .url(url) + .addHeader("Content-Type", "application/json") + .addHeader(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION) + .followRedirects(followRedirects) + .body(body); if (payloadAndHeaders.getHeaders() != null) { for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { @@ -520,7 +646,8 @@ private A2AHttpClient.PostBuilder buildPost( return postBuilder; } - private static > void addStandardHeaders(T builder, PayloadAndHeaders payloadAndHeaders) { + private static > void addStandardHeaders( + T builder, PayloadAndHeaders payloadAndHeaders) { builder.addHeader(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION); if (payloadAndHeaders.getHeaders() != null) { for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { diff --git a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransportConfigBuilder.java b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransportConfigBuilder.java index d9f958df0..d5e467cf2 100644 --- a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransportConfigBuilder.java +++ b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransportConfigBuilder.java @@ -63,7 +63,8 @@ * @see A2AHttpClient * @see org.a2aproject.sdk.client.http.JdkA2AHttpClient */ -public class RestTransportConfigBuilder extends ClientTransportConfigBuilder { +public class RestTransportConfigBuilder + extends ClientTransportConfigBuilder { private @Nullable A2AHttpClient httpClient; private A2AHttpClientConfiguration httpClientConfiguration = A2AHttpClientConfiguration.DEFAULT; @@ -102,9 +103,11 @@ public RestTransportConfigBuilder httpClient(A2AHttpClient httpClient) { } /** Sets the configuration used when this builder creates the HTTP client. */ - public RestTransportConfigBuilder httpClientConfiguration(A2AHttpClientConfiguration httpClientConfiguration) { - this.httpClientConfiguration = org.a2aproject.sdk.util.Assert.checkNotNullParam( - "httpClientConfiguration", httpClientConfiguration); + public RestTransportConfigBuilder httpClientConfiguration( + A2AHttpClientConfiguration httpClientConfiguration) { + this.httpClientConfiguration = + org.a2aproject.sdk.util.Assert.checkNotNullParam( + "httpClientConfiguration", httpClientConfiguration); return this; } diff --git a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransportProvider.java b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransportProvider.java index 9621b950e..334033a10 100644 --- a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransportProvider.java +++ b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransportProvider.java @@ -1,14 +1,15 @@ package org.a2aproject.sdk.client.transport.rest; -import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; +import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.client.transport.spi.ClientTransportProvider; import org.a2aproject.sdk.spec.A2AClientException; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.TransportProtocol; -public class RestTransportProvider implements ClientTransportProvider { +public class RestTransportProvider + implements ClientTransportProvider { @Override public String getTransportProtocol() { @@ -16,13 +17,23 @@ public String getTransportProtocol() { } @Override - public RestTransport create(RestTransportConfig clientTransportConfig, AgentCard agentCard, AgentInterface agentInterface) throws A2AClientException { + public RestTransport create( + RestTransportConfig clientTransportConfig, + AgentCard agentCard, + AgentInterface agentInterface) + throws A2AClientException { RestTransportConfig transportConfig = clientTransportConfig; - if (transportConfig == null) { - transportConfig = new RestTransportConfig(A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT)); + if (transportConfig == null) { + transportConfig = + new RestTransportConfig( + A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT)); } - return new RestTransport(transportConfig.getHttpClient(), agentCard, agentInterface, - transportConfig.getInterceptors(), transportConfig.isFollowRedirects()); + return new RestTransport( + transportConfig.getHttpClient(), + agentCard, + agentInterface, + transportConfig.getInterceptors(), + transportConfig.isFollowRedirects()); } @Override diff --git a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/package-info.java b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/package-info.java index b50c9a0a8..4d918bff4 100644 --- a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/package-info.java +++ b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.transport.rest; import org.jspecify.annotations.NullMarked; - diff --git a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/sse/SSEEventListener.java b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/sse/SSEEventListener.java index 0efe85f62..58ecbd184 100644 --- a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/sse/SSEEventListener.java +++ b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/sse/SSEEventListener.java @@ -1,14 +1,13 @@ package org.a2aproject.sdk.client.transport.rest.sse; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; import java.util.concurrent.Future; import java.util.function.Consumer; import java.util.logging.Logger; - -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.util.JsonFormat; import org.a2aproject.sdk.client.http.ServerSentEvent; -import org.a2aproject.sdk.client.transport.spi.sse.AbstractSSEEventListener; import org.a2aproject.sdk.client.transport.rest.RestErrorMapper; +import org.a2aproject.sdk.client.transport.spi.sse.AbstractSSEEventListener; import org.a2aproject.sdk.grpc.StreamResponse; import org.a2aproject.sdk.grpc.utils.ProtoUtils; import org.a2aproject.sdk.spec.StreamingEventKind; @@ -22,13 +21,14 @@ public class SSEEventListener extends AbstractSSEEventListener { private static final Logger LOGGER = Logger.getLogger(SSEEventListener.class.getName()); - public SSEEventListener(Consumer eventHandler, - @Nullable Consumer errorHandler) { + public SSEEventListener( + Consumer eventHandler, @Nullable Consumer errorHandler) { super(eventHandler, errorHandler); } public void onComplete() { - LOGGER.fine("SSEEventListener.onComplete() called - signaling successful stream completion"); + LOGGER.fine( + "SSEEventListener.onComplete() called - signaling successful stream completion"); signalTerminal(null); } @@ -36,7 +36,8 @@ public void onComplete() { public void onMessage(ServerSentEvent event, @Nullable Future completableFuture) { try { LOGGER.fine("REST SSE raw data: " + event.data()); - org.a2aproject.sdk.grpc.StreamResponse.Builder builder = org.a2aproject.sdk.grpc.StreamResponse.newBuilder(); + org.a2aproject.sdk.grpc.StreamResponse.Builder builder = + org.a2aproject.sdk.grpc.StreamResponse.newBuilder(); JsonFormat.parser().merge(event.data(), builder); parseAndHandleMessage(builder.build(), completableFuture); } catch (InvalidProtocolBufferException e) { @@ -53,23 +54,25 @@ public void onMessage(ServerSentEvent event, @Nullable Future completableF private void parseAndHandleMessage(StreamResponse response, @Nullable Future future) { StreamingEventKind event; switch (response.getPayloadCase()) { - case MESSAGE -> - event = ProtoUtils.FromProto.message(response.getMessage()); - case TASK -> - event = ProtoUtils.FromProto.task(response.getTask()); + case MESSAGE -> event = ProtoUtils.FromProto.message(response.getMessage()); + case TASK -> event = ProtoUtils.FromProto.task(response.getTask()); case STATUS_UPDATE -> - event = ProtoUtils.FromProto.taskStatusUpdateEvent(response.getStatusUpdate()); + event = ProtoUtils.FromProto.taskStatusUpdateEvent(response.getStatusUpdate()); case ARTIFACT_UPDATE -> - event = ProtoUtils.FromProto.taskArtifactUpdateEvent(response.getArtifactUpdate()); + event = + ProtoUtils.FromProto.taskArtifactUpdateEvent( + response.getArtifactUpdate()); default -> { LOGGER.warning("Invalid stream response " + response.getPayloadCase()); - signalTerminal(new IllegalStateException("Invalid stream response from server: " + response.getPayloadCase())); + signalTerminal( + new IllegalStateException( + "Invalid stream response from server: " + + response.getPayloadCase())); return; } } - + // Delegate to base class for common event handling and auto-close logic handleEvent(event, future); } - } diff --git a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/sse/package-info.java b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/sse/package-info.java index a308a0597..623ebc9e9 100644 --- a/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/sse/package-info.java +++ b/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/sse/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.transport.rest.sse; import org.jspecify.annotations.NullMarked; - diff --git a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/JsonRestMessages.java b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/JsonRestMessages.java index 0f28e0d9a..bbac62b19 100644 --- a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/JsonRestMessages.java +++ b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/JsonRestMessages.java @@ -6,7 +6,8 @@ */ public class JsonRestMessages { - static final String SEND_MESSAGE_TEST_REQUEST = """ + static final String SEND_MESSAGE_TEST_REQUEST = + """ { "message": { @@ -19,9 +20,11 @@ public class JsonRestMessages { "metadata": { } } - }"""; + }\ + """; - static final String SEND_MESSAGE_TEST_RESPONSE = """ + static final String SEND_MESSAGE_TEST_RESPONSE = + """ { "task": { "id": "9b511af4-b27c-47fa-aecf-2a93c08a44f8", @@ -44,14 +47,18 @@ public class JsonRestMessages { } ] } - }"""; + }\ + """; - static final String CANCEL_TASK_TEST_REQUEST = """ + static final String CANCEL_TASK_TEST_REQUEST = + """ { "id": "de38c76d-d54c-436c-8b9f-4c2703648d64" - }"""; + }\ + """; - static final String CANCEL_TASK_TEST_RESPONSE = """ + static final String CANCEL_TASK_TEST_RESPONSE = + """ { "id": "de38c76d-d54c-436c-8b9f-4c2703648d64", "contextId": "c295ea44-7543-4f78-b524-7a38915ad6e4", @@ -59,9 +66,11 @@ public class JsonRestMessages { "state": "TASK_STATE_CANCELED" }, "metadata": {} - }"""; + }\ + """; - static final String GET_TASK_TEST_RESPONSE = """ + static final String GET_TASK_TEST_RESPONSE = + """ { "id": "de38c76d-d54c-436c-8b9f-4c2703648d64", "contextId": "c295ea44-7543-4f78-b524-7a38915ad6e4", @@ -102,7 +111,8 @@ public class JsonRestMessages { } """; - static final String AGENT_CARD = """ + static final String AGENT_CARD = + """ { "name": "GeoSpatial Route Planner Agent", "description": "Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", @@ -166,9 +176,11 @@ public class JsonRestMessages { ], "supportsExtendedAgentCard": false, "protocolVersion": "0.2.5" - }"""; + }\ + """; - static final String AGENT_CARD_SUPPORTS_EXTENDED = """ + static final String AGENT_CARD_SUPPORTS_EXTENDED = + """ { "name": "GeoSpatial Route Planner Agent", "description": "Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", @@ -232,9 +244,11 @@ public class JsonRestMessages { ], "supportsExtendedAgentCard": true, "protocolVersion": "0.2.5" - }"""; + }\ + """; - static final String AUTHENTICATION_EXTENDED_AGENT_CARD = """ + static final String AUTHENTICATION_EXTENDED_AGENT_CARD = + """ { "name": "GeoSpatial Route Planner Agent Extended", "description": "Extended description", @@ -304,9 +318,11 @@ public class JsonRestMessages { ], "supportsExtendedAgentCard": true, "protocolVersion": "0.2.5" - }"""; + }\ + """; - static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """ + static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = + """ { "id": "10", "taskId": "de38c76d-d54c-436c-8b9f-4c2703648d64", @@ -314,8 +330,10 @@ public class JsonRestMessages { "authentication": { "scheme": "jwt" } - }"""; - static final String LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """ + }\ + """; + static final String LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = + """ { "configs":[ { @@ -332,10 +350,11 @@ public class JsonRestMessages { "url": "https://test.com/callback" } ] - }"""; - + }\ + """; - static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = """ + static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = + """ { "id": "default-config-id", "taskId": "de38c76d-d54c-436c-8b9f-4c2703648d64", @@ -343,9 +362,11 @@ public class JsonRestMessages { "authentication": { "scheme": "jwt" } - }"""; + }\ + """; - static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """ + static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = + """ { "id": "10", "taskId": "de38c76d-d54c-436c-8b9f-4c2703648d64", @@ -353,10 +374,11 @@ public class JsonRestMessages { "authentication": { "scheme": "jwt" } - }"""; - + }\ + """; - public static final String SEND_MESSAGE_STREAMING_TEST_REQUEST = """ + public static final String SEND_MESSAGE_STREAMING_TEST_REQUEST = + """ { "message": { "role": "ROLE_USER", @@ -371,24 +393,34 @@ public class JsonRestMessages { "configuration": { "acceptedOutputModes": ["text"] } - }"""; - static final String SEND_MESSAGE_STREAMING_TEST_RESPONSE - = "event: message\n" - + "data: {\"task\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"TASK_STATE_SUBMITTED\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"text\":\"Why did the chicken cross the road? To get to the other side!\"}]}],\"metadata\":{}}}\n\n"; + }\ + """; + static final String SEND_MESSAGE_STREAMING_TEST_RESPONSE = + "event: message\n" + + "data:" + + " {\"task\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"TASK_STATE_SUBMITTED\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"text\":\"Why" + + " did the chicken cross the road? To get to the other" + + " side!\"}]}],\"metadata\":{}}}\n\n"; - static final String TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE - = "event: message\n" - + "data: {\"task\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"TASK_STATE_COMPLETED\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"text\":\"Why did the chicken cross the road? To get to the other side!\"}]}],\"metadata\":{}}}\n\n"; - public static final String TASK_RESUBSCRIPTION_TEST_REQUEST = """ + static final String TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE = + "event: message\n" + + "data:" + + " {\"task\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"TASK_STATE_COMPLETED\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"text\":\"Why" + + " did the chicken cross the road? To get to the other" + + " side!\"}]}],\"metadata\":{}}}\n\n"; + public static final String TASK_RESUBSCRIPTION_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "SubscribeToTask", "params": { "id": "task-1234" } - }"""; + }\ + """; - static final String LIST_TASKS_TEST_RESPONSE = """ + static final String LIST_TASKS_TEST_RESPONSE = + """ { "tasks": [ { @@ -402,5 +434,6 @@ public class JsonRestMessages { ], "totalSize": 1, "nextPageToken": "" - }"""; + }\ + """; } diff --git a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestErrorMapperTest.java b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestErrorMapperTest.java index 339b84b20..74ad8f704 100644 --- a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestErrorMapperTest.java +++ b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestErrorMapperTest.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.Map; - import org.a2aproject.sdk.client.http.A2AHttpHeaders; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.spec.A2AClientException; @@ -53,8 +52,9 @@ public void testUnrecognizedGoogleErrorFormatIncludesHeaders() { @Test public void testRecognizedA2AErrorDoesNotWrapInHttpError() { - String body = "{\"error\": {\"code\": 404, \"status\": \"NOT_FOUND\", \"message\": \"Task not found\", " + - "\"details\": [{\"reason\": \"TASK_NOT_FOUND\"}]}}"; + String body = + "{\"error\": {\"code\": 404, \"status\": \"NOT_FOUND\", \"message\": \"Task not" + + " found\", \"details\": [{\"reason\": \"TASK_NOT_FOUND\"}]}}"; A2AClientException ex = RestErrorMapper.mapRestError(body, 404, Map.of()); assertInstanceOf(TaskNotFoundError.class, ex.getCause()); @@ -63,43 +63,44 @@ public void testRecognizedA2AErrorDoesNotWrapInHttpError() { @Test public void testMapRestErrorFromA2AHttpResponse() { Map> headerMap = Map.of("X-Custom", List.of("value")); - A2AHttpResponse response = new A2AHttpResponse() { - @Override - public int status() { - return 500; - } - - @Override - public boolean success() { - return false; - } - - @Override - public String body() { - return ""; - } - - @Override - public A2AHttpHeaders headers() { - return new A2AHttpHeaders() { + A2AHttpResponse response = + new A2AHttpResponse() { + @Override + public int status() { + return 500; + } + @Override - public String firstValue(String name) { - List values = headerMap.get(name); - return values != null && !values.isEmpty() ? values.get(0) : null; + public boolean success() { + return false; } @Override - public List allValues(String name) { - return headerMap.getOrDefault(name, List.of()); + public String body() { + return ""; } @Override - public Map> toMap() { - return headerMap; + public A2AHttpHeaders headers() { + return new A2AHttpHeaders() { + @Override + public String firstValue(String name) { + List values = headerMap.get(name); + return values != null && !values.isEmpty() ? values.get(0) : null; + } + + @Override + public List allValues(String name) { + return headerMap.getOrDefault(name, List.of()); + } + + @Override + public Map> toMap() { + return headerMap; + } + }; } }; - } - }; A2AClientException ex = RestErrorMapper.mapRestError(response); assertInstanceOf(A2AClientHTTPError.class, ex.getCause()); diff --git a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java index 4d5256e9d..35daa88ca 100644 --- a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java +++ b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/RestTransportTest.java @@ -1,12 +1,11 @@ package org.a2aproject.sdk.client.transport.rest; - import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.CANCEL_TASK_TEST_REQUEST; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.CANCEL_TASK_TEST_RESPONSE; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.GET_TASK_TEST_RESPONSE; -import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.LIST_TASKS_TEST_RESPONSE; +import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.SEND_MESSAGE_STREAMING_TEST_REQUEST; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.SEND_MESSAGE_STREAMING_TEST_RESPONSE; import static org.a2aproject.sdk.client.transport.rest.JsonRestMessages.SEND_MESSAGE_TEST_REQUEST; @@ -32,7 +31,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.logging.Logger; - import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor; import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders; @@ -47,7 +45,6 @@ import org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams; import org.a2aproject.sdk.spec.EventKind; import org.a2aproject.sdk.spec.ExtensionSupportRequiredError; -import org.a2aproject.sdk.spec.VersionNotSupportedError; import org.a2aproject.sdk.spec.FilePart; import org.a2aproject.sdk.spec.FileWithBytes; import org.a2aproject.sdk.spec.FileWithUri; @@ -66,6 +63,7 @@ import org.a2aproject.sdk.spec.TaskQueryParams; import org.a2aproject.sdk.spec.TaskState; import org.a2aproject.sdk.spec.TextPart; +import org.a2aproject.sdk.spec.VersionNotSupportedError; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -78,26 +76,33 @@ public class RestTransportTest { private static final Logger LOGGER = Logger.getLogger(RestTransportTest.class.getName()); private static final Throwable SENTINEL = new RuntimeException("not yet called"); private ClientAndServer server; - private static final AgentCard CARD = AgentCard.builder() - .name("Hello World Agent") - .description("Just a hello world agent") - .supportedInterfaces(Collections.singletonList(new org.a2aproject.sdk.spec.AgentInterface("HTTP+JSON", "http://localhost:4001"))) - .version("1.0.0") - .documentationUrl("http://example.com/docs") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(AgentSkill.builder() - .id("hello_world") - .name("Returns hello world") - .description("just returns hello world") - .tags(Collections.singletonList("hello world")) - .examples(List.of("hi", "hello world")) - .build())) - .build(); + private static final AgentCard CARD = + AgentCard.builder() + .name("Hello World Agent") + .description("Just a hello world agent") + .supportedInterfaces( + Collections.singletonList( + new org.a2aproject.sdk.spec.AgentInterface( + "HTTP+JSON", "http://localhost:4001"))) + .version("1.0.0") + .documentationUrl("http://example.com/docs") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills( + Collections.singletonList( + AgentSkill.builder() + .id("hello_world") + .name("Returns hello world") + .description("just returns hello world") + .tags(Collections.singletonList("hello world")) + .examples(List.of("hi", "hello world")) + .build())) + .build(); @BeforeEach public void setUp() throws IOException { @@ -116,38 +121,38 @@ public void configBuilderDisablesRedirectFollowingByDefault() { @Test public void configBuilderEnablesRedirectFollowing() { - assertTrue(new RestTransportConfigBuilder().followRedirects(true).build().isFollowRedirects()); + assertTrue( + new RestTransportConfigBuilder().followRedirects(true).build().isFollowRedirects()); } - public RestTransportTest() { - } + public RestTransportTest() {} /** * Test of sendMessage method, of class JSONRestTransport. */ @Test public void testSendMessage() throws Exception { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("tell me a joke"))) - .contextId("context-1234") - .messageId("message-1234") - .taskId("") - .build(); - this.server.when( - request() - .withMethod("POST") - .withPath("/message:send") - .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) - .respond( - response() - .withStatusCode(200) - .withBody(SEND_MESSAGE_TEST_RESPONSE) - ); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("tell me a joke"))) + .contextId("context-1234") + .messageId("message-1234") + .taskId("") + .build(); + this.server + .when( + request() + .withMethod("POST") + .withPath("/message:send") + .withBody( + JsonBody.json( + SEND_MESSAGE_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(SEND_MESSAGE_TEST_RESPONSE)); MessageSendParams messageSendParams = new MessageSendParams(message, null, null, ""); ClientCallContext context = null; - + RestTransport instance = new RestTransport(CARD); EventKind result = instance.sendMessage(messageSendParams, context); assertEquals("task", result.kind()); @@ -177,20 +182,21 @@ public void testSendMessage() throws Exception { */ @Test public void testCancelTask() throws Exception { - this.server.when( - request() - .withMethod("POST") - .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64:cancel") - .withBody(JsonBody.json(CANCEL_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) - .respond( - response() - .withStatusCode(200) - .withBody(CANCEL_TASK_TEST_RESPONSE) - ); + this.server + .when( + request() + .withMethod("POST") + .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64:cancel") + .withBody( + JsonBody.json( + CANCEL_TASK_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(CANCEL_TASK_TEST_RESPONSE)); ClientCallContext context = null; RestTransport instance = new RestTransport(CARD); - Task task = instance.cancelTask(new CancelTaskParams("de38c76d-d54c-436c-8b9f-4c2703648d64"), context); + Task task = + instance.cancelTask( + new CancelTaskParams("de38c76d-d54c-436c-8b9f-4c2703648d64"), context); assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id()); assertEquals(TaskState.TASK_STATE_CANCELED, task.status().state()); assertNull(task.status().message()); @@ -203,17 +209,13 @@ public void testCancelTask() throws Exception { */ @Test public void testGetTask() throws Exception { - this.server.when( - request() - .withMethod("GET") - .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64") - .withQueryStringParameter("historyLength", "5") - ) - .respond( - response() - .withStatusCode(200) - .withBody(GET_TASK_TEST_RESPONSE) - ); + this.server + .when( + request() + .withMethod("GET") + .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64") + .withQueryStringParameter("historyLength", "5")) + .respond(response().withStatusCode(200).withBody(GET_TASK_TEST_RESPONSE)); ClientCallContext context = null; TaskQueryParams request = new TaskQueryParams("de38c76d-d54c-436c-8b9f-4c2703648d64", 5); RestTransport instance = new RestTransport(CARD); @@ -230,7 +232,9 @@ public void testGetTask() throws Exception { assertNull(artifact.name()); assertEquals(false, artifact.parts().isEmpty()); assertTrue(artifact.parts().get(0) instanceof org.a2aproject.sdk.spec.TextPart); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) artifact.parts().get(0)).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart) artifact.parts().get(0)).text()); assertEquals(1, task.history().size()); Message history = task.history().get(0); assertEquals("message", history.kind()); @@ -256,42 +260,44 @@ public void testGetTask() throws Exception { */ @Test public void testSendMessageStreaming() throws Exception { - this.server.when( - request() - .withMethod("POST") - .withPath("/message:stream") - .withBody(JsonBody.json(SEND_MESSAGE_STREAMING_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) + this.server + .when( + request() + .withMethod("POST") + .withPath("/message:stream") + .withBody( + JsonBody.json( + SEND_MESSAGE_STREAMING_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE)); RestTransport client = new RestTransport(CARD); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("tell me some jokes"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(true) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("tell me some jokes"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(true) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); AtomicReference receivedEvent = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - Consumer eventHandler = event -> { - receivedEvent.set(event); - latch.countDown(); - }; - Consumer errorHandler = error -> { - }; + Consumer eventHandler = + event -> { + receivedEvent.set(event); + latch.countDown(); + }; + Consumer errorHandler = error -> {}; client.sendMessageStreaming(params, eventHandler, errorHandler, null); boolean eventReceived = latch.await(10, TimeUnit.SECONDS); @@ -308,26 +314,31 @@ public void testSendMessageStreaming() throws Exception { @Test public void testCreateTaskPushNotificationConfiguration() throws Exception { LOGGER.info("Testing CreateTaskPushNotificationConfiguration"); - this.server.when( - request() - .withMethod("POST") - .withPath("/tenant/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") - .withBody(JsonBody.json(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) + this.server + .when( + request() + .withMethod("POST") + .withPath( + "/tenant/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") + .withBody( + JsonBody.json( + SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); RestTransport client = new RestTransport(CARD); - TaskPushNotificationConfig pushedConfig = TaskPushNotificationConfig.builder() - .id("default-config-id") - .taskId("de38c76d-d54c-436c-8b9f-4c2703648d64") - .url("https://example.com/callback") - .authentication(new AuthenticationInfo("jwt", null)) - .tenant("tenant") - .build(); - TaskPushNotificationConfig taskPushNotificationConfig = client.createTaskPushNotificationConfiguration(pushedConfig, null); + TaskPushNotificationConfig pushedConfig = + TaskPushNotificationConfig.builder() + .id("default-config-id") + .taskId("de38c76d-d54c-436c-8b9f-4c2703648d64") + .url("https://example.com/callback") + .authentication(new AuthenticationInfo("jwt", null)) + .tenant("tenant") + .build(); + TaskPushNotificationConfig taskPushNotificationConfig = + client.createTaskPushNotificationConfiguration(pushedConfig, null); assertNotNull(taskPushNotificationConfig); assertEquals("https://example.com/callback", taskPushNotificationConfig.url()); AuthenticationInfo authenticationInfo = taskPushNotificationConfig.authentication(); @@ -339,20 +350,23 @@ public void testCreateTaskPushNotificationConfiguration() throws Exception { */ @Test public void testGetTaskPushNotificationConfiguration() throws Exception { - this.server.when( - request() - .withMethod("GET") - .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10") - ) + this.server + .when( + request() + .withMethod("GET") + .withPath( + "/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10")) .respond( response() .withStatusCode(200) - .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); RestTransport client = new RestTransport(CARD); - TaskPushNotificationConfig taskPushNotificationConfig = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams("de38c76d-d54c-436c-8b9f-4c2703648d64", "10"), null); + TaskPushNotificationConfig taskPushNotificationConfig = + client.getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams( + "de38c76d-d54c-436c-8b9f-4c2703648d64", "10"), + null); assertNotNull(taskPushNotificationConfig); assertEquals("https://example.com/callback", taskPushNotificationConfig.url()); AuthenticationInfo authenticationInfo = taskPushNotificationConfig.authentication(); @@ -368,20 +382,26 @@ public void testGetTaskPushNotificationConfigurationWithOmittedId() throws Excep @Test public void testGetTaskPushNotificationConfigurationWithEmptyId() throws Exception { assertGetTaskPushNotificationConfigurationWithDefaultId( - new GetTaskPushNotificationConfigParams("de38c76d-d54c-436c-8b9f-4c2703648d64", "")); + new GetTaskPushNotificationConfigParams( + "de38c76d-d54c-436c-8b9f-4c2703648d64", "")); } private void assertGetTaskPushNotificationConfigurationWithDefaultId( GetTaskPushNotificationConfigParams params) throws Exception { - this.server.when(request() - .withMethod("GET") - .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/")) - .respond(response() - .withStatusCode(200) - .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); + this.server + .when( + request() + .withMethod("GET") + .withPath( + "/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/")) + .respond( + response() + .withStatusCode(200) + .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); RestTransport client = new RestTransport(CARD); - TaskPushNotificationConfig config = client.getTaskPushNotificationConfiguration(params, null); + TaskPushNotificationConfig config = + client.getTaskPushNotificationConfiguration(params, null); assertEquals("https://example.com/callback", config.url()); } @@ -391,20 +411,23 @@ private void assertGetTaskPushNotificationConfigurationWithDefaultId( */ @Test public void testListTaskPushNotificationConfigurations() throws Exception { - this.server.when( - request() - .withMethod("GET") - .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") - ) + this.server + .when( + request() + .withMethod("GET") + .withPath( + "/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs")) .respond( response() .withStatusCode(200) - .withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); RestTransport client = new RestTransport(CARD); - ListTaskPushNotificationConfigsResult result = client.listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigsParams("de38c76d-d54c-436c-8b9f-4c2703648d64"), null); + ListTaskPushNotificationConfigsResult result = + client.listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigsParams( + "de38c76d-d54c-436c-8b9f-4c2703648d64"), + null); assertEquals(2, result.configs().size()); TaskPushNotificationConfig config0 = result.configs().get(0); assertNotNull(config0); @@ -427,18 +450,19 @@ public void testListTaskPushNotificationConfigurations() throws Exception { @Test public void testDeleteTaskPushNotificationConfigurations() throws Exception { LOGGER.info("Testing deleteTaskPushNotificationConfigurations"); - this.server.when( - request() - .withMethod("DELETE") - .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10") - ) - .respond( - response() - .withStatusCode(200) - ); + this.server + .when( + request() + .withMethod("DELETE") + .withPath( + "/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10")) + .respond(response().withStatusCode(200)); ClientCallContext context = null; RestTransport instance = new RestTransport(CARD); - instance.deleteTaskPushNotificationConfigurations(new DeleteTaskPushNotificationConfigParams("de38c76d-d54c-436c-8b9f-4c2703648d64", "10"), context); + instance.deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams( + "de38c76d-d54c-436c-8b9f-4c2703648d64", "10"), + context); } /** @@ -448,34 +472,32 @@ public void testDeleteTaskPushNotificationConfigurations() throws Exception { public void testSubscribe() throws Exception { LOGGER.info("Testing subscribeToTask"); - this.server.when( - request() - .withMethod("POST") - .withPath("/tasks/task-1234:subscribe") - ) + this.server + .when(request().withMethod("POST").withPath("/tasks/task-1234:subscribe")) .respond( response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE) - ); + .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE)); RestTransport client = new RestTransport(CARD); TaskIdParams taskIdParams = new TaskIdParams("task-1234"); AtomicReference receivedEvent = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - Consumer eventHandler = event -> { - receivedEvent.set(event); - latch.countDown(); - }; + Consumer eventHandler = + event -> { + receivedEvent.set(event); + latch.countDown(); + }; Consumer errorHandler = error -> {}; client.subscribeToTask(taskIdParams, eventHandler, errorHandler, null); boolean eventReceived = latch.await(10, TimeUnit.SECONDS); assertTrue(eventReceived); - StreamingEventKind eventKind = receivedEvent.get();; + StreamingEventKind eventKind = receivedEvent.get(); + ; assertNotNull(eventKind); assertInstanceOf(Task.class, eventKind); Task task = (Task) eventKind; @@ -489,7 +511,9 @@ public void testSubscribe() throws Exception { assertEquals("joke", artifact.name()); Part part = artifact.parts().get(0); assertTrue(part instanceof org.a2aproject.sdk.spec.TextPart); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart) part).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart) part).text()); } /** @@ -497,47 +521,54 @@ public void testSubscribe() throws Exception { */ @Test public void testSendMessageStreamingCompletion() throws Exception { - this.server.when( - request() - .withMethod("POST") - .withPath("/message:stream") - .withBody(JsonBody.json(SEND_MESSAGE_STREAMING_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) + this.server + .when( + request() + .withMethod("POST") + .withPath("/message:stream") + .withBody( + JsonBody.json( + SEND_MESSAGE_STREAMING_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE)); RestTransport client = new RestTransport(CARD); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("tell me some jokes"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(true) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("tell me some jokes"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(true) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); CountDownLatch eventLatch = new CountDownLatch(1); CountDownLatch completionLatch = new CountDownLatch(1); AtomicReference completionValue = new AtomicReference<>(SENTINEL); Consumer eventHandler = event -> eventLatch.countDown(); - Consumer errorHandler = error -> { - completionValue.set(error); - completionLatch.countDown(); - }; + Consumer errorHandler = + error -> { + completionValue.set(error); + completionLatch.countDown(); + }; client.sendMessageStreaming(params, eventHandler, errorHandler, null); assertTrue(eventLatch.await(10, TimeUnit.SECONDS), "Should receive streaming event"); - assertTrue(completionLatch.await(10, TimeUnit.SECONDS), "Error handler should be called on stream completion"); - assertNull(completionValue.get(), "Error handler should be called with null for successful completion"); + assertTrue( + completionLatch.await(10, TimeUnit.SECONDS), + "Error handler should be called on stream completion"); + assertNull( + completionValue.get(), + "Error handler should be called with null for successful completion"); } /** @@ -545,17 +576,13 @@ public void testSendMessageStreamingCompletion() throws Exception { */ @Test public void testSubscribeToTaskCompletion() throws Exception { - this.server.when( - request() - .withMethod("POST") - .withPath("/tasks/task-1234:subscribe") - ) + this.server + .when(request().withMethod("POST").withPath("/tasks/task-1234:subscribe")) .respond( response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE) - ); + .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE)); RestTransport client = new RestTransport(CARD); TaskIdParams taskIdParams = new TaskIdParams("task-1234"); @@ -564,15 +591,20 @@ public void testSubscribeToTaskCompletion() throws Exception { CountDownLatch completionLatch = new CountDownLatch(1); AtomicReference completionValue = new AtomicReference<>(SENTINEL); Consumer eventHandler = event -> eventLatch.countDown(); - Consumer errorHandler = error -> { - completionValue.set(error); - completionLatch.countDown(); - }; + Consumer errorHandler = + error -> { + completionValue.set(error); + completionLatch.countDown(); + }; client.subscribeToTask(taskIdParams, eventHandler, errorHandler, null); assertTrue(eventLatch.await(10, TimeUnit.SECONDS), "Should receive streaming event"); - assertTrue(completionLatch.await(10, TimeUnit.SECONDS), "Error handler should be called on stream completion"); - assertNull(completionValue.get(), "Error handler should be called with null for successful completion"); + assertTrue( + completionLatch.await(10, TimeUnit.SECONDS), + "Error handler should be called on stream completion"); + assertNull( + completionValue.get(), + "Error handler should be called with null for successful completion"); } /** @@ -583,32 +615,30 @@ public void testExtensionSupportRequiredErrorUnmarshalling() throws Exception { LOGGER.info("Testing ExtensionSupportRequiredError unmarshalling"); // Mock server returns HTTP 400 with ExtensionSupportRequiredError - String errorResponseBody = """ + String errorResponseBody = + """ { "error": "org.a2aproject.sdk.spec.ExtensionSupportRequiredError", "message": "Extension required: https://example.com/test-extension" } """; - this.server.when( - request() - .withMethod("POST") - .withPath("/message:send") - ) + this.server + .when(request().withMethod("POST").withPath("/message:send")) .respond( response() .withStatusCode(400) .withHeader("Content-Type", "application/json") - .withBody(errorResponseBody) - ); + .withBody(errorResponseBody)); RestTransport client = new RestTransport(CARD); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("test message"))) - .contextId("context-test") - .messageId("message-test") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("test message"))) + .contextId("context-test") + .messageId("message-test") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); // Should throw A2AClientException with ExtensionSupportRequiredError as cause @@ -618,7 +648,8 @@ public void testExtensionSupportRequiredErrorUnmarshalling() throws Exception { } catch (A2AClientException e) { // Verify the cause is ExtensionSupportRequiredError assertInstanceOf(ExtensionSupportRequiredError.class, e.getCause()); - ExtensionSupportRequiredError extensionError = (ExtensionSupportRequiredError) e.getCause(); + ExtensionSupportRequiredError extensionError = + (ExtensionSupportRequiredError) e.getCause(); assertTrue(extensionError.getMessage().contains("https://example.com/test-extension")); } } @@ -631,32 +662,30 @@ public void testVersionNotSupportedErrorUnmarshalling() throws Exception { LOGGER.info("Testing VersionNotSupportedError unmarshalling"); // Mock server returns HTTP 501 with VersionNotSupportedError - String errorResponseBody = """ + String errorResponseBody = + """ { "error": "org.a2aproject.sdk.spec.VersionNotSupportedError", "message": "Protocol version 2.0 is not supported. This agent supports version 1.0" } """; - this.server.when( - request() - .withMethod("POST") - .withPath("/message:send") - ) + this.server + .when(request().withMethod("POST").withPath("/message:send")) .respond( response() .withStatusCode(501) .withHeader("Content-Type", "application/json") - .withBody(errorResponseBody) - ); + .withBody(errorResponseBody)); RestTransport client = new RestTransport(CARD); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("test message"))) - .contextId("context-test") - .messageId("message-test") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("test message"))) + .contextId("context-test") + .messageId("message-test") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); // Should throw A2AClientException with VersionNotSupportedError as cause @@ -680,48 +709,47 @@ public void testVersionNotSupportedErrorUnmarshallingStreaming() throws Exceptio LOGGER.info("Testing VersionNotSupportedError unmarshalling in streaming"); // Mock server returns HTTP 200 with error event in SSE stream - String streamResponseBody = """ + String streamResponseBody = + """ data: {"kind":"error","error":"org.a2aproject.sdk.spec.VersionNotSupportedError","message":"Protocol version 2.0 is not supported. This agent supports version 1.0"} """; - this.server.when( - request() - .withMethod("POST") - .withPath("/message:stream") - ) + this.server + .when(request().withMethod("POST").withPath("/message:stream")) .respond( response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(streamResponseBody) - ); + .withBody(streamResponseBody)); RestTransport client = new RestTransport(CARD); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("test message"))) - .contextId("context-test") - .messageId("message-test") - .build(); - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .returnImmediately(true) - .build(); - MessageSendParams params = MessageSendParams.builder() - .message(message) - .configuration(configuration) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("test message"))) + .contextId("context-test") + .messageId("message-test") + .build(); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder() + .acceptedOutputModes(List.of("text")) + .returnImmediately(true) + .build(); + MessageSendParams params = + MessageSendParams.builder().message(message).configuration(configuration).build(); AtomicReference receivedError = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - Consumer eventHandler = event -> { - // Should not receive events, only error - }; - Consumer errorHandler = error -> { - receivedError.set(error); - latch.countDown(); - }; + Consumer eventHandler = + event -> { + // Should not receive events, only error + }; + Consumer errorHandler = + error -> { + receivedError.set(error); + latch.countDown(); + }; client.sendMessageStreaming(params, eventHandler, errorHandler, null); boolean errorReceived = latch.await(10, TimeUnit.SECONDS); @@ -730,7 +758,8 @@ public void testVersionNotSupportedErrorUnmarshallingStreaming() throws Exceptio assertInstanceOf(A2AClientException.class, receivedError.get()); A2AClientException clientException = (A2AClientException) receivedError.get(); assertInstanceOf(VersionNotSupportedError.class, clientException.getCause()); - VersionNotSupportedError versionError = (VersionNotSupportedError) clientException.getCause(); + VersionNotSupportedError versionError = + (VersionNotSupportedError) clientException.getCause(); assertTrue(versionError.getMessage().contains("2.0")); assertTrue(versionError.getMessage().contains("1.0")); } @@ -744,22 +773,27 @@ public void testListTasksProtoBuilderIncludesStatusTimestampAfter() throws Excep Instant cutoff = Instant.parse("2026-10-01T00:00:00Z"); AtomicReference capturedPayload = new AtomicReference<>(); - ClientCallInterceptor capturingInterceptor = new ClientCallInterceptor() { - @Override - public PayloadAndHeaders intercept(String methodName, Object payload, - java.util.Map headers, AgentCard agentCard, - ClientCallContext clientCallContext) { - capturedPayload.set(payload); - return new PayloadAndHeaders(payload, headers); - } - }; - - this.server.when( - request().withMethod("GET").withPath("/tasks") - ).respond(response().withStatusCode(200).withBody(LIST_TASKS_TEST_RESPONSE)); + ClientCallInterceptor capturingInterceptor = + new ClientCallInterceptor() { + @Override + public PayloadAndHeaders intercept( + String methodName, + Object payload, + java.util.Map headers, + AgentCard agentCard, + ClientCallContext clientCallContext) { + capturedPayload.set(payload); + return new PayloadAndHeaders(payload, headers); + } + }; + + this.server + .when(request().withMethod("GET").withPath("/tasks")) + .respond(response().withStatusCode(200).withBody(LIST_TASKS_TEST_RESPONSE)); AgentInterface agentInterface = new AgentInterface("HTTP+JSON", "http://localhost:4001"); - RestTransport client = new RestTransport(null, CARD, agentInterface, List.of(capturingInterceptor)); + RestTransport client = + new RestTransport(null, CARD, agentInterface, List.of(capturingInterceptor)); ListTasksParams params = ListTasksParams.builder().statusTimestampAfter(cutoff).build(); client.listTasks(params, null); @@ -777,37 +811,52 @@ public PayloadAndHeaders intercept(String methodName, Object payload, * pageSize and pageToken so interceptors see the complete request. */ @Test - public void testListTaskPushNotificationConfigurationsProtoBuilderIncludesPagination() throws Exception { + public void testListTaskPushNotificationConfigurationsProtoBuilderIncludesPagination() + throws Exception { AtomicReference capturedPayload = new AtomicReference<>(); - ClientCallInterceptor capturingInterceptor = new ClientCallInterceptor() { - @Override - public PayloadAndHeaders intercept(String methodName, Object payload, - java.util.Map headers, AgentCard agentCard, - ClientCallContext clientCallContext) { - capturedPayload.set(payload); - return new PayloadAndHeaders(payload, headers); - } - }; - - this.server.when( - request().withMethod("GET") - .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") - ).respond(response().withStatusCode(200).withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); + ClientCallInterceptor capturingInterceptor = + new ClientCallInterceptor() { + @Override + public PayloadAndHeaders intercept( + String methodName, + Object payload, + java.util.Map headers, + AgentCard agentCard, + ClientCallContext clientCallContext) { + capturedPayload.set(payload); + return new PayloadAndHeaders(payload, headers); + } + }; + + this.server + .when( + request() + .withMethod("GET") + .withPath( + "/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs")) + .respond( + response() + .withStatusCode(200) + .withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); AgentInterface agentInterface = new AgentInterface("HTTP+JSON", "http://localhost:4001"); - RestTransport client = new RestTransport(null, CARD, agentInterface, List.of(capturingInterceptor)); - - ListTaskPushNotificationConfigsParams params = ListTaskPushNotificationConfigsParams.builder() - .id("de38c76d-d54c-436c-8b9f-4c2703648d64") - .pageSize(10) - .pageToken("next-page-token") - .build(); + RestTransport client = + new RestTransport(null, CARD, agentInterface, List.of(capturingInterceptor)); + + ListTaskPushNotificationConfigsParams params = + ListTaskPushNotificationConfigsParams.builder() + .id("de38c76d-d54c-436c-8b9f-4c2703648d64") + .pageSize(10) + .pageToken("next-page-token") + .build(); client.listTaskPushNotificationConfigurations(params, null); assertNotNull(capturedPayload.get()); org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest proto = - ((org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder) capturedPayload.get()).build(); + ((org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder) + capturedPayload.get()) + .build(); assertEquals(10, proto.getPageSize(), "proto builder must set pageSize"); assertEquals("next-page-token", proto.getPageToken(), "proto builder must set pageToken"); } @@ -820,22 +869,17 @@ public PayloadAndHeaders intercept(String methodName, Object payload, @Test public void testListTasksIncludesStatusTimestampAfterQueryParam() throws Exception { Instant cutoff = Instant.parse("2026-10-01T00:00:00Z"); - this.server.when( - request() - .withMethod("GET") - .withPath("/tasks") - .withQueryStringParameter("statusTimestampAfter", "2026-10-01T00:00:00Z") - ) - .respond( - response() - .withStatusCode(200) - .withBody(LIST_TASKS_TEST_RESPONSE) - ); + this.server + .when( + request() + .withMethod("GET") + .withPath("/tasks") + .withQueryStringParameter( + "statusTimestampAfter", "2026-10-01T00:00:00Z")) + .respond(response().withStatusCode(200).withBody(LIST_TASKS_TEST_RESPONSE)); RestTransport client = new RestTransport(CARD); - ListTasksParams params = ListTasksParams.builder() - .statusTimestampAfter(cutoff) - .build(); + ListTasksParams params = ListTasksParams.builder().statusTimestampAfter(cutoff).build(); var result = client.listTasks(params, null); assertEquals(1, result.tasks().size()); @@ -848,28 +892,31 @@ public void testListTasksIncludesStatusTimestampAfterQueryParam() throws Excepti * without the fix the mock will not match and the client throws an exception. */ @Test - public void testListTaskPushNotificationConfigurationsIncludesPaginationQueryParams() throws Exception { - this.server.when( - request() - .withMethod("GET") - .withPath("/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") - .withQueryStringParameter("pageSize", "10") - .withQueryStringParameter("pageToken", "next-page-token") - ) + public void testListTaskPushNotificationConfigurationsIncludesPaginationQueryParams() + throws Exception { + this.server + .when( + request() + .withMethod("GET") + .withPath( + "/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") + .withQueryStringParameter("pageSize", "10") + .withQueryStringParameter("pageToken", "next-page-token")) .respond( response() .withStatusCode(200) - .withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); RestTransport client = new RestTransport(CARD); - ListTaskPushNotificationConfigsParams params = ListTaskPushNotificationConfigsParams.builder() - .id("de38c76d-d54c-436c-8b9f-4c2703648d64") - .pageSize(10) - .pageToken("next-page-token") - .build(); - - ListTaskPushNotificationConfigsResult result = client.listTaskPushNotificationConfigurations(params, null); + ListTaskPushNotificationConfigsParams params = + ListTaskPushNotificationConfigsParams.builder() + .id("de38c76d-d54c-436c-8b9f-4c2703648d64") + .pageSize(10) + .pageToken("next-page-token") + .build(); + + ListTaskPushNotificationConfigsResult result = + client.listTaskPushNotificationConfigurations(params, null); assertEquals(2, result.configs().size()); } } diff --git a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/sse/SSEEventListenerTest.java b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/sse/SSEEventListenerTest.java index 65ada80b1..41ee3a8f4 100644 --- a/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/sse/SSEEventListenerTest.java +++ b/client/transport/rest/src/test/java/org/a2aproject/sdk/client/transport/rest/sse/SSEEventListenerTest.java @@ -8,7 +8,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; - import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.StreamingEventKind; @@ -65,23 +64,22 @@ public boolean wasCancelled() { public void testOnMessageWithTaskResult() { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // JSON format from REST SSE stream (protobuf as JSON) - String jsonMessage = """ - { - "task": { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "TASK_STATE_WORKING" + String jsonMessage = + """ + { + "task": { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "TASK_STATE_WORKING" + } + } } - } - } - """; + """; // Call the onMessage method listener.onMessage(new ServerSentEvent(jsonMessage), null); @@ -99,26 +97,25 @@ public void testOnMessageWithTaskResult() { public void testOnMessageWithMessageResult() { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // JSON format from REST SSE stream - String jsonMessage = """ - { - "message": { - "role": "ROLE_AGENT", - "messageId": "msg-123", - "contextId": "context-456", - "parts": [ - { - "text": "Hello, world!" + String jsonMessage = + """ + { + "message": { + "role": "ROLE_AGENT", + "messageId": "msg-123", + "contextId": "context-456", + "parts": [ + { + "text": "Hello, world!" + } + ] } - ] - } - } - """; + } + """; // Call onMessage method listener.onMessage(new ServerSentEvent(jsonMessage), null); @@ -139,23 +136,22 @@ public void testOnMessageWithMessageResult() { public void testOnMessageWithStatusUpdateEvent() { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // JSON format from REST SSE stream - String jsonMessage = """ - { - "statusUpdate": { - "taskId": "1", - "contextId": "2", - "status": { - "state": "TASK_STATE_SUBMITTED" + String jsonMessage = + """ + { + "statusUpdate": { + "taskId": "1", + "contextId": "2", + "status": { + "state": "TASK_STATE_SUBMITTED" + } + } } - } - } - """; + """; // Call onMessage method listener.onMessage(new ServerSentEvent(jsonMessage), null); @@ -174,23 +170,22 @@ public void testOnMessageWithStatusUpdateEvent() { public void testOnMessageWithFinalStatusUpdateEventCancels() { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // JSON format from REST SSE stream with final status - String jsonMessage = """ - { - "statusUpdate": { - "taskId": "1", - "contextId": "2", - "status": { - "state": "TASK_STATE_COMPLETED" + String jsonMessage = + """ + { + "statusUpdate": { + "taskId": "1", + "contextId": "2", + "status": { + "state": "TASK_STATE_COMPLETED" + } + } } - } - } - """; + """; // Call onMessage with a cancellable future CancelCapturingFuture future = new CancelCapturingFuture(); @@ -211,23 +206,22 @@ public void testOnMessageWithFinalStatusUpdateEventCancels() { public void testOnMessageWithCompletedTaskCancels() { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener listener = + new SSEEventListener(event -> receivedEvent.set(event), error -> {}); // JSON format from REST SSE stream with completed task - String jsonMessage = """ - { - "task": { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "TASK_STATE_COMPLETED" + String jsonMessage = + """ + { + "task": { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "TASK_STATE_COMPLETED" + } + } } - } - } - """; + """; // Call onMessage with a cancellable future CancelCapturingFuture future = new CancelCapturingFuture(); @@ -247,10 +241,8 @@ public void testOnMessageWithCompletedTaskCancels() { public void testOnMessageWithInvalidJsonCallsErrorHandler() { // Set up error handler AtomicReference receivedError = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> receivedError.set(error) - ); + SSEEventListener listener = + new SSEEventListener(event -> {}, error -> receivedError.set(error)); // Invalid JSON message String invalidJson = "{ invalid json }"; @@ -266,10 +258,8 @@ public void testOnMessageWithInvalidJsonCallsErrorHandler() { public void testOnMessageWithInvalidPayloadCaseCallsErrorHandler() { // Set up error handler AtomicReference receivedError = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> receivedError.set(error) - ); + SSEEventListener listener = + new SSEEventListener(event -> {}, error -> receivedError.set(error)); // JSON with no recognized payload type (empty object) String jsonMessage = "{}"; @@ -287,10 +277,8 @@ public void testOnMessageWithInvalidPayloadCaseCallsErrorHandler() { public void testOnErrorCallsErrorHandler() { // Set up error handler AtomicReference receivedError = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> receivedError.set(error) - ); + SSEEventListener listener = + new SSEEventListener(event -> {}, error -> receivedError.set(error)); // Create a test error IllegalStateException testError = new IllegalStateException("Test error"); @@ -307,10 +295,8 @@ public void testOnErrorCallsErrorHandler() { public void testOnErrorCancelsFuture() { // Set up error handler AtomicBoolean errorHandlerCalled = new AtomicBoolean(false); - SSEEventListener listener = new SSEEventListener( - event -> {}, - error -> errorHandlerCalled.set(true) - ); + SSEEventListener listener = + new SSEEventListener(event -> {}, error -> errorHandlerCalled.set(true)); // Create a cancel-capturing future CancelCapturingFuture future = new CancelCapturingFuture(); @@ -327,10 +313,10 @@ public void testOnErrorCancelsFuture() { public void testOnMessageWithNullErrorHandler() { // Set up with null error handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener listener = new SSEEventListener( - event -> receivedEvent.set(event), - null // Null error handler - ); + SSEEventListener listener = + new SSEEventListener( + event -> receivedEvent.set(event), null // Null error handler + ); // Invalid JSON message String invalidJson = "{ invalid json }"; diff --git a/client/transport/spi/pom.xml b/client/transport/spi/pom.xml index f75fd1e20..728841c71 100644 --- a/client/transport/spi/pom.xml +++ b/client/transport/spi/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransport.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransport.java index 4eb1393a3..69fe5728d 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransport.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransport.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.client.transport.spi; import java.util.function.Consumer; - import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; import org.a2aproject.sdk.spec.A2AClientException; @@ -48,8 +47,12 @@ EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext con * @param context optional client call context for the request (may be {@code null}) * @throws A2AClientException if setting up the streaming connection fails */ - void sendMessageStreaming(MessageSendParams request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException; + void sendMessageStreaming( + MessageSendParams request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException; /** * Retrieve the current state and history of a specific task. @@ -59,7 +62,8 @@ void sendMessageStreaming(MessageSendParams request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException; + void subscribeToTask( + TaskIdParams request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException; /** * Retrieve the extended AgentCard. @@ -148,11 +159,12 @@ void subscribeToTask(TaskIdParams request, Consumer eventCon * @return the extended agent card * @throws A2AClientException if retrieving the agent card fails for any reason */ - AgentCard getExtendedAgentCard(GetExtendedAgentCardParams params, @Nullable ClientCallContext context) throws A2AClientException; + AgentCard getExtendedAgentCard( + GetExtendedAgentCardParams params, @Nullable ClientCallContext context) + throws A2AClientException; /** * Close the transport and release any associated resources. */ void close(); - } diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportConfig.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportConfig.java index aa18bb656..0b4ca8922 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportConfig.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportConfig.java @@ -1,13 +1,11 @@ package org.a2aproject.sdk.client.transport.spi; import java.util.ArrayList; - +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor; -import java.util.Collections; /** * Base configuration class for A2A client transport protocols. @@ -76,11 +74,10 @@ public List getInterceptors() { * * @param parameters the map of parameters to use (will be copied) */ - public void setParameters(Map parameters) { + public void setParameters(Map parameters) { this.parameters = new HashMap<>(parameters); } - /** * Get the list of configured parameters. *

@@ -89,7 +86,7 @@ public void setParameters(Map parameters) { * * @return an unmodifiable map of configured parameters (never null, but may be empty) */ - public Map getParameters() { + public Map getParameters() { return java.util.Collections.unmodifiableMap(parameters); } } diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportConfigBuilder.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportConfigBuilder.java index ffcf78920..3ca2e23a7 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportConfigBuilder.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportConfigBuilder.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.List; - import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor; /** @@ -34,7 +33,8 @@ * @see ClientTransportConfig * @see org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor */ -public abstract class ClientTransportConfigBuilder, +public abstract class ClientTransportConfigBuilder< + T extends ClientTransportConfig, B extends ClientTransportConfigBuilder> { protected List interceptors = new ArrayList<>(); diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportProvider.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportProvider.java index f34deb4a1..066f7f56b 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportProvider.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/ClientTransportProvider.java @@ -7,7 +7,8 @@ /** * Client transport provider interface. */ -public interface ClientTransportProvider> { +public interface ClientTransportProvider< + T extends ClientTransport, C extends ClientTransportConfig> { /** * Create a client transport. @@ -17,7 +18,8 @@ public interface ClientTransportProvider getTransportProtocolClass(); } - diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/ClientCallInterceptor.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/ClientCallInterceptor.java index 7d9e08fb9..36af1535f 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/ClientCallInterceptor.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/ClientCallInterceptor.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.client.transport.spi.interceptors; import java.util.Map; - import org.a2aproject.sdk.spec.AgentCard; import org.jspecify.annotations.Nullable; @@ -22,6 +21,10 @@ public abstract class ClientCallInterceptor { * @param clientCallContext the {@code ClientCallContext} for this call (may be {@code null}) * @return the potentially modified payload and headers */ - public abstract PayloadAndHeaders intercept(String methodName, @Nullable Object payload, Map headers, - @Nullable AgentCard agentCard, @Nullable ClientCallContext clientCallContext); + public abstract PayloadAndHeaders intercept( + String methodName, + @Nullable Object payload, + Map headers, + @Nullable AgentCard agentCard, + @Nullable ClientCallContext clientCallContext); } diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/PayloadAndHeaders.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/PayloadAndHeaders.java index 03dc61126..48779b466 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/PayloadAndHeaders.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/PayloadAndHeaders.java @@ -3,12 +3,11 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; - import org.jspecify.annotations.Nullable; public class PayloadAndHeaders { - private final @Nullable Object payload; + private final @Nullable Object payload; private final Map headers; public PayloadAndHeaders(@Nullable Object payload, @Nullable Map headers) { diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptor.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptor.java index 2bb1272a2..1ff8db09b 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptor.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptor.java @@ -4,7 +4,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; - import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor; import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders; @@ -36,13 +35,8 @@ public class AuthInterceptor extends ClientCallInterceptor { * Only standard authentication-related headers are permitted. * Header names are stored in lowercase for case-insensitive comparison. */ - private static final Set SAFE_API_KEY_HEADER_NAMES = Set.of( - "authorization", - "x-api-key", - "api-key", - "x-auth-token", - "x-authentication" - ); + private static final Set SAFE_API_KEY_HEADER_NAMES = + Set.of("authorization", "x-api-key", "api-key", "x-auth-token", "x-authentication"); private final CredentialService credentialService; @@ -51,10 +45,17 @@ public AuthInterceptor(final CredentialService credentialService) { } @Override - public PayloadAndHeaders intercept(String methodName, @Nullable Object payload, Map headers, - @Nullable AgentCard agentCard, @Nullable ClientCallContext clientCallContext) { - Map updatedHeaders = new HashMap<>(headers == null ? new HashMap<>() : headers); - if (agentCard == null || agentCard.securityRequirements()== null || agentCard.securitySchemes() == null) { + public PayloadAndHeaders intercept( + String methodName, + @Nullable Object payload, + Map headers, + @Nullable AgentCard agentCard, + @Nullable ClientCallContext clientCallContext) { + Map updatedHeaders = + new HashMap<>(headers == null ? new HashMap<>() : headers); + if (agentCard == null + || agentCard.securityRequirements() == null + || agentCard.securitySchemes() == null) { return new PayloadAndHeaders(payload, updatedHeaders); } for (SecurityRequirement requirement : agentCard.securityRequirements()) { @@ -62,9 +63,12 @@ public PayloadAndHeaders intercept(String methodName, @Nullable Object payload, continue; } for (String securitySchemeName : requirement.schemes().keySet()) { - String credential = credentialService.getCredential(securitySchemeName, clientCallContext); - if (credential != null && agentCard.securitySchemes().containsKey(securitySchemeName)) { - SecurityScheme securityScheme = agentCard.securitySchemes().get(securitySchemeName); + String credential = + credentialService.getCredential(securitySchemeName, clientCallContext); + if (credential != null + && agentCard.securitySchemes().containsKey(securitySchemeName)) { + SecurityScheme securityScheme = + agentCard.securitySchemes().get(securitySchemeName); if (securityScheme == null) { continue; } @@ -81,8 +85,10 @@ public PayloadAndHeaders intercept(String methodName, @Nullable Object payload, || securityScheme instanceof OpenIdConnectSecurityScheme) { updatedHeaders.put(AUTHORIZATION, getBearerValue(credential)); return new PayloadAndHeaders(payload, updatedHeaders); - } else if (securityScheme instanceof APIKeySecurityScheme apiKeySecurityScheme) { - // Only inject API key if it's intended for header transport and the header name is safe + } else if (securityScheme + instanceof APIKeySecurityScheme apiKeySecurityScheme) { + // Only inject API key if it's intended for header transport and the header + // name is safe if (apiKeySecurityScheme.location() == APIKeySecurityScheme.Location.HEADER && isSafeHeaderName(apiKeySecurityScheme.name())) { updatedHeaders.put(apiKeySecurityScheme.name(), credential); diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/CredentialService.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/CredentialService.java index aff6f9b1b..c90301dc3 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/CredentialService.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/CredentialService.java @@ -15,5 +15,6 @@ public interface CredentialService { * @param clientCallContext the client call context, which may be {@code null}. * @return the credential or {@code null} if the credential could not be retrieved */ - @Nullable String getCredential(String securitySchemeName, @Nullable ClientCallContext clientCallContext); + @Nullable String getCredential( + String securitySchemeName, @Nullable ClientCallContext clientCallContext); } diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/InMemoryContextCredentialService.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/InMemoryContextCredentialService.java index f05e92288..89a15bac9 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/InMemoryContextCredentialService.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/InMemoryContextCredentialService.java @@ -3,7 +3,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; - import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.jspecify.annotations.Nullable; @@ -24,15 +23,15 @@ public InMemoryContextCredentialService() { } @Override - public @Nullable String getCredential(String securitySchemeName, - @Nullable ClientCallContext clientCallContext) { + public @Nullable String getCredential( + String securitySchemeName, @Nullable ClientCallContext clientCallContext) { if (clientCallContext == null || !clientCallContext.getState().containsKey(SESSION_ID)) { // no credential to retrieve return null; } Object sessionIdObj = clientCallContext.getState().get(SESSION_ID); - if (! (sessionIdObj instanceof String sessionId)) { + if (!(sessionIdObj instanceof String sessionId)) { return null; } Map sessionCredentials = credentialStore.get(sessionId); @@ -50,6 +49,8 @@ public InMemoryContextCredentialService() { * @param credential the credential string */ public void setCredential(String sessionId, String securitySchemeName, String credential) { - credentialStore.computeIfAbsent(sessionId, k -> new ConcurrentHashMap<>()).put(securitySchemeName, credential); + credentialStore + .computeIfAbsent(sessionId, k -> new ConcurrentHashMap<>()) + .put(securitySchemeName, credential); } } diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/package-info.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/package-info.java index d46b1482d..587cb722a 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/package-info.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.transport.spi.interceptors.auth; import org.jspecify.annotations.NullMarked; - diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/package-info.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/package-info.java index 1016099af..1469e71b8 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/package-info.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/interceptors/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.transport.spi.interceptors; import org.jspecify.annotations.NullMarked; - diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/package-info.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/package-info.java index 5519844a7..0b305f2cc 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/package-info.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.transport.spi; import org.jspecify.annotations.NullMarked; - diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/sse/AbstractSSEEventListener.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/sse/AbstractSSEEventListener.java index 2078bc7ec..d4a070272 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/sse/AbstractSSEEventListener.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/sse/AbstractSSEEventListener.java @@ -4,7 +4,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.logging.Logger; - import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.spec.StreamingEventKind; import org.a2aproject.sdk.spec.Task; @@ -34,8 +33,8 @@ public abstract class AbstractSSEEventListener { * @param eventHandler Handler for processing streaming events * @param errorHandler Optional handler for processing errors */ - protected AbstractSSEEventListener(Consumer eventHandler, - @Nullable Consumer errorHandler) { + protected AbstractSSEEventListener( + Consumer eventHandler, @Nullable Consumer errorHandler) { this.eventHandler = eventHandler; this.errorHandler = errorHandler; } @@ -117,7 +116,9 @@ protected void handleEvent(StreamingEventKind event, @Nullable Future futu // Handles both TaskStatusUpdateEvent and Task objects with final states // This covers late subscriptions to completed tasks and ensures no connection leaks if (shouldAutoClose(event) && future != null) { - LOGGER.fine("Auto-closing SSE connection for final event: " + event.getClass().getSimpleName()); + LOGGER.fine( + "Auto-closing SSE connection for final event: " + + event.getClass().getSimpleName()); signalTerminal(null); future.cancel(true); // close SSE channel } diff --git a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/sse/package-info.java b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/sse/package-info.java index 269dde525..def65cea5 100644 --- a/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/sse/package-info.java +++ b/client/transport/spi/src/main/java/org/a2aproject/sdk/client/transport/spi/sse/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.transport.spi.sse; import org.jspecify.annotations.NullMarked; - diff --git a/client/transport/spi/src/test/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptorTest.java b/client/transport/spi/src/test/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptorTest.java index 3983eff9a..30ef5db50 100644 --- a/client/transport/spi/src/test/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptorTest.java +++ b/client/transport/spi/src/test/java/org/a2aproject/sdk/client/transport/spi/interceptors/auth/AuthInterceptorTest.java @@ -6,7 +6,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallInterceptor; import org.a2aproject.sdk.client.transport.spi.interceptors.PayloadAndHeaders; @@ -44,8 +43,12 @@ public HeaderInterceptor(String headerName, String headerValue) { } @Override - public PayloadAndHeaders intercept(String methodName, Object payload, Map headers, - AgentCard agentCard, ClientCallContext clientCallContext) { + public PayloadAndHeaders intercept( + String methodName, + Object payload, + Map headers, + AgentCard agentCard, + ClientCallContext clientCallContext) { Map updatedHeaders = new HashMap<>(headers); updatedHeaders.put(headerName, headerValue); return new PayloadAndHeaders(payload, updatedHeaders); @@ -61,8 +64,14 @@ private static class AuthTestCase { final String expectedHeaderKey; final String expectedHeaderValue; - AuthTestCase(String url, String sessionId, String schemeName, String credential, - SecurityScheme securityScheme, String expectedHeaderKey, String expectedHeaderValue) { + AuthTestCase( + String url, + String sessionId, + String schemeName, + String credential, + SecurityScheme securityScheme, + String expectedHeaderKey, + String expectedHeaderValue) { this.url = url; this.sessionId = sessionId; this.schemeName = schemeName; @@ -75,78 +84,91 @@ private static class AuthTestCase { @Test public void testAPIKeySecurityScheme() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - APIKeySecurityScheme.TYPE, - "secret-api-key", - new APIKeySecurityScheme(APIKeySecurityScheme.Location.HEADER, "X-API-Key", "API Key authentication"), - "X-API-Key", - "secret-api-key" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + APIKeySecurityScheme.TYPE, + "secret-api-key", + new APIKeySecurityScheme( + APIKeySecurityScheme.Location.HEADER, + "X-API-Key", + "API Key authentication"), + "X-API-Key", + "secret-api-key"); testSecurityScheme(authTestCase); } @Test public void testAPIKeySecurityScheme_SafeHeaderName_Authorization() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - APIKeySecurityScheme.TYPE, - "secret-api-key", - new APIKeySecurityScheme(APIKeySecurityScheme.Location.HEADER, "Authorization", "API Key authentication"), - "Authorization", - "secret-api-key" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + APIKeySecurityScheme.TYPE, + "secret-api-key", + new APIKeySecurityScheme( + APIKeySecurityScheme.Location.HEADER, + "Authorization", + "API Key authentication"), + "Authorization", + "secret-api-key"); testSecurityScheme(authTestCase); } @Test public void testAPIKeySecurityScheme_SafeHeaderName_XAuthToken() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - APIKeySecurityScheme.TYPE, - "secret-api-key", - new APIKeySecurityScheme(APIKeySecurityScheme.Location.HEADER, "X-Auth-Token", "API Key authentication"), - "X-Auth-Token", - "secret-api-key" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + APIKeySecurityScheme.TYPE, + "secret-api-key", + new APIKeySecurityScheme( + APIKeySecurityScheme.Location.HEADER, + "X-Auth-Token", + "API Key authentication"), + "X-Auth-Token", + "secret-api-key"); testSecurityScheme(authTestCase); } - @Test public void testAPIKeySecurityScheme_CaseInsensitiveHeaderName() { // Test that lowercase header names are accepted (case-insensitive comparison) - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - APIKeySecurityScheme.TYPE, - "secret-api-key", - new APIKeySecurityScheme(APIKeySecurityScheme.Location.HEADER, "x-api-key", "API Key authentication"), - "x-api-key", - "secret-api-key" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + APIKeySecurityScheme.TYPE, + "secret-api-key", + new APIKeySecurityScheme( + APIKeySecurityScheme.Location.HEADER, + "x-api-key", + "API Key authentication"), + "x-api-key", + "secret-api-key"); testSecurityScheme(authTestCase); } @Test public void testAPIKeySecurityScheme_CaseInsensitiveHeaderName_Authorization() { // Test that lowercase "authorization" is accepted - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - APIKeySecurityScheme.TYPE, - "secret-api-key", - new APIKeySecurityScheme(APIKeySecurityScheme.Location.HEADER, "authorization", "API Key authentication"), - "authorization", - "secret-api-key" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + APIKeySecurityScheme.TYPE, + "secret-api-key", + new APIKeySecurityScheme( + APIKeySecurityScheme.Location.HEADER, + "authorization", + "API Key authentication"), + "authorization", + "secret-api-key"); testSecurityScheme(authTestCase); } - @Test public void testAPIKeySecurityScheme_UnsafeHeaderName_Rejected() { String sessionId = "session-id"; @@ -156,24 +178,20 @@ public void testAPIKeySecurityScheme_UnsafeHeaderName_Rejected() { credentialStore.setCredential(sessionId, schemeName, credential); // Use an unsafe header name that's not in the allowlist - SecurityScheme securityScheme = new APIKeySecurityScheme( - APIKeySecurityScheme.Location.HEADER, - "X-Malicious-Redirect-Header", - "Unsafe header" - ); + SecurityScheme securityScheme = + new APIKeySecurityScheme( + APIKeySecurityScheme.Location.HEADER, + "X-Malicious-Redirect-Header", + "Unsafe header"); AgentCard agentCard = createAgentCard(schemeName, securityScheme); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); ClientCallContext context = new ClientCallContext(Map.of("sessionId", sessionId), Map.of()); - PayloadAndHeaders result = authInterceptor.intercept( - "SendMessage", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders result = + authInterceptor.intercept( + "SendMessage", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); // Credential should NOT be injected for unsafe header name @@ -190,24 +208,18 @@ public void testAPIKeySecurityScheme_QueryLocation_NotInjectedInHeader() { credentialStore.setCredential(sessionId, schemeName, credential); // API key in query parameter location should not be injected as header - SecurityScheme securityScheme = new APIKeySecurityScheme( - APIKeySecurityScheme.Location.QUERY, - "api_key", - "Query parameter API key" - ); + SecurityScheme securityScheme = + new APIKeySecurityScheme( + APIKeySecurityScheme.Location.QUERY, "api_key", "Query parameter API key"); AgentCard agentCard = createAgentCard(schemeName, securityScheme); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); ClientCallContext context = new ClientCallContext(Map.of("sessionId", sessionId), Map.of()); - PayloadAndHeaders result = authInterceptor.intercept( - "SendMessage", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders result = + authInterceptor.intercept( + "SendMessage", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); // Credential should NOT be injected as header for query location @@ -224,24 +236,20 @@ public void testAPIKeySecurityScheme_CookieLocation_NotInjectedInHeader() { credentialStore.setCredential(sessionId, schemeName, credential); // API key in cookie location should not be injected as header - SecurityScheme securityScheme = new APIKeySecurityScheme( - APIKeySecurityScheme.Location.COOKIE, - "session_token", - "Cookie-based API key" - ); + SecurityScheme securityScheme = + new APIKeySecurityScheme( + APIKeySecurityScheme.Location.COOKIE, + "session_token", + "Cookie-based API key"); AgentCard agentCard = createAgentCard(schemeName, securityScheme); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); ClientCallContext context = new ClientCallContext(Map.of("sessionId", sessionId), Map.of()); - PayloadAndHeaders result = authInterceptor.intercept( - "SendMessage", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders result = + authInterceptor.intercept( + "SendMessage", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); // Credential should NOT be injected as header for cookie location @@ -251,79 +259,82 @@ public void testAPIKeySecurityScheme_CookieLocation_NotInjectedInHeader() { @Test public void testOAuth2SecurityScheme() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - OAuth2SecurityScheme.TYPE, - "secret-oauth-access-token", - new OAuth2SecurityScheme(OAuthFlows.builder().build(), "OAuth2 authentication", null), - "Authorization", - "Bearer secret-oauth-access-token" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + OAuth2SecurityScheme.TYPE, + "secret-oauth-access-token", + new OAuth2SecurityScheme( + OAuthFlows.builder().build(), "OAuth2 authentication", null), + "Authorization", + "Bearer secret-oauth-access-token"); testSecurityScheme(authTestCase); } @Test public void testOidcSecurityScheme() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - OpenIdConnectSecurityScheme.TYPE, - "secret-oidc-id-token", - new OpenIdConnectSecurityScheme("http://provider.com/.well-known/openid-configuration", "OIDC authentication"), - "Authorization", - "Bearer secret-oidc-id-token" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + OpenIdConnectSecurityScheme.TYPE, + "secret-oidc-id-token", + new OpenIdConnectSecurityScheme( + "http://provider.com/.well-known/openid-configuration", + "OIDC authentication"), + "Authorization", + "Bearer secret-oidc-id-token"); testSecurityScheme(authTestCase); } @Test public void testBearerSecurityScheme() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - "bearer", - "bearer-token-123", - new HTTPAuthSecurityScheme(null, "bearer", "Bearer token authentication"), - "Authorization", - "Bearer bearer-token-123" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + "bearer", + "bearer-token-123", + new HTTPAuthSecurityScheme(null, "bearer", "Bearer token authentication"), + "Authorization", + "Bearer bearer-token-123"); testSecurityScheme(authTestCase); } @Test public void testBasicAuthSecurityScheme() { // Credential should be pre-encoded base64("testuser:testpass") = "dGVzdHVzZXI6dGVzdHBhc3M=" - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - "basic", - "dGVzdHVzZXI6dGVzdHBhc3M=", - new HTTPAuthSecurityScheme(null, "basic", "HTTP Basic authentication"), - "Authorization", - "Basic dGVzdHVzZXI6dGVzdHBhc3M=" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + "basic", + "dGVzdHVzZXI6dGVzdHBhc3M=", + new HTTPAuthSecurityScheme(null, "basic", "HTTP Basic authentication"), + "Authorization", + "Basic dGVzdHVzZXI6dGVzdHBhc3M="); testSecurityScheme(authTestCase); } private void testSecurityScheme(AuthTestCase authTestCase) { - credentialStore.setCredential(authTestCase.sessionId, authTestCase.schemeName, authTestCase.credential); + credentialStore.setCredential( + authTestCase.sessionId, authTestCase.schemeName, authTestCase.credential); AgentCard agentCard = createAgentCard(authTestCase.schemeName, authTestCase.securityScheme); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); - ClientCallContext context = new ClientCallContext(Map.of("sessionId", authTestCase.sessionId), Map.of()); + ClientCallContext context = + new ClientCallContext(Map.of("sessionId", authTestCase.sessionId), Map.of()); - PayloadAndHeaders result = authInterceptor.intercept( - "SendMessage", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders result = + authInterceptor.intercept( + "SendMessage", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); - assertEquals(authTestCase.expectedHeaderValue, result.getHeaders().get(authTestCase.expectedHeaderKey)); + assertEquals( + authTestCase.expectedHeaderValue, + result.getHeaders().get(authTestCase.expectedHeaderKey)); } @Test @@ -331,13 +342,13 @@ void testAuthInterceptorWithoutAgentCard() { Map requestPayload = Map.of("foo", "bar"); Map headers = Map.of("foo", "bar"); - PayloadAndHeaders result = authInterceptor.intercept( - "SendMessage", - requestPayload, - headers, - null, // no agent card - new ClientCallContext(Map.of(), Map.of()) - ); + PayloadAndHeaders result = + authInterceptor.intercept( + "SendMessage", + requestPayload, + headers, + null, // no agent card + new ClientCallContext(Map.of(), Map.of())); // should be unchanged assertEquals(requestPayload, result.getPayload()); @@ -356,7 +367,8 @@ void testInMemoryContextCredentialStore() { assertEquals(credential, retrievedCredential); // wrong session ID - ClientCallContext wrongContext = new ClientCallContext(Map.of("sessionId", "wrong-session"), Map.of()); + ClientCallContext wrongContext = + new ClientCallContext(Map.of("sessionId", "wrong-session"), Map.of()); retrievedCredential = credentialStore.getCredential(schemeName, wrongContext); assertNull(retrievedCredential); @@ -383,13 +395,8 @@ void testCustomInterceptor() { Map payload = Map.of("test", "payload"); Map headers = Map.of(); - PayloadAndHeaders result = interceptor.intercept( - "SendMessage", - payload, - headers, - null, - null - ); + PayloadAndHeaders result = + interceptor.intercept("SendMessage", payload, headers, null, null); assertEquals(payload, result.getPayload()); assertEquals(headerValue, result.getHeaders().get(headerName)); @@ -404,30 +411,32 @@ void testAvailableSecuritySchemeNotInAgentCardSecuritySchemes() { credentialStore.setCredential(sessionId, schemeName, credential); // Create agent card with security requirement but no scheme definition - AgentCard agentCard = AgentCard.builder() - .name("missing") - .description("Uses missing scheme definition") - .supportedInterfaces(List.of(new AgentInterface("jsonrpc", "http://agent.com/rpc", ""))) - .version("1.0") - .capabilities(AgentCapabilities.builder().build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .securityRequirements(List.of(SecurityRequirement.builder().scheme(schemeName, List.of()).build())) - .securitySchemes(Map.of()) // no security schemes - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("missing") + .description("Uses missing scheme definition") + .supportedInterfaces( + List.of(new AgentInterface("jsonrpc", "http://agent.com/rpc", ""))) + .version("1.0") + .capabilities(AgentCapabilities.builder().build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .securityRequirements( + List.of( + SecurityRequirement.builder() + .scheme(schemeName, List.of()) + .build())) + .securitySchemes(Map.of()) // no security schemes + .build(); Map requestPayload = Map.of("foo", "bar"); Map headers = Map.of("fizz", "buzz"); ClientCallContext context = new ClientCallContext(Map.of("sessionId", sessionId), Map.of()); - PayloadAndHeaders result = authInterceptor.intercept( - "SendMessage", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders result = + authInterceptor.intercept( + "SendMessage", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); assertEquals(headers, result.getHeaders()); @@ -436,20 +445,21 @@ void testAvailableSecuritySchemeNotInAgentCardSecuritySchemes() { @Test void testNoCredentialAvailable() { String schemeName = "apikey"; - SecurityScheme securityScheme = new APIKeySecurityScheme(APIKeySecurityScheme.Location.HEADER, "X-API-Key", "API Key authentication"); + SecurityScheme securityScheme = + new APIKeySecurityScheme( + APIKeySecurityScheme.Location.HEADER, + "X-API-Key", + "API Key authentication"); AgentCard agentCard = createAgentCard(schemeName, securityScheme); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); - ClientCallContext context = new ClientCallContext(Map.of("sessionId", "session-id"), Map.of()); + ClientCallContext context = + new ClientCallContext(Map.of("sessionId", "session-id"), Map.of()); - PayloadAndHeaders result = authInterceptor.intercept( - "SendMessage", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders result = + authInterceptor.intercept( + "SendMessage", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); assertEquals(headers, result.getHeaders()); // headers should be unchanged @@ -458,29 +468,28 @@ void testNoCredentialAvailable() { @Test void testNoAgentCardSecuritySpecified() { // Arrange - AgentCard agentCard = AgentCard.builder() - .name("nosecuritybot") - .description("A bot with no security requirements") - .supportedInterfaces(List.of(new AgentInterface("jsonrpc", "http://agent.com/rpc"))) - .version("1.0") - .capabilities(AgentCapabilities.builder().build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .securityRequirements(null) // no security info - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("nosecuritybot") + .description("A bot with no security requirements") + .supportedInterfaces( + List.of(new AgentInterface("jsonrpc", "http://agent.com/rpc"))) + .version("1.0") + .capabilities(AgentCapabilities.builder().build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .securityRequirements(null) // no security info + .build(); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); - ClientCallContext context = new ClientCallContext(Map.of("sessionId", "session-id"), Map.of()); + ClientCallContext context = + new ClientCallContext(Map.of("sessionId", "session-id"), Map.of()); - PayloadAndHeaders result = authInterceptor.intercept( - "SendMessage", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders result = + authInterceptor.intercept( + "SendMessage", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); assertEquals(headers, result.getHeaders()); @@ -491,16 +500,20 @@ void testNoAgentCardSecuritySpecified() { */ private AgentCard createAgentCard(String schemeName, SecurityScheme securityScheme) { return AgentCard.builder() - .name(schemeName + "bot") - .description("A bot that uses " + schemeName) - .supportedInterfaces(List.of(new AgentInterface("jsonrpc", "http://agent.com/rpc"))) - .version("1.0") - .capabilities(AgentCapabilities.builder().build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .securityRequirements(List.of(SecurityRequirement.builder().scheme(schemeName, List.of()).build())) - .securitySchemes(Map.of(schemeName, securityScheme)) - .build(); + .name(schemeName + "bot") + .description("A bot that uses " + schemeName) + .supportedInterfaces(List.of(new AgentInterface("jsonrpc", "http://agent.com/rpc"))) + .version("1.0") + .capabilities(AgentCapabilities.builder().build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .securityRequirements( + List.of( + SecurityRequirement.builder() + .scheme(schemeName, List.of()) + .build())) + .securitySchemes(Map.of(schemeName, securityScheme)) + .build(); } } diff --git a/client/transport/spi/src/test/java/org/a2aproject/sdk/client/transport/spi/sse/SSEEventListenerTest.java b/client/transport/spi/src/test/java/org/a2aproject/sdk/client/transport/spi/sse/SSEEventListenerTest.java index 7f1c4dc2a..d00ce3b63 100644 --- a/client/transport/spi/src/test/java/org/a2aproject/sdk/client/transport/spi/sse/SSEEventListenerTest.java +++ b/client/transport/spi/src/test/java/org/a2aproject/sdk/client/transport/spi/sse/SSEEventListenerTest.java @@ -10,7 +10,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; - import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.StreamingEventKind; @@ -42,8 +41,9 @@ public class SSEEventListenerTest { private static class TestSSEEventListener extends AbstractSSEEventListener { private StreamingEventKind eventToHandle; - public TestSSEEventListener(Consumer eventHandler, - @Nullable Consumer errorHandler) { + public TestSSEEventListener( + Consumer eventHandler, + @Nullable Consumer errorHandler) { super(eventHandler, errorHandler); } @@ -117,18 +117,16 @@ private static Task createTask(TaskState state) { private static TaskStatusUpdateEvent createTaskStatusUpdateEvent(TaskState state) { return new TaskStatusUpdateEvent( - TEST_TASK_ID, - new TaskStatus(state), - TEST_CONTEXT_ID, - null - ); + TEST_TASK_ID, new TaskStatus(state), TEST_CONTEXT_ID, null); } - private static TestSSEEventListener createListenerWithEventCapture(AtomicReference eventCapture) { + private static TestSSEEventListener createListenerWithEventCapture( + AtomicReference eventCapture) { return new TestSSEEventListener(eventCapture::set, null); } - private static TestSSEEventListener createListenerWithErrorCapture(AtomicReference errorCapture) { + private static TestSSEEventListener createListenerWithErrorCapture( + AtomicReference errorCapture) { return new TestSSEEventListener(event -> {}, errorCapture::set); } @@ -189,10 +187,8 @@ public void testOnErrorWithNullErrorHandler() { @Test public void testOnErrorCancelsFuture() { AtomicBoolean errorHandlerCalled = new AtomicBoolean(false); - TestSSEEventListener listener = new TestSSEEventListener( - event -> {}, - error -> errorHandlerCalled.set(true) - ); + TestSSEEventListener listener = + new TestSSEEventListener(event -> {}, error -> errorHandlerCalled.set(true)); CancelCapturingFuture future = new CancelCapturingFuture(); @@ -205,7 +201,8 @@ public void testOnErrorCancelsFuture() { @Test public void testShouldAutoCloseWithFinalTaskStatusUpdateEvent() { TestSSEEventListener listener = createBasicListener(); - TaskStatusUpdateEvent finalEvent = createTaskStatusUpdateEvent(TaskState.TASK_STATE_COMPLETED); + TaskStatusUpdateEvent finalEvent = + createTaskStatusUpdateEvent(TaskState.TASK_STATE_COMPLETED); assertTrue(listener.shouldAutoClose(finalEvent)); } @@ -213,28 +210,35 @@ public void testShouldAutoCloseWithFinalTaskStatusUpdateEvent() { @Test public void testShouldAutoCloseWithNonFinalTaskStatusUpdateEvent() { TestSSEEventListener listener = createBasicListener(); - TaskStatusUpdateEvent nonFinalEvent = createTaskStatusUpdateEvent(TaskState.TASK_STATE_WORKING); + TaskStatusUpdateEvent nonFinalEvent = + createTaskStatusUpdateEvent(TaskState.TASK_STATE_WORKING); assertFalse(listener.shouldAutoClose(nonFinalEvent)); } @ParameterizedTest - @EnumSource(value = TaskState.class, names = {"TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED"}) + @EnumSource( + value = TaskState.class, + names = {"TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED"}) public void testShouldAutoCloseWithFinalTaskStates(TaskState finalState) { TestSSEEventListener listener = createBasicListener(); Task task = createTask(finalState); - assertTrue(listener.shouldAutoClose(task), + assertTrue( + listener.shouldAutoClose(task), "Task with state " + finalState + " should trigger auto-close"); } @ParameterizedTest - @EnumSource(value = TaskState.class, names = {"TASK_STATE_WORKING", "TASK_STATE_SUBMITTED"}) + @EnumSource( + value = TaskState.class, + names = {"TASK_STATE_WORKING", "TASK_STATE_SUBMITTED"}) public void testShouldAutoCloseWithNonFinalTaskStates(TaskState nonFinalState) { TestSSEEventListener listener = createBasicListener(); Task task = createTask(nonFinalState); - assertFalse(listener.shouldAutoClose(task), + assertFalse( + listener.shouldAutoClose(task), "Task with state " + nonFinalState + " should not trigger auto-close"); } @@ -251,7 +255,8 @@ public void testAutoCloseCancelsFutureForFinalEvent() { AtomicReference receivedEvent = new AtomicReference<>(); TestSSEEventListener listener = createListenerWithEventCapture(receivedEvent); - TaskStatusUpdateEvent finalEvent = createTaskStatusUpdateEvent(TaskState.TASK_STATE_COMPLETED); + TaskStatusUpdateEvent finalEvent = + createTaskStatusUpdateEvent(TaskState.TASK_STATE_COMPLETED); CancelCapturingFuture future = new CancelCapturingFuture(); listener.setEventToHandle(finalEvent); @@ -282,7 +287,8 @@ public void testAutoCloseWithNullFuture() { AtomicReference receivedEvent = new AtomicReference<>(); TestSSEEventListener listener = createListenerWithEventCapture(receivedEvent); - TaskStatusUpdateEvent finalEvent = createTaskStatusUpdateEvent(TaskState.TASK_STATE_COMPLETED); + TaskStatusUpdateEvent finalEvent = + createTaskStatusUpdateEvent(TaskState.TASK_STATE_COMPLETED); // Should not throw with null future listener.setEventToHandle(finalEvent); @@ -290,4 +296,4 @@ public void testAutoCloseWithNullFuture() { assertNotNull(receivedEvent.get()); } -} \ No newline at end of file +} diff --git a/common/pom.xml b/common/pom.xml index c1b3b4851..7bc324227 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -28,4 +26,4 @@ - \ No newline at end of file + diff --git a/common/src/main/java/org/a2aproject/sdk/common/A2AErrorMessages.java b/common/src/main/java/org/a2aproject/sdk/common/A2AErrorMessages.java index 606167cf2..81e9a3f28 100644 --- a/common/src/main/java/org/a2aproject/sdk/common/A2AErrorMessages.java +++ b/common/src/main/java/org/a2aproject/sdk/common/A2AErrorMessages.java @@ -3,11 +3,13 @@ /** Standard error message constants for A2A authentication and authorization failures. */ public final class A2AErrorMessages { - private A2AErrorMessages() { - } + private A2AErrorMessages() {} /** Error message returned when client credentials are missing or invalid. */ - public static final String AUTHENTICATION_FAILED = "Authentication failed: Client credentials are missing or invalid"; + public static final String AUTHENTICATION_FAILED = + "Authentication failed: Client credentials are missing or invalid"; + /** Error message returned when the client lacks permission for the requested operation. */ - public static final String AUTHORIZATION_FAILED = "Authorization failed: Client does not have permission for the operation"; + public static final String AUTHORIZATION_FAILED = + "Authorization failed: Client does not have permission for the operation"; } diff --git a/common/src/main/java/org/a2aproject/sdk/common/A2AHeaders.java b/common/src/main/java/org/a2aproject/sdk/common/A2AHeaders.java index 5d1c05e36..a590b5d9a 100644 --- a/common/src/main/java/org/a2aproject/sdk/common/A2AHeaders.java +++ b/common/src/main/java/org/a2aproject/sdk/common/A2AHeaders.java @@ -4,7 +4,7 @@ * Common A2A protocol headers and constants. */ public final class A2AHeaders { - + /** * HTTP header name for A2A protocol version. * Used to communicate the protocol version that the client is using. @@ -21,7 +21,7 @@ public final class A2AHeaders { * HTTP header name for a push notification token. */ public static final String X_A2A_NOTIFICATION_TOKEN = "X-A2A-Notification-Token"; - + private A2AHeaders() { // Utility class } diff --git a/common/src/main/java/org/a2aproject/sdk/common/package-info.java b/common/src/main/java/org/a2aproject/sdk/common/package-info.java index 61d0ce6ef..f3c756b70 100644 --- a/common/src/main/java/org/a2aproject/sdk/common/package-info.java +++ b/common/src/main/java/org/a2aproject/sdk/common/package-info.java @@ -3,4 +3,3 @@ package org.a2aproject.sdk.common; import org.jspecify.annotations.NullMarked; - diff --git a/common/src/main/java/org/a2aproject/sdk/util/Assert.java b/common/src/main/java/org/a2aproject/sdk/util/Assert.java index 128a89b98..d3ed8336f 100644 --- a/common/src/main/java/org/a2aproject/sdk/util/Assert.java +++ b/common/src/main/java/org/a2aproject/sdk/util/Assert.java @@ -5,8 +5,7 @@ /** Parameter validation utilities. */ public final class Assert { - private Assert() { - } + private Assert() {} /** * Checks whether the value is {@code null}, empty, or blank. @@ -27,7 +26,8 @@ public static boolean isNullOrEmpty(@Nullable String value) { * @return the value that was passed in * @throws IllegalArgumentException if the value is {@code null} */ - public static @NotNull T checkNotNullParam(String name, @Nullable T value) throws IllegalArgumentException { + public static @NotNull T checkNotNullParam(String name, @Nullable T value) + throws IllegalArgumentException { checkNotNullParamChecked("name", name); if (value == null) { throw new IllegalArgumentException("Parameter '" + name + "' may not be null"); @@ -48,9 +48,8 @@ private static void checkNotNullParamChecked(final String name, final @Nulla * @throws IllegalArgumentException if the value is not a valid JSON-RPC id type */ public static void isValidJsonRpcId(@Nullable Object value) { - if (! (value == null || value instanceof String || value instanceof Number)) { + if (!(value == null || value instanceof String || value instanceof Number)) { throw new IllegalArgumentException("JSON-RPC id must be null, a String, or a Number"); } } - } diff --git a/common/src/main/java/org/a2aproject/sdk/util/HtmlEscapeUtils.java b/common/src/main/java/org/a2aproject/sdk/util/HtmlEscapeUtils.java index 778827675..41a547391 100644 --- a/common/src/main/java/org/a2aproject/sdk/util/HtmlEscapeUtils.java +++ b/common/src/main/java/org/a2aproject/sdk/util/HtmlEscapeUtils.java @@ -6,8 +6,7 @@ */ public final class HtmlEscapeUtils { - private HtmlEscapeUtils() { - } + private HtmlEscapeUtils() {} /** * Removes HTML escaping applied by Gson's {@code JsonWriter} when @@ -31,10 +30,13 @@ public static String removeHtmlEscaping(String json) { if (!json.contains("\\u00")) { return json; } - return json.replace("\\u003c", "<").replace("\\u003C", "<") - .replace("\\u003e", ">").replace("\\u003E", ">") + return json.replace("\\u003c", "<") + .replace("\\u003C", "<") + .replace("\\u003e", ">") + .replace("\\u003E", ">") .replace("\\u0026", "&") - .replace("\\u003d", "=").replace("\\u003D", "=") + .replace("\\u003d", "=") + .replace("\\u003D", "=") .replace("\\u0027", "'"); } } diff --git a/common/src/main/java/org/a2aproject/sdk/util/HttpHeaderUtils.java b/common/src/main/java/org/a2aproject/sdk/util/HttpHeaderUtils.java index b6b59a492..7e887d768 100644 --- a/common/src/main/java/org/a2aproject/sdk/util/HttpHeaderUtils.java +++ b/common/src/main/java/org/a2aproject/sdk/util/HttpHeaderUtils.java @@ -11,8 +11,7 @@ */ public final class HttpHeaderUtils { - private HttpHeaderUtils() { - } + private HttpHeaderUtils() {} /** * Creates an unmodifiable, case-insensitive copy of the given header map. @@ -25,13 +24,13 @@ private HttpHeaderUtils() { * @param headers the source header map * @return an unmodifiable, case-insensitive map */ - public static Map> copyOfCaseInsensitive(Map> headers) { + public static Map> copyOfCaseInsensitive( + Map> headers) { TreeMap> copy = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); for (Map.Entry> entry : headers.entrySet()) { if (entry.getKey() != null && entry.getValue() != null) { - List cleanValues = entry.getValue().stream() - .filter(Objects::nonNull) - .toList(); + List cleanValues = + entry.getValue().stream().filter(Objects::nonNull).toList(); copy.put(entry.getKey(), cleanValues); } } diff --git a/common/src/main/java/org/a2aproject/sdk/util/NotNull.java b/common/src/main/java/org/a2aproject/sdk/util/NotNull.java index 92dd8f43b..ecb8b449b 100644 --- a/common/src/main/java/org/a2aproject/sdk/util/NotNull.java +++ b/common/src/main/java/org/a2aproject/sdk/util/NotNull.java @@ -7,8 +7,13 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.CLASS) -@Target({ ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE }) +@Target({ + ElementType.FIELD, + ElementType.LOCAL_VARIABLE, + ElementType.METHOD, + ElementType.PARAMETER, + ElementType.TYPE_USE +}) @Documented /** Marks an element as non-null for static analysis. */ -public @interface NotNull { -} +public @interface NotNull {} diff --git a/common/src/main/java/org/a2aproject/sdk/util/package-info.java b/common/src/main/java/org/a2aproject/sdk/util/package-info.java index e9de08fb7..3d24928c5 100644 --- a/common/src/main/java/org/a2aproject/sdk/util/package-info.java +++ b/common/src/main/java/org/a2aproject/sdk/util/package-info.java @@ -3,4 +3,3 @@ package org.a2aproject.sdk.util; import org.jspecify.annotations.NullMarked; - diff --git a/common/src/test/java/org/a2aproject/sdk/util/HtmlEscapeUtilsTest.java b/common/src/test/java/org/a2aproject/sdk/util/HtmlEscapeUtilsTest.java index 5bf235de6..5a7804d18 100644 --- a/common/src/test/java/org/a2aproject/sdk/util/HtmlEscapeUtilsTest.java +++ b/common/src/test/java/org/a2aproject/sdk/util/HtmlEscapeUtilsTest.java @@ -8,7 +8,8 @@ public class HtmlEscapeUtilsTest { @Test public void removeHtmlEscaping_restoresAngleBrackets() { - assertEquals("", HtmlEscapeUtils.removeHtmlEscaping("\\u003cevent-topic\\u003e")); + assertEquals( + "", HtmlEscapeUtils.removeHtmlEscaping("\\u003cevent-topic\\u003e")); } @Test @@ -28,7 +29,8 @@ public void removeHtmlEscaping_restoresApostrophe() { @Test public void removeHtmlEscaping_handlesMultipleEscapes() { - assertEquals("&", + assertEquals( + "&", HtmlEscapeUtils.removeHtmlEscaping("\\u003ctag\\u003e\\u0026\\u003c/tag\\u003e")); } @@ -45,7 +47,8 @@ public void removeHtmlEscaping_handlesEmptyString() { @Test public void removeHtmlEscaping_restoresUppercaseAngleBrackets() { - assertEquals("", HtmlEscapeUtils.removeHtmlEscaping("\\u003Cevent-topic\\u003E")); + assertEquals( + "", HtmlEscapeUtils.removeHtmlEscaping("\\u003Cevent-topic\\u003E")); } @Test @@ -55,7 +58,7 @@ public void removeHtmlEscaping_restoresUppercaseEquals() { @Test public void removeHtmlEscaping_handlesMixedCase() { - assertEquals("=value", HtmlEscapeUtils.removeHtmlEscaping("\\u003ctag\\u003E\\u003Dvalue")); + assertEquals( + "=value", HtmlEscapeUtils.removeHtmlEscaping("\\u003ctag\\u003E\\u003Dvalue")); } - } diff --git a/compat-0.3/client/base/pom.xml b/compat-0.3/client/base/pom.xml index 648ab6894..e1484da53 100644 --- a/compat-0.3/client/base/pom.xml +++ b/compat-0.3/client/base/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -81,4 +79,4 @@ - \ No newline at end of file + diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/A2A_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/A2A_v0_3.java index dfbc37386..225eb9487 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/A2A_v0_3.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/A2A_v0_3.java @@ -3,7 +3,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.compat03.client.http.A2ACardResolver_v0_3; @@ -14,7 +13,6 @@ import org.a2aproject.sdk.compat03.spec.Part_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; - /** * Constants and utility methods related to the A2A protocol. */ @@ -82,7 +80,8 @@ public static Message_v0_3 createUserTextMessage(String text, String contextId, * @param taskId the task ID to use (optional) * @return the agent message */ - public static Message_v0_3 createAgentTextMessage(String text, String contextId, String taskId) { + public static Message_v0_3 createAgentTextMessage( + String text, String contextId, String taskId) { return toMessage(text, Message_v0_3.Role.AGENT, null, contextId, taskId); } @@ -94,7 +93,8 @@ public static Message_v0_3 createAgentTextMessage(String text, String contextId, * @param taskId the task ID to use (optional) * @return the agent message */ - public static Message_v0_3 createAgentPartsMessage(List> parts, String contextId, String taskId) { + public static Message_v0_3 createAgentPartsMessage( + List> parts, String contextId, String taskId) { if (parts == null || parts.isEmpty()) { throw new IllegalArgumentException("Parts cannot be null or empty"); } @@ -105,24 +105,36 @@ private static Message_v0_3 toMessage(String text, Message_v0_3.Role role, Strin return toMessage(text, role, messageId, null, null); } - private static Message_v0_3 toMessage(String text, Message_v0_3.Role role, String messageId, String contextId, String taskId) { - Message_v0_3.Builder messageBuilder = new Message_v0_3.Builder() - .role(role) - .parts(Collections.singletonList(new TextPart_v0_3(text))) - .contextId(contextId) - .taskId(taskId); + private static Message_v0_3 toMessage( + String text, + Message_v0_3.Role role, + String messageId, + String contextId, + String taskId) { + Message_v0_3.Builder messageBuilder = + new Message_v0_3.Builder() + .role(role) + .parts(Collections.singletonList(new TextPart_v0_3(text))) + .contextId(contextId) + .taskId(taskId); if (messageId != null) { messageBuilder.messageId(messageId); } return messageBuilder.build(); } - private static Message_v0_3 toMessage(List> parts, Message_v0_3.Role role, String messageId, String contextId, String taskId) { - Message_v0_3.Builder messageBuilder = new Message_v0_3.Builder() - .role(role) - .parts(parts) - .contextId(contextId) - .taskId(taskId); + private static Message_v0_3 toMessage( + List> parts, + Message_v0_3.Role role, + String messageId, + String contextId, + String taskId) { + Message_v0_3.Builder messageBuilder = + new Message_v0_3.Builder() + .role(role) + .parts(parts) + .contextId(contextId) + .taskId(taskId); if (messageId != null) { messageBuilder.messageId(messageId); } @@ -137,7 +149,8 @@ private static Message_v0_3 toMessage(List> parts, Message_v0_3.Rol * @throws A2AClientError_v0_3 If an HTTP error occurs fetching the card * @throws A2AClientJSONError_v0_3 If the response body cannot be decoded as JSON or validated against the AgentCard schema */ - public static AgentCard_v0_3 getAgentCard(String agentUrl) throws A2AClientError_v0_3, A2AClientJSONError_v0_3 { + public static AgentCard_v0_3 getAgentCard(String agentUrl) + throws A2AClientError_v0_3, A2AClientJSONError_v0_3 { return getAgentCard(A2AHttpClientFactory.create(), agentUrl); } @@ -150,7 +163,8 @@ public static AgentCard_v0_3 getAgentCard(String agentUrl) throws A2AClientError * @throws A2AClientError_v0_3 If an HTTP error occurs fetching the card * @throws A2AClientJSONError_v0_3 If the response body cannot be decoded as JSON or validated against the AgentCard schema */ - public static AgentCard_v0_3 getAgentCard(A2AHttpClient httpClient, String agentUrl) throws A2AClientError_v0_3, A2AClientJSONError_v0_3 { + public static AgentCard_v0_3 getAgentCard(A2AHttpClient httpClient, String agentUrl) + throws A2AClientError_v0_3, A2AClientJSONError_v0_3 { return getAgentCard(httpClient, agentUrl, null, null); } @@ -165,7 +179,9 @@ public static AgentCard_v0_3 getAgentCard(A2AHttpClient httpClient, String agent * @throws A2AClientError_v0_3 If an HTTP error occurs fetching the card * @throws A2AClientJSONError_v0_3 If the response body cannot be decoded as JSON or validated against the AgentCard schema */ - public static AgentCard_v0_3 getAgentCard(String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError_v0_3, A2AClientJSONError_v0_3 { + public static AgentCard_v0_3 getAgentCard( + String agentUrl, String relativeCardPath, Map authHeaders) + throws A2AClientError_v0_3, A2AClientJSONError_v0_3 { return getAgentCard(A2AHttpClientFactory.create(), agentUrl, relativeCardPath, authHeaders); } @@ -181,8 +197,14 @@ public static AgentCard_v0_3 getAgentCard(String agentUrl, String relativeCardPa * @throws A2AClientError_v0_3 If an HTTP error occurs fetching the card * @throws A2AClientJSONError_v0_3 If the response body cannot be decoded as JSON or validated against the AgentCard schema */ - public static AgentCard_v0_3 getAgentCard(A2AHttpClient httpClient, String agentUrl, String relativeCardPath, Map authHeaders) throws A2AClientError_v0_3, A2AClientJSONError_v0_3 { - A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(httpClient, agentUrl, relativeCardPath, authHeaders); + public static AgentCard_v0_3 getAgentCard( + A2AHttpClient httpClient, + String agentUrl, + String relativeCardPath, + Map authHeaders) + throws A2AClientError_v0_3, A2AClientJSONError_v0_3 { + A2ACardResolver_v0_3 resolver = + new A2ACardResolver_v0_3(httpClient, agentUrl, relativeCardPath, authHeaders); return resolver.getAgentCard(); } } diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/AbstractClient_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/AbstractClient_v0_3.java index d27372f69..d3c9b3d53 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/AbstractClient_v0_3.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/AbstractClient_v0_3.java @@ -6,7 +6,6 @@ import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; - import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; @@ -15,10 +14,10 @@ import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -37,7 +36,9 @@ public AbstractClient_v0_3(List> co this(consumers, null); } - public AbstractClient_v0_3(@NonNull List> consumers, @Nullable Consumer streamingErrorHandler) { + public AbstractClient_v0_3( + @NonNull List> consumers, + @Nullable Consumer streamingErrorHandler) { checkNotNullParam("consumers", consumers); this.consumers = consumers; this.streamingErrorHandler = streamingErrorHandler; @@ -72,7 +73,8 @@ public void sendMessage(Message_v0_3 request) throws A2AClientException_v0_3 { * @param context optional client call context for the request (may be {@code null}) * @throws A2AClientException_v0_3 if sending the message fails for any reason */ - public abstract void sendMessage(Message_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + public abstract void sendMessage(Message_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Send a message to the remote agent. This method will automatically use @@ -88,9 +90,11 @@ public void sendMessage(Message_v0_3 request) throws A2AClientException_v0_3 { * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs * @throws A2AClientException_v0_3 if sending the message fails for any reason */ - public void sendMessage(Message_v0_3 request, - List> consumers, - Consumer streamingErrorHandler) throws A2AClientException_v0_3 { + public void sendMessage( + Message_v0_3 request, + List> consumers, + Consumer streamingErrorHandler) + throws A2AClientException_v0_3 { sendMessage(request, consumers, streamingErrorHandler, null); } @@ -109,10 +113,12 @@ public void sendMessage(Message_v0_3 request, * @param context optional client call context for the request (may be {@code null}) * @throws A2AClientException_v0_3 if sending the message fails for any reason */ - public abstract void sendMessage(Message_v0_3 request, - List> consumers, - Consumer streamingErrorHandler, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + public abstract void sendMessage( + Message_v0_3 request, + List> consumers, + Consumer streamingErrorHandler, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Send a message to the remote agent. This method will automatically use @@ -128,8 +134,11 @@ public abstract void sendMessage(Message_v0_3 request, * @param metadata the optional metadata to include when sending the message * @throws A2AClientException_v0_3 if sending the message fails for any reason */ - public void sendMessage(Message_v0_3 request, PushNotificationConfig_v0_3 pushNotificationConfiguration, - Map metadata) throws A2AClientException_v0_3 { + public void sendMessage( + Message_v0_3 request, + PushNotificationConfig_v0_3 pushNotificationConfiguration, + Map metadata) + throws A2AClientException_v0_3 { sendMessage(request, pushNotificationConfiguration, metadata, null); } @@ -148,8 +157,12 @@ public void sendMessage(Message_v0_3 request, PushNotificationConfig_v0_3 pushNo * @param context optional client call context for the request (may be {@code null}) * @throws A2AClientException_v0_3 if sending the message fails for any reason */ - public abstract void sendMessage(Message_v0_3 request, PushNotificationConfig_v0_3 pushNotificationConfiguration, - Map metadata, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + public abstract void sendMessage( + Message_v0_3 request, + PushNotificationConfig_v0_3 pushNotificationConfiguration, + Map metadata, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Retrieve the current state and history of a specific task. @@ -170,7 +183,9 @@ public Task_v0_3 getTask(TaskQueryParams_v0_3 request) throws A2AClientException * @return the task * @throws A2AClientException_v0_3 if retrieving the task fails for any reason */ - public abstract Task_v0_3 getTask(TaskQueryParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + public abstract Task_v0_3 getTask( + TaskQueryParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Request the agent to cancel a specific task. @@ -191,7 +206,9 @@ public Task_v0_3 cancelTask(TaskIdParams_v0_3 request) throws A2AClientException * @return the cancelled task * @throws A2AClientException_v0_3 if cancelling the task fails for any reason */ - public abstract Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + public abstract Task_v0_3 cancelTask( + TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Set or update the push notification configuration for a specific task. @@ -214,8 +231,8 @@ public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration( * @throws A2AClientException_v0_3 if setting the task push notification configuration fails for any reason */ public abstract TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration( - TaskPushNotificationConfig_v0_3 request, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + TaskPushNotificationConfig_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Retrieve the push notification configuration for a specific task. @@ -239,7 +256,8 @@ public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration( */ public abstract TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration( GetTaskPushNotificationConfigParams_v0_3 request, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Retrieve the list of push notification configurations for a specific task. @@ -263,7 +281,8 @@ public List listTaskPushNotificationConfigurati */ public abstract List listTaskPushNotificationConfigurations( ListTaskPushNotificationConfigParams_v0_3 request, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Delete the list of push notification configurations for a specific task. @@ -285,7 +304,8 @@ public void deleteTaskPushNotificationConfigurations( */ public abstract void deleteTaskPushNotificationConfigurations( DeleteTaskPushNotificationConfigParams_v0_3 request, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Resubscribe to a task's event stream. @@ -312,7 +332,9 @@ public void resubscribe(TaskIdParams_v0_3 request) throws A2AClientException_v0_ * @param context optional client call context for the request (may be {@code null}) * @throws A2AClientException_v0_3 if resubscribing fails for any reason */ - public abstract void resubscribe(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + public abstract void resubscribe( + TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Resubscribe to a task's event stream. @@ -326,8 +348,11 @@ public void resubscribe(TaskIdParams_v0_3 request) throws A2AClientException_v0_ * @param streamingErrorHandler an error handler that should be used for the streaming case if an error occurs * @throws A2AClientException_v0_3 if resubscribing fails for any reason */ - public void resubscribe(TaskIdParams_v0_3 request, List> consumers, - Consumer streamingErrorHandler) throws A2AClientException_v0_3 { + public void resubscribe( + TaskIdParams_v0_3 request, + List> consumers, + Consumer streamingErrorHandler) + throws A2AClientException_v0_3 { resubscribe(request, consumers, streamingErrorHandler, null); } @@ -344,8 +369,12 @@ public void resubscribe(TaskIdParams_v0_3 request, List> consumers, - Consumer streamingErrorHandler, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + public abstract void resubscribe( + TaskIdParams_v0_3 request, + List> consumers, + Consumer streamingErrorHandler, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Retrieve the AgentCard. @@ -364,7 +393,8 @@ public AgentCard_v0_3 getAgentCard() throws A2AClientException_v0_3 { * @return the AgentCard * @throws A2AClientException_v0_3 if retrieving the agent card fails for any reason */ - public abstract AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + public abstract AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Close the transport and release any associated resources. @@ -388,5 +418,4 @@ void consume(ClientEvent_v0_3 clientEventOrMessage, AgentCard_v0_3 agentCard) { public @Nullable Consumer getStreamingErrorHandler() { return streamingErrorHandler; } - -} \ No newline at end of file +} diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3.java index b2fe661c2..59fea87f5 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3.java @@ -1,15 +1,5 @@ package org.a2aproject.sdk.compat03.client; -import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3; -import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3; -import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfig_v0_3; -import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3; -import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; -import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; -import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3; -import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; - import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; @@ -18,19 +8,36 @@ import java.util.ServiceLoader; import java.util.function.BiConsumer; import java.util.function.Consumer; +import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3; +import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfig_v0_3; +import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3; +import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3; +import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; +import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; +import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3; +import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; public class ClientBuilder_v0_3 { - private static final Map>> transportProviderRegistry = new HashMap<>(); - private static final Map, String> transportProtocolMapping = new HashMap<>(); + private static final Map< + String, + ClientTransportProvider_v0_3< + ? extends ClientTransport_v0_3, + ? extends ClientTransportConfig_v0_3>> + transportProviderRegistry = new HashMap<>(); + private static final Map, String> + transportProtocolMapping = new HashMap<>(); static { - ServiceLoader loader = ServiceLoader.load(ClientTransportProvider_v0_3.class); + ServiceLoader loader = + ServiceLoader.load(ClientTransportProvider_v0_3.class); for (ClientTransportProvider_v0_3 transport : loader) { transportProviderRegistry.put(transport.getTransportProtocol(), transport); - transportProtocolMapping.put(transport.getTransportProtocolClass(), transport.getTransportProtocol()); + transportProtocolMapping.put( + transport.getTransportProtocolClass(), transport.getTransportProtocol()); } } @@ -40,17 +47,24 @@ public class ClientBuilder_v0_3 { private @Nullable Consumer streamErrorHandler; private ClientConfig_v0_3 clientConfig = new ClientConfig_v0_3.Builder().build(); - private final Map, ClientTransportConfig_v0_3> clientTransports = new LinkedHashMap<>(); + private final Map< + Class, + ClientTransportConfig_v0_3> + clientTransports = new LinkedHashMap<>(); ClientBuilder_v0_3(@NonNull AgentCard_v0_3 agentCard) { this.agentCard = agentCard; } - public ClientBuilder_v0_3 withTransport(Class clazz, ClientTransportConfigBuilder_v0_3, ?> configBuilder) { + public ClientBuilder_v0_3 withTransport( + Class clazz, + ClientTransportConfigBuilder_v0_3, ?> + configBuilder) { return withTransport(clazz, configBuilder.build()); } - public ClientBuilder_v0_3 withTransport(Class clazz, ClientTransportConfig_v0_3 config) { + public ClientBuilder_v0_3 withTransport( + Class clazz, ClientTransportConfig_v0_3 config) { clientTransports.put(clazz, config); return this; @@ -61,7 +75,8 @@ public ClientBuilder_v0_3 addConsumer(BiConsumer> consumers) { + public ClientBuilder_v0_3 addConsumers( + List> consumers) { this.consumers.addAll(consumers); return this; } @@ -83,7 +98,8 @@ public Client_v0_3 build() throws A2AClientException_v0_3 { ClientTransport_v0_3 clientTransport = buildClientTransport(); - return new Client_v0_3(agentCard, clientConfig, clientTransport, consumers, streamErrorHandler); + return new Client_v0_3( + agentCard, clientConfig, clientTransport, consumers, streamErrorHandler); } @SuppressWarnings("unchecked") @@ -92,20 +108,26 @@ private ClientTransport_v0_3 buildClientTransport() throws A2AClientException_v0 AgentInterface_v0_3 agentInterface = findBestClientTransport(); // Get the transport provider associated with the protocol - ClientTransportProvider_v0_3 clientTransportProvider = transportProviderRegistry.get(agentInterface.transport()); + ClientTransportProvider_v0_3 clientTransportProvider = + transportProviderRegistry.get(agentInterface.transport()); if (clientTransportProvider == null) { - throw new A2AClientException_v0_3("No client available for " + agentInterface.transport()); + throw new A2AClientException_v0_3( + "No client available for " + agentInterface.transport()); } - Class transportProtocolClass = clientTransportProvider.getTransportProtocolClass(); + Class transportProtocolClass = + clientTransportProvider.getTransportProtocolClass(); // Retrieve the configuration associated with the preferred transport - ClientTransportConfig_v0_3 clientTransportConfig = clientTransports.get(transportProtocolClass); + ClientTransportConfig_v0_3 clientTransportConfig = + clientTransports.get(transportProtocolClass); if (clientTransportConfig == null) { - throw new A2AClientException_v0_3("Missing required TransportConfig for " + agentInterface.transport()); + throw new A2AClientException_v0_3( + "Missing required TransportConfig for " + agentInterface.transport()); } - return clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface.url()); + return clientTransportProvider.create( + clientTransportConfig, agentCard, agentInterface.url()); } private Map getServerPreferredTransports() { @@ -113,7 +135,8 @@ private Map getServerPreferredTransports() { serverPreferredTransports.put(agentCard.preferredTransport(), agentCard.url()); if (agentCard.additionalInterfaces() != null) { for (AgentInterface_v0_3 agentInterface : agentCard.additionalInterfaces()) { - serverPreferredTransports.putIfAbsent(agentInterface.transport(), agentInterface.url()); + serverPreferredTransports.putIfAbsent( + agentInterface.transport(), agentInterface.url()); } } return serverPreferredTransports; @@ -126,7 +149,9 @@ private List getClientPreferredTransports() { // default to JSONRPC if not specified supportedClientTransports.add(TransportProtocol_v0_3.JSONRPC.asString()); } else { - clientTransports.forEach((aClass, clientTransportConfig) -> supportedClientTransports.add(transportProtocolMapping.get(aClass))); + clientTransports.forEach( + (aClass, clientTransportConfig) -> + supportedClientTransports.add(transportProtocolMapping.get(aClass))); } return supportedClientTransports; } @@ -160,7 +185,7 @@ private AgentInterface_v0_3 findBestClientTransport() throws A2AClientException_ if (transportProtocol == null || transportUrl == null) { throw new A2AClientException_v0_3("No compatible transport found"); } - if (! transportProviderRegistry.containsKey(transportProtocol)) { + if (!transportProviderRegistry.containsKey(transportProtocol)) { throw new A2AClientException_v0_3("No client available for " + transportProtocol); } diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientEvent_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientEvent_v0_3.java index 498814a56..6912ca457 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientEvent_v0_3.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientEvent_v0_3.java @@ -1,4 +1,4 @@ package org.a2aproject.sdk.compat03.client; -public sealed interface ClientEvent_v0_3 permits MessageEvent_v0_3, TaskEvent_v0_3, TaskUpdateEvent_v0_3 { -} +public sealed interface ClientEvent_v0_3 + permits MessageEvent_v0_3, TaskEvent_v0_3, TaskUpdateEvent_v0_3 {} diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3.java index e02c2d898..7c4a6ad0d 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3.java @@ -6,7 +6,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientInvalidArgsError_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientInvalidStateError_v0_3; @@ -44,13 +43,15 @@ Task_v0_3 getCurrentTask() throws A2AClientInvalidStateError_v0_3 { Task_v0_3 saveTaskEvent(Task_v0_3 task) throws A2AClientInvalidArgsError_v0_3 { if (currentTask != null && !currentTask.id().equals(task.id())) { - throw new A2AClientInvalidArgsError_v0_3("Task is already set, create new manager for new tasks."); + throw new A2AClientInvalidArgsError_v0_3( + "Task is already set, create new manager for new tasks."); } saveTask(task); return task; } - Task_v0_3 saveTaskEvent(TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent) throws A2AClientError_v0_3 { + Task_v0_3 saveTaskEvent(TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent) + throws A2AClientError_v0_3 { if (taskId == null) { taskId = taskStatusUpdateEvent.taskId(); } @@ -59,11 +60,12 @@ Task_v0_3 saveTaskEvent(TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent) throws } Task_v0_3 task = currentTask; if (task == null) { - task = new Task_v0_3.Builder() - .status(new TaskStatus_v0_3(TaskState_v0_3.UNKNOWN)) - .id(taskId) - .contextId(contextId == null ? "" : contextId) - .build(); + task = + new Task_v0_3.Builder() + .status(new TaskStatus_v0_3(TaskState_v0_3.UNKNOWN)) + .id(taskId) + .contextId(contextId == null ? "" : contextId) + .build(); } Task_v0_3.Builder taskBuilder = new Task_v0_3.Builder(task); @@ -73,7 +75,8 @@ Task_v0_3 saveTaskEvent(TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent) throws taskBuilder.history(history); } if (taskStatusUpdateEvent.metadata() != null) { - Map newMetadata = task.metadata() != null ? new HashMap<>(task.metadata()) : new HashMap<>(); + Map newMetadata = + task.metadata() != null ? new HashMap<>(task.metadata()) : new HashMap<>(); newMetadata.putAll(taskStatusUpdateEvent.metadata()); taskBuilder.metadata(newMetadata); } @@ -91,11 +94,12 @@ Task_v0_3 saveTaskEvent(TaskArtifactUpdateEvent_v0_3 taskArtifactUpdateEvent) { } Task_v0_3 task = currentTask; if (task == null) { - task = new Task_v0_3.Builder() - .status(new TaskStatus_v0_3(TaskState_v0_3.UNKNOWN)) - .id(taskId) - .contextId(contextId == null ? "" : contextId) - .build(); + task = + new Task_v0_3.Builder() + .status(new TaskStatus_v0_3(TaskState_v0_3.UNKNOWN)) + .id(taskId) + .contextId(contextId == null ? "" : contextId) + .build(); } currentTask = appendArtifactToTask(task, taskArtifactUpdateEvent, taskId); return currentTask; @@ -114,7 +118,8 @@ Task_v0_3 updateWithMessage(Message_v0_3 message, Task_v0_3 task) { List history = new ArrayList<>(task.history()); if (task.status().message() != null) { history.add(task.status().message()); - taskBuilder.status(new TaskStatus_v0_3(task.status().state(), null, task.status().timestamp())); + taskBuilder.status( + new TaskStatus_v0_3(task.status().state(), null, task.status().timestamp())); } history.add(message); taskBuilder.history(history); @@ -129,4 +134,4 @@ private void saveTask(Task_v0_3 task) { contextId = currentTask.contextId(); } } -} \ No newline at end of file +} diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/Client_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/Client_v0_3.java index d264326d2..671277e5f 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/Client_v0_3.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/Client_v0_3.java @@ -1,13 +1,14 @@ package org.a2aproject.sdk.compat03.client; +import static org.a2aproject.sdk.util.Assert.checkNotNullParam; + import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; - import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3; -import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3; +import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientInvalidStateError_v0_3; @@ -16,20 +17,17 @@ import org.a2aproject.sdk.compat03.spec.EventKind_v0_3; import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3; import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3; -import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; - -import static org.a2aproject.sdk.util.Assert.checkNotNullParam; - +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -39,8 +37,12 @@ public class Client_v0_3 extends AbstractClient_v0_3 { private final ClientTransport_v0_3 clientTransport; private AgentCard_v0_3 agentCard; - Client_v0_3(AgentCard_v0_3 agentCard, ClientConfig_v0_3 clientConfig, ClientTransport_v0_3 clientTransport, - List> consumers, @Nullable Consumer streamingErrorHandler) { + Client_v0_3( + AgentCard_v0_3 agentCard, + ClientConfig_v0_3 clientConfig, + ClientTransport_v0_3 clientTransport, + List> consumers, + @Nullable Consumer streamingErrorHandler) { super(consumers, streamingErrorHandler); checkNotNullParam("agentCard", agentCard); @@ -54,79 +56,105 @@ public static ClientBuilder_v0_3 builder(AgentCard_v0_3 agentCard) { } @Override - public void sendMessage(Message_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void sendMessage(Message_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { MessageSendParams_v0_3 messageSendParams = getMessageSendParams(request, clientConfig); sendMessage(messageSendParams, null, null, context); } @Override - public void sendMessage(Message_v0_3 request, List> consumers, - Consumer streamingErrorHandler, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void sendMessage( + Message_v0_3 request, + List> consumers, + Consumer streamingErrorHandler, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { MessageSendParams_v0_3 messageSendParams = getMessageSendParams(request, clientConfig); sendMessage(messageSendParams, consumers, streamingErrorHandler, context); } @Override - public void sendMessage(Message_v0_3 request, PushNotificationConfig_v0_3 pushNotificationConfiguration, - Map metatadata, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { - MessageSendConfiguration_v0_3 messageSendConfiguration = createMessageSendConfiguration(pushNotificationConfiguration); - - MessageSendParams_v0_3 messageSendParams = new MessageSendParams_v0_3.Builder() - .message(request) - .configuration(messageSendConfiguration) - .metadata(metatadata) - .build(); + public void sendMessage( + Message_v0_3 request, + PushNotificationConfig_v0_3 pushNotificationConfiguration, + Map metatadata, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { + MessageSendConfiguration_v0_3 messageSendConfiguration = + createMessageSendConfiguration(pushNotificationConfiguration); + + MessageSendParams_v0_3 messageSendParams = + new MessageSendParams_v0_3.Builder() + .message(request) + .configuration(messageSendConfiguration) + .metadata(metatadata) + .build(); sendMessage(messageSendParams, null, null, context); } @Override - public Task_v0_3 getTask(TaskQueryParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public Task_v0_3 getTask(TaskQueryParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { return clientTransport.getTask(request, context); } @Override - public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { return clientTransport.cancelTask(request, context); } @Override public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration( - TaskPushNotificationConfig_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + TaskPushNotificationConfig_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { return clientTransport.setTaskPushNotificationConfiguration(request, context); } @Override public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration( - GetTaskPushNotificationConfigParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + GetTaskPushNotificationConfigParams_v0_3 request, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { return clientTransport.getTaskPushNotificationConfiguration(request, context); } @Override public List listTaskPushNotificationConfigurations( - ListTaskPushNotificationConfigParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + ListTaskPushNotificationConfigParams_v0_3 request, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { return clientTransport.listTaskPushNotificationConfigurations(request, context); } @Override public void deleteTaskPushNotificationConfigurations( - DeleteTaskPushNotificationConfigParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + DeleteTaskPushNotificationConfigParams_v0_3 request, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { clientTransport.deleteTaskPushNotificationConfigurations(request, context); } @Override - public void resubscribe(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { - resubscribeToTask(request, null, null, context); + public void resubscribe(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { + resubscribeToTask(request, null, null, context); } @Override - public void resubscribe(TaskIdParams_v0_3 request, @Nullable List> consumers, - @Nullable Consumer streamingErrorHandler, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void resubscribe( + TaskIdParams_v0_3 request, + @Nullable List> consumers, + @Nullable Consumer streamingErrorHandler, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { resubscribeToTask(request, consumers, streamingErrorHandler, context); } @Override - public AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { agentCard = clientTransport.getAgentCard(context); return agentCard; } @@ -136,7 +164,9 @@ public void close() { clientTransport.close(); } - private ClientEvent_v0_3 getClientEvent(StreamingEventKind_v0_3 event, ClientTaskManager_v0_3 taskManager) throws A2AClientError_v0_3 { + private ClientEvent_v0_3 getClientEvent( + StreamingEventKind_v0_3 event, ClientTaskManager_v0_3 taskManager) + throws A2AClientError_v0_3 { if (event instanceof Message_v0_3 message) { return new MessageEvent_v0_3(message); } else if (event instanceof Task_v0_3 task) { @@ -153,7 +183,8 @@ private ClientEvent_v0_3 getClientEvent(StreamingEventKind_v0_3 event, ClientTas } } - private MessageSendConfiguration_v0_3 createMessageSendConfiguration(@Nullable PushNotificationConfig_v0_3 pushNotificationConfig) { + private MessageSendConfiguration_v0_3 createMessageSendConfiguration( + @Nullable PushNotificationConfig_v0_3 pushNotificationConfig) { return new MessageSendConfiguration_v0_3.Builder() .acceptedOutputModes(clientConfig.getAcceptedOutputModes()) .blocking(!clientConfig.isPolling()) @@ -162,9 +193,13 @@ private MessageSendConfiguration_v0_3 createMessageSendConfiguration(@Nullable P .build(); } - private void sendMessage(MessageSendParams_v0_3 messageSendParams, @Nullable List> consumers, - @Nullable Consumer errorHandler, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { - if (! clientConfig.isStreaming() || ! agentCard.capabilities().streaming()) { + private void sendMessage( + MessageSendParams_v0_3 messageSendParams, + @Nullable List> consumers, + @Nullable Consumer errorHandler, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { + if (!clientConfig.isStreaming() || !agentCard.capabilities().streaming()) { EventKind_v0_3 eventKind = clientTransport.sendMessage(messageSendParams, context); ClientEvent_v0_3 clientEvent; if (eventKind instanceof Task_v0_3 task) { @@ -177,37 +212,46 @@ private void sendMessage(MessageSendParams_v0_3 messageSendParams, @Nullable Lis } else { ClientTaskManager_v0_3 tracker = new ClientTaskManager_v0_3(); Consumer overriddenErrorHandler = getOverriddenErrorHandler(errorHandler); - Consumer eventHandler = event -> { - try { - ClientEvent_v0_3 clientEvent = getClientEvent(event, tracker); - consume(clientEvent, agentCard, consumers); - } catch (A2AClientError_v0_3 e) { - overriddenErrorHandler.accept(e); - } - }; - clientTransport.sendMessageStreaming(messageSendParams, eventHandler, overriddenErrorHandler, context); + Consumer eventHandler = + event -> { + try { + ClientEvent_v0_3 clientEvent = getClientEvent(event, tracker); + consume(clientEvent, agentCard, consumers); + } catch (A2AClientError_v0_3 e) { + overriddenErrorHandler.accept(e); + } + }; + clientTransport.sendMessageStreaming( + messageSendParams, eventHandler, overriddenErrorHandler, context); } } - private void resubscribeToTask(TaskIdParams_v0_3 request, @Nullable List> consumers, - @Nullable Consumer errorHandler, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { - if (! clientConfig.isStreaming() || ! agentCard.capabilities().streaming()) { - throw new A2AClientException_v0_3("Client and/or server does not support resubscription"); + private void resubscribeToTask( + TaskIdParams_v0_3 request, + @Nullable List> consumers, + @Nullable Consumer errorHandler, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { + if (!clientConfig.isStreaming() || !agentCard.capabilities().streaming()) { + throw new A2AClientException_v0_3( + "Client and/or server does not support resubscription"); } ClientTaskManager_v0_3 tracker = new ClientTaskManager_v0_3(); Consumer overriddenErrorHandler = getOverriddenErrorHandler(errorHandler); - Consumer eventHandler = event -> { - try { - ClientEvent_v0_3 clientEvent = getClientEvent(event, tracker); - consume(clientEvent, agentCard, consumers); - } catch (A2AClientError_v0_3 e) { - overriddenErrorHandler.accept(e); - } - }; + Consumer eventHandler = + event -> { + try { + ClientEvent_v0_3 clientEvent = getClientEvent(event, tracker); + consume(clientEvent, agentCard, consumers); + } catch (A2AClientError_v0_3 e) { + overriddenErrorHandler.accept(e); + } + }; clientTransport.resubscribe(request, eventHandler, overriddenErrorHandler, context); } - private @NonNull Consumer getOverriddenErrorHandler(@Nullable Consumer errorHandler) { + private @NonNull Consumer getOverriddenErrorHandler( + @Nullable Consumer errorHandler) { return e -> { if (errorHandler != null) { errorHandler.accept(e); @@ -219,7 +263,10 @@ private void resubscribeToTask(TaskIdParams_v0_3 request, @Nullable List> consumers) { + private void consume( + ClientEvent_v0_3 clientEvent, + AgentCard_v0_3 agentCard, + @Nullable List> consumers) { if (consumers != null) { // use specified consumers for (BiConsumer consumer : consumers) { @@ -231,8 +278,10 @@ private void consume(ClientEvent_v0_3 clientEvent, AgentCard_v0_3 agentCard, @Nu } } - private MessageSendParams_v0_3 getMessageSendParams(Message_v0_3 request, ClientConfig_v0_3 clientConfig) { - MessageSendConfiguration_v0_3 messageSendConfiguration = createMessageSendConfiguration(clientConfig.getPushNotificationConfig()); + private MessageSendParams_v0_3 getMessageSendParams( + Message_v0_3 request, ClientConfig_v0_3 clientConfig) { + MessageSendConfiguration_v0_3 messageSendConfiguration = + createMessageSendConfiguration(clientConfig.getPushNotificationConfig()); return new MessageSendParams_v0_3.Builder() .message(request) diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/MessageEvent_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/MessageEvent_v0_3.java index 2c321e0da..161a2a9a5 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/MessageEvent_v0_3.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/MessageEvent_v0_3.java @@ -22,5 +22,3 @@ public Message_v0_3 getMessage() { return message; } } - - diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskUpdateEvent_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskUpdateEvent_v0_3.java index 9459a2b04..0e1c18074 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskUpdateEvent_v0_3.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/TaskUpdateEvent_v0_3.java @@ -33,5 +33,4 @@ public Task_v0_3 getTask() { public UpdateEvent_v0_3 getUpdateEvent() { return updateEvent; } - } diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/ClientConfig_v0_3.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/ClientConfig_v0_3.java index ca143a45e..c8c9474d0 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/ClientConfig_v0_3.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/ClientConfig_v0_3.java @@ -1,11 +1,10 @@ package org.a2aproject.sdk.compat03.client.config; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; -import java.util.ArrayList; -import java.util.HashMap; import org.jspecify.annotations.Nullable; /** @@ -24,7 +23,8 @@ public class ClientConfig_v0_3 { private ClientConfig_v0_3(Builder builder) { this.streaming = builder.streaming == null ? true : builder.streaming; this.polling = builder.polling == null ? false : builder.polling; - this.useClientPreference = builder.useClientPreference == null ? false : builder.useClientPreference; + this.useClientPreference = + builder.useClientPreference == null ? false : builder.useClientPreference; this.acceptedOutputModes = builder.acceptedOutputModes; this.pushNotificationConfig = builder.pushNotificationConfig; this.historyLength = builder.historyLength; @@ -92,7 +92,8 @@ public Builder setAcceptedOutputModes(List acceptedOutputModes) { return this; } - public Builder setPushNotificationConfig(PushNotificationConfig_v0_3 pushNotificationConfig) { + public Builder setPushNotificationConfig( + PushNotificationConfig_v0_3 pushNotificationConfig) { this.pushNotificationConfig = pushNotificationConfig; return this; } @@ -111,4 +112,4 @@ public ClientConfig_v0_3 build() { return new ClientConfig_v0_3(this); } } -} \ No newline at end of file +} diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/package-info.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/package-info.java index bfae93f4b..c900706b8 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/package-info.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/config/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.compat03.client.config; import org.jspecify.annotations.NullMarked; - diff --git a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/package-info.java b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/package-info.java index 9bd22f637..319ce2aeb 100644 --- a/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/package-info.java +++ b/compat-0.3/client/base/src/main/java/org/a2aproject/sdk/compat03/client/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.compat03.client; import org.jspecify.annotations.NullMarked; - diff --git a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/A2A_v0_3_Test.java b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/A2A_v0_3_Test.java index f5a8ad5ef..93cbcfa53 100644 --- a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/A2A_v0_3_Test.java +++ b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/A2A_v0_3_Test.java @@ -1,17 +1,16 @@ package org.a2aproject.sdk.compat03; -import org.a2aproject.sdk.compat03.spec.Message_v0_3; -import org.a2aproject.sdk.compat03.spec.Part_v0_3; -import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import java.util.Arrays; import java.util.Collections; import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; +import org.a2aproject.sdk.compat03.spec.Part_v0_3; +import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; +import org.junit.jupiter.api.Test; public class A2A_v0_3_Test { @@ -19,7 +18,7 @@ public class A2A_v0_3_Test { public void testToUserMessage() { String text = "Hello, world!"; Message_v0_3 message = A2A_v0_3.toUserMessage(text); - + assertEquals(Message_v0_3.Role.USER, message.role()); assertEquals(1, message.parts().size()); assertEquals(text, ((TextPart_v0_3) message.parts().get(0)).text()); @@ -33,7 +32,7 @@ public void testToUserMessageWithId() { String text = "Hello, world!"; String messageId = "test-message-id"; Message_v0_3 message = A2A_v0_3.toUserMessage(text, messageId); - + assertEquals(Message_v0_3.Role.USER, message.role()); assertEquals(messageId, message.messageId()); } @@ -42,7 +41,7 @@ public void testToUserMessageWithId() { public void testToAgentMessage() { String text = "Hello, I'm an agent!"; Message_v0_3 message = A2A_v0_3.toAgentMessage(text); - + assertEquals(Message_v0_3.Role.AGENT, message.role()); assertEquals(1, message.parts().size()); assertEquals(text, ((TextPart_v0_3) message.parts().get(0)).text()); @@ -54,7 +53,7 @@ public void testToAgentMessageWithId() { String text = "Hello, I'm an agent!"; String messageId = "agent-message-id"; Message_v0_3 message = A2A_v0_3.toAgentMessage(text, messageId); - + assertEquals(Message_v0_3.Role.AGENT, message.role()); assertEquals(messageId, message.messageId()); } @@ -64,9 +63,9 @@ public void testCreateUserTextMessage() { String text = "User message with context"; String contextId = "context-123"; String taskId = "task-456"; - + Message_v0_3 message = A2A_v0_3.createUserTextMessage(text, contextId, taskId); - + assertEquals(Message_v0_3.Role.USER, message.role()); assertEquals(contextId, message.contextId()); assertEquals(taskId, message.taskId()); @@ -80,9 +79,9 @@ public void testCreateUserTextMessage() { @Test public void testCreateUserTextMessageWithNullParams() { String text = "Simple user message"; - + Message_v0_3 message = A2A_v0_3.createUserTextMessage(text, null, null); - + assertEquals(Message_v0_3.Role.USER, message.role()); assertNull(message.contextId()); assertNull(message.taskId()); @@ -95,9 +94,9 @@ public void testCreateAgentTextMessage() { String text = "Agent message with context"; String contextId = "context-789"; String taskId = "task-012"; - + Message_v0_3 message = A2A_v0_3.createAgentTextMessage(text, contextId, taskId); - + assertEquals(Message_v0_3.Role.AGENT, message.role()); assertEquals(contextId, message.contextId()); assertEquals(taskId, message.taskId()); @@ -108,15 +107,13 @@ public void testCreateAgentTextMessage() { @Test public void testCreateAgentPartsMessage() { - List> parts = Arrays.asList( - new TextPart_v0_3("Part 1"), - new TextPart_v0_3("Part 2") - ); + List> parts = + Arrays.asList(new TextPart_v0_3("Part 1"), new TextPart_v0_3("Part 2")); String contextId = "context-parts"; String taskId = "task-parts"; - + Message_v0_3 message = A2A_v0_3.createAgentPartsMessage(parts, contextId, taskId); - + assertEquals(Message_v0_3.Role.AGENT, message.role()); assertEquals(contextId, message.contextId()); assertEquals(taskId, message.taskId()); @@ -144,4 +141,4 @@ public void testCreateAgentPartsMessageWithEmptyParts() { assertEquals("Parts cannot be null or empty", e.getMessage()); } } -} \ No newline at end of file +} diff --git a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/AuthenticationAuthorization_v0_3_Test.java b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/AuthenticationAuthorization_v0_3_Test.java index 228a08c09..171d02b32 100644 --- a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/AuthenticationAuthorization_v0_3_Test.java +++ b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/AuthenticationAuthorization_v0_3_Test.java @@ -1,12 +1,29 @@ package org.a2aproject.sdk.compat03.client; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.stub.StreamObserver; +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3; -import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; +import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc; import org.a2aproject.sdk.compat03.grpc.SendMessageRequest; import org.a2aproject.sdk.compat03.grpc.SendMessageResponse; @@ -19,33 +36,15 @@ import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -import io.grpc.ManagedChannel; -import io.grpc.Server; -import io.grpc.Status; -import io.grpc.inprocess.InProcessChannelBuilder; -import io.grpc.inprocess.InProcessServerBuilder; -import io.grpc.stub.StreamObserver; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockserver.integration.ClientAndServer; -import java.io.IOException; -import java.util.Collections; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; - /** * Tests for handling HTTP 401 (Unauthorized) and 403 (Forbidden) responses * when the client sends streaming and non-streaming messages. - * + * * These tests verify that the client properly fails when the server returns * authentication or authorization errors. */ @@ -65,37 +64,49 @@ public class AuthenticationAuthorization_v0_3_Test { @BeforeEach public void setUp() { server = new ClientAndServer(4001); - MESSAGE = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("test message"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - + MESSAGE = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("test message"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + grpcServerName = InProcessServerBuilder.generateName(); - agentCard = new AgentCard_v0_3.Builder() - .name("Test Agent") - .description("Test agent for auth tests") - .url(AGENT_URL) - .version("1.0.0") - .capabilities(new AgentCapabilities_v0_3.Builder() - .streaming(true) // Support streaming for all tests - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(new AgentSkill_v0_3.Builder() - .id("test_skill") - .name("Test skill") - .description("Test skill") - .tags(Collections.singletonList("test")) - .build())) - .protocolVersion("0.3.0") - .additionalInterfaces(java.util.Arrays.asList( - new AgentInterface_v0_3(TransportProtocol_v0_3.JSONRPC.asString(), AGENT_URL), - new AgentInterface_v0_3(TransportProtocol_v0_3.HTTP_JSON.asString(), AGENT_URL), - new AgentInterface_v0_3(TransportProtocol_v0_3.GRPC.asString(), grpcServerName))) - .build(); + agentCard = + new AgentCard_v0_3.Builder() + .name("Test Agent") + .description("Test agent for auth tests") + .url(AGENT_URL) + .version("1.0.0") + .capabilities( + new AgentCapabilities_v0_3.Builder() + .streaming(true) // Support streaming for all tests + .build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills( + Collections.singletonList( + new AgentSkill_v0_3.Builder() + .id("test_skill") + .name("Test skill") + .description("Test skill") + .tags(Collections.singletonList("test")) + .build())) + .protocolVersion("0.3.0") + .additionalInterfaces( + java.util.Arrays.asList( + new AgentInterface_v0_3( + TransportProtocol_v0_3.JSONRPC.asString(), + AGENT_URL), + new AgentInterface_v0_3( + TransportProtocol_v0_3.HTTP_JSON.asString(), + AGENT_URL), + new AgentInterface_v0_3( + TransportProtocol_v0_3.GRPC.asString(), + grpcServerName))) + .build(); } @AfterEach @@ -114,20 +125,17 @@ public void tearDown() { @Test public void testJsonRpcNonStreamingUnauthenticated() throws A2AClientException_v0_3 { // Mock server to return 401 for non-streaming message - server.when( - request() - .withMethod("POST") - .withPath("/") - ).respond( - response() - .withStatusCode(401) - ); + server.when(request().withMethod("POST").withPath("/")) + .respond(response().withStatusCode(401)); Client_v0_3 client = getJSONRPCClientBuilder(false).build(); - A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException_v0_3 exception = + assertThrows( + A2AClientException_v0_3.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE)); } @@ -135,20 +143,17 @@ public void testJsonRpcNonStreamingUnauthenticated() throws A2AClientException_v @Test public void testJsonRpcNonStreamingUnauthorized() throws A2AClientException_v0_3 { // Mock server to return 403 for non-streaming message - server.when( - request() - .withMethod("POST") - .withPath("/") - ).respond( - response() - .withStatusCode(403) - ); + server.when(request().withMethod("POST").withPath("/")) + .respond(response().withStatusCode(403)); Client_v0_3 client = getJSONRPCClientBuilder(false).build(); - A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException_v0_3 exception = + assertThrows( + A2AClientException_v0_3.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE)); } @@ -156,35 +161,19 @@ public void testJsonRpcNonStreamingUnauthorized() throws A2AClientException_v0_3 @Test public void testJsonRpcStreamingUnauthenticated() throws Exception { // Mock server to return 401 for streaming message - server.when( - request() - .withMethod("POST") - .withPath("/") - ).respond( - response() - .withStatusCode(401) - ); - - assertStreamingError( - getJSONRPCClientBuilder(true), - AUTHENTICATION_FAILED_MESSAGE); + server.when(request().withMethod("POST").withPath("/")) + .respond(response().withStatusCode(401)); + + assertStreamingError(getJSONRPCClientBuilder(true), AUTHENTICATION_FAILED_MESSAGE); } @Test public void testJsonRpcStreamingUnauthorized() throws Exception { // Mock server to return 403 for streaming message - server.when( - request() - .withMethod("POST") - .withPath("/") - ).respond( - response() - .withStatusCode(403) - ); - - assertStreamingError( - getJSONRPCClientBuilder(true), - AUTHORIZATION_FAILED_MESSAGE); + server.when(request().withMethod("POST").withPath("/")) + .respond(response().withStatusCode(403)); + + assertStreamingError(getJSONRPCClientBuilder(true), AUTHORIZATION_FAILED_MESSAGE); } // ========== REST Transport Tests ========== @@ -192,20 +181,17 @@ public void testJsonRpcStreamingUnauthorized() throws Exception { @Test public void testRestNonStreamingUnauthenticated() throws A2AClientException_v0_3 { // Mock server to return 401 for non-streaming message - server.when( - request() - .withMethod("POST") - .withPath("/v1/message:send") - ).respond( - response() - .withStatusCode(401) - ); + server.when(request().withMethod("POST").withPath("/v1/message:send")) + .respond(response().withStatusCode(401)); Client_v0_3 client = getRestClientBuilder(false).build(); - A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException_v0_3 exception = + assertThrows( + A2AClientException_v0_3.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE)); } @@ -213,20 +199,17 @@ public void testRestNonStreamingUnauthenticated() throws A2AClientException_v0_3 @Test public void testRestNonStreamingUnauthorized() throws A2AClientException_v0_3 { // Mock server to return 403 for non-streaming message - server.when( - request() - .withMethod("POST") - .withPath("/v1/message:send") - ).respond( - response() - .withStatusCode(403) - ); + server.when(request().withMethod("POST").withPath("/v1/message:send")) + .respond(response().withStatusCode(403)); Client_v0_3 client = getRestClientBuilder(false).build(); - A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException_v0_3 exception = + assertThrows( + A2AClientException_v0_3.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE)); } @@ -234,35 +217,19 @@ public void testRestNonStreamingUnauthorized() throws A2AClientException_v0_3 { @Test public void testRestStreamingUnauthenticated() throws Exception { // Mock server to return 401 for streaming message - server.when( - request() - .withMethod("POST") - .withPath("/v1/message:stream") - ).respond( - response() - .withStatusCode(401) - ); - - assertStreamingError( - getRestClientBuilder(true), - AUTHENTICATION_FAILED_MESSAGE); + server.when(request().withMethod("POST").withPath("/v1/message:stream")) + .respond(response().withStatusCode(401)); + + assertStreamingError(getRestClientBuilder(true), AUTHENTICATION_FAILED_MESSAGE); } @Test public void testRestStreamingUnauthorized() throws Exception { // Mock server to return 403 for streaming message - server.when( - request() - .withMethod("POST") - .withPath("/v1/message:stream") - ).respond( - response() - .withStatusCode(403) - ); - - assertStreamingError( - getRestClientBuilder(true), - AUTHORIZATION_FAILED_MESSAGE); + server.when(request().withMethod("POST").withPath("/v1/message:stream")) + .respond(response().withStatusCode(403)); + + assertStreamingError(getRestClientBuilder(true), AUTHORIZATION_FAILED_MESSAGE); } // ========== gRPC Transport Tests ========== @@ -273,9 +240,12 @@ public void testGrpcNonStreamingUnauthenticated() throws Exception { Client_v0_3 client = getGrpcClientBuilder(false).build(); - A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException_v0_3 exception = + assertThrows( + A2AClientException_v0_3.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHENTICATION_FAILED_MESSAGE)); } @@ -286,9 +256,12 @@ public void testGrpcNonStreamingUnauthorized() throws Exception { Client_v0_3 client = getGrpcClientBuilder(false).build(); - A2AClientException_v0_3 exception = assertThrows(A2AClientException_v0_3.class, () -> { - client.sendMessage(MESSAGE); - }); + A2AClientException_v0_3 exception = + assertThrows( + A2AClientException_v0_3.class, + () -> { + client.sendMessage(MESSAGE); + }); assertTrue(exception.getMessage().contains(AUTHORIZATION_FAILED_MESSAGE)); } @@ -297,24 +270,21 @@ public void testGrpcNonStreamingUnauthorized() throws Exception { public void testGrpcStreamingUnauthenticated() throws Exception { setupGrpcServer(Status.UNAUTHENTICATED); - assertStreamingError( - getGrpcClientBuilder(true), - AUTHENTICATION_FAILED_MESSAGE); + assertStreamingError(getGrpcClientBuilder(true), AUTHENTICATION_FAILED_MESSAGE); } @Test public void testGrpcStreamingUnauthorized() throws Exception { setupGrpcServer(Status.PERMISSION_DENIED); - assertStreamingError( - getGrpcClientBuilder(true), - AUTHORIZATION_FAILED_MESSAGE); + assertStreamingError(getGrpcClientBuilder(true), AUTHORIZATION_FAILED_MESSAGE); } private ClientBuilder_v0_3 getJSONRPCClientBuilder(boolean streaming) { return Client_v0_3.builder(agentCard) .clientConfig(new ClientConfig_v0_3.Builder().setStreaming(streaming).build()) - .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()); + .withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()); } private ClientBuilder_v0_3 getRestClientBuilder(boolean streaming) { @@ -326,55 +296,72 @@ private ClientBuilder_v0_3 getRestClientBuilder(boolean streaming) { private ClientBuilder_v0_3 getGrpcClientBuilder(boolean streaming) { return Client_v0_3.builder(agentCard) .clientConfig(new ClientConfig_v0_3.Builder().setStreaming(streaming).build()) - .withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3() - .channelFactory(target -> grpcChannel)); + .withTransport( + GrpcTransport_v0_3.class, + new GrpcTransportConfigBuilder_v0_3() + .channelFactory(target -> grpcChannel)); } - private void assertStreamingError(ClientBuilder_v0_3 clientBuilder, String expectedErrorMessage) throws Exception { + private void assertStreamingError(ClientBuilder_v0_3 clientBuilder, String expectedErrorMessage) + throws Exception { AtomicReference errorRef = new AtomicReference<>(); CountDownLatch errorLatch = new CountDownLatch(1); - Consumer errorHandler = error -> { - errorRef.set(error); - errorLatch.countDown(); - }; + Consumer errorHandler = + error -> { + errorRef.set(error); + errorLatch.countDown(); + }; Client_v0_3 client = clientBuilder.streamingErrorHandler(errorHandler).build(); try { client.sendMessage(MESSAGE); // If no immediate exception, wait for async error - assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called"); + assertTrue( + errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called"); Throwable error = errorRef.get(); - assertTrue(error.getMessage().contains(expectedErrorMessage), - "Expected error message to contain '" + expectedErrorMessage + "' but got: " + error.getMessage()); + assertTrue( + error.getMessage().contains(expectedErrorMessage), + "Expected error message to contain '" + + expectedErrorMessage + + "' but got: " + + error.getMessage()); } catch (Exception e) { // Immediate exception is also acceptable - assertTrue(e.getMessage().contains(expectedErrorMessage), - "Expected error message to contain '" + expectedErrorMessage + "' but got: " + e.getMessage()); + assertTrue( + e.getMessage().contains(expectedErrorMessage), + "Expected error message to contain '" + + expectedErrorMessage + + "' but got: " + + e.getMessage()); } } private void setupGrpcServer(Status status) throws IOException { grpcServerName = InProcessServerBuilder.generateName(); - grpcServer = InProcessServerBuilder.forName(grpcServerName) - .directExecutor() - .addService(new A2AServiceGrpc.A2AServiceImplBase() { - @Override - public void sendMessage(SendMessageRequest request, StreamObserver responseObserver) { - responseObserver.onError(status.asRuntimeException()); - } - - @Override - public void sendStreamingMessage(SendMessageRequest request, StreamObserver responseObserver) { - responseObserver.onError(status.asRuntimeException()); - } - }) - .build() - .start(); - - grpcChannel = InProcessChannelBuilder.forName(grpcServerName) - .directExecutor() - .build(); + grpcServer = + InProcessServerBuilder.forName(grpcServerName) + .directExecutor() + .addService( + new A2AServiceGrpc.A2AServiceImplBase() { + @Override + public void sendMessage( + SendMessageRequest request, + StreamObserver responseObserver) { + responseObserver.onError(status.asRuntimeException()); + } + + @Override + public void sendStreamingMessage( + SendMessageRequest request, + StreamObserver responseObserver) { + responseObserver.onError(status.asRuntimeException()); + } + }) + .build() + .start(); + + grpcChannel = InProcessChannelBuilder.forName(grpcServerName).directExecutor().build(); } -} \ No newline at end of file +} diff --git a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3_Test.java b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3_Test.java index fa910f041..15552b208 100644 --- a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3_Test.java +++ b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientBuilder_v0_3_Test.java @@ -1,12 +1,14 @@ package org.a2aproject.sdk.compat03.client; -import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3; +import java.util.Collections; +import java.util.List; import org.a2aproject.sdk.client.http.JdkA2AHttpClient; -import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; +import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3; import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfig_v0_3; +import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfig_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; @@ -16,80 +18,111 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import java.util.Collections; -import java.util.List; - public class ClientBuilder_v0_3_Test { - private AgentCard_v0_3 card = new AgentCard_v0_3.Builder() - .name("Hello World Agent") - .description("Just a hello world agent") - .url("http://localhost:9999") - .version("1.0.0") - .documentationUrl("http://example.com/docs") - .capabilities(new AgentCapabilities_v0_3.Builder() - .streaming(true) - .pushNotifications(true) - .stateTransitionHistory(true) - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(new AgentSkill_v0_3.Builder() - .id("hello_world") - .name("Returns hello world") - .description("just returns hello world") - .tags(Collections.singletonList("hello world")) - .examples(List.of("hi", "hello world")) - .build())) - .protocolVersion("0.3.0") - .additionalInterfaces(List.of( - new AgentInterface_v0_3(TransportProtocol_v0_3.JSONRPC.asString(), "http://localhost:9999"))) - .build(); + private AgentCard_v0_3 card = + new AgentCard_v0_3.Builder() + .name("Hello World Agent") + .description("Just a hello world agent") + .url("http://localhost:9999") + .version("1.0.0") + .documentationUrl("http://example.com/docs") + .capabilities( + new AgentCapabilities_v0_3.Builder() + .streaming(true) + .pushNotifications(true) + .stateTransitionHistory(true) + .build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills( + Collections.singletonList( + new AgentSkill_v0_3.Builder() + .id("hello_world") + .name("Returns hello world") + .description("just returns hello world") + .tags(Collections.singletonList("hello world")) + .examples(List.of("hi", "hello world")) + .build())) + .protocolVersion("0.3.0") + .additionalInterfaces( + List.of( + new AgentInterface_v0_3( + TransportProtocol_v0_3.JSONRPC.asString(), + "http://localhost:9999"))) + .build(); @Test public void shouldNotFindCompatibleTransport() throws A2AClientException_v0_3 { - A2AClientException_v0_3 exception = Assertions.assertThrows(A2AClientException_v0_3.class, - () -> Client_v0_3 - .builder(card) - .clientConfig(new ClientConfig_v0_3.Builder().setUseClientPreference(true).build()) - .withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3() - .channelFactory(s -> null)) - .build()); + A2AClientException_v0_3 exception = + Assertions.assertThrows( + A2AClientException_v0_3.class, + () -> + Client_v0_3.builder(card) + .clientConfig( + new ClientConfig_v0_3.Builder() + .setUseClientPreference(true) + .build()) + .withTransport( + GrpcTransport_v0_3.class, + new GrpcTransportConfigBuilder_v0_3() + .channelFactory(s -> null)) + .build()); - Assertions.assertTrue(exception.getMessage() != null && exception.getMessage().contains("No compatible transport found")); + Assertions.assertTrue( + exception.getMessage() != null + && exception.getMessage().contains("No compatible transport found")); } @Test public void shouldNotFindConfigurationTransport() throws A2AClientException_v0_3 { - A2AClientException_v0_3 exception = Assertions.assertThrows(A2AClientException_v0_3.class, - () -> Client_v0_3 - .builder(card) - .clientConfig(new ClientConfig_v0_3.Builder().setUseClientPreference(true).build()) - .build()); + A2AClientException_v0_3 exception = + Assertions.assertThrows( + A2AClientException_v0_3.class, + () -> + Client_v0_3.builder(card) + .clientConfig( + new ClientConfig_v0_3.Builder() + .setUseClientPreference(true) + .build()) + .build()); - Assertions.assertTrue(exception.getMessage() != null && exception.getMessage().startsWith("Missing required TransportConfig for")); + Assertions.assertTrue( + exception.getMessage() != null + && exception + .getMessage() + .startsWith("Missing required TransportConfig for")); } @Test public void shouldCreateJSONRPCClient() throws A2AClientException_v0_3 { - Client_v0_3 client = Client_v0_3 - .builder(card) - .clientConfig(new ClientConfig_v0_3.Builder().setUseClientPreference(true).build()) - .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3() - .addInterceptor(null) - .httpClient(null)) - .build(); + Client_v0_3 client = + Client_v0_3.builder(card) + .clientConfig( + new ClientConfig_v0_3.Builder() + .setUseClientPreference(true) + .build()) + .withTransport( + JSONRPCTransport_v0_3.class, + new JSONRPCTransportConfigBuilder_v0_3() + .addInterceptor(null) + .httpClient(null)) + .build(); Assertions.assertNotNull(client); } @Test public void shouldCreateClient_differentConfigurations() throws A2AClientException_v0_3 { - Client_v0_3 client = Client_v0_3 - .builder(card) - .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()) - .withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfig_v0_3(new JdkA2AHttpClient())) - .build(); + Client_v0_3 client = + Client_v0_3.builder(card) + .withTransport( + JSONRPCTransport_v0_3.class, + new JSONRPCTransportConfigBuilder_v0_3()) + .withTransport( + JSONRPCTransport_v0_3.class, + new JSONRPCTransportConfig_v0_3(new JdkA2AHttpClient())) + .build(); Assertions.assertNotNull(client); } diff --git a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3_Test.java b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3_Test.java index ec6f6eeaf..a67c90a72 100644 --- a/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3_Test.java +++ b/compat-0.3/client/base/src/test/java/org/a2aproject/sdk/compat03/client/ClientTaskManager_v0_3_Test.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; - import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; @@ -18,32 +17,42 @@ public class ClientTaskManager_v0_3_Test { @Test public void testStatusMessagesMoveToHistoryWhenSuperseded() throws Exception { ClientTaskManager_v0_3 taskManager = new ClientTaskManager_v0_3(); - Message_v0_3 workingMessage = new Message_v0_3.Builder() - .messageId("working-message") - .role(Message_v0_3.Role.AGENT) - .parts(new TextPart_v0_3("working")) - .build(); - Message_v0_3 completedMessage = new Message_v0_3.Builder() - .messageId("completed-message") - .role(Message_v0_3.Role.AGENT) - .parts(new TextPart_v0_3("completed")) - .build(); - - Task_v0_3 workingTask = taskManager.saveTaskEvent(new TaskStatusUpdateEvent_v0_3.Builder() - .taskId("task-123") - .contextId("context-123") - .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING, workingMessage, null)) - .build()); + Message_v0_3 workingMessage = + new Message_v0_3.Builder() + .messageId("working-message") + .role(Message_v0_3.Role.AGENT) + .parts(new TextPart_v0_3("working")) + .build(); + Message_v0_3 completedMessage = + new Message_v0_3.Builder() + .messageId("completed-message") + .role(Message_v0_3.Role.AGENT) + .parts(new TextPart_v0_3("completed")) + .build(); + + Task_v0_3 workingTask = + taskManager.saveTaskEvent( + new TaskStatusUpdateEvent_v0_3.Builder() + .taskId("task-123") + .contextId("context-123") + .status( + new TaskStatus_v0_3( + TaskState_v0_3.WORKING, workingMessage, null)) + .build()); assertTrue(workingTask.history().isEmpty()); assertEquals(workingMessage, workingTask.status().message()); - Task_v0_3 completedTask = taskManager.saveTaskEvent(new TaskStatusUpdateEvent_v0_3.Builder() - .taskId("task-123") - .contextId("context-123") - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED, completedMessage, null)) - .isFinal(true) - .build()); + Task_v0_3 completedTask = + taskManager.saveTaskEvent( + new TaskStatusUpdateEvent_v0_3.Builder() + .taskId("task-123") + .contextId("context-123") + .status( + new TaskStatus_v0_3( + TaskState_v0_3.COMPLETED, completedMessage, null)) + .isFinal(true) + .build()); assertEquals(List.of(workingMessage), completedTask.history()); assertEquals(completedMessage, completedTask.status().message()); diff --git a/compat-0.3/client/transport/grpc/pom.xml b/compat-0.3/client/transport/grpc/pom.xml index af1fe85f0..5f90a7530 100644 --- a/compat-0.3/client/transport/grpc/pom.xml +++ b/compat-0.3/client/transport/grpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -54,4 +52,4 @@ - \ No newline at end of file + diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3.java index fc7e8a93c..97c4d30f6 100644 --- a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3.java +++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3.java @@ -1,15 +1,13 @@ package org.a2aproject.sdk.compat03.client.transport.grpc; +import static org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3.FromProto; -import org.a2aproject.sdk.compat03.grpc.StreamResponse; -import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; import io.grpc.stub.StreamObserver; - import java.util.function.Consumer; import java.util.logging.Logger; - +import org.a2aproject.sdk.compat03.grpc.StreamResponse; +import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; import org.jspecify.annotations.Nullable; -import static org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3.FromProto; public class EventStreamObserver_v0_3 implements StreamObserver { @@ -17,7 +15,9 @@ public class EventStreamObserver_v0_3 implements StreamObserver private final Consumer eventHandler; @Nullable private final Consumer errorHandler; - public EventStreamObserver_v0_3(Consumer eventHandler, @Nullable Consumer errorHandler) { + public EventStreamObserver_v0_3( + Consumer eventHandler, + @Nullable Consumer errorHandler) { this.eventHandler = eventHandler; this.errorHandler = errorHandler; } @@ -41,7 +41,10 @@ public void onNext(StreamResponse response) { default: LOGGER.warning("Invalid stream response " + response.getPayloadCase()); if (errorHandler != null) { - errorHandler.accept(new IllegalStateException("Invalid stream response from server: " + response.getPayloadCase())); + errorHandler.accept( + new IllegalStateException( + "Invalid stream response from server: " + + response.getPayloadCase())); } return; } @@ -53,7 +56,8 @@ public void onError(Throwable t) { if (errorHandler != null) { // Map gRPC errors to proper A2A exceptions if (t instanceof io.grpc.StatusRuntimeException) { - errorHandler.accept(GrpcErrorMapper_v0_3.mapGrpcError((io.grpc.StatusRuntimeException) t)); + errorHandler.accept( + GrpcErrorMapper_v0_3.mapGrpcError((io.grpc.StatusRuntimeException) t)); } else { errorHandler.accept(t); } diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3.java index a66bbf610..4ce20b4f4 100644 --- a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3.java +++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3.java @@ -1,5 +1,8 @@ package org.a2aproject.sdk.compat03.client.transport.grpc; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; import org.a2aproject.sdk.common.A2AErrorMessages; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3; @@ -13,9 +16,6 @@ import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3; -import io.grpc.Status; -import io.grpc.StatusException; -import io.grpc.StatusRuntimeException; /** * Utility class to map gRPC StatusRuntimeException to appropriate A2A error types @@ -27,8 +27,10 @@ public static A2AClientException_v0_3 mapGrpcError(StatusRuntimeException e) { return mapGrpcError(e, "gRPC error: "); } - public static A2AClientException_v0_3 mapGrpcError(StatusRuntimeException e, String errorPrefix) { - return mapGrpcErrorInternal(e.getStatus().getCode(), e.getStatus().getDescription(), e, errorPrefix); + public static A2AClientException_v0_3 mapGrpcError( + StatusRuntimeException e, String errorPrefix) { + return mapGrpcErrorInternal( + e.getStatus().getCode(), e.getStatus().getDescription(), e, errorPrefix); } // Overload for StatusException (gRPC 1.77+ compatibility) @@ -37,7 +39,8 @@ public static A2AClientException_v0_3 mapGrpcError(StatusException e) { } public static A2AClientException_v0_3 mapGrpcError(StatusException e, String errorPrefix) { - return mapGrpcErrorInternal(e.getStatus().getCode(), e.getStatus().getDescription(), e, errorPrefix); + return mapGrpcErrorInternal( + e.getStatus().getCode(), e.getStatus().getDescription(), e, errorPrefix); } // Dispatcher for multi-catch (StatusRuntimeException | StatusException) @@ -51,50 +54,75 @@ public static A2AClientException_v0_3 mapGrpcError(Exception e, String errorPref } } - private static A2AClientException_v0_3 mapGrpcErrorInternal(Status.Code code, @org.jspecify.annotations.Nullable String description, @org.jspecify.annotations.Nullable Throwable cause, String errorPrefix) { - + private static A2AClientException_v0_3 mapGrpcErrorInternal( + Status.Code code, + @org.jspecify.annotations.Nullable String description, + @org.jspecify.annotations.Nullable Throwable cause, + String errorPrefix) { + // Extract the actual error type from the description if possible // (using description because the same code can map to multiple errors - // see GrpcHandler#handleError) if (description != null) { if (description.contains("TaskNotFoundError")) { - return new A2AClientException_v0_3(errorPrefix + description, new TaskNotFoundError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + description, new TaskNotFoundError_v0_3()); } else if (description.contains("UnsupportedOperationError")) { - return new A2AClientException_v0_3(errorPrefix + description, new UnsupportedOperationError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + description, new UnsupportedOperationError_v0_3()); } else if (description.contains("InvalidParamsError")) { - return new A2AClientException_v0_3(errorPrefix + description, new InvalidParamsError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + description, new InvalidParamsError_v0_3()); } else if (description.contains("InvalidRequestError")) { - return new A2AClientException_v0_3(errorPrefix + description, new InvalidRequestError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + description, new InvalidRequestError_v0_3()); } else if (description.contains("MethodNotFoundError")) { - return new A2AClientException_v0_3(errorPrefix + description, new MethodNotFoundError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + description, new MethodNotFoundError_v0_3()); } else if (description.contains("TaskNotCancelableError")) { - return new A2AClientException_v0_3(errorPrefix + description, new TaskNotCancelableError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + description, new TaskNotCancelableError_v0_3()); } else if (description.contains("PushNotificationNotSupportedError")) { - return new A2AClientException_v0_3(errorPrefix + description, new PushNotificationNotSupportedError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + description, new PushNotificationNotSupportedError_v0_3()); } else if (description.contains("JSONParseError")) { - return new A2AClientException_v0_3(errorPrefix + description, new JSONParseError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + description, new JSONParseError_v0_3()); } else if (description.contains("ContentTypeNotSupportedError")) { - return new A2AClientException_v0_3(errorPrefix + description, new ContentTypeNotSupportedError_v0_3(null, description, null)); + return new A2AClientException_v0_3( + errorPrefix + description, + new ContentTypeNotSupportedError_v0_3(null, description, null)); } else if (description.contains("InvalidAgentResponseError")) { - return new A2AClientException_v0_3(errorPrefix + description, new InvalidAgentResponseError_v0_3(null, description, null)); + return new A2AClientException_v0_3( + errorPrefix + description, + new InvalidAgentResponseError_v0_3(null, description, null)); } } - + // Fall back to mapping based on status code - String message = description != null ? description : (cause != null ? cause.getMessage() : "Unknown error"); + String message = + description != null + ? description + : (cause != null ? cause.getMessage() : "Unknown error"); switch (code) { case NOT_FOUND: - return new A2AClientException_v0_3(errorPrefix + message, new TaskNotFoundError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + message, new TaskNotFoundError_v0_3()); case UNIMPLEMENTED: - return new A2AClientException_v0_3(errorPrefix + message, new UnsupportedOperationError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + message, new UnsupportedOperationError_v0_3()); case INVALID_ARGUMENT: - return new A2AClientException_v0_3(errorPrefix + message, new InvalidParamsError_v0_3()); + return new A2AClientException_v0_3( + errorPrefix + message, new InvalidParamsError_v0_3()); case INTERNAL: - return new A2AClientException_v0_3(errorPrefix + message, new InternalError_v0_3(null, message, null)); + return new A2AClientException_v0_3( + errorPrefix + message, new InternalError_v0_3(null, message, null)); case UNAUTHENTICATED: - return new A2AClientException_v0_3(errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED); + return new A2AClientException_v0_3( + errorPrefix + A2AErrorMessages.AUTHENTICATION_FAILED); case PERMISSION_DENIED: - return new A2AClientException_v0_3(errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED); + return new A2AClientException_v0_3( + errorPrefix + A2AErrorMessages.AUTHORIZATION_FAILED); default: return new A2AClientException_v0_3(errorPrefix + message, cause); } diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfigBuilder_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfigBuilder_v0_3.java index cfc3b97c6..06824f22c 100644 --- a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfigBuilder_v0_3.java +++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfigBuilder_v0_3.java @@ -1,18 +1,19 @@ package org.a2aproject.sdk.compat03.client.transport.grpc; -import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder_v0_3; -import org.a2aproject.sdk.util.Assert; import io.grpc.Channel; - import java.util.function.Function; - +import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; -public class GrpcTransportConfigBuilder_v0_3 extends ClientTransportConfigBuilder_v0_3 { +public class GrpcTransportConfigBuilder_v0_3 + extends ClientTransportConfigBuilder_v0_3< + GrpcTransportConfig_v0_3, GrpcTransportConfigBuilder_v0_3> { private @Nullable Function channelFactory; - public GrpcTransportConfigBuilder_v0_3 channelFactory(Function channelFactory) { + public GrpcTransportConfigBuilder_v0_3 channelFactory( + Function channelFactory) { Assert.checkNotNullParam("channelFactory", channelFactory); this.channelFactory = channelFactory; @@ -29,4 +30,4 @@ public GrpcTransportConfig_v0_3 build() { config.setInterceptors(interceptors); return config; } -} \ No newline at end of file +} diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfig_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfig_v0_3.java index 34122cc53..71e3c9fef 100644 --- a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfig_v0_3.java +++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportConfig_v0_3.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.compat03.client.transport.grpc; -import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfig_v0_3; -import org.a2aproject.sdk.util.Assert; import io.grpc.Channel; - import java.util.function.Function; +import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfig_v0_3; +import org.a2aproject.sdk.util.Assert; public class GrpcTransportConfig_v0_3 extends ClientTransportConfig_v0_3 { @@ -18,4 +17,4 @@ public GrpcTransportConfig_v0_3(Function channelFactory) { public Function getChannelFactory() { return this.channelFactory; } -} \ No newline at end of file +} diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportProvider_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportProvider_v0_3.java index 1faac044a..f55580fea 100644 --- a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportProvider_v0_3.java +++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransportProvider_v0_3.java @@ -1,23 +1,27 @@ package org.a2aproject.sdk.compat03.client.transport.grpc; +import io.grpc.Channel; import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportProvider_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -import io.grpc.Channel; /** * Provider for gRPC transport implementation. */ -public class GrpcTransportProvider_v0_3 implements ClientTransportProvider_v0_3 { +public class GrpcTransportProvider_v0_3 + implements ClientTransportProvider_v0_3 { @Override - public GrpcTransport_v0_3 create(GrpcTransportConfig_v0_3 grpcTransportConfig, AgentCard_v0_3 agentCard, String agentUrl) throws A2AClientException_v0_3 { + public GrpcTransport_v0_3 create( + GrpcTransportConfig_v0_3 grpcTransportConfig, AgentCard_v0_3 agentCard, String agentUrl) + throws A2AClientException_v0_3 { // not making use of the interceptors for gRPC for now Channel channel = grpcTransportConfig.getChannelFactory().apply(agentUrl); if (channel != null) { - return new GrpcTransport_v0_3(channel, agentCard, grpcTransportConfig.getInterceptors()); + return new GrpcTransport_v0_3( + channel, agentCard, grpcTransportConfig.getInterceptors()); } throw new A2AClientException_v0_3("Missing required GrpcTransportConfig"); diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java index 50f73e641..83ed65863 100644 --- a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java +++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcTransport_v0_3.java @@ -2,13 +2,18 @@ import static org.a2aproject.sdk.util.Assert.checkNotNullParam; +import io.grpc.Channel; +import io.grpc.Metadata; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.stream.Collectors; - -import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3; +import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.PayloadAndHeaders_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; @@ -27,7 +32,6 @@ import org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequest; import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3.FromProto; import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3.ToProto; -import io.grpc.StatusException; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3; @@ -44,26 +48,19 @@ import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest_v0_3; import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3; -import io.grpc.Channel; -import io.grpc.Metadata; -import io.grpc.StatusRuntimeException; -import io.grpc.stub.MetadataUtils; -import io.grpc.stub.StreamObserver; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.jspecify.annotations.Nullable; public class GrpcTransport_v0_3 implements ClientTransport_v0_3 { - private static final Metadata.Key AUTHORIZATION_METADATA_KEY = Metadata.Key.of( - AuthInterceptor_v0_3.AUTHORIZATION, - Metadata.ASCII_STRING_MARSHALLER); - private static final Metadata.Key EXTENSIONS_KEY = Metadata.Key.of( - "X-A2A-Extensions", - Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key AUTHORIZATION_METADATA_KEY = + Metadata.Key.of(AuthInterceptor_v0_3.AUTHORIZATION, Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key EXTENSIONS_KEY = + Metadata.Key.of("X-A2A-Extensions", Metadata.ASCII_STRING_MARSHALLER); private final A2AServiceBlockingV2Stub blockingStub; private final A2AServiceStub asyncStub; private final @Nullable List interceptors; @@ -73,7 +70,10 @@ public GrpcTransport_v0_3(Channel channel, AgentCard_v0_3 agentCard) { this(channel, agentCard, null); } - public GrpcTransport_v0_3(Channel channel, AgentCard_v0_3 agentCard, @Nullable List interceptors) { + public GrpcTransport_v0_3( + Channel channel, + AgentCard_v0_3 agentCard, + @Nullable List interceptors) { checkNotNullParam("channel", channel); checkNotNullParam("agentCard", agentCard); this.asyncStub = A2AServiceGrpc.newStub(channel); @@ -83,22 +83,27 @@ public GrpcTransport_v0_3(Channel channel, AgentCard_v0_3 agentCard, @Nullable L } @Override - public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public EventKind_v0_3 sendMessage( + MessageSendParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); SendMessageRequest sendMessageRequest = createGrpcSendMessageRequest(request, context); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SendMessageRequest_v0_3.METHOD, sendMessageRequest, - agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + SendMessageRequest_v0_3.METHOD, sendMessageRequest, agentCard, context); try { - A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders); + A2AServiceBlockingV2Stub stubWithMetadata = + createBlockingStubWithMetadata(context, payloadAndHeaders); SendMessageResponse response = stubWithMetadata.sendMessage(sendMessageRequest); if (response.hasMsg()) { return FromProto.message(response.getMsg()); } else if (response.hasTask()) { return FromProto.task(response.getTask()); } else { - throw new A2AClientException_v0_3("Server response did not contain a message or task"); + throw new A2AClientException_v0_3( + "Server response did not contain a message or task"); } } catch (StatusRuntimeException | StatusException e) { throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to send message: "); @@ -106,36 +111,46 @@ public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, @Nullable Clie } @Override - public void sendMessageStreaming(MessageSendParams_v0_3 request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void sendMessageStreaming( + MessageSendParams_v0_3 request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); checkNotNullParam("eventConsumer", eventConsumer); SendMessageRequest grpcRequest = createGrpcSendMessageRequest(request, context); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SendStreamingMessageRequest_v0_3.METHOD, - grpcRequest, agentCard, context); - StreamObserver streamObserver = new EventStreamObserver_v0_3(eventConsumer, errorConsumer); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + SendStreamingMessageRequest_v0_3.METHOD, grpcRequest, agentCard, context); + StreamObserver streamObserver = + new EventStreamObserver_v0_3(eventConsumer, errorConsumer); try { - A2AServiceStub stubWithMetadata = createAsyncStubWithMetadata(context, payloadAndHeaders); + A2AServiceStub stubWithMetadata = + createAsyncStubWithMetadata(context, payloadAndHeaders); stubWithMetadata.sendStreamingMessage(grpcRequest, streamObserver); } catch (StatusRuntimeException e) { - throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to send streaming message request: "); + throw GrpcErrorMapper_v0_3.mapGrpcError( + e, "Failed to send streaming message request: "); } } @Override - public Task_v0_3 getTask(TaskQueryParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public Task_v0_3 getTask(TaskQueryParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); GetTaskRequest.Builder requestBuilder = GetTaskRequest.newBuilder(); requestBuilder.setName("tasks/" + request.id()); requestBuilder.setHistoryLength(request.historyLength()); GetTaskRequest getTaskRequest = requestBuilder.build(); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetTaskRequest_v0_3.METHOD, getTaskRequest, - agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors(GetTaskRequest_v0_3.METHOD, getTaskRequest, agentCard, context); try { - A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders); + A2AServiceBlockingV2Stub stubWithMetadata = + createBlockingStubWithMetadata(context, payloadAndHeaders); return FromProto.task(stubWithMetadata.getTask(getTaskRequest)); } catch (StatusRuntimeException | StatusException e) { throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to get task: "); @@ -143,17 +158,19 @@ public Task_v0_3 getTask(TaskQueryParams_v0_3 request, @Nullable ClientCallConte } @Override - public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - CancelTaskRequest cancelTaskRequest = CancelTaskRequest.newBuilder() - .setName("tasks/" + request.id()) - .build(); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(CancelTaskRequest_v0_3.METHOD, cancelTaskRequest, - agentCard, context); + CancelTaskRequest cancelTaskRequest = + CancelTaskRequest.newBuilder().setName("tasks/" + request.id()).build(); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + CancelTaskRequest_v0_3.METHOD, cancelTaskRequest, agentCard, context); try { - A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders); + A2AServiceBlockingV2Stub stubWithMetadata = + createBlockingStubWithMetadata(context, payloadAndHeaders); return FromProto.task(stubWithMetadata.cancelTask(cancelTaskRequest)); } catch (StatusRuntimeException | StatusException e) { throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to cancel task: "); @@ -161,125 +178,174 @@ public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, @Nullable ClientCallConte } @Override - public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration(TaskPushNotificationConfig_v0_3 request, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration( + TaskPushNotificationConfig_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); String configId = request.pushNotificationConfig().id(); - CreateTaskPushNotificationConfigRequest grpcRequest = CreateTaskPushNotificationConfigRequest.newBuilder() - .setParent("tasks/" + request.taskId()) - .setConfig(ToProto.taskPushNotificationConfig(request)) - .setConfigId(configId != null ? configId : request.taskId()) - .build(); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SetTaskPushNotificationConfigRequest_v0_3.METHOD, - grpcRequest, agentCard, context); + CreateTaskPushNotificationConfigRequest grpcRequest = + CreateTaskPushNotificationConfigRequest.newBuilder() + .setParent("tasks/" + request.taskId()) + .setConfig(ToProto.taskPushNotificationConfig(request)) + .setConfigId(configId != null ? configId : request.taskId()) + .build(); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + SetTaskPushNotificationConfigRequest_v0_3.METHOD, + grpcRequest, + agentCard, + context); try { - A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders); - return FromProto.taskPushNotificationConfig(stubWithMetadata.createTaskPushNotificationConfig(grpcRequest)); + A2AServiceBlockingV2Stub stubWithMetadata = + createBlockingStubWithMetadata(context, payloadAndHeaders); + return FromProto.taskPushNotificationConfig( + stubWithMetadata.createTaskPushNotificationConfig(grpcRequest)); } catch (StatusRuntimeException | StatusException e) { - throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to create task push notification config: "); + throw GrpcErrorMapper_v0_3.mapGrpcError( + e, "Failed to create task push notification config: "); } } @Override public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration( GetTaskPushNotificationConfigParams_v0_3 request, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - GetTaskPushNotificationConfigRequest grpcRequest = GetTaskPushNotificationConfigRequest.newBuilder() - .setName(getTaskPushNotificationConfigName(request)) - .build(); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetTaskPushNotificationConfigRequest_v0_3.METHOD, - grpcRequest, agentCard, context); + GetTaskPushNotificationConfigRequest grpcRequest = + GetTaskPushNotificationConfigRequest.newBuilder() + .setName(getTaskPushNotificationConfigName(request)) + .build(); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + GetTaskPushNotificationConfigRequest_v0_3.METHOD, + grpcRequest, + agentCard, + context); try { - A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders); - return FromProto.taskPushNotificationConfig(stubWithMetadata.getTaskPushNotificationConfig(grpcRequest)); + A2AServiceBlockingV2Stub stubWithMetadata = + createBlockingStubWithMetadata(context, payloadAndHeaders); + return FromProto.taskPushNotificationConfig( + stubWithMetadata.getTaskPushNotificationConfig(grpcRequest)); } catch (StatusRuntimeException | StatusException e) { - throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to get task push notification config: "); + throw GrpcErrorMapper_v0_3.mapGrpcError( + e, "Failed to get task push notification config: "); } } @Override public List listTaskPushNotificationConfigurations( ListTaskPushNotificationConfigParams_v0_3 request, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - ListTaskPushNotificationConfigRequest grpcRequest = ListTaskPushNotificationConfigRequest.newBuilder() - .setParent("tasks/" + request.id()) - .build(); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(ListTaskPushNotificationConfigRequest_v0_3.METHOD, - grpcRequest, agentCard, context); + ListTaskPushNotificationConfigRequest grpcRequest = + ListTaskPushNotificationConfigRequest.newBuilder() + .setParent("tasks/" + request.id()) + .build(); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + ListTaskPushNotificationConfigRequest_v0_3.METHOD, + grpcRequest, + agentCard, + context); try { - A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders); - return stubWithMetadata.listTaskPushNotificationConfig(grpcRequest).getConfigsList().stream() + A2AServiceBlockingV2Stub stubWithMetadata = + createBlockingStubWithMetadata(context, payloadAndHeaders); + return stubWithMetadata + .listTaskPushNotificationConfig(grpcRequest) + .getConfigsList() + .stream() .map(FromProto::taskPushNotificationConfig) .collect(Collectors.toList()); } catch (StatusRuntimeException | StatusException e) { - throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to list task push notification config: "); + throw GrpcErrorMapper_v0_3.mapGrpcError( + e, "Failed to list task push notification config: "); } } @Override - public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams_v0_3 request, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void deleteTaskPushNotificationConfigurations( + DeleteTaskPushNotificationConfigParams_v0_3 request, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - DeleteTaskPushNotificationConfigRequest grpcRequest = DeleteTaskPushNotificationConfigRequest.newBuilder() - .setName(getTaskPushNotificationConfigName(request.id(), request.pushNotificationConfigId())) - .build(); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(DeleteTaskPushNotificationConfigRequest_v0_3.METHOD, - grpcRequest, agentCard, context); + DeleteTaskPushNotificationConfigRequest grpcRequest = + DeleteTaskPushNotificationConfigRequest.newBuilder() + .setName( + getTaskPushNotificationConfigName( + request.id(), request.pushNotificationConfigId())) + .build(); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + DeleteTaskPushNotificationConfigRequest_v0_3.METHOD, + grpcRequest, + agentCard, + context); try { - A2AServiceBlockingV2Stub stubWithMetadata = createBlockingStubWithMetadata(context, payloadAndHeaders); + A2AServiceBlockingV2Stub stubWithMetadata = + createBlockingStubWithMetadata(context, payloadAndHeaders); stubWithMetadata.deleteTaskPushNotificationConfig(grpcRequest); } catch (StatusRuntimeException | StatusException e) { - throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to delete task push notification config: "); + throw GrpcErrorMapper_v0_3.mapGrpcError( + e, "Failed to delete task push notification config: "); } } @Override - public void resubscribe(TaskIdParams_v0_3 request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void resubscribe( + TaskIdParams_v0_3 request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); checkNotNullParam("eventConsumer", eventConsumer); - TaskSubscriptionRequest grpcRequest = TaskSubscriptionRequest.newBuilder() - .setName("tasks/" + request.id()) - .build(); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(TaskResubscriptionRequest_v0_3.METHOD, - grpcRequest, agentCard, context); + TaskSubscriptionRequest grpcRequest = + TaskSubscriptionRequest.newBuilder().setName("tasks/" + request.id()).build(); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + TaskResubscriptionRequest_v0_3.METHOD, grpcRequest, agentCard, context); - StreamObserver streamObserver = new EventStreamObserver_v0_3(eventConsumer, errorConsumer); + StreamObserver streamObserver = + new EventStreamObserver_v0_3(eventConsumer, errorConsumer); try { - A2AServiceStub stubWithMetadata = createAsyncStubWithMetadata(context, payloadAndHeaders); + A2AServiceStub stubWithMetadata = + createAsyncStubWithMetadata(context, payloadAndHeaders); stubWithMetadata.taskSubscription(grpcRequest, streamObserver); } catch (StatusRuntimeException e) { - throw GrpcErrorMapper_v0_3.mapGrpcError(e, "Failed to resubscribe task push notification config: "); + throw GrpcErrorMapper_v0_3.mapGrpcError( + e, "Failed to resubscribe task push notification config: "); } } @Override - public AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + 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; } @Override - public void close() { - } + public void close() {} - private SendMessageRequest createGrpcSendMessageRequest(MessageSendParams_v0_3 messageSendParams, @Nullable ClientCallContext_v0_3 context) { + private SendMessageRequest createGrpcSendMessageRequest( + MessageSendParams_v0_3 messageSendParams, @Nullable ClientCallContext_v0_3 context) { SendMessageRequest.Builder builder = SendMessageRequest.newBuilder(); builder.setRequest(ToProto.message(messageSendParams.message())); if (messageSendParams.configuration() != null) { - builder.setConfiguration(ToProto.messageSendConfiguration(messageSendParams.configuration())); + builder.setConfiguration( + ToProto.messageSendConfiguration(messageSendParams.configuration())); } if (messageSendParams.metadata() != null) { builder.setMetadata(ToProto.struct(messageSendParams.metadata())); @@ -294,38 +360,43 @@ private SendMessageRequest createGrpcSendMessageRequest(MessageSendParams_v0_3 m * @param payloadAndHeaders the payload and headers wrapper, may be null * @return the gRPC metadata */ - private Metadata createGrpcMetadata(@Nullable ClientCallContext_v0_3 context, @Nullable PayloadAndHeaders_v0_3 payloadAndHeaders) { + private Metadata createGrpcMetadata( + @Nullable ClientCallContext_v0_3 context, + @Nullable PayloadAndHeaders_v0_3 payloadAndHeaders) { Metadata metadata = new Metadata(); - + if (context != null && context.getHeaders() != null) { // Set X-A2A-Extensions header if present String extensionsHeader = context.getHeaders().get("X-A2A-Extensions"); if (extensionsHeader != null) { metadata.put(EXTENSIONS_KEY, extensionsHeader); } - + // Add other headers as needed in the future // For now, we only handle X-A2A-Extensions } if (payloadAndHeaders != null && payloadAndHeaders.getHeaders() != null) { // Handle all headers from interceptors (including auth headers) - for (Map.Entry headerEntry : payloadAndHeaders.getHeaders().entrySet()) { + for (Map.Entry headerEntry : + payloadAndHeaders.getHeaders().entrySet()) { String headerName = headerEntry.getKey(); String headerValue = headerEntry.getValue(); - + if (headerValue != null) { - // Use static key for common Authorization header, create dynamic keys for others + // Use static key for common Authorization header, create dynamic keys for + // others if (AuthInterceptor_v0_3.AUTHORIZATION.equals(headerName)) { metadata.put(AUTHORIZATION_METADATA_KEY, headerValue); } else { // Create a metadata key dynamically for API keys and other custom headers - Metadata.Key metadataKey = Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key metadataKey = + Metadata.Key.of(headerName, Metadata.ASCII_STRING_MARSHALLER); metadata.put(metadataKey, headerValue); } } } } - + return metadata; } @@ -336,8 +407,8 @@ private Metadata createGrpcMetadata(@Nullable ClientCallContext_v0_3 context, @N * @param payloadAndHeaders the payloadAndHeaders after applying any interceptors * @return blocking stub with metadata interceptor */ - private A2AServiceBlockingV2Stub createBlockingStubWithMetadata(@Nullable ClientCallContext_v0_3 context, - PayloadAndHeaders_v0_3 payloadAndHeaders) { + private A2AServiceBlockingV2Stub createBlockingStubWithMetadata( + @Nullable ClientCallContext_v0_3 context, PayloadAndHeaders_v0_3 payloadAndHeaders) { Metadata metadata = createGrpcMetadata(context, payloadAndHeaders); return blockingStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)); } @@ -349,17 +420,19 @@ private A2AServiceBlockingV2Stub createBlockingStubWithMetadata(@Nullable Client * @param payloadAndHeaders the payloadAndHeaders after applying any interceptors * @return async stub with metadata interceptor */ - private A2AServiceStub createAsyncStubWithMetadata(@Nullable ClientCallContext_v0_3 context, - PayloadAndHeaders_v0_3 payloadAndHeaders) { + private A2AServiceStub createAsyncStubWithMetadata( + @Nullable ClientCallContext_v0_3 context, PayloadAndHeaders_v0_3 payloadAndHeaders) { Metadata metadata = createGrpcMetadata(context, payloadAndHeaders); return asyncStub.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)); } - private String getTaskPushNotificationConfigName(GetTaskPushNotificationConfigParams_v0_3 params) { + private String getTaskPushNotificationConfigName( + GetTaskPushNotificationConfigParams_v0_3 params) { return getTaskPushNotificationConfigName(params.id(), params.pushNotificationConfigId()); } - private String getTaskPushNotificationConfigName(String taskId, @Nullable String pushNotificationConfigId) { + private String getTaskPushNotificationConfigName( + String taskId, @Nullable String pushNotificationConfigId) { StringBuilder name = new StringBuilder(); name.append("tasks/"); name.append(taskId); @@ -367,23 +440,31 @@ private String getTaskPushNotificationConfigName(String taskId, @Nullable String name.append("/pushNotificationConfigs/"); name.append(pushNotificationConfigId); } - //name.append("/pushNotificationConfigs/"); + // name.append("/pushNotificationConfigs/"); // Use taskId as default config ID if none provided - //name.append(pushNotificationConfigId != null ? pushNotificationConfigId : taskId); + // name.append(pushNotificationConfigId != null ? pushNotificationConfigId : taskId); return name.toString(); } - private PayloadAndHeaders_v0_3 applyInterceptors(String methodName, Object payload, - AgentCard_v0_3 agentCard, @Nullable ClientCallContext_v0_3 clientCallContext) { - PayloadAndHeaders_v0_3 payloadAndHeaders = new PayloadAndHeaders_v0_3(payload, - clientCallContext != null ? clientCallContext.getHeaders() : null); - if (interceptors != null && ! interceptors.isEmpty()) { + private PayloadAndHeaders_v0_3 applyInterceptors( + String methodName, + Object payload, + AgentCard_v0_3 agentCard, + @Nullable ClientCallContext_v0_3 clientCallContext) { + PayloadAndHeaders_v0_3 payloadAndHeaders = + new PayloadAndHeaders_v0_3( + payload, clientCallContext != null ? clientCallContext.getHeaders() : null); + if (interceptors != null && !interceptors.isEmpty()) { for (ClientCallInterceptor_v0_3 interceptor : interceptors) { - payloadAndHeaders = interceptor.intercept(methodName, payloadAndHeaders.getPayload(), - payloadAndHeaders.getHeaders(), agentCard, clientCallContext); + payloadAndHeaders = + interceptor.intercept( + methodName, + payloadAndHeaders.getPayload(), + payloadAndHeaders.getHeaders(), + agentCard, + clientCallContext); } } return payloadAndHeaders; } - -} \ No newline at end of file +} diff --git a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/package-info.java b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/package-info.java index 71bb5c883..53c26f883 100644 --- a/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/package-info.java +++ b/compat-0.3/client/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/grpc/package-info.java @@ -1,4 +1,4 @@ @NullMarked package org.a2aproject.sdk.compat03.client.transport.grpc; -import org.jspecify.annotations.NullMarked; \ No newline at end of file +import org.jspecify.annotations.NullMarked; diff --git a/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3_Test.java b/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3_Test.java index d8f26d14c..420cf458a 100644 --- a/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3_Test.java +++ b/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/EventStreamObserver_v0_3_Test.java @@ -18,12 +18,15 @@ void onCompleted_callsErrorHandlerWithNull() { Consumer eventHandler = events::add; Consumer errorHandler = errors::add; - EventStreamObserver_v0_3 observer = new EventStreamObserver_v0_3(eventHandler, errorHandler); + EventStreamObserver_v0_3 observer = + new EventStreamObserver_v0_3(eventHandler, errorHandler); observer.onCompleted(); assertEquals(1, errors.size(), "errorHandler should have been called once"); - assertNull(errors.get(0), "errorHandler should be called with null to signal clean completion"); + assertNull( + errors.get(0), + "errorHandler should be called with null to signal clean completion"); assertEquals(0, events.size(), "eventHandler should not be called"); } diff --git a/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3_Test.java b/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3_Test.java index b2e402f03..46ab4a4ca 100644 --- a/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3_Test.java +++ b/compat-0.3/client/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/grpc/GrpcErrorMapper_v0_3_Test.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3; import org.a2aproject.sdk.compat03.spec.InternalError_v0_3; @@ -16,8 +18,6 @@ import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3; -import io.grpc.Status; -import io.grpc.StatusRuntimeException; import org.junit.jupiter.api.Test; /** @@ -29,9 +29,8 @@ public class GrpcErrorMapper_v0_3_Test { @Test public void testTaskNotFoundErrorByDescription() { String errorMessage = "TaskNotFoundError: Task task-123 not found"; - StatusRuntimeException grpcException = Status.NOT_FOUND - .withDescription(errorMessage) - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.NOT_FOUND.withDescription(errorMessage).asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -44,9 +43,8 @@ public void testTaskNotFoundErrorByDescription() { @Test public void testTaskNotFoundErrorByStatusCode() { // Test fallback to status code mapping when description doesn't contain error type - StatusRuntimeException grpcException = Status.NOT_FOUND - .withDescription("Generic not found error") - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.NOT_FOUND.withDescription("Generic not found error").asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -58,9 +56,8 @@ public void testTaskNotFoundErrorByStatusCode() { @Test public void testUnsupportedOperationErrorByDescription() { String errorMessage = "UnsupportedOperationError: Operation not supported"; - StatusRuntimeException grpcException = Status.UNIMPLEMENTED - .withDescription(errorMessage) - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.UNIMPLEMENTED.withDescription(errorMessage).asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -71,9 +68,10 @@ public void testUnsupportedOperationErrorByDescription() { @Test public void testUnsupportedOperationErrorByStatusCode() { - StatusRuntimeException grpcException = Status.UNIMPLEMENTED - .withDescription("Generic unimplemented error") - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.UNIMPLEMENTED + .withDescription("Generic unimplemented error") + .asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -85,9 +83,8 @@ public void testUnsupportedOperationErrorByStatusCode() { @Test public void testInvalidParamsErrorByDescription() { String errorMessage = "InvalidParamsError: Invalid parameters provided"; - StatusRuntimeException grpcException = Status.INVALID_ARGUMENT - .withDescription(errorMessage) - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.INVALID_ARGUMENT.withDescription(errorMessage).asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -98,9 +95,10 @@ public void testInvalidParamsErrorByDescription() { @Test public void testInvalidParamsErrorByStatusCode() { - StatusRuntimeException grpcException = Status.INVALID_ARGUMENT - .withDescription("Generic invalid argument") - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.INVALID_ARGUMENT + .withDescription("Generic invalid argument") + .asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -112,9 +110,8 @@ public void testInvalidParamsErrorByStatusCode() { @Test public void testInvalidRequestError() { String errorMessage = "InvalidRequestError: Request is malformed"; - StatusRuntimeException grpcException = Status.INVALID_ARGUMENT - .withDescription(errorMessage) - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.INVALID_ARGUMENT.withDescription(errorMessage).asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -126,9 +123,8 @@ public void testInvalidRequestError() { @Test public void testMethodNotFoundError() { String errorMessage = "MethodNotFoundError: Method does not exist"; - StatusRuntimeException grpcException = Status.NOT_FOUND - .withDescription(errorMessage) - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.NOT_FOUND.withDescription(errorMessage).asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -140,9 +136,8 @@ public void testMethodNotFoundError() { @Test public void testTaskNotCancelableError() { String errorMessage = "TaskNotCancelableError: Task cannot be cancelled"; - StatusRuntimeException grpcException = Status.UNIMPLEMENTED - .withDescription(errorMessage) - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.UNIMPLEMENTED.withDescription(errorMessage).asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -154,9 +149,8 @@ public void testTaskNotCancelableError() { @Test public void testPushNotificationNotSupportedError() { String errorMessage = "PushNotificationNotSupportedError: Push notifications not supported"; - StatusRuntimeException grpcException = Status.UNIMPLEMENTED - .withDescription(errorMessage) - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.UNIMPLEMENTED.withDescription(errorMessage).asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -168,9 +162,8 @@ public void testPushNotificationNotSupportedError() { @Test public void testJSONParseError() { String errorMessage = "JSONParseError: Failed to parse JSON"; - StatusRuntimeException grpcException = Status.INTERNAL - .withDescription(errorMessage) - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.INTERNAL.withDescription(errorMessage).asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -181,10 +174,10 @@ public void testJSONParseError() { @Test public void testContentTypeNotSupportedError() { - String errorMessage = "ContentTypeNotSupportedError: Content type application/xml not supported"; - StatusRuntimeException grpcException = Status.INVALID_ARGUMENT - .withDescription(errorMessage) - .asRuntimeException(); + String errorMessage = + "ContentTypeNotSupportedError: Content type application/xml not supported"; + StatusRuntimeException grpcException = + Status.INVALID_ARGUMENT.withDescription(errorMessage).asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -192,17 +185,20 @@ public void testContentTypeNotSupportedError() { assertNotNull(result.getCause()); assertInstanceOf(ContentTypeNotSupportedError_v0_3.class, result.getCause()); - ContentTypeNotSupportedError_v0_3 contentTypeError = (ContentTypeNotSupportedError_v0_3) result.getCause(); + ContentTypeNotSupportedError_v0_3 contentTypeError = + (ContentTypeNotSupportedError_v0_3) result.getCause(); assertNotNull(contentTypeError.getMessage()); - assertTrue(contentTypeError.getMessage().contains("Content type application/xml not supported")); + assertTrue( + contentTypeError + .getMessage() + .contains("Content type application/xml not supported")); } @Test public void testInvalidAgentResponseError() { String errorMessage = "InvalidAgentResponseError: Agent response is invalid"; - StatusRuntimeException grpcException = Status.INTERNAL - .withDescription(errorMessage) - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.INTERNAL.withDescription(errorMessage).asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -210,16 +206,16 @@ public void testInvalidAgentResponseError() { assertNotNull(result.getCause()); assertInstanceOf(InvalidAgentResponseError_v0_3.class, result.getCause()); - InvalidAgentResponseError_v0_3 agentResponseError = (InvalidAgentResponseError_v0_3) result.getCause(); + InvalidAgentResponseError_v0_3 agentResponseError = + (InvalidAgentResponseError_v0_3) result.getCause(); assertNotNull(agentResponseError.getMessage()); assertTrue(agentResponseError.getMessage().contains("Agent response is invalid")); } @Test public void testInternalErrorByStatusCode() { - StatusRuntimeException grpcException = Status.INTERNAL - .withDescription("Internal server error") - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.INTERNAL.withDescription("Internal server error").asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -231,12 +227,12 @@ public void testInternalErrorByStatusCode() { @Test public void testCustomErrorPrefix() { String errorMessage = "TaskNotFoundError: Task not found"; - StatusRuntimeException grpcException = Status.NOT_FOUND - .withDescription(errorMessage) - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.NOT_FOUND.withDescription(errorMessage).asRuntimeException(); String customPrefix = "Custom Error: "; - A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException, customPrefix); + A2AClientException_v0_3 result = + GrpcErrorMapper_v0_3.mapGrpcError(grpcException, customPrefix); assertNotNull(result); assertTrue(result.getMessage().startsWith(customPrefix)); @@ -245,9 +241,10 @@ public void testCustomErrorPrefix() { @Test public void testAuthenticationFailed() { - StatusRuntimeException grpcException = Status.UNAUTHENTICATED - .withDescription("Authentication failed") - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.UNAUTHENTICATED + .withDescription("Authentication failed") + .asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -257,9 +254,8 @@ public void testAuthenticationFailed() { @Test public void testAuthorizationFailed() { - StatusRuntimeException grpcException = Status.PERMISSION_DENIED - .withDescription("Permission denied") - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.PERMISSION_DENIED.withDescription("Permission denied").asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -269,9 +265,8 @@ public void testAuthorizationFailed() { @Test public void testUnknownStatusCode() { - StatusRuntimeException grpcException = Status.DEADLINE_EXCEEDED - .withDescription("Request timeout") - .asRuntimeException(); + StatusRuntimeException grpcException = + Status.DEADLINE_EXCEEDED.withDescription("Request timeout").asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); @@ -281,8 +276,7 @@ public void testUnknownStatusCode() { @Test public void testNullDescription() { - StatusRuntimeException grpcException = Status.NOT_FOUND - .asRuntimeException(); + StatusRuntimeException grpcException = Status.NOT_FOUND.asRuntimeException(); A2AClientException_v0_3 result = GrpcErrorMapper_v0_3.mapGrpcError(grpcException); diff --git a/compat-0.3/client/transport/jsonrpc/pom.xml b/compat-0.3/client/transport/jsonrpc/pom.xml index b0b917bd2..02a21ce5f 100644 --- a/compat-0.3/client/transport/jsonrpc/pom.xml +++ b/compat-0.3/client/transport/jsonrpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -46,4 +44,4 @@ - \ No newline at end of file + diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfigBuilder_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfigBuilder_v0_3.java index 557832b97..5b52922b2 100644 --- a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfigBuilder_v0_3.java +++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportConfigBuilder_v0_3.java @@ -4,7 +4,9 @@ import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder_v0_3; -public class JSONRPCTransportConfigBuilder_v0_3 extends ClientTransportConfigBuilder_v0_3 { +public class JSONRPCTransportConfigBuilder_v0_3 + extends ClientTransportConfigBuilder_v0_3< + JSONRPCTransportConfig_v0_3, JSONRPCTransportConfigBuilder_v0_3> { private A2AHttpClient httpClient; diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportProvider_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportProvider_v0_3.java index 3787bf117..ac5ca5fc9 100644 --- a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportProvider_v0_3.java +++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportProvider_v0_3.java @@ -6,15 +6,25 @@ import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -public class JSONRPCTransportProvider_v0_3 implements ClientTransportProvider_v0_3 { +public class JSONRPCTransportProvider_v0_3 + implements ClientTransportProvider_v0_3< + JSONRPCTransport_v0_3, JSONRPCTransportConfig_v0_3> { @Override - public JSONRPCTransport_v0_3 create(JSONRPCTransportConfig_v0_3 clientTransportConfig, AgentCard_v0_3 agentCard, String agentUrl) throws A2AClientException_v0_3 { + public JSONRPCTransport_v0_3 create( + JSONRPCTransportConfig_v0_3 clientTransportConfig, + AgentCard_v0_3 agentCard, + String agentUrl) + throws A2AClientException_v0_3 { if (clientTransportConfig == null) { clientTransportConfig = new JSONRPCTransportConfig_v0_3(A2AHttpClientFactory.create()); } - return new JSONRPCTransport_v0_3(clientTransportConfig.getHttpClient(), agentCard, agentUrl, clientTransportConfig.getInterceptors()); + return new JSONRPCTransport_v0_3( + clientTransportConfig.getHttpClient(), + agentCard, + agentUrl, + clientTransportConfig.getInterceptors()); } @Override diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3.java index e7ee7eb47..71eb1e162 100644 --- a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3.java +++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3.java @@ -5,27 +5,28 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; - -import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; -import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.client.http.A2AHttpResponse; -import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.compat03.client.http.A2ACardResolver_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.sse.SSEEventListener_v0_3; +import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.PayloadAndHeaders_v0_3; -import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3; +import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; +import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3; import org.a2aproject.sdk.compat03.spec.CancelTaskResponse_v0_3; - import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams_v0_3; +import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3; +import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigResponse_v0_3; import org.a2aproject.sdk.compat03.spec.EventKind_v0_3; import org.a2aproject.sdk.compat03.spec.GetAuthenticatedExtendedCardRequest_v0_3; import org.a2aproject.sdk.compat03.spec.GetAuthenticatedExtendedCardResponse_v0_3; @@ -37,12 +38,9 @@ import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3; import org.a2aproject.sdk.compat03.spec.JSONRPCMessage_v0_3; import org.a2aproject.sdk.compat03.spec.JSONRPCResponse_v0_3; - import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3; import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest_v0_3; import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigResponse_v0_3; -import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigResponse_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; import org.a2aproject.sdk.compat03.spec.SendMessageRequest_v0_3; import org.a2aproject.sdk.compat03.spec.SendMessageResponse_v0_3; @@ -50,25 +48,35 @@ import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3; import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigResponse_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.sse.SSEEventListener_v0_3; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; public class JSONRPCTransport_v0_3 implements ClientTransport_v0_3 { - private static final Class SEND_MESSAGE_RESPONSE_REFERENCE = SendMessageResponse_v0_3.class; - private static final Class GET_TASK_RESPONSE_REFERENCE = GetTaskResponse_v0_3.class; - private static final Class CANCEL_TASK_RESPONSE_REFERENCE = CancelTaskResponse_v0_3.class; - private static final Class GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = GetTaskPushNotificationConfigResponse_v0_3.class; - private static final Class SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = SetTaskPushNotificationConfigResponse_v0_3.class; - private static final Class LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = ListTaskPushNotificationConfigResponse_v0_3.class; - private static final Class DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = DeleteTaskPushNotificationConfigResponse_v0_3.class; - private static final Class GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE = GetAuthenticatedExtendedCardResponse_v0_3.class; + private static final Class SEND_MESSAGE_RESPONSE_REFERENCE = + SendMessageResponse_v0_3.class; + private static final Class GET_TASK_RESPONSE_REFERENCE = + GetTaskResponse_v0_3.class; + private static final Class CANCEL_TASK_RESPONSE_REFERENCE = + CancelTaskResponse_v0_3.class; + private static final Class + GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = + GetTaskPushNotificationConfigResponse_v0_3.class; + private static final Class + SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = + SetTaskPushNotificationConfigResponse_v0_3.class; + private static final Class + LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = + ListTaskPushNotificationConfigResponse_v0_3.class; + private static final Class + DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = + DeleteTaskPushNotificationConfigResponse_v0_3.class; + private static final Class + GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE = + GetAuthenticatedExtendedCardResponse_v0_3.class; private final A2AHttpClient httpClient; private final String agentUrl; @@ -84,8 +92,11 @@ public JSONRPCTransport_v0_3(AgentCard_v0_3 agentCard) { this(null, agentCard, agentCard.url(), null); } - public JSONRPCTransport_v0_3(A2AHttpClient httpClient, AgentCard_v0_3 agentCard, - String agentUrl, List interceptors) { + public JSONRPCTransport_v0_3( + A2AHttpClient httpClient, + AgentCard_v0_3 agentCard, + String agentUrl, + List interceptors) { this.httpClient = httpClient == null ? A2AHttpClientFactory.create() : httpClient; this.agentCard = agentCard; this.agentUrl = agentUrl; @@ -94,20 +105,25 @@ public JSONRPCTransport_v0_3(A2AHttpClient httpClient, AgentCard_v0_3 agentCard, } @Override - public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public EventKind_v0_3 sendMessage( + MessageSendParams_v0_3 request, ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - SendMessageRequest_v0_3 sendMessageRequest = new SendMessageRequest_v0_3.Builder() - .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) - .method(SendMessageRequest_v0_3.METHOD) - .params(request) - .build(); // id will be randomly generated + SendMessageRequest_v0_3 sendMessageRequest = + new SendMessageRequest_v0_3.Builder() + .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) + .method(SendMessageRequest_v0_3.METHOD) + .params(request) + .build(); // id will be randomly generated - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SendMessageRequest_v0_3.METHOD, sendMessageRequest, - agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + SendMessageRequest_v0_3.METHOD, sendMessageRequest, agentCard, context); try { String httpResponseBody = sendPostRequest(payloadAndHeaders); - SendMessageResponse_v0_3 response = unmarshalResponse(httpResponseBody, SEND_MESSAGE_RESPONSE_REFERENCE); + SendMessageResponse_v0_3 response = + unmarshalResponse(httpResponseBody, SEND_MESSAGE_RESPONSE_REFERENCE); return response.getResult(); } catch (A2AClientException_v0_3 e) { throw e; @@ -117,55 +133,71 @@ public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, ClientCallCont } @Override - public void sendMessageStreaming(MessageSendParams_v0_3 request, Consumer eventConsumer, - Consumer errorConsumer, ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void sendMessageStreaming( + MessageSendParams_v0_3 request, + Consumer eventConsumer, + Consumer errorConsumer, + ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); checkNotNullParam("eventConsumer", eventConsumer); - SendStreamingMessageRequest_v0_3 sendStreamingMessageRequest = new SendStreamingMessageRequest_v0_3.Builder() - .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) - .method(SendStreamingMessageRequest_v0_3.METHOD) - .params(request) - .build(); // id will be randomly generated - - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SendStreamingMessageRequest_v0_3.METHOD, - sendStreamingMessageRequest, agentCard, context); + SendStreamingMessageRequest_v0_3 sendStreamingMessageRequest = + new SendStreamingMessageRequest_v0_3.Builder() + .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) + .method(SendStreamingMessageRequest_v0_3.METHOD) + .params(request) + .build(); // id will be randomly generated + + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + SendStreamingMessageRequest_v0_3.METHOD, + sendStreamingMessageRequest, + agentCard, + context); AtomicReference> ref = new AtomicReference<>(); - SSEEventListener_v0_3 sseEventListener = new SSEEventListener_v0_3(eventConsumer, errorConsumer); + SSEEventListener_v0_3 sseEventListener = + new SSEEventListener_v0_3(eventConsumer, errorConsumer); try { A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders); - ref.set(builder.postAsyncSSE( - event -> sseEventListener.onMessage(event.data(), ref.get()), - throwable -> sseEventListener.onError(throwable, ref.get()), - () -> { - // Signal normal stream completion to error handler (null error means success) - sseEventListener.onComplete(); - })); + ref.set( + builder.postAsyncSSE( + event -> sseEventListener.onMessage(event.data(), ref.get()), + throwable -> sseEventListener.onError(throwable, ref.get()), + () -> { + // Signal normal stream completion to error handler (null error + // means success) + sseEventListener.onComplete(); + })); } catch (IOException e) { throw new A2AClientException_v0_3("Failed to send streaming message request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException_v0_3("Send streaming message request timed out: " + e, e); } catch (JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to process JSON for streaming message request: " + e, e); + throw new A2AClientException_v0_3( + "Failed to process JSON for streaming message request: " + e, e); } } @Override - public Task_v0_3 getTask(TaskQueryParams_v0_3 request, ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public Task_v0_3 getTask(TaskQueryParams_v0_3 request, ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - GetTaskRequest_v0_3 getTaskRequest = new GetTaskRequest_v0_3.Builder() - .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) - .method(GetTaskRequest_v0_3.METHOD) - .params(request) - .build(); // id will be randomly generated + GetTaskRequest_v0_3 getTaskRequest = + new GetTaskRequest_v0_3.Builder() + .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) + .method(GetTaskRequest_v0_3.METHOD) + .params(request) + .build(); // id will be randomly generated - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetTaskRequest_v0_3.METHOD, getTaskRequest, - agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors(GetTaskRequest_v0_3.METHOD, getTaskRequest, agentCard, context); try { String httpResponseBody = sendPostRequest(payloadAndHeaders); - GetTaskResponse_v0_3 response = unmarshalResponse(httpResponseBody, GET_TASK_RESPONSE_REFERENCE); + GetTaskResponse_v0_3 response = + unmarshalResponse(httpResponseBody, GET_TASK_RESPONSE_REFERENCE); return response.getResult(); } catch (A2AClientException_v0_3 e) { throw e; @@ -175,20 +207,24 @@ public Task_v0_3 getTask(TaskQueryParams_v0_3 request, ClientCallContext_v0_3 co } @Override - public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - CancelTaskRequest_v0_3 cancelTaskRequest = new CancelTaskRequest_v0_3.Builder() - .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) - .method(CancelTaskRequest_v0_3.METHOD) - .params(request) - .build(); // id will be randomly generated + CancelTaskRequest_v0_3 cancelTaskRequest = + new CancelTaskRequest_v0_3.Builder() + .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) + .method(CancelTaskRequest_v0_3.METHOD) + .params(request) + .build(); // id will be randomly generated - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(CancelTaskRequest_v0_3.METHOD, cancelTaskRequest, - agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + CancelTaskRequest_v0_3.METHOD, cancelTaskRequest, agentCard, context); try { String httpResponseBody = sendPostRequest(payloadAndHeaders); - CancelTaskResponse_v0_3 response = unmarshalResponse(httpResponseBody, CANCEL_TASK_RESPONSE_REFERENCE); + CancelTaskResponse_v0_3 response = + unmarshalResponse(httpResponseBody, CANCEL_TASK_RESPONSE_REFERENCE); return response.getResult(); } catch (A2AClientException_v0_3 e) { throw e; @@ -198,146 +234,195 @@ public Task_v0_3 cancelTask(TaskIdParams_v0_3 request, ClientCallContext_v0_3 co } @Override - public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration(TaskPushNotificationConfig_v0_3 request, - ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration( + TaskPushNotificationConfig_v0_3 request, ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - SetTaskPushNotificationConfigRequest_v0_3 setTaskPushNotificationRequest = new SetTaskPushNotificationConfigRequest_v0_3.Builder() - .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) - .method(SetTaskPushNotificationConfigRequest_v0_3.METHOD) - .params(request) - .build(); // id will be randomly generated - - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SetTaskPushNotificationConfigRequest_v0_3.METHOD, - setTaskPushNotificationRequest, agentCard, context); + SetTaskPushNotificationConfigRequest_v0_3 setTaskPushNotificationRequest = + new SetTaskPushNotificationConfigRequest_v0_3.Builder() + .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) + .method(SetTaskPushNotificationConfigRequest_v0_3.METHOD) + .params(request) + .build(); // id will be randomly generated + + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + SetTaskPushNotificationConfigRequest_v0_3.METHOD, + setTaskPushNotificationRequest, + agentCard, + context); try { String httpResponseBody = sendPostRequest(payloadAndHeaders); - SetTaskPushNotificationConfigResponse_v0_3 response = unmarshalResponse(httpResponseBody, - SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); + SetTaskPushNotificationConfigResponse_v0_3 response = + unmarshalResponse( + httpResponseBody, SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); return response.getResult(); } catch (A2AClientException_v0_3 e) { throw e; } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to set task push notification config: " + e, e); + throw new A2AClientException_v0_3( + "Failed to set task push notification config: " + e, e); } } @Override - public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams_v0_3 request, - ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration( + GetTaskPushNotificationConfigParams_v0_3 request, ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - GetTaskPushNotificationConfigRequest_v0_3 getTaskPushNotificationRequest = new GetTaskPushNotificationConfigRequest_v0_3.Builder() - .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) - .method(GetTaskPushNotificationConfigRequest_v0_3.METHOD) - .params(request) - .build(); // id will be randomly generated - - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetTaskPushNotificationConfigRequest_v0_3.METHOD, - getTaskPushNotificationRequest, agentCard, context); + GetTaskPushNotificationConfigRequest_v0_3 getTaskPushNotificationRequest = + new GetTaskPushNotificationConfigRequest_v0_3.Builder() + .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) + .method(GetTaskPushNotificationConfigRequest_v0_3.METHOD) + .params(request) + .build(); // id will be randomly generated + + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + GetTaskPushNotificationConfigRequest_v0_3.METHOD, + getTaskPushNotificationRequest, + agentCard, + context); try { String httpResponseBody = sendPostRequest(payloadAndHeaders); - GetTaskPushNotificationConfigResponse_v0_3 response = unmarshalResponse(httpResponseBody, - GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); + GetTaskPushNotificationConfigResponse_v0_3 response = + unmarshalResponse( + httpResponseBody, GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); return response.getResult(); } catch (A2AClientException_v0_3 e) { throw e; } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to get task push notification config: " + e, e); + throw new A2AClientException_v0_3( + "Failed to get task push notification config: " + e, e); } } @Override public List listTaskPushNotificationConfigurations( - ListTaskPushNotificationConfigParams_v0_3 request, - ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + ListTaskPushNotificationConfigParams_v0_3 request, ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - ListTaskPushNotificationConfigRequest_v0_3 listTaskPushNotificationRequest = new ListTaskPushNotificationConfigRequest_v0_3.Builder() - .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) - .method(ListTaskPushNotificationConfigRequest_v0_3.METHOD) - .params(request) - .build(); // id will be randomly generated - - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(ListTaskPushNotificationConfigRequest_v0_3.METHOD, - listTaskPushNotificationRequest, agentCard, context); + ListTaskPushNotificationConfigRequest_v0_3 listTaskPushNotificationRequest = + new ListTaskPushNotificationConfigRequest_v0_3.Builder() + .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) + .method(ListTaskPushNotificationConfigRequest_v0_3.METHOD) + .params(request) + .build(); // id will be randomly generated + + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + ListTaskPushNotificationConfigRequest_v0_3.METHOD, + listTaskPushNotificationRequest, + agentCard, + context); try { String httpResponseBody = sendPostRequest(payloadAndHeaders); - ListTaskPushNotificationConfigResponse_v0_3 response = unmarshalResponse(httpResponseBody, - LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); + ListTaskPushNotificationConfigResponse_v0_3 response = + unmarshalResponse( + httpResponseBody, + LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); return response.getResult(); } catch (A2AClientException_v0_3 e) { throw e; } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to list task push notification configs: " + e, e); + throw new A2AClientException_v0_3( + "Failed to list task push notification configs: " + e, e); } } @Override - public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams_v0_3 request, - ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void deleteTaskPushNotificationConfigurations( + DeleteTaskPushNotificationConfigParams_v0_3 request, ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - DeleteTaskPushNotificationConfigRequest_v0_3 deleteTaskPushNotificationRequest = new DeleteTaskPushNotificationConfigRequest_v0_3.Builder() - .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) - .method(DeleteTaskPushNotificationConfigRequest_v0_3.METHOD) - .params(request) - .build(); // id will be randomly generated - - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(DeleteTaskPushNotificationConfigRequest_v0_3.METHOD, - deleteTaskPushNotificationRequest, agentCard, context); + DeleteTaskPushNotificationConfigRequest_v0_3 deleteTaskPushNotificationRequest = + new DeleteTaskPushNotificationConfigRequest_v0_3.Builder() + .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) + .method(DeleteTaskPushNotificationConfigRequest_v0_3.METHOD) + .params(request) + .build(); // id will be randomly generated + + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + DeleteTaskPushNotificationConfigRequest_v0_3.METHOD, + deleteTaskPushNotificationRequest, + agentCard, + context); try { String httpResponseBody = sendPostRequest(payloadAndHeaders); - unmarshalResponse(httpResponseBody, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); + unmarshalResponse( + httpResponseBody, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); } catch (A2AClientException_v0_3 e) { throw e; } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to delete task push notification configs: " + e, e); + throw new A2AClientException_v0_3( + "Failed to delete task push notification configs: " + e, e); } } @Override - public void resubscribe(TaskIdParams_v0_3 request, Consumer eventConsumer, - Consumer errorConsumer, ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void resubscribe( + TaskIdParams_v0_3 request, + Consumer eventConsumer, + Consumer errorConsumer, + ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); checkNotNullParam("eventConsumer", eventConsumer); checkNotNullParam("errorConsumer", errorConsumer); - TaskResubscriptionRequest_v0_3 taskResubscriptionRequest = new TaskResubscriptionRequest_v0_3.Builder() - .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) - .method(TaskResubscriptionRequest_v0_3.METHOD) - .params(request) - .build(); // id will be randomly generated - - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(TaskResubscriptionRequest_v0_3.METHOD, - taskResubscriptionRequest, agentCard, context); + TaskResubscriptionRequest_v0_3 taskResubscriptionRequest = + new TaskResubscriptionRequest_v0_3.Builder() + .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) + .method(TaskResubscriptionRequest_v0_3.METHOD) + .params(request) + .build(); // id will be randomly generated + + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + TaskResubscriptionRequest_v0_3.METHOD, + taskResubscriptionRequest, + agentCard, + context); AtomicReference> ref = new AtomicReference<>(); - SSEEventListener_v0_3 sseEventListener = new SSEEventListener_v0_3(eventConsumer, errorConsumer); + SSEEventListener_v0_3 sseEventListener = + new SSEEventListener_v0_3(eventConsumer, errorConsumer); try { A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders); - ref.set(builder.postAsyncSSE( - event -> sseEventListener.onMessage(event.data(), ref.get()), - throwable -> sseEventListener.onError(throwable, ref.get()), - () -> { - // Signal normal stream completion to error handler (null error means success) - sseEventListener.onComplete(); - })); + ref.set( + builder.postAsyncSSE( + event -> sseEventListener.onMessage(event.data(), ref.get()), + throwable -> sseEventListener.onError(throwable, ref.get()), + () -> { + // Signal normal stream completion to error handler (null error + // means success) + sseEventListener.onComplete(); + })); } catch (IOException e) { - throw new A2AClientException_v0_3("Failed to send task resubscription request: " + e, e); + throw new A2AClientException_v0_3( + "Failed to send task resubscription request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException_v0_3("Task resubscription request timed out: " + e, e); } catch (JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to process JSON for task resubscription request: " + e, e); + throw new A2AClientException_v0_3( + "Failed to process JSON for task resubscription request: " + e, e); } } @Override - public AgentCard_v0_3 getAgentCard(ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public AgentCard_v0_3 getAgentCard(ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { A2ACardResolver_v0_3 resolver; try { if (agentCard == null) { - resolver = new A2ACardResolver_v0_3(httpClient, agentUrl, null, getHttpHeaders(context)); + resolver = + new A2ACardResolver_v0_3( + httpClient, agentUrl, null, getHttpHeaders(context)); agentCard = resolver.getAgentCard(); needsExtendedCard = agentCard.supportsAuthenticatedExtendedCard(); } @@ -345,25 +430,33 @@ public AgentCard_v0_3 getAgentCard(ClientCallContext_v0_3 context) throws A2ACli return agentCard; } - GetAuthenticatedExtendedCardRequest_v0_3 getExtendedAgentCardRequest = new GetAuthenticatedExtendedCardRequest_v0_3.Builder() - .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) - .method(GetAuthenticatedExtendedCardRequest_v0_3.METHOD) - .build(); // id will be randomly generated + GetAuthenticatedExtendedCardRequest_v0_3 getExtendedAgentCardRequest = + new GetAuthenticatedExtendedCardRequest_v0_3.Builder() + .jsonrpc(JSONRPCMessage_v0_3.JSONRPC_VERSION) + .method(GetAuthenticatedExtendedCardRequest_v0_3.METHOD) + .build(); // id will be randomly generated - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetAuthenticatedExtendedCardRequest_v0_3.METHOD, - getExtendedAgentCardRequest, agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + GetAuthenticatedExtendedCardRequest_v0_3.METHOD, + getExtendedAgentCardRequest, + agentCard, + context); try { String httpResponseBody = sendPostRequest(payloadAndHeaders); - GetAuthenticatedExtendedCardResponse_v0_3 response = unmarshalResponse(httpResponseBody, - GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE); + GetAuthenticatedExtendedCardResponse_v0_3 response = + unmarshalResponse( + httpResponseBody, + GET_AUTHENTICATED_EXTENDED_CARD_RESPONSE_REFERENCE); agentCard = response.getResult(); needsExtendedCard = false; return agentCard; } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to get authenticated extended agent card: " + e, e); + throw new A2AClientException_v0_3( + "Failed to get authenticated extended agent card: " + e, e); } - } catch(A2AClientError_v0_3 e){ + } catch (A2AClientError_v0_3 e) { throw new A2AClientException_v0_3("Failed to get agent card: " + e, e); } } @@ -373,19 +466,29 @@ public void close() { // no-op } - private PayloadAndHeaders_v0_3 applyInterceptors(String methodName, Object payload, - AgentCard_v0_3 agentCard, ClientCallContext_v0_3 clientCallContext) { - PayloadAndHeaders_v0_3 payloadAndHeaders = new PayloadAndHeaders_v0_3(payload, getHttpHeaders(clientCallContext)); - if (interceptors != null && ! interceptors.isEmpty()) { + private PayloadAndHeaders_v0_3 applyInterceptors( + String methodName, + Object payload, + AgentCard_v0_3 agentCard, + ClientCallContext_v0_3 clientCallContext) { + PayloadAndHeaders_v0_3 payloadAndHeaders = + new PayloadAndHeaders_v0_3(payload, getHttpHeaders(clientCallContext)); + if (interceptors != null && !interceptors.isEmpty()) { for (ClientCallInterceptor_v0_3 interceptor : interceptors) { - payloadAndHeaders = interceptor.intercept(methodName, payloadAndHeaders.getPayload(), - payloadAndHeaders.getHeaders(), agentCard, clientCallContext); + payloadAndHeaders = + interceptor.intercept( + methodName, + payloadAndHeaders.getPayload(), + payloadAndHeaders.getHeaders(), + agentCard, + clientCallContext); } } return payloadAndHeaders; } - private String sendPostRequest(PayloadAndHeaders_v0_3 payloadAndHeaders) throws IOException, InterruptedException, JsonProcessingException_v0_3 { + private String sendPostRequest(PayloadAndHeaders_v0_3 payloadAndHeaders) + throws IOException, InterruptedException, JsonProcessingException_v0_3 { A2AHttpClient.PostBuilder builder = createPostBuilder(payloadAndHeaders); A2AHttpResponse response = builder.post(); if (!response.success()) { @@ -394,11 +497,14 @@ private String sendPostRequest(PayloadAndHeaders_v0_3 payloadAndHeaders) throws return response.body(); } - private A2AHttpClient.PostBuilder createPostBuilder(PayloadAndHeaders_v0_3 payloadAndHeaders) throws JsonProcessingException_v0_3 { - A2AHttpClient.PostBuilder postBuilder = httpClient.createPost() - .url(agentUrl) - .addHeader("Content-Type", "application/json") - .body(JsonUtil_v0_3.toJson(payloadAndHeaders.getPayload())); + private A2AHttpClient.PostBuilder createPostBuilder(PayloadAndHeaders_v0_3 payloadAndHeaders) + throws JsonProcessingException_v0_3 { + A2AHttpClient.PostBuilder postBuilder = + httpClient + .createPost() + .url(agentUrl) + .addHeader("Content-Type", "application/json") + .body(JsonUtil_v0_3.toJson(payloadAndHeaders.getPayload())); if (payloadAndHeaders.getHeaders() != null) { for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { @@ -409,12 +515,15 @@ private A2AHttpClient.PostBuilder createPostBuilder(PayloadAndHeaders_v0_3 paylo return postBuilder; } - private > T unmarshalResponse(String response, Class responseClass) + private > T unmarshalResponse( + String response, Class responseClass) throws A2AClientException_v0_3, JsonProcessingException_v0_3 { T value = JsonUtil_v0_3.fromJson(response, responseClass); JSONRPCError_v0_3 error = value.getError(); if (error != null) { - throw new A2AClientException_v0_3(error.getMessage() + (error.getData() != null ? ": " + error.getData() : ""), error); + throw new A2AClientException_v0_3( + error.getMessage() + (error.getData() != null ? ": " + error.getData() : ""), + error); } return value; } @@ -422,4 +531,4 @@ private > T unmarshalResponse(String response, private Map getHttpHeaders(ClientCallContext_v0_3 context) { return context != null ? context.getHeaders() : null; } -} \ No newline at end of file +} diff --git a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java index 991769a2d..5f12a4a12 100644 --- a/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java +++ b/compat-0.3/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3.java @@ -3,25 +3,24 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.logging.Logger; import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.logging.Logger; - public class SSEEventListener_v0_3 { private static final Logger LOGGER = Logger.getLogger(SSEEventListener_v0_3.class.getName()); private final Consumer eventHandler; private final Consumer errorHandler; private final AtomicBoolean terminalSignaled = new AtomicBoolean(false); - public SSEEventListener_v0_3(Consumer eventHandler, - Consumer errorHandler) { + public SSEEventListener_v0_3( + Consumer eventHandler, Consumer errorHandler) { this.eventHandler = eventHandler; this.errorHandler = errorHandler; } @@ -58,18 +57,23 @@ private void signalTerminal(Throwable error) { } public void onComplete() { - LOGGER.fine("SSEEventListener.onComplete() called - signaling successful stream completion"); + LOGGER.fine( + "SSEEventListener.onComplete() called - signaling successful stream completion"); signalTerminal(null); } - private void handleMessage(JsonObject jsonObject, Future future) throws JsonProcessingException_v0_3 { + private void handleMessage(JsonObject jsonObject, Future future) + throws JsonProcessingException_v0_3 { if (jsonObject.has("error")) { - JSONRPCError_v0_3 error = JsonUtil_v0_3.fromJson(jsonObject.get("error").toString(), JSONRPCError_v0_3.class); + JSONRPCError_v0_3 error = + JsonUtil_v0_3.fromJson( + jsonObject.get("error").toString(), JSONRPCError_v0_3.class); signalTerminal(error); } else if (jsonObject.has("result")) { // result can be a Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent String resultJson = jsonObject.get("result").toString(); - StreamingEventKind_v0_3 event = JsonUtil_v0_3.fromJson(resultJson, StreamingEventKind_v0_3.class); + StreamingEventKind_v0_3 event = + JsonUtil_v0_3.fromJson(resultJson, StreamingEventKind_v0_3.class); eventHandler.accept(event); if (event instanceof TaskStatusUpdateEvent_v0_3 tsue && tsue.isFinal()) { signalTerminal(null); @@ -79,5 +83,4 @@ private void handleMessage(JsonObject jsonObject, Future future) throws Js throw new IllegalArgumentException("Unknown message type"); } } - } diff --git a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportStreaming_v0_3_Test.java b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportStreaming_v0_3_Test.java index 022012607..d5b2d743f 100644 --- a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportStreaming_v0_3_Test.java +++ b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransportStreaming_v0_3_Test.java @@ -17,18 +17,16 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; - import org.a2aproject.sdk.compat03.spec.Artifact_v0_3; -import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,22 +52,25 @@ public void tearDown() { public void testSendStreamingMessageParams() { // The goal here is just to verify the correct parameters are being used // This is a unit test of the parameter construction, not the streaming itself - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("test message"))) - .contextId("context-test") - .messageId("message-test") - .build(); - - MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder() - .acceptedOutputModes(List.of("text")) - .blocking(false) - .build(); - - MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder() - .message(message) - .configuration(configuration) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("test message"))) + .contextId("context-test") + .messageId("message-test") + .build(); + + MessageSendConfiguration_v0_3 configuration = + new MessageSendConfiguration_v0_3.Builder() + .acceptedOutputModes(List.of("text")) + .blocking(false) + .build(); + + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3.Builder() + .message(message) + .configuration(configuration) + .build(); assertNotNull(params); assertEquals(message, params.message()); @@ -80,42 +81,47 @@ public void testSendStreamingMessageParams() { @Test public void testA2AClientSendStreamingMessage() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_STREAMING_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SEND_MESSAGE_STREAMING_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("tell me some jokes"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder() - .acceptedOutputModes(List.of("text")) - .blocking(false) - .build(); - MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder() - .message(message) - .configuration(configuration) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("tell me some jokes"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration_v0_3 configuration = + new MessageSendConfiguration_v0_3.Builder() + .acceptedOutputModes(List.of("text")) + .blocking(false) + .build(); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3.Builder() + .message(message) + .configuration(configuration) + .build(); AtomicReference receivedEvent = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - Consumer eventHandler = event -> { - receivedEvent.set(event); - latch.countDown(); - }; + Consumer eventHandler = + event -> { + receivedEvent.set(event); + latch.countDown(); + }; Consumer errorHandler = error -> {}; client.sendMessageStreaming(params, eventHandler, errorHandler, null); @@ -126,36 +132,39 @@ public void testA2AClientSendStreamingMessage() throws Exception { @Test public void testA2AClientResubscribeToTask() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(TASK_RESUBSCRIPTION_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + TASK_RESUBSCRIPTION_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE) - ); + .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); TaskIdParams_v0_3 taskIdParams = new TaskIdParams_v0_3("task-1234"); AtomicReference receivedEvent = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - Consumer eventHandler = event -> { - receivedEvent.set(event); - latch.countDown(); - }; + Consumer eventHandler = + event -> { + receivedEvent.set(event); + latch.countDown(); + }; Consumer errorHandler = error -> {}; client.resubscribe(taskIdParams, eventHandler, errorHandler, null); boolean eventReceived = latch.await(10, TimeUnit.SECONDS); assertTrue(eventReceived); - StreamingEventKind_v0_3 eventKind = receivedEvent.get();; + StreamingEventKind_v0_3 eventKind = receivedEvent.get(); + ; assertNotNull(eventKind); assertInstanceOf(Task_v0_3.class, eventKind); Task_v0_3 task = (Task_v0_3) eventKind; @@ -169,6 +178,8 @@ public void testA2AClientResubscribeToTask() throws Exception { assertEquals("joke", artifact.name()); Part_v0_3 part = artifact.parts().get(0); assertEquals(Part_v0_3.Kind.TEXT, ((TextPart_v0_3) part).kind()); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) part).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart_v0_3) part).text()); } -} \ No newline at end of file +} diff --git a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3_Test.java b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3_Test.java index 2af1a8cf3..5d942c04a 100644 --- a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3_Test.java +++ b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JSONRPCTransport_v0_3_Test.java @@ -37,7 +37,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3; @@ -50,22 +49,21 @@ import org.a2aproject.sdk.compat03.spec.FileWithBytes_v0_3; import org.a2aproject.sdk.compat03.spec.FileWithUri_v0_3; import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3; -import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.OpenIdConnectSecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationAuthenticationInfo_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.SecurityScheme_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -89,41 +87,42 @@ public void tearDown() { @Test public void testA2AClientSendMessage() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) - .respond( - response() - .withStatusCode(200) - .withBody(SEND_MESSAGE_TEST_RESPONSE) - ); + .withBody( + JsonBody.json( + SEND_MESSAGE_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(SEND_MESSAGE_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder() - .acceptedOutputModes(List.of("text")) - .blocking(true) - .build(); - MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder() - .message(message) - .configuration(configuration) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration_v0_3 configuration = + new MessageSendConfiguration_v0_3.Builder() + .acceptedOutputModes(List.of("text")) + .blocking(true) + .build(); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3.Builder() + .message(message) + .configuration(configuration) + .build(); EventKind_v0_3 result = client.sendMessage(params, null); assertInstanceOf(Task_v0_3.class, result); Task_v0_3 task = (Task_v0_3) result; assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id()); assertNotNull(task.contextId()); - assertEquals(TaskState_v0_3.COMPLETED,task.status().state()); + assertEquals(TaskState_v0_3.COMPLETED, task.status().state()); assertEquals(1, task.artifacts().size()); Artifact_v0_3 artifact = task.artifacts().get(0); assertEquals("artifact-1", artifact.artifactId()); @@ -131,40 +130,46 @@ public void testA2AClientSendMessage() throws Exception { assertEquals(1, artifact.parts().size()); Part_v0_3 part = artifact.parts().get(0); assertEquals(Part_v0_3.Kind.TEXT, ((TextPart_v0_3) part).kind()); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) part).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart_v0_3) part).text()); assertTrue(task.metadata().isEmpty()); } @Test public void testA2AClientSendMessageWithMessageResponse() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE) - ); + .withBody(SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder() - .acceptedOutputModes(List.of("text")) - .blocking(true) - .build(); - MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder() - .message(message) - .configuration(configuration) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration_v0_3 configuration = + new MessageSendConfiguration_v0_3.Builder() + .acceptedOutputModes(List.of("text")) + .blocking(true) + .build(); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3.Builder() + .message(message) + .configuration(configuration) + .build(); EventKind_v0_3 result = client.sendMessage(params, null); assertInstanceOf(Message_v0_3.class, result); @@ -172,41 +177,43 @@ public void testA2AClientSendMessageWithMessageResponse() throws Exception { assertEquals(Message_v0_3.Role.AGENT, agentMessage.role()); Part_v0_3 part = agentMessage.parts().get(0); assertEquals(Part_v0_3.Kind.TEXT, ((TextPart_v0_3) part).kind()); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) part).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart_v0_3) part).text()); assertEquals("msg-456", agentMessage.messageId()); } - @Test public void testA2AClientSendMessageWithError() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_WITH_ERROR_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) - .respond( - response() - .withStatusCode(200) - .withBody(SEND_MESSAGE_ERROR_TEST_RESPONSE) - ); + .withBody( + JsonBody.json( + SEND_MESSAGE_WITH_ERROR_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(SEND_MESSAGE_ERROR_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder() - .acceptedOutputModes(List.of("text")) - .blocking(true) - .build(); - MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder() - .message(message) - .configuration(configuration) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration_v0_3 configuration = + new MessageSendConfiguration_v0_3.Builder() + .acceptedOutputModes(List.of("text")) + .blocking(true) + .build(); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3.Builder() + .message(message) + .configuration(configuration) + .build(); try { client.sendMessage(params, null); @@ -218,22 +225,21 @@ public void testA2AClientSendMessageWithError() throws Exception { @Test public void testA2AClientGetTask() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(GET_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) - .respond( - response() - .withStatusCode(200) - .withBody(GET_TASK_TEST_RESPONSE) - ); + .withBody( + JsonBody.json( + GET_TASK_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(GET_TASK_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); - Task_v0_3 task = client.getTask(new TaskQueryParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", - 10), null); + Task_v0_3 task = + client.getTask( + new TaskQueryParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", 10), null); assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id()); assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", task.contextId()); assertEquals(TaskState_v0_3.COMPLETED, task.status().state()); @@ -243,7 +249,9 @@ public void testA2AClientGetTask() throws Exception { assertEquals("artifact-1", artifact.artifactId()); Part_v0_3 part = artifact.parts().get(0); assertEquals(Part_v0_3.Kind.TEXT, ((TextPart_v0_3) part).kind()); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) part).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart_v0_3) part).text()); assertTrue(task.metadata().isEmpty()); List history = task.history(); assertNotNull(history); @@ -255,7 +263,7 @@ public void testA2AClientGetTask() throws Exception { assertEquals(3, parts.size()); part = parts.get(0); assertEquals(Part_v0_3.Kind.TEXT, ((TextPart_v0_3) part).kind()); - assertEquals("tell me a joke", ((TextPart_v0_3)part).text()); + assertEquals("tell me a joke", ((TextPart_v0_3) part).text()); part = parts.get(1); assertEquals(Part_v0_3.Kind.FILE, ((FilePart_v0_3) part).kind()); FileContent_v0_3 filePart = ((FilePart_v0_3) part).file(); @@ -271,22 +279,23 @@ public void testA2AClientGetTask() throws Exception { @Test public void testA2AClientCancelTask() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(CANCEL_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) - .respond( - response() - .withStatusCode(200) - .withBody(CANCEL_TASK_TEST_RESPONSE) - ); + .withBody( + JsonBody.json( + CANCEL_TASK_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(CANCEL_TASK_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); - Task_v0_3 task = client.cancelTask(new TaskIdParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", - new HashMap<>()), null); + Task_v0_3 task = + client.cancelTask( + new TaskIdParams_v0_3( + "de38c76d-d54c-436c-8b9f-4c2703648d64", new HashMap<>()), + null); assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id()); assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", task.contextId()); assertEquals(TaskState_v0_3.CANCELED, task.status().state()); @@ -295,93 +304,107 @@ public void testA2AClientCancelTask() throws Exception { @Test public void testA2AClientGetTaskPushNotificationConfig() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); - TaskPushNotificationConfig_v0_3 taskPushNotificationConfig = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", null, - new HashMap<>()), null); - PushNotificationConfig_v0_3 pushNotificationConfig = taskPushNotificationConfig.pushNotificationConfig(); + TaskPushNotificationConfig_v0_3 taskPushNotificationConfig = + client.getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams_v0_3( + "de38c76d-d54c-436c-8b9f-4c2703648d64", null, new HashMap<>()), + null); + PushNotificationConfig_v0_3 pushNotificationConfig = + taskPushNotificationConfig.pushNotificationConfig(); assertNotNull(pushNotificationConfig); assertEquals("https://example.com/callback", pushNotificationConfig.url()); - PushNotificationAuthenticationInfo_v0_3 authenticationInfo = pushNotificationConfig.authentication(); + PushNotificationAuthenticationInfo_v0_3 authenticationInfo = + pushNotificationConfig.authentication(); assertTrue(authenticationInfo.schemes().size() == 1); assertEquals("jwt", authenticationInfo.schemes().get(0)); } @Test public void testA2AClientSetTaskPushNotificationConfig() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); - TaskPushNotificationConfig_v0_3 taskPushNotificationConfig = client.setTaskPushNotificationConfiguration( - new TaskPushNotificationConfig_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", - new PushNotificationConfig_v0_3.Builder() - .url("https://example.com/callback") - .authenticationInfo(new PushNotificationAuthenticationInfo_v0_3(Collections.singletonList("jwt"), - null)) - .build()), null); - PushNotificationConfig_v0_3 pushNotificationConfig = taskPushNotificationConfig.pushNotificationConfig(); + TaskPushNotificationConfig_v0_3 taskPushNotificationConfig = + client.setTaskPushNotificationConfiguration( + new TaskPushNotificationConfig_v0_3( + "de38c76d-d54c-436c-8b9f-4c2703648d64", + new PushNotificationConfig_v0_3.Builder() + .url("https://example.com/callback") + .authenticationInfo( + new PushNotificationAuthenticationInfo_v0_3( + Collections.singletonList("jwt"), null)) + .build()), + null); + PushNotificationConfig_v0_3 pushNotificationConfig = + taskPushNotificationConfig.pushNotificationConfig(); assertNotNull(pushNotificationConfig); assertEquals("https://example.com/callback", pushNotificationConfig.url()); - PushNotificationAuthenticationInfo_v0_3 authenticationInfo = pushNotificationConfig.authentication(); + PushNotificationAuthenticationInfo_v0_3 authenticationInfo = + pushNotificationConfig.authentication(); assertEquals(1, authenticationInfo.schemes().size()); assertEquals("jwt", authenticationInfo.schemes().get(0)); } - @Test public void testA2AClientGetAgentCard() throws Exception { - this.server.when( - request() - .withMethod("GET") - .withPath("/.well-known/agent-card.json") - ) - .respond( - response() - .withStatusCode(200) - .withBody(AGENT_CARD) - ); + this.server + .when(request().withMethod("GET").withPath("/.well-known/agent-card.json")) + .respond(response().withStatusCode(200).withBody(AGENT_CARD)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); AgentCard_v0_3 agentCard = client.getAgentCard(null); assertEquals("GeoSpatial Route Planner Agent", agentCard.name()); - assertEquals("Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", agentCard.description()); + assertEquals( + "Provides advanced route planning, traffic analysis, and custom map generation" + + " services. This agent can calculate optimal routes, estimate travel times" + + " considering real-time traffic, and create personalized maps with points of" + + " interest.", + agentCard.description()); assertEquals("https://georoute-agent.example.com/a2a/v1", agentCard.url()); assertEquals("Example Geo Services Inc.", agentCard.provider().organization()); assertEquals("https://www.examplegeoservices.com", agentCard.provider().url()); assertEquals("1.2.0", agentCard.version()); - assertEquals("https://docs.examplegeoservices.com/georoute-agent/api", agentCard.documentationUrl()); + assertEquals( + "https://docs.examplegeoservices.com/georoute-agent/api", + agentCard.documentationUrl()); assertTrue(agentCard.capabilities().streaming()); assertTrue(agentCard.capabilities().pushNotifications()); assertFalse(agentCard.capabilities().stateTransitionHistory()); Map securitySchemes = agentCard.securitySchemes(); assertNotNull(securitySchemes); - OpenIdConnectSecurityScheme_v0_3 google = (OpenIdConnectSecurityScheme_v0_3) securitySchemes.get("google"); + OpenIdConnectSecurityScheme_v0_3 google = + (OpenIdConnectSecurityScheme_v0_3) securitySchemes.get("google"); assertEquals("openIdConnect", google.type()); - assertEquals("https://accounts.google.com/.well-known/openid-configuration", google.openIdConnectUrl()); + assertEquals( + "https://accounts.google.com/.well-known/openid-configuration", + google.openIdConnectUrl()); List>> security = agentCard.security(); assertEquals(1, security.size()); Map> securityMap = security.get(0); @@ -395,22 +418,39 @@ public void testA2AClientGetAgentCard() throws Exception { List skills = agentCard.skills(); assertEquals("route-optimizer-traffic", skills.get(0).id()); assertEquals("Traffic-Aware Route Optimizer", skills.get(0).name()); - assertEquals("Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).", skills.get(0).description()); + assertEquals( + "Calculates the optimal driving route between two or more locations, taking into" + + " account real-time traffic conditions, road closures, and user preferences" + + " (e.g., avoid tolls, prefer highways).", + skills.get(0).description()); List tags = List.of("maps", "routing", "navigation", "directions", "traffic"); assertEquals(tags, skills.get(0).tags()); - List examples = List.of("Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.", - "{\"origin\": {\"lat\": 37.422, \"lng\": -122.084}, \"destination\": {\"lat\": 37.7749, \"lng\": -122.4194}, \"preferences\": [\"avoid_ferries\"]}"); + List examples = + List.of( + "Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San" + + " Francisco International Airport' avoiding tolls.", + "{\"origin\": {\"lat\": 37.422, \"lng\": -122.084}, \"destination\":" + + " {\"lat\": 37.7749, \"lng\": -122.4194}, \"preferences\":" + + " [\"avoid_ferries\"]}"); assertEquals(examples, skills.get(0).examples()); assertEquals(defaultInputModes, skills.get(0).inputModes()); - List outputModes = List.of("application/json", "application/vnd.geo+json", "text/html"); + List outputModes = + List.of("application/json", "application/vnd.geo+json", "text/html"); assertEquals(outputModes, skills.get(0).outputModes()); assertEquals("custom-map-generator", skills.get(1).id()); assertEquals("Personalized Map Generator", skills.get(1).name()); - assertEquals("Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.", skills.get(1).description()); + assertEquals( + "Creates custom map images or interactive map views based on user-defined points of" + + " interest, routes, and style preferences. Can overlay data layers.", + skills.get(1).description()); tags = List.of("maps", "customization", "visualization", "cartography"); assertEquals(tags, skills.get(1).tags()); - examples = List.of("Generate a map of my upcoming road trip with all planned stops highlighted.", - "Show me a map visualizing all coffee shops within a 1-mile radius of my current location."); + examples = + List.of( + "Generate a map of my upcoming road trip with all planned stops" + + " highlighted.", + "Show me a map visualizing all coffee shops within a 1-mile radius of my" + + " current location."); assertEquals(examples, skills.get(1).examples()); List inputModes = List.of("application/json"); assertEquals(inputModes, skills.get(1).inputModes()); @@ -422,9 +462,18 @@ public void testA2AClientGetAgentCard() throws Exception { assertEquals("JSONRPC", agentCard.preferredTransport()); List additionalInterfaces = agentCard.additionalInterfaces(); assertEquals(3, additionalInterfaces.size()); - AgentInterface_v0_3 jsonrpc = new AgentInterface_v0_3(TransportProtocol_v0_3.JSONRPC.asString(), "https://georoute-agent.example.com/a2a/v1"); - AgentInterface_v0_3 grpc = new AgentInterface_v0_3(TransportProtocol_v0_3.GRPC.asString(), "https://georoute-agent.example.com/a2a/grpc"); - AgentInterface_v0_3 httpJson = new AgentInterface_v0_3(TransportProtocol_v0_3.HTTP_JSON.asString(), "https://georoute-agent.example.com/a2a/json"); + AgentInterface_v0_3 jsonrpc = + new AgentInterface_v0_3( + TransportProtocol_v0_3.JSONRPC.asString(), + "https://georoute-agent.example.com/a2a/v1"); + AgentInterface_v0_3 grpc = + new AgentInterface_v0_3( + TransportProtocol_v0_3.GRPC.asString(), + "https://georoute-agent.example.com/a2a/grpc"); + AgentInterface_v0_3 httpJson = + new AgentInterface_v0_3( + TransportProtocol_v0_3.HTTP_JSON.asString(), + "https://georoute-agent.example.com/a2a/json"); assertEquals(jsonrpc, additionalInterfaces.get(0)); assertEquals(grpc, additionalInterfaces.get(1)); assertEquals(httpJson, additionalInterfaces.get(2)); @@ -432,27 +481,22 @@ public void testA2AClientGetAgentCard() throws Exception { @Test public void testA2AClientGetAuthenticatedExtendedAgentCard() throws Exception { - this.server.when( - request() - .withMethod("GET") - .withPath("/.well-known/agent-card.json") - ) - .respond( - response() - .withStatusCode(200) - .withBody(AGENT_CARD_SUPPORTS_EXTENDED) - ); - this.server.when( + this.server + .when(request().withMethod("GET").withPath("/.well-known/agent-card.json")) + .respond(response().withStatusCode(200).withBody(AGENT_CARD_SUPPORTS_EXTENDED)); + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) + .withBody( + JsonBody.json( + GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE) - ); + .withBody(GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); AgentCard_v0_3 agentCard = client.getAgentCard(null); @@ -462,15 +506,20 @@ public void testA2AClientGetAuthenticatedExtendedAgentCard() throws Exception { assertEquals("Example Geo Services Inc.", agentCard.provider().organization()); assertEquals("https://www.examplegeoservices.com", agentCard.provider().url()); assertEquals("1.2.0", agentCard.version()); - assertEquals("https://docs.examplegeoservices.com/georoute-agent/api", agentCard.documentationUrl()); + assertEquals( + "https://docs.examplegeoservices.com/georoute-agent/api", + agentCard.documentationUrl()); assertTrue(agentCard.capabilities().streaming()); assertTrue(agentCard.capabilities().pushNotifications()); assertFalse(agentCard.capabilities().stateTransitionHistory()); Map securitySchemes = agentCard.securitySchemes(); assertNotNull(securitySchemes); - OpenIdConnectSecurityScheme_v0_3 google = (OpenIdConnectSecurityScheme_v0_3) securitySchemes.get("google"); + OpenIdConnectSecurityScheme_v0_3 google = + (OpenIdConnectSecurityScheme_v0_3) securitySchemes.get("google"); assertEquals("openIdConnect", google.type()); - assertEquals("https://accounts.google.com/.well-known/openid-configuration", google.openIdConnectUrl()); + assertEquals( + "https://accounts.google.com/.well-known/openid-configuration", + google.openIdConnectUrl()); List>> security = agentCard.security(); assertEquals(1, security.size()); Map> securityMap = security.get(0); @@ -484,22 +533,39 @@ public void testA2AClientGetAuthenticatedExtendedAgentCard() throws Exception { List skills = agentCard.skills(); assertEquals("route-optimizer-traffic", skills.get(0).id()); assertEquals("Traffic-Aware Route Optimizer", skills.get(0).name()); - assertEquals("Calculates the optimal driving route between two or more locations, taking into account real-time traffic conditions, road closures, and user preferences (e.g., avoid tolls, prefer highways).", skills.get(0).description()); + assertEquals( + "Calculates the optimal driving route between two or more locations, taking into" + + " account real-time traffic conditions, road closures, and user preferences" + + " (e.g., avoid tolls, prefer highways).", + skills.get(0).description()); List tags = List.of("maps", "routing", "navigation", "directions", "traffic"); assertEquals(tags, skills.get(0).tags()); - List examples = List.of("Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San Francisco International Airport' avoiding tolls.", - "{\"origin\": {\"lat\": 37.422, \"lng\": -122.084}, \"destination\": {\"lat\": 37.7749, \"lng\": -122.4194}, \"preferences\": [\"avoid_ferries\"]}"); + List examples = + List.of( + "Plan a route from '1600 Amphitheatre Parkway, Mountain View, CA' to 'San" + + " Francisco International Airport' avoiding tolls.", + "{\"origin\": {\"lat\": 37.422, \"lng\": -122.084}, \"destination\":" + + " {\"lat\": 37.7749, \"lng\": -122.4194}, \"preferences\":" + + " [\"avoid_ferries\"]}"); assertEquals(examples, skills.get(0).examples()); assertEquals(defaultInputModes, skills.get(0).inputModes()); - List outputModes = List.of("application/json", "application/vnd.geo+json", "text/html"); + List outputModes = + List.of("application/json", "application/vnd.geo+json", "text/html"); assertEquals(outputModes, skills.get(0).outputModes()); assertEquals("custom-map-generator", skills.get(1).id()); assertEquals("Personalized Map Generator", skills.get(1).name()); - assertEquals("Creates custom map images or interactive map views based on user-defined points of interest, routes, and style preferences. Can overlay data layers.", skills.get(1).description()); + assertEquals( + "Creates custom map images or interactive map views based on user-defined points of" + + " interest, routes, and style preferences. Can overlay data layers.", + skills.get(1).description()); tags = List.of("maps", "customization", "visualization", "cartography"); assertEquals(tags, skills.get(1).tags()); - examples = List.of("Generate a map of my upcoming road trip with all planned stops highlighted.", - "Show me a map visualizing all coffee shops within a 1-mile radius of my current location."); + examples = + List.of( + "Generate a map of my upcoming road trip with all planned stops" + + " highlighted.", + "Show me a map visualizing all coffee shops within a 1-mile radius of my" + + " current location."); assertEquals(examples, skills.get(1).examples()); List inputModes = List.of("application/json"); assertEquals(inputModes, skills.get(1).inputModes()); @@ -516,37 +582,45 @@ public void testA2AClientGetAuthenticatedExtendedAgentCard() throws Exception { @Test public void testA2AClientSendMessageWithFilePart() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(List.of( - new TextPart_v0_3("analyze this image"), - new FilePart_v0_3(new FileWithUri_v0_3("image/jpeg", null, "file:///path/to/image.jpg")) - )) - .contextId("context-1234") - .messageId("message-1234-with-file") - .build(); - MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder() - .acceptedOutputModes(List.of("text")) - .blocking(true) - .build(); - MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder() - .message(message) - .configuration(configuration) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts( + List.of( + new TextPart_v0_3("analyze this image"), + new FilePart_v0_3( + new FileWithUri_v0_3( + "image/jpeg", + null, + "file:///path/to/image.jpg")))) + .contextId("context-1234") + .messageId("message-1234-with-file") + .build(); + MessageSendConfiguration_v0_3 configuration = + new MessageSendConfiguration_v0_3.Builder() + .acceptedOutputModes(List.of("text")) + .blocking(true) + .build(); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3.Builder() + .message(message) + .configuration(configuration) + .build(); EventKind_v0_3 result = client.sendMessage(params, null); assertInstanceOf(Task_v0_3.class, result); @@ -561,24 +635,27 @@ public void testA2AClientSendMessageWithFilePart() throws Exception { assertEquals(1, artifact.parts().size()); Part_v0_3 part = artifact.parts().get(0); assertEquals(Part_v0_3.Kind.TEXT, ((TextPart_v0_3) part).kind()); - assertEquals("This is an image of a cat sitting on a windowsill.", ((TextPart_v0_3) part).text()); + assertEquals( + "This is an image of a cat sitting on a windowsill.", + ((TextPart_v0_3) part).text()); assertTrue(task.metadata().isEmpty()); } @Test public void testA2AClientSendMessageWithDataPart() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); @@ -588,23 +665,26 @@ public void testA2AClientSendMessageWithDataPart() throws Exception { data.put("location", "San Francisco"); data.put("timestamp", "2024-01-15T10:30:00Z"); - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(List.of( - new TextPart_v0_3("process this data"), - new DataPart_v0_3(data) - )) - .contextId("context-1234") - .messageId("message-1234-with-data") - .build(); - MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder() - .acceptedOutputModes(List.of("text")) - .blocking(true) - .build(); - MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder() - .message(message) - .configuration(configuration) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts( + List.of( + new TextPart_v0_3("process this data"), + new DataPart_v0_3(data))) + .contextId("context-1234") + .messageId("message-1234-with-data") + .build(); + MessageSendConfiguration_v0_3 configuration = + new MessageSendConfiguration_v0_3.Builder() + .acceptedOutputModes(List.of("text")) + .blocking(true) + .build(); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3.Builder() + .message(message) + .configuration(configuration) + .build(); EventKind_v0_3 result = client.sendMessage(params, null); assertInstanceOf(Task_v0_3.class, result); @@ -619,24 +699,28 @@ public void testA2AClientSendMessageWithDataPart() throws Exception { assertEquals(1, artifact.parts().size()); Part_v0_3 part = artifact.parts().get(0); assertEquals(Part_v0_3.Kind.TEXT, ((TextPart_v0_3) part).kind()); - assertEquals("Processed weather data: Temperature is 25.5°C, humidity is 60.2% in San Francisco.", ((TextPart_v0_3) part).text()); + assertEquals( + "Processed weather data: Temperature is 25.5°C, humidity is 60.2% in San" + + " Francisco.", + ((TextPart_v0_3) part).text()); assertTrue(task.metadata().isEmpty()); } @Test public void testA2AClientSendMessageWithMixedParts() throws Exception { - this.server.when( + this.server + .when( request() .withMethod("POST") .withPath("/") - .withBody(JsonBody.json(SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - - ) + .withBody( + JsonBody.json( + SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE)); JSONRPCTransport_v0_3 client = new JSONRPCTransport_v0_3("http://localhost:4001"); @@ -645,24 +729,29 @@ public void testA2AClientSendMessageWithMixedParts() throws Exception { data.put("dataPoints", List.of(10, 20, 30, 40)); data.put("labels", List.of("Q1", "Q2", "Q3", "Q4")); - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(List.of( - new TextPart_v0_3("analyze this data and image"), - new FilePart_v0_3(new FileWithBytes_v0_3("image/png", "chart.png", "aGVsbG8=")), - new DataPart_v0_3(data) - )) - .contextId("context-1234") - .messageId("message-1234-with-mixed") - .build(); - MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder() - .acceptedOutputModes(List.of("text")) - .blocking(true) - .build(); - MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder() - .message(message) - .configuration(configuration) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts( + List.of( + new TextPart_v0_3("analyze this data and image"), + new FilePart_v0_3( + new FileWithBytes_v0_3( + "image/png", "chart.png", "aGVsbG8=")), + new DataPart_v0_3(data))) + .contextId("context-1234") + .messageId("message-1234-with-mixed") + .build(); + MessageSendConfiguration_v0_3 configuration = + new MessageSendConfiguration_v0_3.Builder() + .acceptedOutputModes(List.of("text")) + .blocking(true) + .build(); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3.Builder() + .message(message) + .configuration(configuration) + .build(); EventKind_v0_3 result = client.sendMessage(params, null); assertInstanceOf(Task_v0_3.class, result); @@ -677,7 +766,10 @@ public void testA2AClientSendMessageWithMixedParts() throws Exception { assertEquals(1, artifact.parts().size()); Part_v0_3 part = artifact.parts().get(0); assertEquals(Part_v0_3.Kind.TEXT, ((TextPart_v0_3) part).kind()); - assertEquals("Analyzed chart image and data: Bar chart showing quarterly data with values [10, 20, 30, 40].", ((TextPart_v0_3) part).text()); + assertEquals( + "Analyzed chart image and data: Bar chart showing quarterly data with values [10," + + " 20, 30, 40].", + ((TextPart_v0_3) part).text()); assertTrue(task.metadata().isEmpty()); } -} \ No newline at end of file +} diff --git a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JsonMessages_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JsonMessages_v0_3.java index a23a4b5e6..810aa74f1 100644 --- a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JsonMessages_v0_3.java +++ b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JsonMessages_v0_3.java @@ -6,7 +6,8 @@ */ public class JsonMessages_v0_3 { - static final String AGENT_CARD = """ + static final String AGENT_CARD = + """ { "protocolVersion": "0.2.9", "name": "GeoSpatial Route Planner Agent", @@ -81,9 +82,11 @@ public class JsonMessages_v0_3 { "signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ" } ] - }"""; + }\ + """; - static final String AUTHENTICATION_EXTENDED_AGENT_CARD = """ + static final String AUTHENTICATION_EXTENDED_AGENT_CARD = + """ { "name": "GeoSpatial Route Planner Agent Extended", "description": "Extended description", @@ -158,9 +161,11 @@ public class JsonMessages_v0_3 { "signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ" } ] - }"""; + }\ + """; - static final String SEND_MESSAGE_TEST_REQUEST = """ + static final String SEND_MESSAGE_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -182,9 +187,11 @@ public class JsonMessages_v0_3 { "blocking": true }, } - }"""; + }\ + """; - static final String SEND_MESSAGE_TEST_RESPONSE = """ + static final String SEND_MESSAGE_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -208,9 +215,11 @@ public class JsonMessages_v0_3 { "metadata": {}, "kind": "task" } - }"""; + }\ + """; - static final String SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE = """ + static final String SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -232,10 +241,11 @@ public class JsonMessages_v0_3 { "blocking": true }, } - }"""; - + }\ + """; - static final String SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE = """ + static final String SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE = + """ { "jsonrpc": "2.0", "id": 1, @@ -250,9 +260,11 @@ public class JsonMessages_v0_3 { "messageId": "msg-456", "kind": "message" } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_ERROR_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_ERROR_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -274,9 +286,11 @@ public class JsonMessages_v0_3 { "blocking": true }, } - }"""; + }\ + """; - static final String SEND_MESSAGE_ERROR_TEST_RESPONSE = """ + static final String SEND_MESSAGE_ERROR_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "error": { @@ -284,9 +298,11 @@ public class JsonMessages_v0_3 { "message": "Invalid parameters", "data": "Hello world" } - }"""; + }\ + """; - static final String GET_TASK_TEST_REQUEST = """ + static final String GET_TASK_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "tasks/get", @@ -297,7 +313,8 @@ public class JsonMessages_v0_3 { } """; - static final String GET_TASK_TEST_RESPONSE = """ + static final String GET_TASK_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -350,7 +367,8 @@ public class JsonMessages_v0_3 { } """; - static final String CANCEL_TASK_TEST_REQUEST = """ + static final String CANCEL_TASK_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "tasks/cancel", @@ -361,7 +379,8 @@ public class JsonMessages_v0_3 { } """; - static final String CANCEL_TASK_TEST_RESPONSE = """ + static final String CANCEL_TASK_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -376,7 +395,8 @@ public class JsonMessages_v0_3 { } """; - static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = """ + static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "tasks/pushNotificationConfig/get", @@ -387,7 +407,8 @@ public class JsonMessages_v0_3 { } """; - static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """ + static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -402,7 +423,8 @@ public class JsonMessages_v0_3 { } """; - static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = """ + static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "tasks/pushNotificationConfig/set", @@ -415,9 +437,11 @@ public class JsonMessages_v0_3 { } } } - }"""; + }\ + """; - static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """ + static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -432,7 +456,8 @@ public class JsonMessages_v0_3 { } """; - static final String SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -461,9 +486,11 @@ public class JsonMessages_v0_3 { "blocking": true } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE = """ + static final String SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -487,9 +514,11 @@ public class JsonMessages_v0_3 { "metadata": {}, "kind": "task" } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -520,9 +549,11 @@ public class JsonMessages_v0_3 { "blocking": true } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE = """ + static final String SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -546,9 +577,11 @@ public class JsonMessages_v0_3 { "metadata": {}, "kind": "task" } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -586,9 +619,11 @@ public class JsonMessages_v0_3 { "blocking": true } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE = """ + static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -612,16 +647,19 @@ public class JsonMessages_v0_3 { "metadata": {}, "kind": "task" } - }"""; + }\ + """; - static final String GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST = """ + static final String GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST = + """ { "jsonrpc": "2.0", "method": "agent/getAuthenticatedExtendedCard" } """; - static final String GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE = """ + static final String GET_AUTHENTICATED_EXTENDED_AGENT_CARD_RESPONSE = + """ { "jsonrpc": "2.0", "id": "1", @@ -700,9 +738,11 @@ public class JsonMessages_v0_3 { } ] } - }"""; + }\ + """; - static final String AGENT_CARD_SUPPORTS_EXTENDED = """ + static final String AGENT_CARD_SUPPORTS_EXTENDED = + """ { "name": "GeoSpatial Route Planner Agent", "description": "Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", @@ -765,5 +805,6 @@ public class JsonMessages_v0_3 { ], "supportsAuthenticatedExtendedCard": true, "protocolVersion": "0.2.5" - }"""; + }\ + """; } diff --git a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JsonStreamingMessages_v0_3.java b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JsonStreamingMessages_v0_3.java index feb12572a..08f91baa6 100644 --- a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JsonStreamingMessages_v0_3.java +++ b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/JsonStreamingMessages_v0_3.java @@ -5,7 +5,8 @@ */ public class JsonStreamingMessages_v0_3 { - public static final String STREAMING_TASK_EVENT = """ + public static final String STREAMING_TASK_EVENT = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -20,8 +21,8 @@ public class JsonStreamingMessages_v0_3 { } """; - - public static final String STREAMING_MESSAGE_EVENT = """ + public static final String STREAMING_MESSAGE_EVENT = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -37,9 +38,11 @@ public class JsonStreamingMessages_v0_3 { } ] } - }"""; + }\ + """; - public static final String STREAMING_STATUS_UPDATE_EVENT = """ + public static final String STREAMING_STATUS_UPDATE_EVENT = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -52,9 +55,11 @@ public class JsonStreamingMessages_v0_3 { "final": false, "kind": "status-update" } - }"""; + }\ + """; - public static final String STREAMING_STATUS_UPDATE_EVENT_FINAL = """ + public static final String STREAMING_STATUS_UPDATE_EVENT_FINAL = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -67,31 +72,35 @@ public class JsonStreamingMessages_v0_3 { "final": true, "kind": "status-update" } - }"""; + }\ + """; - public static final String STREAMING_ARTIFACT_UPDATE_EVENT = """ - data: { - "jsonrpc": "2.0", - "id": "1234", - "result": { - "kind": "artifact-update", - "taskId": "1", - "contextId": "2", - "append": false, - "lastChunk": true, - "artifact": { - "artifactId": "artifact-1", - "parts": [ - { - "kind": "text", - "text": "Why did the chicken cross the road? To get to the other side!" - } - ] - } - } - }"""; + public static final String STREAMING_ARTIFACT_UPDATE_EVENT = + """ + data: { + "jsonrpc": "2.0", + "id": "1234", + "result": { + "kind": "artifact-update", + "taskId": "1", + "contextId": "2", + "append": false, + "lastChunk": true, + "artifact": { + "artifactId": "artifact-1", + "parts": [ + { + "kind": "text", + "text": "Why did the chicken cross the road? To get to the other side!" + } + ] + } + } + }\ + """; - public static final String STREAMING_ERROR_EVENT = """ + public static final String STREAMING_ERROR_EVENT = + """ data: { "jsonrpc": "2.0", "id": "1234", @@ -100,9 +109,11 @@ public class JsonStreamingMessages_v0_3 { "message": "Invalid parameters", "data": "Missing required field" } - }"""; + }\ + """; - public static final String SEND_MESSAGE_STREAMING_TEST_REQUEST = """ + public static final String SEND_MESSAGE_STREAMING_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "message/stream", @@ -124,22 +135,31 @@ public class JsonStreamingMessages_v0_3 { "blocking": false }, } - }"""; + }\ + """; static final String SEND_MESSAGE_STREAMING_TEST_RESPONSE = - "event: message\n" + - "data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"completed\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"kind\":\"text\",\"text\":\"Why did the chicken cross the road? To get to the other side!\"}]}],\"metadata\":{},\"kind\":\"task\"}}\n\n"; + "event: message\n" + + "data:" + + " {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"completed\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"kind\":\"text\",\"text\":\"Why" + + " did the chicken cross the road? To get to the other" + + " side!\"}]}],\"metadata\":{},\"kind\":\"task\"}}\n\n"; static final String TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE = - "event: message\n" + - "data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"completed\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"kind\":\"text\",\"text\":\"Why did the chicken cross the road? To get to the other side!\"}]}],\"metadata\":{},\"kind\":\"task\"}}\n\n"; + "event: message\n" + + "data:" + + " {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"completed\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"kind\":\"text\",\"text\":\"Why" + + " did the chicken cross the road? To get to the other" + + " side!\"}]}],\"metadata\":{},\"kind\":\"task\"}}\n\n"; - public static final String TASK_RESUBSCRIPTION_TEST_REQUEST = """ + public static final String TASK_RESUBSCRIPTION_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "tasks/resubscribe", "params": { "id": "task-1234" } - }"""; -} \ No newline at end of file + }\ + """; +} diff --git a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3_Test.java b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3_Test.java index 0f22828a5..e258d2c5d 100644 --- a/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3_Test.java +++ b/compat-0.3/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/client/transport/jsonrpc/sse/SSEEventListener_v0_3_Test.java @@ -9,27 +9,26 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; - import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JsonStreamingMessages_v0_3; import org.a2aproject.sdk.compat03.spec.Artifact_v0_3; import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; +import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; import org.junit.jupiter.api.Test; @@ -39,15 +38,14 @@ public class SSEEventListener_v0_3_Test { public void testOnEventWithTaskResult() throws Exception { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3(event -> receivedEvent.set(event), error -> {}); // Parse the task event JSON - String eventData = JsonStreamingMessages_v0_3.STREAMING_TASK_EVENT.substring( - JsonStreamingMessages_v0_3.STREAMING_TASK_EVENT.indexOf("{")); - + String eventData = + JsonStreamingMessages_v0_3.STREAMING_TASK_EVENT.substring( + JsonStreamingMessages_v0_3.STREAMING_TASK_EVENT.indexOf("{")); + // Call the onEvent method directly listener.onMessage(eventData, null); @@ -64,15 +62,14 @@ public void testOnEventWithTaskResult() throws Exception { public void testOnEventWithMessageResult() throws Exception { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3(event -> receivedEvent.set(event), error -> {}); // Parse the message event JSON - String eventData = JsonStreamingMessages_v0_3.STREAMING_MESSAGE_EVENT.substring( - JsonStreamingMessages_v0_3.STREAMING_MESSAGE_EVENT.indexOf("{")); - + String eventData = + JsonStreamingMessages_v0_3.STREAMING_MESSAGE_EVENT.substring( + JsonStreamingMessages_v0_3.STREAMING_MESSAGE_EVENT.indexOf("{")); + // Call onEvent method listener.onMessage(eventData, null); @@ -92,14 +89,13 @@ public void testOnEventWithMessageResult() throws Exception { public void testOnEventWithTaskStatusUpdateEventEvent() throws Exception { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3(event -> receivedEvent.set(event), error -> {}); // Parse the message event JSON - String eventData = JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT.substring( - JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT.indexOf("{")); + String eventData = + JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT.substring( + JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT.indexOf("{")); // Call onEvent method listener.onMessage(eventData, null); @@ -107,7 +103,8 @@ public void testOnEventWithTaskStatusUpdateEventEvent() throws Exception { // Verify the event was processed correctly assertNotNull(receivedEvent.get()); assertTrue(receivedEvent.get() instanceof TaskStatusUpdateEvent_v0_3); - TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent = (TaskStatusUpdateEvent_v0_3) receivedEvent.get(); + TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent = + (TaskStatusUpdateEvent_v0_3) receivedEvent.get(); assertEquals("1", taskStatusUpdateEvent.taskId()); assertEquals("2", taskStatusUpdateEvent.contextId()); assertFalse(taskStatusUpdateEvent.isFinal()); @@ -118,14 +115,13 @@ public void testOnEventWithTaskStatusUpdateEventEvent() throws Exception { public void testOnEventWithTaskArtifactUpdateEventEvent() throws Exception { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3(event -> receivedEvent.set(event), error -> {}); // Parse the message event JSON - String eventData = JsonStreamingMessages_v0_3.STREAMING_ARTIFACT_UPDATE_EVENT.substring( - JsonStreamingMessages_v0_3.STREAMING_ARTIFACT_UPDATE_EVENT.indexOf("{")); + String eventData = + JsonStreamingMessages_v0_3.STREAMING_ARTIFACT_UPDATE_EVENT.substring( + JsonStreamingMessages_v0_3.STREAMING_ARTIFACT_UPDATE_EVENT.indexOf("{")); // Call onEvent method listener.onMessage(eventData, null); @@ -134,7 +130,8 @@ public void testOnEventWithTaskArtifactUpdateEventEvent() throws Exception { assertNotNull(receivedEvent.get()); assertTrue(receivedEvent.get() instanceof TaskArtifactUpdateEvent_v0_3); - TaskArtifactUpdateEvent_v0_3 taskArtifactUpdateEvent = (TaskArtifactUpdateEvent_v0_3) receivedEvent.get(); + TaskArtifactUpdateEvent_v0_3 taskArtifactUpdateEvent = + (TaskArtifactUpdateEvent_v0_3) receivedEvent.get(); assertEquals("1", taskArtifactUpdateEvent.taskId()); assertEquals("2", taskArtifactUpdateEvent.contextId()); assertFalse(taskArtifactUpdateEvent.append()); @@ -143,22 +140,23 @@ public void testOnEventWithTaskArtifactUpdateEventEvent() throws Exception { assertEquals("artifact-1", artifact.artifactId()); assertEquals(1, artifact.parts().size()); assertEquals(Part_v0_3.Kind.TEXT, ((TextPart_v0_3) artifact.parts().get(0)).kind()); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) artifact.parts().get(0)).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart_v0_3) artifact.parts().get(0)).text()); } @Test public void testOnEventWithError() throws Exception { // Set up event handler AtomicReference receivedError = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> {}, - error -> receivedError.set(error) - ); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3(event -> {}, error -> receivedError.set(error)); // Parse the error event JSON - String eventData = JsonStreamingMessages_v0_3.STREAMING_ERROR_EVENT.substring( - JsonStreamingMessages_v0_3.STREAMING_ERROR_EVENT.indexOf("{")); - + String eventData = + JsonStreamingMessages_v0_3.STREAMING_ERROR_EVENT.substring( + JsonStreamingMessages_v0_3.STREAMING_ERROR_EVENT.indexOf("{")); + // Call onEvent method listener.onMessage(eventData, null); @@ -174,10 +172,8 @@ public void testOnEventWithError() throws Exception { @Test public void testOnFailure() { AtomicBoolean failureHandlerCalled = new AtomicBoolean(false); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> {}, - error -> failureHandlerCalled.set(true) - ); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3(event -> {}, error -> failureHandlerCalled.set(true)); // Simulate a failure CancelCapturingFuture future = new CancelCapturingFuture(); @@ -191,35 +187,32 @@ public void testOnFailure() { @Test public void testFinalTaskStatusUpdateEventCancels() { - TaskStatusUpdateEvent_v0_3 tsue = new TaskStatusUpdateEvent_v0_3.Builder() - .taskId("1234") - .contextId("xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) - .isFinal(true) - .build(); + TaskStatusUpdateEvent_v0_3 tsue = + new TaskStatusUpdateEvent_v0_3.Builder() + .taskId("1234") + .contextId("xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) + .isFinal(true) + .build(); // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> receivedEvent.set(event), - error -> {} - ); - - + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3(event -> receivedEvent.set(event), error -> {}); } @Test public void testOnEventWithFinalTaskStatusUpdateEventEventCancels() throws Exception { // Set up event handler AtomicReference receivedEvent = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> receivedEvent.set(event), - error -> {} - ); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3(event -> receivedEvent.set(event), error -> {}); // Parse the message event JSON - String eventData = JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( - JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{")); + String eventData = + JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( + JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf( + "{")); // Call onEvent method CancelCapturingFuture future = new CancelCapturingFuture(); @@ -228,7 +221,8 @@ public void testOnEventWithFinalTaskStatusUpdateEventEventCancels() throws Excep // Verify the event was processed correctly assertNotNull(receivedEvent.get()); assertTrue(receivedEvent.get() instanceof TaskStatusUpdateEvent_v0_3); - TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent = (TaskStatusUpdateEvent_v0_3) receivedEvent.get(); + TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent = + (TaskStatusUpdateEvent_v0_3) receivedEvent.get(); assertEquals("1", taskStatusUpdateEvent.taskId()); assertEquals("2", taskStatusUpdateEvent.contextId()); assertTrue(taskStatusUpdateEvent.isFinal()); @@ -237,15 +231,17 @@ public void testOnEventWithFinalTaskStatusUpdateEventEventCancels() throws Excep assertTrue(future.cancelHandlerCalled); } - - @Test public void testOnCompleteThenOnErrorDeliversSingleTerminalCallback() { AtomicInteger terminalCount = new AtomicInteger(0); AtomicReference lastArg = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> {}, - error -> { terminalCount.incrementAndGet(); lastArg.set(error); }); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3( + event -> {}, + error -> { + terminalCount.incrementAndGet(); + lastArg.set(error); + }); listener.onComplete(); listener.onError(new RuntimeException("late error"), new CancelCapturingFuture()); @@ -258,9 +254,13 @@ public void testOnCompleteThenOnErrorDeliversSingleTerminalCallback() { public void testOnErrorThenOnCompleteDeliversSingleTerminalCallback() { AtomicInteger terminalCount = new AtomicInteger(0); AtomicReference lastArg = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> {}, - error -> { terminalCount.incrementAndGet(); lastArg.set(error); }); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3( + event -> {}, + error -> { + terminalCount.incrementAndGet(); + lastArg.set(error); + }); RuntimeException boom = new RuntimeException("first error"); listener.onError(boom, new CancelCapturingFuture()); @@ -274,9 +274,13 @@ public void testOnErrorThenOnCompleteDeliversSingleTerminalCallback() { public void testRepeatedOnCompleteDeliversSingleTerminalCallback() { AtomicInteger terminalCount = new AtomicInteger(0); AtomicReference lastArg = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> {}, - error -> { terminalCount.incrementAndGet(); lastArg.set(error); }); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3( + event -> {}, + error -> { + terminalCount.incrementAndGet(); + lastArg.set(error); + }); listener.onComplete(); listener.onComplete(); @@ -289,9 +293,13 @@ public void testRepeatedOnCompleteDeliversSingleTerminalCallback() { public void testConcurrentTerminalSignalsDeliverExactlyOneCallback() throws Exception { AtomicInteger terminalCount = new AtomicInteger(0); AtomicReference lastArg = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> {}, - error -> { terminalCount.incrementAndGet(); lastArg.set(error); }); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3( + event -> {}, + error -> { + terminalCount.incrementAndGet(); + lastArg.set(error); + }); int threads = 32; ExecutorService pool = Executors.newFixedThreadPool(threads); @@ -299,20 +307,22 @@ public void testConcurrentTerminalSignalsDeliverExactlyOneCallback() throws Exce CountDownLatch done = new CountDownLatch(threads); for (int i = 0; i < threads; i++) { final boolean complete = (i % 2 == 0); - pool.submit(() -> { - try { - start.await(); - if (complete) { - listener.onComplete(); - } else { - listener.onError(new RuntimeException("race"), new CancelCapturingFuture()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } finally { - done.countDown(); - } - }); + pool.submit( + () -> { + try { + start.await(); + if (complete) { + listener.onComplete(); + } else { + listener.onError( + new RuntimeException("race"), new CancelCapturingFuture()); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + done.countDown(); + } + }); } start.countDown(); assertTrue(done.await(10, TimeUnit.SECONDS)); @@ -325,15 +335,18 @@ public void testConcurrentTerminalSignalsDeliverExactlyOneCallback() throws Exce public void testFinalEventThenOnErrorDeliversNormalCompletionOnly() { AtomicInteger terminalCount = new AtomicInteger(0); AtomicReference terminalError = new AtomicReference<>(); - SSEEventListener_v0_3 listener = new SSEEventListener_v0_3( - event -> {}, - error -> { - terminalCount.incrementAndGet(); - terminalError.set(error); - }); - - String eventData = JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( - JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf("{")); + SSEEventListener_v0_3 listener = + new SSEEventListener_v0_3( + event -> {}, + error -> { + terminalCount.incrementAndGet(); + terminalError.set(error); + }); + + String eventData = + JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT_FINAL.substring( + JsonStreamingMessages_v0_3.STREAMING_STATUS_UPDATE_EVENT_FINAL.indexOf( + "{")); CancelCapturingFuture future = new CancelCapturingFuture(); listener.onMessage(eventData, future); listener.onError(new RuntimeException("cancelled after final event"), future); @@ -346,8 +359,7 @@ public void testFinalEventThenOnErrorDeliversNormalCompletionOnly() { private static class CancelCapturingFuture implements Future { private boolean cancelHandlerCalled; - public CancelCapturingFuture() { - } + public CancelCapturingFuture() {} @Override public boolean cancel(boolean mayInterruptIfRunning) { @@ -371,7 +383,8 @@ public Void get() throws InterruptedException, ExecutionException { } @Override - public Void get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + public Void get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { return null; } } diff --git a/compat-0.3/client/transport/rest/pom.xml b/compat-0.3/client/transport/rest/pom.xml index f0d76463f..e5f0aad3b 100644 --- a/compat-0.3/client/transport/rest/pom.xml +++ b/compat-0.3/client/transport/rest/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -59,4 +57,4 @@ - \ No newline at end of file + diff --git a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3.java b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3.java index d9b6d1368..c5d930939 100644 --- a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3.java +++ b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestErrorMapper_v0_3.java @@ -2,9 +2,11 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; -import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.AuthenticatedExtendedCardNotConfiguredError_v0_3; import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3; @@ -18,8 +20,6 @@ import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3; -import java.util.logging.Level; -import java.util.logging.Logger; /** * Utility class to A2AHttpResponse to appropriate A2A error types @@ -41,34 +41,58 @@ public static A2AClientException_v0_3 mapRestError(String body, int code) { return mapRestError("", "", code); } catch (JsonProcessingException_v0_3 ex) { Logger.getLogger(RestErrorMapper_v0_3.class.getName()).log(Level.SEVERE, null, ex); - return new A2AClientException_v0_3("Failed to parse error response: " + ex.getMessage()); + return new A2AClientException_v0_3( + "Failed to parse error response: " + ex.getMessage()); } } private static String safeGetString(JsonObject obj, String fieldName) { if (obj.has(fieldName)) { JsonElement element = obj.get(fieldName); - if (element != null && element.isJsonPrimitive() && element.getAsJsonPrimitive().isString()) { + if (element != null + && element.isJsonPrimitive() + && element.getAsJsonPrimitive().isString()) { return element.getAsString(); } } return ""; } - public static A2AClientException_v0_3 mapRestError(String className, String errorMessage, int code) { + public static A2AClientException_v0_3 mapRestError( + String className, String errorMessage, int code) { return switch (className) { - case "org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3" -> new A2AClientException_v0_3(errorMessage, new TaskNotFoundError_v0_3()); - case "org.a2aproject.sdk.compat03.spec.AuthenticatedExtendedCardNotConfiguredError_v0_3" -> new A2AClientException_v0_3(errorMessage, new AuthenticatedExtendedCardNotConfiguredError_v0_3(null, errorMessage, null)); - case "org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3" -> new A2AClientException_v0_3(errorMessage, new ContentTypeNotSupportedError_v0_3(null, null, errorMessage)); - case "org.a2aproject.sdk.compat03.spec.InternalError_v0_3" -> new A2AClientException_v0_3(errorMessage, new InternalError_v0_3(errorMessage)); - case "org.a2aproject.sdk.compat03.spec.InvalidAgentResponseError_v0_3" -> new A2AClientException_v0_3(errorMessage, new InvalidAgentResponseError_v0_3(null, null, errorMessage)); - case "org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3" -> new A2AClientException_v0_3(errorMessage, new InvalidParamsError_v0_3()); - case "org.a2aproject.sdk.compat03.spec.InvalidRequestError_v0_3" -> new A2AClientException_v0_3(errorMessage, new InvalidRequestError_v0_3()); - case "org.a2aproject.sdk.compat03.spec.JSONParseError_v0_3" -> new A2AClientException_v0_3(errorMessage, new JSONParseError_v0_3()); - case "org.a2aproject.sdk.compat03.spec.MethodNotFoundError_v0_3" -> new A2AClientException_v0_3(errorMessage, new MethodNotFoundError_v0_3()); - case "org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError_v0_3" -> new A2AClientException_v0_3(errorMessage, new PushNotificationNotSupportedError_v0_3()); - case "org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3" -> new A2AClientException_v0_3(errorMessage, new TaskNotCancelableError_v0_3()); - case "org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3" -> new A2AClientException_v0_3(errorMessage, new UnsupportedOperationError_v0_3()); + case "org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3" -> + new A2AClientException_v0_3(errorMessage, new TaskNotFoundError_v0_3()); + case "org.a2aproject.sdk.compat03.spec.AuthenticatedExtendedCardNotConfiguredError_v0_3" -> + new A2AClientException_v0_3( + errorMessage, + new AuthenticatedExtendedCardNotConfiguredError_v0_3( + null, errorMessage, null)); + case "org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3" -> + new A2AClientException_v0_3( + errorMessage, + new ContentTypeNotSupportedError_v0_3(null, null, errorMessage)); + case "org.a2aproject.sdk.compat03.spec.InternalError_v0_3" -> + new A2AClientException_v0_3(errorMessage, new InternalError_v0_3(errorMessage)); + case "org.a2aproject.sdk.compat03.spec.InvalidAgentResponseError_v0_3" -> + new A2AClientException_v0_3( + errorMessage, + new InvalidAgentResponseError_v0_3(null, null, errorMessage)); + case "org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3" -> + new A2AClientException_v0_3(errorMessage, new InvalidParamsError_v0_3()); + case "org.a2aproject.sdk.compat03.spec.InvalidRequestError_v0_3" -> + new A2AClientException_v0_3(errorMessage, new InvalidRequestError_v0_3()); + case "org.a2aproject.sdk.compat03.spec.JSONParseError_v0_3" -> + new A2AClientException_v0_3(errorMessage, new JSONParseError_v0_3()); + case "org.a2aproject.sdk.compat03.spec.MethodNotFoundError_v0_3" -> + new A2AClientException_v0_3(errorMessage, new MethodNotFoundError_v0_3()); + case "org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError_v0_3" -> + new A2AClientException_v0_3( + errorMessage, new PushNotificationNotSupportedError_v0_3()); + case "org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3" -> + new A2AClientException_v0_3(errorMessage, new TaskNotCancelableError_v0_3()); + case "org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3" -> + new A2AClientException_v0_3(errorMessage, new UnsupportedOperationError_v0_3()); default -> new A2AClientException_v0_3(errorMessage); }; } diff --git a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransportConfigBuilder_v0_3.java b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransportConfigBuilder_v0_3.java index cab5e3a82..404580b1e 100644 --- a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransportConfigBuilder_v0_3.java +++ b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransportConfigBuilder_v0_3.java @@ -5,7 +5,9 @@ import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransportConfigBuilder_v0_3; import org.jspecify.annotations.Nullable; -public class RestTransportConfigBuilder_v0_3 extends ClientTransportConfigBuilder_v0_3 { +public class RestTransportConfigBuilder_v0_3 + extends ClientTransportConfigBuilder_v0_3< + RestTransportConfig_v0_3, RestTransportConfigBuilder_v0_3> { private @Nullable A2AHttpClient httpClient; diff --git a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransportProvider_v0_3.java b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransportProvider_v0_3.java index 46cf3fa1b..c559a4b36 100644 --- a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransportProvider_v0_3.java +++ b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransportProvider_v0_3.java @@ -6,7 +6,8 @@ import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -public class RestTransportProvider_v0_3 implements ClientTransportProvider_v0_3 { +public class RestTransportProvider_v0_3 + implements ClientTransportProvider_v0_3 { @Override public String getTransportProtocol() { @@ -14,11 +15,19 @@ public String getTransportProtocol() { } @Override - public RestTransport_v0_3 create(RestTransportConfig_v0_3 clientTransportConfig, AgentCard_v0_3 agentCard, String agentUrl) throws A2AClientException_v0_3 { + public RestTransport_v0_3 create( + RestTransportConfig_v0_3 clientTransportConfig, + AgentCard_v0_3 agentCard, + String agentUrl) + throws A2AClientException_v0_3 { if (clientTransportConfig == null) { clientTransportConfig = new RestTransportConfig_v0_3(A2AHttpClientFactory.create()); } - return new RestTransport_v0_3(clientTransportConfig.getHttpClient(), agentCard, agentUrl, clientTransportConfig.getInterceptors()); + return new RestTransport_v0_3( + clientTransportConfig.getHttpClient(), + agentCard, + agentUrl, + clientTransportConfig.getInterceptors()); } @Override diff --git a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3.java b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3.java index 272e3c2f1..36091c6b2 100644 --- a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3.java +++ b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3.java @@ -2,14 +2,21 @@ import static org.a2aproject.sdk.util.Assert.checkNotNullParam; -import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageOrBuilder; import com.google.protobuf.util.JsonFormat; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.client.http.A2AHttpResponse; -import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.compat03.client.http.A2ACardResolver_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.sse.RestSSEEventListener_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.ClientTransport_v0_3; @@ -21,41 +28,33 @@ import org.a2aproject.sdk.compat03.grpc.GetTaskPushNotificationConfigRequest; import org.a2aproject.sdk.compat03.grpc.GetTaskRequest; import org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigRequest; -import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.GetTaskRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.SendMessageRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; +import org.a2aproject.sdk.compat03.grpc.utils.ProtoJsonUtils_v0_3; +import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3; +import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; +import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; +import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; +import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3; import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams_v0_3; +import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3; import org.a2aproject.sdk.compat03.spec.EventKind_v0_3; import org.a2aproject.sdk.compat03.spec.GetAuthenticatedExtendedCardRequest_v0_3; import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3; +import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigRequest_v0_3; +import org.a2aproject.sdk.compat03.spec.GetTaskRequest_v0_3; import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3; +import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; +import org.a2aproject.sdk.compat03.spec.SendMessageRequest_v0_3; +import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest_v0_3; +import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; +import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; -import org.a2aproject.sdk.compat03.grpc.utils.ProtoJsonUtils_v0_3; -import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3; -import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3; -import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3; -import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; +import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.grpc.utils.ProtoJsonUtils; import org.jspecify.annotations.Nullable; @@ -72,22 +71,33 @@ public RestTransport_v0_3(AgentCard_v0_3 agentCard) { this(null, agentCard, agentCard.url(), null); } - public RestTransport_v0_3(@Nullable A2AHttpClient httpClient, AgentCard_v0_3 agentCard, - String agentUrl, @Nullable List interceptors) { + public RestTransport_v0_3( + @Nullable A2AHttpClient httpClient, + AgentCard_v0_3 agentCard, + String agentUrl, + @Nullable List interceptors) { this.httpClient = httpClient == null ? A2AHttpClientFactory.create() : httpClient; this.agentCard = agentCard; - this.agentUrl = agentUrl.endsWith("/") ? agentUrl.substring(0, agentUrl.length() - 1) : agentUrl; + this.agentUrl = + agentUrl.endsWith("/") ? agentUrl.substring(0, agentUrl.length() - 1) : agentUrl; this.interceptors = interceptors; } @Override - public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 messageSendParams, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public EventKind_v0_3 sendMessage( + MessageSendParams_v0_3 messageSendParams, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("messageSendParams", messageSendParams); - org.a2aproject.sdk.compat03.grpc.SendMessageRequest.Builder builder = org.a2aproject.sdk.compat03.grpc.SendMessageRequest.newBuilder(ProtoUtils_v0_3.ToProto.sendMessageRequest(messageSendParams)); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SendMessageRequest_v0_3.METHOD, builder, agentCard, context); + org.a2aproject.sdk.compat03.grpc.SendMessageRequest.Builder builder = + org.a2aproject.sdk.compat03.grpc.SendMessageRequest.newBuilder( + ProtoUtils_v0_3.ToProto.sendMessageRequest(messageSendParams)); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors(SendMessageRequest_v0_3.METHOD, builder, agentCard, context); try { - String httpResponseBody = sendPostRequest(agentUrl + "/v1/message:send", payloadAndHeaders); - org.a2aproject.sdk.compat03.grpc.SendMessageResponse.Builder responseBuilder = org.a2aproject.sdk.compat03.grpc.SendMessageResponse.newBuilder(); + String httpResponseBody = + sendPostRequest(agentUrl + "/v1/message:send", payloadAndHeaders); + org.a2aproject.sdk.compat03.grpc.SendMessageResponse.Builder responseBuilder = + org.a2aproject.sdk.compat03.grpc.SendMessageResponse.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); if (responseBuilder.hasMsg()) { return ProtoUtils_v0_3.FromProto.message(responseBuilder.getMsg()); @@ -95,7 +105,8 @@ public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 messageSendParams, @Nul if (responseBuilder.hasTask()) { return ProtoUtils_v0_3.FromProto.task(responseBuilder.getTask()); } - throw new A2AClientException_v0_3("Failed to send message, wrong response:" + httpResponseBody); + throw new A2AClientException_v0_3( + "Failed to send message, wrong response:" + httpResponseBody); } catch (A2AClientException_v0_3 e) { throw e; } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) { @@ -104,43 +115,61 @@ public EventKind_v0_3 sendMessage(MessageSendParams_v0_3 messageSendParams, @Nul } @Override - public void sendMessageStreaming(MessageSendParams_v0_3 messageSendParams, Consumer eventConsumer, Consumer errorConsumer, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void sendMessageStreaming( + MessageSendParams_v0_3 messageSendParams, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", messageSendParams); checkNotNullParam("eventConsumer", eventConsumer); checkNotNullParam("messageSendParams", messageSendParams); - org.a2aproject.sdk.compat03.grpc.SendMessageRequest.Builder builder = org.a2aproject.sdk.compat03.grpc.SendMessageRequest.newBuilder(ProtoUtils_v0_3.ToProto.sendMessageRequest(messageSendParams)); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SendStreamingMessageRequest_v0_3.METHOD, - builder, agentCard, context); + org.a2aproject.sdk.compat03.grpc.SendMessageRequest.Builder builder = + org.a2aproject.sdk.compat03.grpc.SendMessageRequest.newBuilder( + ProtoUtils_v0_3.ToProto.sendMessageRequest(messageSendParams)); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + SendStreamingMessageRequest_v0_3.METHOD, builder, agentCard, context); AtomicReference> ref = new AtomicReference<>(); - RestSSEEventListener_v0_3 sseEventListener = new RestSSEEventListener_v0_3(eventConsumer, errorConsumer); + RestSSEEventListener_v0_3 sseEventListener = + new RestSSEEventListener_v0_3(eventConsumer, errorConsumer); try { - A2AHttpClient.PostBuilder postBuilder = createPostBuilder(agentUrl + "/v1/message:stream", payloadAndHeaders); - ref.set(postBuilder.postAsyncSSE( - event -> sseEventListener.onMessage(event.data(), ref.get()), - throwable -> sseEventListener.onError(throwable, ref.get()), - () -> { - // We don't need to do anything special on completion - })); + A2AHttpClient.PostBuilder postBuilder = + createPostBuilder(agentUrl + "/v1/message:stream", payloadAndHeaders); + ref.set( + postBuilder.postAsyncSSE( + event -> sseEventListener.onMessage(event.data(), ref.get()), + throwable -> sseEventListener.onError(throwable, ref.get()), + () -> { + // We don't need to do anything special on completion + })); } catch (IOException e) { throw new A2AClientException_v0_3("Failed to send streaming message request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException_v0_3("Send streaming message request timed out: " + e, e); } catch (JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to process JSON for streaming message request: " + e, e); + throw new A2AClientException_v0_3( + "Failed to process JSON for streaming message request: " + e, e); } } @Override - public Task_v0_3 getTask(TaskQueryParams_v0_3 taskQueryParams, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public Task_v0_3 getTask( + TaskQueryParams_v0_3 taskQueryParams, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("taskQueryParams", taskQueryParams); GetTaskRequest.Builder builder = GetTaskRequest.newBuilder(); builder.setName("tasks/" + taskQueryParams.id()); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetTaskRequest_v0_3.METHOD, builder, - agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors(GetTaskRequest_v0_3.METHOD, builder, agentCard, context); try { String url; if (taskQueryParams.historyLength() > 0) { - url = agentUrl + String.format("/v1/tasks/%s?historyLength=%d", taskQueryParams.id(), taskQueryParams.historyLength()); + url = + agentUrl + + String.format( + "/v1/tasks/%s?historyLength=%d", + taskQueryParams.id(), taskQueryParams.historyLength()); } else { url = agentUrl + String.format("/v1/tasks/%1s", taskQueryParams.id()); } @@ -155,7 +184,8 @@ public Task_v0_3 getTask(TaskQueryParams_v0_3 taskQueryParams, @Nullable ClientC throw RestErrorMapper_v0_3.mapRestError(response); } String httpResponseBody = response.body(); - org.a2aproject.sdk.compat03.grpc.Task.Builder responseBuilder = org.a2aproject.sdk.compat03.grpc.Task.newBuilder(); + org.a2aproject.sdk.compat03.grpc.Task.Builder responseBuilder = + org.a2aproject.sdk.compat03.grpc.Task.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return ProtoUtils_v0_3.FromProto.task(responseBuilder); } catch (A2AClientException_v0_3 e) { @@ -166,15 +196,21 @@ public Task_v0_3 getTask(TaskQueryParams_v0_3 taskQueryParams, @Nullable ClientC } @Override - public Task_v0_3 cancelTask(TaskIdParams_v0_3 taskIdParams, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public Task_v0_3 cancelTask( + TaskIdParams_v0_3 taskIdParams, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("taskIdParams", taskIdParams); CancelTaskRequest.Builder builder = CancelTaskRequest.newBuilder(); builder.setName("tasks/" + taskIdParams.id()); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(CancelTaskRequest_v0_3.METHOD, builder, - agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors(CancelTaskRequest_v0_3.METHOD, builder, agentCard, context); try { - String httpResponseBody = sendPostRequest(agentUrl + String.format("/v1/tasks/%1s:cancel", taskIdParams.id()), payloadAndHeaders); - org.a2aproject.sdk.compat03.grpc.Task.Builder responseBuilder = org.a2aproject.sdk.compat03.grpc.Task.newBuilder(); + String httpResponseBody = + sendPostRequest( + agentUrl + String.format("/v1/tasks/%1s:cancel", taskIdParams.id()), + payloadAndHeaders); + org.a2aproject.sdk.compat03.grpc.Task.Builder responseBuilder = + org.a2aproject.sdk.compat03.grpc.Task.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return ProtoUtils_v0_3.FromProto.task(responseBuilder); } catch (A2AClientException_v0_3 e) { @@ -185,38 +221,70 @@ public Task_v0_3 cancelTask(TaskIdParams_v0_3 taskIdParams, @Nullable ClientCall } @Override - public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration(TaskPushNotificationConfig_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public TaskPushNotificationConfig_v0_3 setTaskPushNotificationConfiguration( + TaskPushNotificationConfig_v0_3 request, @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - CreateTaskPushNotificationConfigRequest.Builder builder = CreateTaskPushNotificationConfigRequest.newBuilder(); + CreateTaskPushNotificationConfigRequest.Builder builder = + CreateTaskPushNotificationConfigRequest.newBuilder(); builder.setConfig(ProtoUtils_v0_3.ToProto.taskPushNotificationConfig(request)) .setParent("tasks/" + request.taskId()); if (request.pushNotificationConfig().id() != null) { builder.setConfigId(request.pushNotificationConfig().id()); } - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(SetTaskPushNotificationConfigRequest_v0_3.METHOD, builder, agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + SetTaskPushNotificationConfigRequest_v0_3.METHOD, + builder, + agentCard, + context); try { - String httpResponseBody = sendPostRequest(agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs", request.taskId()), payloadAndHeaders); - org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.Builder responseBuilder = org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder(); + String httpResponseBody = + sendPostRequest( + agentUrl + + String.format( + "/v1/tasks/%1s/pushNotificationConfigs", + request.taskId()), + payloadAndHeaders); + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.Builder responseBuilder = + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return ProtoUtils_v0_3.FromProto.taskPushNotificationConfig(responseBuilder); } catch (A2AClientException_v0_3 e) { throw e; } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to set task push notification config: " + e, e); + throw new A2AClientException_v0_3( + "Failed to set task push notification config: " + e, e); } } @Override - public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration( + GetTaskPushNotificationConfigParams_v0_3 request, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); // When configId is not specified, use taskId as the default configId - String configId = request.pushNotificationConfigId() != null ? request.pushNotificationConfigId() : request.id(); - GetTaskPushNotificationConfigRequest.Builder builder = GetTaskPushNotificationConfigRequest.newBuilder(); - builder.setName(String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.id(), configId)); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetTaskPushNotificationConfigRequest_v0_3.METHOD, builder, - agentCard, context); + String configId = + request.pushNotificationConfigId() != null + ? request.pushNotificationConfigId() + : request.id(); + GetTaskPushNotificationConfigRequest.Builder builder = + GetTaskPushNotificationConfigRequest.newBuilder(); + builder.setName( + String.format("/tasks/%1s/pushNotificationConfigs/%2s", request.id(), configId)); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + GetTaskPushNotificationConfigRequest_v0_3.METHOD, + builder, + agentCard, + context); try { - String url = agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs/%2s", request.id(), configId); + String url = + agentUrl + + String.format( + "/v1/tasks/%1s/pushNotificationConfigs/%2s", + request.id(), configId); A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url); if (payloadAndHeaders.getHeaders() != null) { for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { @@ -228,7 +296,8 @@ public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration(GetT throw RestErrorMapper_v0_3.mapRestError(response); } String httpResponseBody = response.body(); - org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.Builder responseBuilder = org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder(); + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.Builder responseBuilder = + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return ProtoUtils_v0_3.FromProto.taskPushNotificationConfig(responseBuilder); } catch (A2AClientException_v0_3 e) { @@ -239,14 +308,23 @@ public TaskPushNotificationConfig_v0_3 getTaskPushNotificationConfiguration(GetT } @Override - public List listTaskPushNotificationConfigurations(ListTaskPushNotificationConfigParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public List listTaskPushNotificationConfigurations( + ListTaskPushNotificationConfigParams_v0_3 request, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - ListTaskPushNotificationConfigRequest.Builder builder = ListTaskPushNotificationConfigRequest.newBuilder(); + ListTaskPushNotificationConfigRequest.Builder builder = + ListTaskPushNotificationConfigRequest.newBuilder(); builder.setParent(String.format("tasks/%1s/pushNotificationConfigs", request.id())); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(ListTaskPushNotificationConfigRequest_v0_3.METHOD, builder, - agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + ListTaskPushNotificationConfigRequest_v0_3.METHOD, + builder, + agentCard, + context); try { - String url = agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs", request.id()); + String url = + agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs", request.id()); A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url); if (payloadAndHeaders.getHeaders() != null) { for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { @@ -258,7 +336,10 @@ public List listTaskPushNotificationConfigurati throw RestErrorMapper_v0_3.mapRestError(response); } String httpResponseBody = response.body(); - org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse.Builder responseBuilder = org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse.newBuilder(); + org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse.Builder + responseBuilder = + org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse + .newBuilder(); JsonFormat.parser().merge(httpResponseBody, responseBuilder); return ProtoUtils_v0_3.FromProto.listTaskPushNotificationConfigParams(responseBuilder); } catch (A2AClientException_v0_3 e) { @@ -269,13 +350,26 @@ public List listTaskPushNotificationConfigurati } @Override - public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void deleteTaskPushNotificationConfigurations( + DeleteTaskPushNotificationConfigParams_v0_3 request, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequestOrBuilder builder = org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder(); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(DeleteTaskPushNotificationConfigRequest_v0_3.METHOD, builder, - agentCard, context); + org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequestOrBuilder builder = + org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequest + .newBuilder(); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + DeleteTaskPushNotificationConfigRequest_v0_3.METHOD, + builder, + agentCard, + context); try { - String url = agentUrl + String.format("/v1/tasks/%1s/pushNotificationConfigs/%2s", request.id(), request.pushNotificationConfigId()); + String url = + agentUrl + + String.format( + "/v1/tasks/%1s/pushNotificationConfigs/%2s", + request.id(), request.pushNotificationConfigId()); A2AHttpClient.DeleteBuilder deleteBuilder = httpClient.createDelete().url(url); if (payloadAndHeaders.getHeaders() != null) { for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { @@ -294,47 +388,63 @@ public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationC } @Override - public void resubscribe(TaskIdParams_v0_3 request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public void resubscribe( + TaskIdParams_v0_3 request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { checkNotNullParam("request", request); - org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequest.Builder builder = org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequest.newBuilder(); + org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequest.Builder builder = + org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequest.newBuilder(); builder.setName("tasks/" + request.id()); - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(TaskResubscriptionRequest_v0_3.METHOD, builder, - agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + TaskResubscriptionRequest_v0_3.METHOD, builder, agentCard, context); AtomicReference> ref = new AtomicReference<>(); - RestSSEEventListener_v0_3 sseEventListener = new RestSSEEventListener_v0_3(eventConsumer, errorConsumer); + RestSSEEventListener_v0_3 sseEventListener = + new RestSSEEventListener_v0_3(eventConsumer, errorConsumer); try { String url = agentUrl + String.format("/v1/tasks/%1s:subscribe", request.id()); A2AHttpClient.PostBuilder postBuilder = createPostBuilder(url, payloadAndHeaders); - ref.set(postBuilder.postAsyncSSE( - event -> sseEventListener.onMessage(event.data(), ref.get()), - throwable -> sseEventListener.onError(throwable, ref.get()), - () -> { - // We don't need to do anything special on completion - })); + ref.set( + postBuilder.postAsyncSSE( + event -> sseEventListener.onMessage(event.data(), ref.get()), + throwable -> sseEventListener.onError(throwable, ref.get()), + () -> { + // We don't need to do anything special on completion + })); } catch (IOException e) { throw new A2AClientException_v0_3("Failed to send streaming message request: " + e, e); } catch (InterruptedException e) { throw new A2AClientException_v0_3("Send streaming message request timed out: " + e, e); } catch (JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to process JSON for streaming message request: " + e, e); + throw new A2AClientException_v0_3( + "Failed to process JSON for streaming message request: " + e, e); } } @Override - public AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3 { + public AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3 { A2ACardResolver_v0_3 resolver; try { if (agentCard == null) { - resolver = new A2ACardResolver_v0_3(httpClient, agentUrl, null, getHttpHeaders(context)); + resolver = + new A2ACardResolver_v0_3( + httpClient, agentUrl, null, getHttpHeaders(context)); agentCard = resolver.getAgentCard(); needsExtendedCard = agentCard.supportsAuthenticatedExtendedCard(); } if (!needsExtendedCard) { return agentCard; } - PayloadAndHeaders_v0_3 payloadAndHeaders = applyInterceptors(GetAuthenticatedExtendedCardRequest_v0_3.METHOD, null, - agentCard, context); + PayloadAndHeaders_v0_3 payloadAndHeaders = + applyInterceptors( + GetAuthenticatedExtendedCardRequest_v0_3.METHOD, + null, + agentCard, + context); String url = agentUrl + String.format("/v1/card"); A2AHttpClient.GetBuilder getBuilder = httpClient.createGet().url(url); if (payloadAndHeaders.getHeaders() != null) { @@ -351,7 +461,8 @@ public AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) thr needsExtendedCard = false; return agentCard; } catch (IOException | InterruptedException | JsonProcessingException_v0_3 e) { - throw new A2AClientException_v0_3("Failed to get authenticated extended agent card: " + e, e); + throw new A2AClientException_v0_3( + "Failed to get authenticated extended agent card: " + e, e); } catch (A2AClientError_v0_3 e) { throw new A2AClientException_v0_3("Failed to get agent card: " + e, e); } @@ -362,19 +473,29 @@ public void close() { // no-op } - private PayloadAndHeaders_v0_3 applyInterceptors(String methodName, @Nullable MessageOrBuilder payload, - AgentCard_v0_3 agentCard, @Nullable ClientCallContext_v0_3 clientCallContext) { - PayloadAndHeaders_v0_3 payloadAndHeaders = new PayloadAndHeaders_v0_3(payload, getHttpHeaders(clientCallContext)); + private PayloadAndHeaders_v0_3 applyInterceptors( + String methodName, + @Nullable MessageOrBuilder payload, + AgentCard_v0_3 agentCard, + @Nullable ClientCallContext_v0_3 clientCallContext) { + PayloadAndHeaders_v0_3 payloadAndHeaders = + new PayloadAndHeaders_v0_3(payload, getHttpHeaders(clientCallContext)); if (interceptors != null && !interceptors.isEmpty()) { for (ClientCallInterceptor_v0_3 interceptor : interceptors) { - payloadAndHeaders = interceptor.intercept(methodName, payloadAndHeaders.getPayload(), - payloadAndHeaders.getHeaders(), agentCard, clientCallContext); + payloadAndHeaders = + interceptor.intercept( + methodName, + payloadAndHeaders.getPayload(), + payloadAndHeaders.getHeaders(), + agentCard, + clientCallContext); } } return payloadAndHeaders; } - 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 { MessageOrBuilder payload = (MessageOrBuilder) payloadAndHeaders.getPayload(); String body = payload != null ? ProtoJsonUtils.toJson(JsonFormat.printer(), payload) : ""; if (LOGGER.isLoggable(Level.FINE)) { @@ -391,19 +512,27 @@ private String sendPostRequest(String url, PayloadAndHeaders_v0_3 payloadAndHead return response.body(); } - private A2AHttpClient.PostBuilder createPostBuilder(String url, PayloadAndHeaders_v0_3 payloadAndHeaders) throws JsonProcessingException_v0_3, InvalidProtocolBufferException { - String body = ProtoJsonUtils_v0_3.toJson(JsonFormat.printer(), (MessageOrBuilder) payloadAndHeaders.getPayload()); + private A2AHttpClient.PostBuilder createPostBuilder( + String url, PayloadAndHeaders_v0_3 payloadAndHeaders) + throws JsonProcessingException_v0_3, InvalidProtocolBufferException { + String body = + ProtoJsonUtils_v0_3.toJson( + JsonFormat.printer(), (MessageOrBuilder) payloadAndHeaders.getPayload()); if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine(body); } return buildPost(url, payloadAndHeaders, body); } - private A2AHttpClient.PostBuilder buildPost(String url, PayloadAndHeaders_v0_3 payloadAndHeaders, String body) throws JsonProcessingException_v0_3 { - A2AHttpClient.PostBuilder postBuilder = httpClient.createPost() - .url(url) - .addHeader("Content-Type", "application/json") - .body(body); + private A2AHttpClient.PostBuilder buildPost( + String url, PayloadAndHeaders_v0_3 payloadAndHeaders, String body) + throws JsonProcessingException_v0_3 { + A2AHttpClient.PostBuilder postBuilder = + httpClient + .createPost() + .url(url) + .addHeader("Content-Type", "application/json") + .body(body); if (payloadAndHeaders.getHeaders() != null) { for (Map.Entry entry : payloadAndHeaders.getHeaders().entrySet()) { diff --git a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/package-info.java b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/package-info.java index 746922520..3d2dd57f0 100644 --- a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/package-info.java +++ b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.compat03.client.transport.rest; import org.jspecify.annotations.NullMarked; - diff --git a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/RestSSEEventListener_v0_3.java b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/RestSSEEventListener_v0_3.java index 50dd3690d..b013214d3 100644 --- a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/RestSSEEventListener_v0_3.java +++ b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/RestSSEEventListener_v0_3.java @@ -1,11 +1,10 @@ package org.a2aproject.sdk.compat03.client.transport.rest.sse; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; import java.util.concurrent.Future; import java.util.function.Consumer; import java.util.logging.Logger; - -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.util.JsonFormat; import org.a2aproject.sdk.compat03.client.transport.rest.RestErrorMapper_v0_3; import org.a2aproject.sdk.compat03.grpc.StreamResponse; import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3; @@ -14,12 +13,13 @@ public class RestSSEEventListener_v0_3 { - private static final Logger LOGGER = Logger.getLogger(RestSSEEventListener_v0_3.class.getName()); + private static final Logger LOGGER = + Logger.getLogger(RestSSEEventListener_v0_3.class.getName()); private final Consumer eventHandler; private final Consumer errorHandler; - public RestSSEEventListener_v0_3(Consumer eventHandler, - Consumer errorHandler) { + public RestSSEEventListener_v0_3( + Consumer eventHandler, Consumer errorHandler) { this.eventHandler = eventHandler; this.errorHandler = errorHandler; } @@ -27,7 +27,8 @@ public RestSSEEventListener_v0_3(Consumer eventHandler, public void onMessage(String message, @Nullable Future completableFuture) { try { LOGGER.fine("Streaming message received: " + message); - org.a2aproject.sdk.compat03.grpc.StreamResponse.Builder builder = org.a2aproject.sdk.compat03.grpc.StreamResponse.newBuilder(); + org.a2aproject.sdk.compat03.grpc.StreamResponse.Builder builder = + org.a2aproject.sdk.compat03.grpc.StreamResponse.newBuilder(); JsonFormat.parser().merge(message, builder); handleMessage(builder.build()); } catch (InvalidProtocolBufferException e) { @@ -47,21 +48,25 @@ public void onError(Throwable throwable, @Nullable Future future) { private void handleMessage(StreamResponse response) { StreamingEventKind_v0_3 event; switch (response.getPayloadCase()) { - case MSG -> - event = ProtoUtils_v0_3.FromProto.message(response.getMsg()); - case TASK -> - event = ProtoUtils_v0_3.FromProto.task(response.getTask()); + case MSG -> event = ProtoUtils_v0_3.FromProto.message(response.getMsg()); + case TASK -> event = ProtoUtils_v0_3.FromProto.task(response.getTask()); case STATUS_UPDATE -> - event = ProtoUtils_v0_3.FromProto.taskStatusUpdateEvent(response.getStatusUpdate()); + event = + ProtoUtils_v0_3.FromProto.taskStatusUpdateEvent( + response.getStatusUpdate()); case ARTIFACT_UPDATE -> - event = ProtoUtils_v0_3.FromProto.taskArtifactUpdateEvent(response.getArtifactUpdate()); + event = + ProtoUtils_v0_3.FromProto.taskArtifactUpdateEvent( + response.getArtifactUpdate()); default -> { LOGGER.warning("Invalid stream response " + response.getPayloadCase()); - errorHandler.accept(new IllegalStateException("Invalid stream response from server: " + response.getPayloadCase())); + errorHandler.accept( + new IllegalStateException( + "Invalid stream response from server: " + + response.getPayloadCase())); return; } } eventHandler.accept(event); } - } diff --git a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/package-info.java b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/package-info.java index 38dfbf147..ee9ca212a 100644 --- a/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/package-info.java +++ b/compat-0.3/client/transport/rest/src/main/java/org/a2aproject/sdk/compat03/client/transport/rest/sse/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.compat03.client.transport.rest.sse; import org.jspecify.annotations.NullMarked; - diff --git a/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/JsonRestMessages_v0_3.java b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/JsonRestMessages_v0_3.java index f061d1d58..43c6df8b8 100644 --- a/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/JsonRestMessages_v0_3.java +++ b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/JsonRestMessages_v0_3.java @@ -6,7 +6,8 @@ */ public class JsonRestMessages_v0_3 { - static final String SEND_MESSAGE_TEST_REQUEST = """ + static final String SEND_MESSAGE_TEST_REQUEST = + """ { "message": { @@ -19,9 +20,11 @@ public class JsonRestMessages_v0_3 { "metadata": { } } - }"""; + }\ + """; - static final String SEND_MESSAGE_TEST_RESPONSE = """ + static final String SEND_MESSAGE_TEST_RESPONSE = + """ { "task": { "id": "9b511af4-b27c-47fa-aecf-2a93c08a44f8", @@ -44,14 +47,18 @@ public class JsonRestMessages_v0_3 { } ] } - }"""; + }\ + """; - static final String CANCEL_TASK_TEST_REQUEST = """ + static final String CANCEL_TASK_TEST_REQUEST = + """ { "name": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64" - }"""; + }\ + """; - static final String CANCEL_TASK_TEST_RESPONSE = """ + static final String CANCEL_TASK_TEST_RESPONSE = + """ { "id": "de38c76d-d54c-436c-8b9f-4c2703648d64", "contextId": "c295ea44-7543-4f78-b524-7a38915ad6e4", @@ -59,9 +66,11 @@ public class JsonRestMessages_v0_3 { "state": "TASK_STATE_CANCELLED" }, "metadata": {} - }"""; + }\ + """; - static final String GET_TASK_TEST_RESPONSE = """ + static final String GET_TASK_TEST_RESPONSE = + """ { "id": "de38c76d-d54c-436c-8b9f-4c2703648d64", "contextId": "c295ea44-7543-4f78-b524-7a38915ad6e4", @@ -107,7 +116,8 @@ public class JsonRestMessages_v0_3 { } """; - static final String AGENT_CARD = """ + static final String AGENT_CARD = + """ { "name": "GeoSpatial Route Planner Agent", "description": "Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", @@ -170,9 +180,11 @@ public class JsonRestMessages_v0_3 { ], "supportsAuthenticatedExtendedCard": false, "protocolVersion": "0.2.5" - }"""; + }\ + """; - static final String AGENT_CARD_SUPPORTS_EXTENDED = """ + static final String AGENT_CARD_SUPPORTS_EXTENDED = + """ { "name": "GeoSpatial Route Planner Agent", "description": "Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", @@ -235,9 +247,11 @@ public class JsonRestMessages_v0_3 { ], "supportsAuthenticatedExtendedCard": true, "protocolVersion": "0.2.5" - }"""; + }\ + """; - static final String AUTHENTICATION_EXTENDED_AGENT_CARD = """ + static final String AUTHENTICATION_EXTENDED_AGENT_CARD = + """ { "name": "GeoSpatial Route Planner Agent Extended", "description": "Extended description", @@ -306,9 +320,11 @@ public class JsonRestMessages_v0_3 { ], "supportsAuthenticatedExtendedCard": true, "protocolVersion": "0.2.5" - }"""; + }\ + """; - static final String SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE = """ + static final String SEND_MESSAGE_TEST_REQUEST_WITH_MESSAGE_RESPONSE = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -330,9 +346,11 @@ public class JsonRestMessages_v0_3 { "blocking": true }, } - }"""; + }\ + """; - static final String SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE = """ + static final String SEND_MESSAGE_TEST_RESPONSE_WITH_MESSAGE_RESPONSE = + """ { "jsonrpc": "2.0", "id": 1, @@ -347,9 +365,11 @@ public class JsonRestMessages_v0_3 { "messageId": "msg-456", "kind": "message" } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_ERROR_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_ERROR_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -371,9 +391,11 @@ public class JsonRestMessages_v0_3 { "blocking": true }, } - }"""; + }\ + """; - static final String SEND_MESSAGE_ERROR_TEST_RESPONSE = """ + static final String SEND_MESSAGE_ERROR_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "error": { @@ -381,9 +403,11 @@ public class JsonRestMessages_v0_3 { "message": "Invalid parameters", "data": "Hello world" } - }"""; + }\ + """; - static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """ + static final String GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = + """ { "name": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10", "pushNotificationConfig": { @@ -392,8 +416,10 @@ public class JsonRestMessages_v0_3 { "schemes": ["jwt"] } } - }"""; - static final String LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """ + }\ + """; + static final String LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = + """ { "configs":[ { @@ -412,10 +438,11 @@ public class JsonRestMessages_v0_3 { } } ] - }"""; - + }\ + """; - static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = """ + static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST = + """ { "parent": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64", "config": { @@ -427,9 +454,11 @@ public class JsonRestMessages_v0_3 { } } } - }"""; + }\ + """; - static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = """ + static final String SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE = + """ { "name": "tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10", "pushNotificationConfig": { @@ -438,9 +467,11 @@ public class JsonRestMessages_v0_3 { "schemes": ["jwt"] } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_FILE_PART_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -469,9 +500,11 @@ public class JsonRestMessages_v0_3 { "blocking": true } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE = """ + static final String SEND_MESSAGE_WITH_FILE_PART_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -495,9 +528,11 @@ public class JsonRestMessages_v0_3 { "metadata": {}, "kind": "task" } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_DATA_PART_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -528,9 +563,11 @@ public class JsonRestMessages_v0_3 { "blocking": true } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE = """ + static final String SEND_MESSAGE_WITH_DATA_PART_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -554,9 +591,11 @@ public class JsonRestMessages_v0_3 { "metadata": {}, "kind": "task" } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST = """ + static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "message/send", @@ -594,9 +633,11 @@ public class JsonRestMessages_v0_3 { "blocking": true } } - }"""; + }\ + """; - static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE = """ + static final String SEND_MESSAGE_WITH_MIXED_PARTS_TEST_RESPONSE = + """ { "jsonrpc": "2.0", "result": { @@ -620,9 +661,11 @@ public class JsonRestMessages_v0_3 { "metadata": {}, "kind": "task" } - }"""; + }\ + """; - public static final String SEND_MESSAGE_STREAMING_TEST_REQUEST = """ + public static final String SEND_MESSAGE_STREAMING_TEST_REQUEST = + """ { "message": { "role": "ROLE_USER", @@ -637,20 +680,29 @@ public class JsonRestMessages_v0_3 { "configuration": { "acceptedOutputModes": ["text"] } - }"""; - static final String SEND_MESSAGE_STREAMING_TEST_RESPONSE - = "event: message\n" - + "data: {\"task\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"TASK_STATE_SUBMITTED\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"text\":\"Why did the chicken cross the road? To get to the other side!\"}]}],\"metadata\":{}}}\n\n"; + }\ + """; + static final String SEND_MESSAGE_STREAMING_TEST_RESPONSE = + "event: message\n" + + "data:" + + " {\"task\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"TASK_STATE_SUBMITTED\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"text\":\"Why" + + " did the chicken cross the road? To get to the other" + + " side!\"}]}],\"metadata\":{}}}\n\n"; - static final String TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE - = "event: message\n" - + "data: {\"task\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"TASK_STATE_COMPLETED\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"text\":\"Why did the chicken cross the road? To get to the other side!\"}]}],\"metadata\":{}}}\n\n"; - public static final String TASK_RESUBSCRIPTION_TEST_REQUEST = """ + static final String TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE = + "event: message\n" + + "data:" + + " {\"task\":{\"id\":\"2\",\"contextId\":\"context-1234\",\"status\":{\"state\":\"TASK_STATE_COMPLETED\"},\"artifacts\":[{\"artifactId\":\"artifact-1\",\"name\":\"joke\",\"parts\":[{\"text\":\"Why" + + " did the chicken cross the road? To get to the other" + + " side!\"}]}],\"metadata\":{}}}\n\n"; + public static final String TASK_RESUBSCRIPTION_TEST_REQUEST = + """ { "jsonrpc": "2.0", "method": "tasks/resubscribe", "params": { "id": "task-1234" } - }"""; + }\ + """; } diff --git a/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3_Test.java b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3_Test.java index de51b8693..7aef094cc 100644 --- a/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3_Test.java +++ b/compat-0.3/client/transport/rest/src/test/java/org/a2aproject/sdk/compat03/client/transport/rest/RestTransport_v0_3_Test.java @@ -1,15 +1,14 @@ package org.a2aproject.sdk.compat03.client.transport.rest; - import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.CANCEL_TASK_TEST_REQUEST; import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.CANCEL_TASK_TEST_RESPONSE; import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE; import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.GET_TASK_TEST_RESPONSE; import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE; +import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SEND_MESSAGE_STREAMING_TEST_REQUEST; import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SEND_MESSAGE_STREAMING_TEST_RESPONSE; import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SEND_MESSAGE_TEST_REQUEST; import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SEND_MESSAGE_TEST_RESPONSE; -import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SEND_MESSAGE_STREAMING_TEST_REQUEST; import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST; import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE; import static org.a2aproject.sdk.compat03.client.transport.rest.JsonRestMessages_v0_3.TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE; @@ -21,6 +20,15 @@ import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.logging.Logger; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; @@ -33,29 +41,20 @@ import org.a2aproject.sdk.compat03.spec.FileWithUri_v0_3; import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3; import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3; -import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3.Kind; import org.a2aproject.sdk.compat03.spec.PushNotificationAuthenticationInfo_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.logging.Logger; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -67,28 +66,32 @@ public class RestTransport_v0_3_Test { private static final Logger LOGGER = Logger.getLogger(RestTransport_v0_3_Test.class.getName()); private ClientAndServer server; - private static final AgentCard_v0_3 CARD = new AgentCard_v0_3.Builder() - .name("Hello World Agent") - .description("Just a hello world agent") - .url("http://localhost:4001") - .version("1.0.0") - .documentationUrl("http://example.com/docs") - .capabilities(new AgentCapabilities_v0_3.Builder() - .streaming(true) - .pushNotifications(true) - .stateTransitionHistory(true) - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(new AgentSkill_v0_3.Builder() - .id("hello_world") - .name("Returns hello world") - .description("just returns hello world") - .tags(Collections.singletonList("hello world")) - .examples(List.of("hi", "hello world")) - .build())) - .protocolVersion("0.3.0") - .build(); + private static final AgentCard_v0_3 CARD = + new AgentCard_v0_3.Builder() + .name("Hello World Agent") + .description("Just a hello world agent") + .url("http://localhost:4001") + .version("1.0.0") + .documentationUrl("http://example.com/docs") + .capabilities( + new AgentCapabilities_v0_3.Builder() + .streaming(true) + .pushNotifications(true) + .stateTransitionHistory(true) + .build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills( + Collections.singletonList( + new AgentSkill_v0_3.Builder() + .id("hello_world") + .name("Returns hello world") + .description("just returns hello world") + .tags(Collections.singletonList("hello world")) + .examples(List.of("hi", "hello world")) + .build())) + .protocolVersion("0.3.0") + .build(); @BeforeEach public void setUp() throws IOException { @@ -100,35 +103,34 @@ public void tearDown() { server.stop(); } - public RestTransport_v0_3_Test() { - } + public RestTransport_v0_3_Test() {} /** * Test of sendMessage method, of class JSONRestTransport. */ @Test public void testSendMessage() throws Exception { - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) - .contextId("context-1234") - .messageId("message-1234") - .taskId("") - .build(); - this.server.when( - request() - .withMethod("POST") - .withPath("/v1/message:send") - .withBody(JsonBody.json(SEND_MESSAGE_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) - .respond( - response() - .withStatusCode(200) - .withBody(SEND_MESSAGE_TEST_RESPONSE) - ); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) + .contextId("context-1234") + .messageId("message-1234") + .taskId("") + .build(); + this.server + .when( + request() + .withMethod("POST") + .withPath("/v1/message:send") + .withBody( + JsonBody.json( + SEND_MESSAGE_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(SEND_MESSAGE_TEST_RESPONSE)); MessageSendParams_v0_3 messageSendParams = new MessageSendParams_v0_3(message, null, null); ClientCallContext_v0_3 context = null; - + RestTransport_v0_3 instance = new RestTransport_v0_3(CARD); EventKind_v0_3 result = instance.sendMessage(messageSendParams, context); assertEquals("task", result.kind()); @@ -158,21 +160,23 @@ public void testSendMessage() throws Exception { */ @Test public void testCancelTask() throws Exception { - this.server.when( - request() - .withMethod("POST") - .withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64:cancel") - .withBody(JsonBody.json(CANCEL_TASK_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) - .respond( - response() - .withStatusCode(200) - .withBody(CANCEL_TASK_TEST_RESPONSE) - ); + this.server + .when( + request() + .withMethod("POST") + .withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64:cancel") + .withBody( + JsonBody.json( + CANCEL_TASK_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) + .respond(response().withStatusCode(200).withBody(CANCEL_TASK_TEST_RESPONSE)); ClientCallContext_v0_3 context = null; RestTransport_v0_3 instance = new RestTransport_v0_3(CARD); - Task_v0_3 task = instance.cancelTask(new TaskIdParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", - new HashMap<>()), context); + Task_v0_3 task = + instance.cancelTask( + new TaskIdParams_v0_3( + "de38c76d-d54c-436c-8b9f-4c2703648d64", new HashMap<>()), + context); assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id()); assertEquals(TaskState_v0_3.CANCELED, task.status().state()); assertNull(task.status().message()); @@ -184,19 +188,16 @@ public void testCancelTask() throws Exception { */ @Test public void testGetTask() throws Exception { - this.server.when( - request() - .withMethod("GET") - .withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64") - .withQueryStringParameter("historyLength", "5") - ) - .respond( - response() - .withStatusCode(200) - .withBody(GET_TASK_TEST_RESPONSE) - ); + this.server + .when( + request() + .withMethod("GET") + .withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64") + .withQueryStringParameter("historyLength", "5")) + .respond(response().withStatusCode(200).withBody(GET_TASK_TEST_RESPONSE)); ClientCallContext_v0_3 context = null; - TaskQueryParams_v0_3 request = new TaskQueryParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", 5); + TaskQueryParams_v0_3 request = + new TaskQueryParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", 5); RestTransport_v0_3 instance = new RestTransport_v0_3(CARD); Task_v0_3 task = instance.getTask(request, context); assertEquals("de38c76d-d54c-436c-8b9f-4c2703648d64", task.id()); @@ -210,7 +211,9 @@ public void testGetTask() throws Exception { assertEquals("", artifact.name()); assertEquals(false, artifact.parts().isEmpty()); assertEquals(Kind.TEXT, ((TextPart_v0_3) artifact.parts().get(0)).kind()); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) artifact.parts().get(0)).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart_v0_3) artifact.parts().get(0)).text()); assertEquals(1, task.history().size()); Message_v0_3 history = task.history().get(0); assertEquals("message", history.kind()); @@ -238,42 +241,47 @@ public void testGetTask() throws Exception { */ @Test public void testSendMessageStreaming() throws Exception { - this.server.when( - request() - .withMethod("POST") - .withPath("/v1/message:stream") - .withBody(JsonBody.json(SEND_MESSAGE_STREAMING_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) + this.server + .when( + request() + .withMethod("POST") + .withPath("/v1/message:stream") + .withBody( + JsonBody.json( + SEND_MESSAGE_STREAMING_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE) - ); + .withBody(SEND_MESSAGE_STREAMING_TEST_RESPONSE)); RestTransport_v0_3 client = new RestTransport_v0_3(CARD); - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("tell me some jokes"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); - MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder() - .acceptedOutputModes(List.of("text")) - .blocking(false) - .build(); - MessageSendParams_v0_3 params = new MessageSendParams_v0_3.Builder() - .message(message) - .configuration(configuration) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("tell me some jokes"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); + MessageSendConfiguration_v0_3 configuration = + new MessageSendConfiguration_v0_3.Builder() + .acceptedOutputModes(List.of("text")) + .blocking(false) + .build(); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3.Builder() + .message(message) + .configuration(configuration) + .build(); AtomicReference receivedEvent = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - Consumer eventHandler = event -> { - receivedEvent.set(event); - latch.countDown(); - }; - Consumer errorHandler = error -> { - }; + Consumer eventHandler = + event -> { + receivedEvent.set(event); + latch.countDown(); + }; + Consumer errorHandler = error -> {}; client.sendMessageStreaming(params, eventHandler, errorHandler, null); boolean eventReceived = latch.await(10, TimeUnit.SECONDS); @@ -290,30 +298,38 @@ public void testSendMessageStreaming() throws Exception { @Test public void testSetTaskPushNotificationConfiguration() throws Exception { LOGGER.info("Testing setTaskPushNotificationConfiguration"); - this.server.when( - request() - .withMethod("POST") - .withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") - .withBody(JsonBody.json(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, MatchType.ONLY_MATCHING_FIELDS)) - ) + this.server + .when( + request() + .withMethod("POST") + .withPath( + "/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") + .withBody( + JsonBody.json( + SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_REQUEST, + MatchType.ONLY_MATCHING_FIELDS))) .respond( response() .withStatusCode(200) - .withBody(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(SET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); RestTransport_v0_3 client = new RestTransport_v0_3(CARD); - TaskPushNotificationConfig_v0_3 pushedConfig = new TaskPushNotificationConfig_v0_3( - "de38c76d-d54c-436c-8b9f-4c2703648d64", - new PushNotificationConfig_v0_3.Builder() - .url("https://example.com/callback") - .authenticationInfo( - new PushNotificationAuthenticationInfo_v0_3(Collections.singletonList("jwt"), null)) - .build()); - TaskPushNotificationConfig_v0_3 taskPushNotificationConfig = client.setTaskPushNotificationConfiguration(pushedConfig, null); - PushNotificationConfig_v0_3 pushNotificationConfig = taskPushNotificationConfig.pushNotificationConfig(); + TaskPushNotificationConfig_v0_3 pushedConfig = + new TaskPushNotificationConfig_v0_3( + "de38c76d-d54c-436c-8b9f-4c2703648d64", + new PushNotificationConfig_v0_3.Builder() + .url("https://example.com/callback") + .authenticationInfo( + new PushNotificationAuthenticationInfo_v0_3( + Collections.singletonList("jwt"), null)) + .build()); + TaskPushNotificationConfig_v0_3 taskPushNotificationConfig = + client.setTaskPushNotificationConfiguration(pushedConfig, null); + PushNotificationConfig_v0_3 pushNotificationConfig = + taskPushNotificationConfig.pushNotificationConfig(); assertNotNull(pushNotificationConfig); assertEquals("https://example.com/callback", pushNotificationConfig.url()); - PushNotificationAuthenticationInfo_v0_3 authenticationInfo = pushNotificationConfig.authentication(); + PushNotificationAuthenticationInfo_v0_3 authenticationInfo = + pushNotificationConfig.authentication(); assertEquals(1, authenticationInfo.schemes().size()); assertEquals("jwt", authenticationInfo.schemes().get(0)); } @@ -323,25 +339,29 @@ public void testSetTaskPushNotificationConfiguration() throws Exception { */ @Test public void testGetTaskPushNotificationConfiguration() throws Exception { - this.server.when( - request() - .withMethod("GET") - .withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10") - ) + this.server + .when( + request() + .withMethod("GET") + .withPath( + "/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10")) .respond( response() .withStatusCode(200) - .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(GET_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); RestTransport_v0_3 client = new RestTransport_v0_3(CARD); - TaskPushNotificationConfig_v0_3 taskPushNotificationConfig = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", "10", - new HashMap<>()), null); - PushNotificationConfig_v0_3 pushNotificationConfig = taskPushNotificationConfig.pushNotificationConfig(); + TaskPushNotificationConfig_v0_3 taskPushNotificationConfig = + client.getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams_v0_3( + "de38c76d-d54c-436c-8b9f-4c2703648d64", "10", new HashMap<>()), + null); + PushNotificationConfig_v0_3 pushNotificationConfig = + taskPushNotificationConfig.pushNotificationConfig(); assertNotNull(pushNotificationConfig); assertEquals("https://example.com/callback", pushNotificationConfig.url()); - PushNotificationAuthenticationInfo_v0_3 authenticationInfo = pushNotificationConfig.authentication(); + PushNotificationAuthenticationInfo_v0_3 authenticationInfo = + pushNotificationConfig.authentication(); assertTrue(authenticationInfo.schemes().size() == 1); assertEquals("jwt", authenticationInfo.schemes().get(0)); } @@ -351,26 +371,31 @@ public void testGetTaskPushNotificationConfiguration() throws Exception { */ @Test public void testListTaskPushNotificationConfigurations() throws Exception { - this.server.when( - request() - .withMethod("GET") - .withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs") - ) + this.server + .when( + request() + .withMethod("GET") + .withPath( + "/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs")) .respond( response() .withStatusCode(200) - .withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE) - ); + .withBody(LIST_TASK_PUSH_NOTIFICATION_CONFIG_TEST_RESPONSE)); RestTransport_v0_3 client = new RestTransport_v0_3(CARD); - List taskPushNotificationConfigs = client.listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", new HashMap<>()), null); + List taskPushNotificationConfigs = + client.listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams_v0_3( + "de38c76d-d54c-436c-8b9f-4c2703648d64", new HashMap<>()), + null); assertEquals(2, taskPushNotificationConfigs.size()); - PushNotificationConfig_v0_3 pushNotificationConfig = taskPushNotificationConfigs.get(0).pushNotificationConfig(); + PushNotificationConfig_v0_3 pushNotificationConfig = + taskPushNotificationConfigs.get(0).pushNotificationConfig(); assertNotNull(pushNotificationConfig); assertEquals("https://example.com/callback", pushNotificationConfig.url()); assertEquals("10", pushNotificationConfig.id()); - PushNotificationAuthenticationInfo_v0_3 authenticationInfo = pushNotificationConfig.authentication(); + PushNotificationAuthenticationInfo_v0_3 authenticationInfo = + pushNotificationConfig.authentication(); assertTrue(authenticationInfo.schemes().size() == 1); assertEquals("jwt", authenticationInfo.schemes().get(0)); assertEquals("", authenticationInfo.credentials()); @@ -388,18 +413,19 @@ public void testListTaskPushNotificationConfigurations() throws Exception { @Test public void testDeleteTaskPushNotificationConfigurations() throws Exception { LOGGER.info("Testing deleteTaskPushNotificationConfigurations"); - this.server.when( - request() - .withMethod("DELETE") - .withPath("/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10") - ) - .respond( - response() - .withStatusCode(200) - ); + this.server + .when( + request() + .withMethod("DELETE") + .withPath( + "/v1/tasks/de38c76d-d54c-436c-8b9f-4c2703648d64/pushNotificationConfigs/10")) + .respond(response().withStatusCode(200)); ClientCallContext_v0_3 context = null; RestTransport_v0_3 instance = new RestTransport_v0_3(CARD); - instance.deleteTaskPushNotificationConfigurations(new DeleteTaskPushNotificationConfigParams_v0_3("de38c76d-d54c-436c-8b9f-4c2703648d64", "10"), context); + instance.deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams_v0_3( + "de38c76d-d54c-436c-8b9f-4c2703648d64", "10"), + context); } /** @@ -408,35 +434,33 @@ public void testDeleteTaskPushNotificationConfigurations() throws Exception { @Test public void testResubscribe() throws Exception { LOGGER.info("Testing resubscribe"); - - this.server.when( - request() - .withMethod("POST") - .withPath("/v1/tasks/task-1234:subscribe") - ) + + this.server + .when(request().withMethod("POST").withPath("/v1/tasks/task-1234:subscribe")) .respond( response() .withStatusCode(200) .withHeader("Content-Type", "text/event-stream") - .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE) - ); + .withBody(TASK_RESUBSCRIPTION_REQUEST_TEST_RESPONSE)); RestTransport_v0_3 client = new RestTransport_v0_3(CARD); TaskIdParams_v0_3 taskIdParams = new TaskIdParams_v0_3("task-1234"); AtomicReference receivedEvent = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - Consumer eventHandler = event -> { - receivedEvent.set(event); - latch.countDown(); - }; + Consumer eventHandler = + event -> { + receivedEvent.set(event); + latch.countDown(); + }; Consumer errorHandler = error -> {}; client.resubscribe(taskIdParams, eventHandler, errorHandler, null); boolean eventReceived = latch.await(10, TimeUnit.SECONDS); assertTrue(eventReceived); - StreamingEventKind_v0_3 eventKind = receivedEvent.get();; + StreamingEventKind_v0_3 eventKind = receivedEvent.get(); + ; assertNotNull(eventKind); assertInstanceOf(Task_v0_3.class, eventKind); Task_v0_3 task = (Task_v0_3) eventKind; @@ -450,6 +474,8 @@ public void testResubscribe() throws Exception { assertEquals("joke", artifact.name()); Part_v0_3 part = artifact.parts().get(0); assertEquals(Part_v0_3.Kind.TEXT, ((TextPart_v0_3) part).kind()); - assertEquals("Why did the chicken cross the road? To get to the other side!", ((TextPart_v0_3) part).text()); + assertEquals( + "Why did the chicken cross the road? To get to the other side!", + ((TextPart_v0_3) part).text()); } } diff --git a/compat-0.3/client/transport/spi/pom.xml b/compat-0.3/client/transport/spi/pom.xml index 77419708b..24981bc1c 100644 --- a/compat-0.3/client/transport/spi/pom.xml +++ b/compat-0.3/client/transport/spi/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3.java index a5fb77a6b..651bbe56c 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3.java @@ -1,11 +1,10 @@ package org.a2aproject.sdk.compat03.client.http; +import static org.a2aproject.sdk.util.Assert.checkNotNullParam; + import java.io.IOException; import java.net.URISyntaxException; import java.util.Map; - -import static org.a2aproject.sdk.util.Assert.checkNotNullParam; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientFactory; import org.a2aproject.sdk.client.http.A2AHttpResponse; @@ -40,7 +39,8 @@ public A2ACardResolver_v0_3(String baseUrl) throws A2AClientError_v0_3 { * @param baseUrl the base URL for the agent whose agent card we want to retrieve * @throws A2AClientError_v0_3 if the URL for the agent is invalid */ - public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl) throws A2AClientError_v0_3 { + public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl) + throws A2AClientError_v0_3 { this(httpClient, baseUrl, null, null); } @@ -51,7 +51,8 @@ public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl) throws A2A * agent URL, defaults to ".well-known/agent-card.json" * @throws A2AClientError_v0_3 if the URL for the agent is invalid */ - public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl, String agentCardPath) throws A2AClientError_v0_3 { + public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl, String agentCardPath) + throws A2AClientError_v0_3 { this(httpClient, baseUrl, agentCardPath, null); } @@ -63,15 +64,23 @@ public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl, String age * @param authHeaders the HTTP authentication headers to use. May be {@code null} * @throws A2AClientError_v0_3 if the URL for the agent is invalid */ - public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl, @Nullable String agentCardPath, - @Nullable Map authHeaders) throws A2AClientError_v0_3 { + public A2ACardResolver_v0_3( + A2AHttpClient httpClient, + String baseUrl, + @Nullable String agentCardPath, + @Nullable Map authHeaders) + throws A2AClientError_v0_3 { checkNotNullParam("httpClient", httpClient); checkNotNullParam("baseUrl", baseUrl); this.httpClient = httpClient; - String effectiveAgentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? Utils.DEFAULT_AGENT_CARD_PATH : agentCardPath; + String effectiveAgentCardPath = + agentCardPath == null || agentCardPath.isEmpty() + ? Utils.DEFAULT_AGENT_CARD_PATH + : agentCardPath; try { Utils.validateAbsoluteUrl(baseUrl); - this.url = Utils.buildCardUrl(Utils.stripWellKnownSuffix(baseUrl), effectiveAgentCardPath); + this.url = + Utils.buildCardUrl(Utils.stripWellKnownSuffix(baseUrl), effectiveAgentCardPath); } catch (URISyntaxException e) { throw new A2AClientError_v0_3("Invalid agent URL", e); } @@ -86,9 +95,8 @@ public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl, @Nullable * @throws A2AClientJSONError_v0_3 If the response body cannot be decoded as JSON or validated against the AgentCard schema */ public AgentCard_v0_3 getAgentCard() throws A2AClientError_v0_3, A2AClientJSONError_v0_3 { - A2AHttpClient.GetBuilder builder = httpClient.createGet() - .url(url) - .addHeader("Content-Type", "application/json"); + A2AHttpClient.GetBuilder builder = + httpClient.createGet().url(url).addHeader("Content-Type", "application/json"); if (authHeaders != null) { builder.addHeaders(authHeaders); @@ -113,8 +121,5 @@ public AgentCard_v0_3 getAgentCard() throws A2AClientError_v0_3, A2AClientJSONEr } catch (JsonProcessingException_v0_3 e) { throw new A2AClientJSONError_v0_3("Could not unmarshal agent card response", e); } - } - - } diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportConfigBuilder_v0_3.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportConfigBuilder_v0_3.java index 645d2f209..72237ae96 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportConfigBuilder_v0_3.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportConfigBuilder_v0_3.java @@ -1,11 +1,11 @@ package org.a2aproject.sdk.compat03.client.transport.spi; -import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3; - import java.util.ArrayList; import java.util.List; +import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3; -public abstract class ClientTransportConfigBuilder_v0_3, +public abstract class ClientTransportConfigBuilder_v0_3< + T extends ClientTransportConfig_v0_3, B extends ClientTransportConfigBuilder_v0_3> { protected List interceptors = new ArrayList<>(); diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportConfig_v0_3.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportConfig_v0_3.java index f81011d1c..be0d80794 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportConfig_v0_3.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportConfig_v0_3.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.compat03.client.transport.spi; -import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3; import java.util.ArrayList; - import java.util.List; +import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3; /** * Configuration for an A2A client transport. @@ -19,4 +18,4 @@ public void setInterceptors(List interceptors) { public List getInterceptors() { return interceptors; } -} \ No newline at end of file +} diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportProvider_v0_3.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportProvider_v0_3.java index 962825f40..376501e9c 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportProvider_v0_3.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransportProvider_v0_3.java @@ -6,7 +6,8 @@ /** * Client transport provider interface. */ -public interface ClientTransportProvider_v0_3> { +public interface ClientTransportProvider_v0_3< + T extends ClientTransport_v0_3, C extends ClientTransportConfig_v0_3> { /** * Create a client transport. @@ -17,8 +18,8 @@ public interface ClientTransportProvider_v0_3 getTransportProtocolClass(); } - diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransport_v0_3.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransport_v0_3.java index ddc0b920f..466fee6e7 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransport_v0_3.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/ClientTransport_v0_3.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.function.Consumer; - import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; @@ -12,10 +11,10 @@ import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.jspecify.annotations.Nullable; /** @@ -31,7 +30,8 @@ public interface ClientTransport_v0_3 { * @return the response, either a Task or Message * @throws A2AClientException_v0_3 if sending the message fails for any reason */ - EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) + EventKind_v0_3 sendMessage( + MessageSendParams_v0_3 request, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; /** @@ -43,8 +43,12 @@ EventKind_v0_3 sendMessage(MessageSendParams_v0_3 request, @Nullable ClientCallC * @param context optional client call context for the request (may be {@code null}) * @throws A2AClientException_v0_3 if setting up the streaming connection fails */ - void sendMessageStreaming(MessageSendParams_v0_3 request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + void sendMessageStreaming( + MessageSendParams_v0_3 request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Retrieve the current state and history of a specific task. @@ -54,7 +58,8 @@ void sendMessageStreaming(MessageSendParams_v0_3 request, Consumer listTaskPushNotificationConfigurations( ListTaskPushNotificationConfigParams_v0_3 request, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Delete the list of push notification configurations for a specific task. @@ -110,7 +119,8 @@ List listTaskPushNotificationConfigurations( */ void deleteTaskPushNotificationConfigurations( DeleteTaskPushNotificationConfigParams_v0_3 request, - @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Reconnect to get task updates for an existing task. @@ -121,8 +131,12 @@ void deleteTaskPushNotificationConfigurations( * @param context optional client call context for the request (may be {@code null}) * @throws A2AClientException_v0_3 if resubscribing to the task fails for any reason */ - void resubscribe(TaskIdParams_v0_3 request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + void resubscribe( + TaskIdParams_v0_3 request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Retrieve the AgentCard. @@ -131,11 +145,11 @@ void resubscribe(TaskIdParams_v0_3 request, Consumer ev * @return the AgentCard * @throws A2AClientException_v0_3 if retrieving the agent card fails for any reason */ - AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) throws A2AClientException_v0_3; + AgentCard_v0_3 getAgentCard(@Nullable ClientCallContext_v0_3 context) + throws A2AClientException_v0_3; /** * Close the transport and release any associated resources. */ void close(); - } diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/ClientCallInterceptor_v0_3.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/ClientCallInterceptor_v0_3.java index fc0e76584..85f3331f3 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/ClientCallInterceptor_v0_3.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/ClientCallInterceptor_v0_3.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.compat03.client.transport.spi.interceptors; import java.util.Map; - import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.jspecify.annotations.Nullable; @@ -22,6 +21,10 @@ public abstract class ClientCallInterceptor_v0_3 { * @param clientCallContext the {@code ClientCallContext} for this call (may be {@code null}) * @return the potentially modified payload and headers */ - public abstract PayloadAndHeaders_v0_3 intercept(String methodName, @Nullable Object payload, Map headers, - AgentCard_v0_3 agentCard, @Nullable ClientCallContext_v0_3 clientCallContext); + public abstract PayloadAndHeaders_v0_3 intercept( + String methodName, + @Nullable Object payload, + Map headers, + AgentCard_v0_3 agentCard, + @Nullable ClientCallContext_v0_3 clientCallContext); } diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/PayloadAndHeaders_v0_3.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/PayloadAndHeaders_v0_3.java index bc08d174c..10065b8c3 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/PayloadAndHeaders_v0_3.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/PayloadAndHeaders_v0_3.java @@ -7,7 +7,7 @@ public class PayloadAndHeaders_v0_3 { - private final @Nullable Object payload; + private final @Nullable Object payload; private final Map headers; public PayloadAndHeaders_v0_3(@Nullable Object payload, @Nullable Map headers) { diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/AuthInterceptor_v0_3.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/AuthInterceptor_v0_3.java index 5ff604a18..6e74b8711 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/AuthInterceptor_v0_3.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/AuthInterceptor_v0_3.java @@ -5,7 +5,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; - import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.PayloadAndHeaders_v0_3; @@ -36,13 +35,8 @@ public class AuthInterceptor_v0_3 extends ClientCallInterceptor_v0_3 { * Only standard authentication-related headers are permitted. * Header names are stored in lowercase for case-insensitive comparison. */ - private static final Set SAFE_API_KEY_HEADER_NAMES = Set.of( - "authorization", - "x-api-key", - "api-key", - "x-auth-token", - "x-authentication" - ); + private static final Set SAFE_API_KEY_HEADER_NAMES = + Set.of("authorization", "x-api-key", "api-key", "x-auth-token", "x-authentication"); private final CredentialService_v0_3 credentialService; @@ -51,21 +45,32 @@ public AuthInterceptor_v0_3(final CredentialService_v0_3 credentialService) { } @Override - public PayloadAndHeaders_v0_3 intercept(String methodName, @Nullable Object payload, Map headers, - AgentCard_v0_3 agentCard, @Nullable ClientCallContext_v0_3 clientCallContext) { - Map updatedHeaders = new HashMap<>(headers == null ? new HashMap<>() : headers); - if (agentCard == null || agentCard.security() == null || agentCard.securitySchemes() == null) { + public PayloadAndHeaders_v0_3 intercept( + String methodName, + @Nullable Object payload, + Map headers, + AgentCard_v0_3 agentCard, + @Nullable ClientCallContext_v0_3 clientCallContext) { + Map updatedHeaders = + new HashMap<>(headers == null ? new HashMap<>() : headers); + if (agentCard == null + || agentCard.security() == null + || agentCard.securitySchemes() == null) { return new PayloadAndHeaders_v0_3(payload, updatedHeaders); } for (Map> requirement : agentCard.security()) { for (String securitySchemeName : requirement.keySet()) { - String credential = credentialService.getCredential(securitySchemeName, clientCallContext); - if (credential != null && agentCard.securitySchemes().containsKey(securitySchemeName)) { - SecurityScheme_v0_3 securityScheme = agentCard.securitySchemes().get(securitySchemeName); + String credential = + credentialService.getCredential(securitySchemeName, clientCallContext); + if (credential != null + && agentCard.securitySchemes().containsKey(securitySchemeName)) { + SecurityScheme_v0_3 securityScheme = + agentCard.securitySchemes().get(securitySchemeName); if (securityScheme == null) { continue; } - if (securityScheme instanceof HTTPAuthSecurityScheme_v0_3 httpAuthSecurityScheme) { + if (securityScheme + instanceof HTTPAuthSecurityScheme_v0_3 httpAuthSecurityScheme) { String scheme = httpAuthSecurityScheme.scheme().toLowerCase(Locale.ROOT); if (scheme.equals(BEARER_SCHEME)) { updatedHeaders.put(AUTHORIZATION, getBearerValue(credential)); @@ -78,8 +83,10 @@ public PayloadAndHeaders_v0_3 intercept(String methodName, @Nullable Object payl || securityScheme instanceof OpenIdConnectSecurityScheme_v0_3) { updatedHeaders.put(AUTHORIZATION, getBearerValue(credential)); return new PayloadAndHeaders_v0_3(payload, updatedHeaders); - } else if (securityScheme instanceof APIKeySecurityScheme_v0_3 apiKeySecurityScheme) { - // Only inject API key if it's intended for header transport and the header name is safe + } else if (securityScheme + instanceof APIKeySecurityScheme_v0_3 apiKeySecurityScheme) { + // Only inject API key if it's intended for header transport and the header + // name is safe if ("header".equals(apiKeySecurityScheme.in()) && isSafeHeaderName(apiKeySecurityScheme.name())) { updatedHeaders.put(apiKeySecurityScheme.name(), credential); diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/CredentialService_v0_3.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/CredentialService_v0_3.java index 05a320eb4..881f1ec75 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/CredentialService_v0_3.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/CredentialService_v0_3.java @@ -15,5 +15,6 @@ public interface CredentialService_v0_3 { * @param clientCallContext the client call context, which may be {@code null}. * @return the credential or {@code null} if the credential could not be retrieved */ - @Nullable String getCredential(String securitySchemeName, @Nullable ClientCallContext_v0_3 clientCallContext); + @Nullable String getCredential( + String securitySchemeName, @Nullable ClientCallContext_v0_3 clientCallContext); } diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/InMemoryContextCredentialService_v0_3.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/InMemoryContextCredentialService_v0_3.java index 21f77dcc8..907bf22bb 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/InMemoryContextCredentialService_v0_3.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/InMemoryContextCredentialService_v0_3.java @@ -3,7 +3,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; - import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.jspecify.annotations.Nullable; @@ -24,15 +23,15 @@ public InMemoryContextCredentialService_v0_3() { } @Override - public @Nullable String getCredential(String securitySchemeName, - @Nullable ClientCallContext_v0_3 clientCallContext) { + public @Nullable String getCredential( + String securitySchemeName, @Nullable ClientCallContext_v0_3 clientCallContext) { if (clientCallContext == null || !clientCallContext.getState().containsKey(SESSION_ID)) { // no credential to retrieve return null; } Object sessionIdObj = clientCallContext.getState().get(SESSION_ID); - if (! (sessionIdObj instanceof String sessionId)) { + if (!(sessionIdObj instanceof String sessionId)) { return null; } Map sessionCredentials = credentialStore.get(sessionId); @@ -50,6 +49,8 @@ public InMemoryContextCredentialService_v0_3() { * @param credential the credential string */ public void setCredential(String sessionId, String securitySchemeName, String credential) { - credentialStore.computeIfAbsent(sessionId, k -> new ConcurrentHashMap<>()).put(securitySchemeName, credential); + credentialStore + .computeIfAbsent(sessionId, k -> new ConcurrentHashMap<>()) + .put(securitySchemeName, credential); } } diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/package-info.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/package-info.java index d9805799c..2ac4b8a39 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/package-info.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth; import org.jspecify.annotations.NullMarked; - diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/package-info.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/package-info.java index bda43ec7b..51904e094 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/package-info.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.compat03.client.transport.spi.interceptors; import org.jspecify.annotations.NullMarked; - diff --git a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/package-info.java b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/package-info.java index 850736146..4be693526 100644 --- a/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/package-info.java +++ b/compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/transport/spi/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.compat03.client.transport.spi; import org.jspecify.annotations.NullMarked; - diff --git a/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java b/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java index 1575d67a2..9650ae40d 100644 --- a/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java +++ b/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java @@ -9,7 +9,6 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.client.http.ServerSentEvent; @@ -33,7 +32,6 @@ public void testConstructorStripsSlashes() throws Exception { assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); - resolver = new A2ACardResolver_v0_3(client, "http://example.com"); card = resolver.getAgentCard(); @@ -52,13 +50,17 @@ public void testConstructorStripsSlashes() throws Exception { assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); // baseUrl with trailing slash, agentCardPath without leading slash - resolver = new A2ACardResolver_v0_3(client, "http://example.com/", AGENT_CARD_PATH.substring(1)); + resolver = + new A2ACardResolver_v0_3( + client, "http://example.com/", AGENT_CARD_PATH.substring(1)); card = resolver.getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); // baseUrl without trailing slash, agentCardPath without leading slash - resolver = new A2ACardResolver_v0_3(client, "http://example.com", AGENT_CARD_PATH.substring(1)); + resolver = + new A2ACardResolver_v0_3( + client, "http://example.com", AGENT_CARD_PATH.substring(1)); card = resolver.getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); @@ -77,7 +79,6 @@ public void testConstructorStripsSlashes() throws Exception { assertEquals("http://example.com/jsonrpc" + AGENT_CARD_PATH, client.url); } - @Test public void testBaseUrl_alreadyContainsWellKnownPath() throws Exception { TestHttpClient client = new TestHttpClient(); @@ -95,8 +96,11 @@ public void testFullWellKnownUrlWithCustomAgentCardPath() throws Exception { TestHttpClient client = new TestHttpClient(); client.body = JsonMessages_v0_3.AGENT_CARD; - A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3( - client, "https://example.com/spec03" + AGENT_CARD_PATH, "/custom/card.json"); + A2ACardResolver_v0_3 resolver = + new A2ACardResolver_v0_3( + client, + "https://example.com/spec03" + AGENT_CARD_PATH, + "/custom/card.json"); resolver.getAgentCard(); assertEquals("https://example.com/spec03/custom/card.json", client.url); @@ -110,7 +114,8 @@ public void testGetAgentCardSuccess() throws Exception { A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, "http://example.com/"); AgentCard_v0_3 card = resolver.getAgentCard(); - AgentCard_v0_3 expectedCard = JsonUtil_v0_3.fromJson(JsonMessages_v0_3.AGENT_CARD, AgentCard_v0_3.class); + AgentCard_v0_3 expectedCard = + JsonUtil_v0_3.fromJson(JsonMessages_v0_3.AGENT_CARD, AgentCard_v0_3.class); String expected = JsonUtil_v0_3.toJson(expectedCard); String requestCardString = JsonUtil_v0_3.toJson(card); @@ -133,7 +138,6 @@ public void testGetAgentCardJsonDecodeError() throws Exception { assertFalse(success); } - @Test public void testParseV1AgentCardWithUrlAndPreferredTransport() throws Exception { TestHttpClient client = new TestHttpClient(); @@ -225,7 +229,11 @@ public String body() { } @Override - public CompletableFuture getAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { + public CompletableFuture getAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) + throws IOException, InterruptedException { return null; } @@ -246,5 +254,4 @@ public GetBuilder addHeaders(Map headers) { } } } - } diff --git a/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java b/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java index 9b9a4ec0c..693d4a548 100644 --- a/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java +++ b/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java @@ -6,7 +6,8 @@ */ public class JsonMessages_v0_3 { - static final String AGENT_CARD = """ + static final String AGENT_CARD = + """ { "protocolVersion": "0.2.9", "name": "GeoSpatial Route Planner Agent", @@ -81,14 +82,16 @@ public class JsonMessages_v0_3 { "signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ" } ] - }"""; + }\ + """; /** * A v1.0 format agent card returned by a current server. Contains the new {@code supportedInterfaces} * and {@code securityRequirements} fields (v1.0-only), plus the backward-compat {@code url} and * {@code preferredTransport} fields that the v0.3 client relies on. */ - static final String V1_AGENT_CARD = """ + static final String V1_AGENT_CARD = + """ { "name": "Test Agent", "description": "A test agent", @@ -108,13 +111,15 @@ public class JsonMessages_v0_3 { "defaultOutputModes": ["text/plain"], "skills": [], "securityRequirements": [{"oauth2": ["read"]}] - }"""; + }\ + """; /** * A v1.0 format agent card without an explicit {@code preferredTransport}. The v0.3 client must * fall back to the default transport ("JSONRPC"). */ - static final String V1_AGENT_CARD_NO_PREFERRED_TRANSPORT = """ + static final String V1_AGENT_CARD_NO_PREFERRED_TRANSPORT = + """ { "name": "Minimal Agent", "description": "Minimal agent card", @@ -131,9 +136,11 @@ public class JsonMessages_v0_3 { "defaultInputModes": ["text/plain"], "defaultOutputModes": ["text/plain"], "skills": [] - }"""; + }\ + """; - static final String AUTHENTICATION_EXTENDED_AGENT_CARD = """ + static final String AUTHENTICATION_EXTENDED_AGENT_CARD = + """ { "name": "GeoSpatial Route Planner Agent Extended", "description": "Extended description", @@ -208,7 +215,6 @@ public class JsonMessages_v0_3 { "signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ" } ] - }"""; - - + }\ + """; } diff --git a/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/AuthInterceptor_v0_3_Test.java b/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/AuthInterceptor_v0_3_Test.java index c987c9197..046f08ab8 100644 --- a/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/AuthInterceptor_v0_3_Test.java +++ b/compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/transport/spi/interceptors/auth/AuthInterceptor_v0_3_Test.java @@ -6,7 +6,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallContext_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.ClientCallInterceptor_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.PayloadAndHeaders_v0_3; @@ -18,7 +17,6 @@ import org.a2aproject.sdk.compat03.spec.OAuthFlows_v0_3; import org.a2aproject.sdk.compat03.spec.OpenIdConnectSecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.SecurityScheme_v0_3; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,8 +41,12 @@ public HeaderInterceptor(String headerName, String headerValue) { } @Override - public PayloadAndHeaders_v0_3 intercept(String methodName, Object payload, Map headers, - AgentCard_v0_3 agentCard, ClientCallContext_v0_3 clientCallContext) { + public PayloadAndHeaders_v0_3 intercept( + String methodName, + Object payload, + Map headers, + AgentCard_v0_3 agentCard, + ClientCallContext_v0_3 clientCallContext) { Map updatedHeaders = new HashMap<>(headers); updatedHeaders.put(headerName, headerValue); return new PayloadAndHeaders_v0_3(payload, updatedHeaders); @@ -60,8 +62,14 @@ private static class AuthTestCase { final String expectedHeaderKey; final String expectedHeaderValue; - AuthTestCase(String url, String sessionId, String schemeName, String credential, - SecurityScheme_v0_3 securityScheme, String expectedHeaderKey, String expectedHeaderValue) { + AuthTestCase( + String url, + String sessionId, + String schemeName, + String credential, + SecurityScheme_v0_3 securityScheme, + String expectedHeaderKey, + String expectedHeaderValue) { this.url = url; this.sessionId = sessionId; this.schemeName = schemeName; @@ -74,73 +82,78 @@ private static class AuthTestCase { @Test public void testAPIKeySecurityScheme() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - APIKeySecurityScheme_v0_3.TYPE, - "secret-api-key", - new APIKeySecurityScheme_v0_3("header", "X-API-Key", "API Key authentication"), - "X-API-Key", - "secret-api-key" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + APIKeySecurityScheme_v0_3.TYPE, + "secret-api-key", + new APIKeySecurityScheme_v0_3( + "header", "X-API-Key", "API Key authentication"), + "X-API-Key", + "secret-api-key"); testSecurityScheme(authTestCase); } @Test public void testAPIKeySecurityScheme_SafeHeaderName_Authorization() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - APIKeySecurityScheme_v0_3.TYPE, - "secret-api-key", - new APIKeySecurityScheme_v0_3("header", "Authorization", "API Key authentication"), - "Authorization", - "secret-api-key" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + APIKeySecurityScheme_v0_3.TYPE, + "secret-api-key", + new APIKeySecurityScheme_v0_3( + "header", "Authorization", "API Key authentication"), + "Authorization", + "secret-api-key"); testSecurityScheme(authTestCase); } @Test public void testAPIKeySecurityScheme_SafeHeaderName_XAuthToken() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - APIKeySecurityScheme_v0_3.TYPE, - "secret-api-key", - new APIKeySecurityScheme_v0_3("header", "X-Auth-Token", "API Key authentication"), - "X-Auth-Token", - "secret-api-key" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + APIKeySecurityScheme_v0_3.TYPE, + "secret-api-key", + new APIKeySecurityScheme_v0_3( + "header", "X-Auth-Token", "API Key authentication"), + "X-Auth-Token", + "secret-api-key"); testSecurityScheme(authTestCase); } @Test public void testAPIKeySecurityScheme_CaseInsensitiveHeaderName() { // Test that lowercase header names are accepted (case-insensitive comparison) - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - APIKeySecurityScheme_v0_3.TYPE, - "secret-api-key", - new APIKeySecurityScheme_v0_3("header", "x-api-key", "API Key authentication"), - "x-api-key", - "secret-api-key" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + APIKeySecurityScheme_v0_3.TYPE, + "secret-api-key", + new APIKeySecurityScheme_v0_3( + "header", "x-api-key", "API Key authentication"), + "x-api-key", + "secret-api-key"); testSecurityScheme(authTestCase); } @Test public void testAPIKeySecurityScheme_CaseInsensitiveHeaderName_Authorization() { // Test that lowercase "authorization" is accepted - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - APIKeySecurityScheme_v0_3.TYPE, - "secret-api-key", - new APIKeySecurityScheme_v0_3("header", "authorization", "API Key authentication"), - "authorization", - "secret-api-key" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + APIKeySecurityScheme_v0_3.TYPE, + "secret-api-key", + new APIKeySecurityScheme_v0_3( + "header", "authorization", "API Key authentication"), + "authorization", + "secret-api-key"); testSecurityScheme(authTestCase); } @@ -153,24 +166,19 @@ public void testAPIKeySecurityScheme_UnsafeHeaderName_Rejected() { credentialStore.setCredential(sessionId, schemeName, credential); // Use an unsafe header name that's not in the allowlist - SecurityScheme_v0_3 securityScheme = new APIKeySecurityScheme_v0_3( - "header", - "X-Malicious-Redirect-Header", - "Unsafe header" - ); + SecurityScheme_v0_3 securityScheme = + new APIKeySecurityScheme_v0_3( + "header", "X-Malicious-Redirect-Header", "Unsafe header"); AgentCard_v0_3 agentCard = createAgentCard(schemeName, securityScheme); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); - ClientCallContext_v0_3 context = new ClientCallContext_v0_3(Map.of("sessionId", sessionId), Map.of()); + ClientCallContext_v0_3 context = + new ClientCallContext_v0_3(Map.of("sessionId", sessionId), Map.of()); - PayloadAndHeaders_v0_3 result = authInterceptor.intercept( - "message/send", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders_v0_3 result = + authInterceptor.intercept( + "message/send", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); // Credential should NOT be injected for unsafe header name @@ -187,24 +195,18 @@ public void testAPIKeySecurityScheme_QueryLocation_NotInjectedInHeader() { credentialStore.setCredential(sessionId, schemeName, credential); // API key in query parameter location should not be injected as header - SecurityScheme_v0_3 securityScheme = new APIKeySecurityScheme_v0_3( - "query", - "api_key", - "Query parameter API key" - ); + SecurityScheme_v0_3 securityScheme = + new APIKeySecurityScheme_v0_3("query", "api_key", "Query parameter API key"); AgentCard_v0_3 agentCard = createAgentCard(schemeName, securityScheme); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); - ClientCallContext_v0_3 context = new ClientCallContext_v0_3(Map.of("sessionId", sessionId), Map.of()); + ClientCallContext_v0_3 context = + new ClientCallContext_v0_3(Map.of("sessionId", sessionId), Map.of()); - PayloadAndHeaders_v0_3 result = authInterceptor.intercept( - "message/send", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders_v0_3 result = + authInterceptor.intercept( + "message/send", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); // Credential should NOT be injected as header for query location @@ -221,24 +223,18 @@ public void testAPIKeySecurityScheme_CookieLocation_NotInjectedInHeader() { credentialStore.setCredential(sessionId, schemeName, credential); // API key in cookie location should not be injected as header - SecurityScheme_v0_3 securityScheme = new APIKeySecurityScheme_v0_3( - "cookie", - "session_token", - "Cookie-based API key" - ); + SecurityScheme_v0_3 securityScheme = + new APIKeySecurityScheme_v0_3("cookie", "session_token", "Cookie-based API key"); AgentCard_v0_3 agentCard = createAgentCard(schemeName, securityScheme); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); - ClientCallContext_v0_3 context = new ClientCallContext_v0_3(Map.of("sessionId", sessionId), Map.of()); + ClientCallContext_v0_3 context = + new ClientCallContext_v0_3(Map.of("sessionId", sessionId), Map.of()); - PayloadAndHeaders_v0_3 result = authInterceptor.intercept( - "message/send", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders_v0_3 result = + authInterceptor.intercept( + "message/send", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); // Credential should NOT be injected as header for cookie location @@ -248,64 +244,71 @@ public void testAPIKeySecurityScheme_CookieLocation_NotInjectedInHeader() { @Test public void testOAuth2SecurityScheme() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - OAuth2SecurityScheme_v0_3.TYPE, - "secret-oauth-access-token", - new OAuth2SecurityScheme_v0_3(new OAuthFlows_v0_3.Builder().build(), "OAuth2 authentication", null), - "Authorization", - "Bearer secret-oauth-access-token" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + OAuth2SecurityScheme_v0_3.TYPE, + "secret-oauth-access-token", + new OAuth2SecurityScheme_v0_3( + new OAuthFlows_v0_3.Builder().build(), + "OAuth2 authentication", + null), + "Authorization", + "Bearer secret-oauth-access-token"); testSecurityScheme(authTestCase); } @Test public void testOidcSecurityScheme() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - OpenIdConnectSecurityScheme_v0_3.TYPE, - "secret-oidc-id-token", - new OpenIdConnectSecurityScheme_v0_3("http://provider.com/.well-known/openid-configuration", "OIDC authentication"), - "Authorization", - "Bearer secret-oidc-id-token" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + OpenIdConnectSecurityScheme_v0_3.TYPE, + "secret-oidc-id-token", + new OpenIdConnectSecurityScheme_v0_3( + "http://provider.com/.well-known/openid-configuration", + "OIDC authentication"), + "Authorization", + "Bearer secret-oidc-id-token"); testSecurityScheme(authTestCase); } @Test public void testBearerSecurityScheme() { - AuthTestCase authTestCase = new AuthTestCase( - "http://agent.com/rpc", - "session-id", - "bearer", - "bearer-token-123", - new HTTPAuthSecurityScheme_v0_3(null, "bearer", "Bearer token authentication"), - "Authorization", - "Bearer bearer-token-123" - ); + AuthTestCase authTestCase = + new AuthTestCase( + "http://agent.com/rpc", + "session-id", + "bearer", + "bearer-token-123", + new HTTPAuthSecurityScheme_v0_3( + null, "bearer", "Bearer token authentication"), + "Authorization", + "Bearer bearer-token-123"); testSecurityScheme(authTestCase); } private void testSecurityScheme(AuthTestCase authTestCase) { - credentialStore.setCredential(authTestCase.sessionId, authTestCase.schemeName, authTestCase.credential); + credentialStore.setCredential( + authTestCase.sessionId, authTestCase.schemeName, authTestCase.credential); - AgentCard_v0_3 agentCard = createAgentCard(authTestCase.schemeName, authTestCase.securityScheme); + AgentCard_v0_3 agentCard = + createAgentCard(authTestCase.schemeName, authTestCase.securityScheme); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); - ClientCallContext_v0_3 context = new ClientCallContext_v0_3(Map.of("sessionId", authTestCase.sessionId), Map.of()); + ClientCallContext_v0_3 context = + new ClientCallContext_v0_3(Map.of("sessionId", authTestCase.sessionId), Map.of()); - PayloadAndHeaders_v0_3 result = authInterceptor.intercept( - "message/send", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders_v0_3 result = + authInterceptor.intercept( + "message/send", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); - assertEquals(authTestCase.expectedHeaderValue, result.getHeaders().get(authTestCase.expectedHeaderKey)); + assertEquals( + authTestCase.expectedHeaderValue, + result.getHeaders().get(authTestCase.expectedHeaderKey)); } @Test @@ -313,13 +316,13 @@ void testAuthInterceptorWithoutAgentCard() { Map requestPayload = Map.of("foo", "bar"); Map headers = Map.of("foo", "bar"); - PayloadAndHeaders_v0_3 result = authInterceptor.intercept( - "message/send", - requestPayload, - headers, - null, // no agent card - new ClientCallContext_v0_3(Map.of(), Map.of()) - ); + PayloadAndHeaders_v0_3 result = + authInterceptor.intercept( + "message/send", + requestPayload, + headers, + null, // no agent card + new ClientCallContext_v0_3(Map.of(), Map.of())); // should be unchanged assertEquals(requestPayload, result.getPayload()); @@ -333,12 +336,14 @@ void testInMemoryContextCredentialStore() { String credential = "test-token"; credentialStore.setCredential(sessionId, schemeName, credential); - ClientCallContext_v0_3 context = new ClientCallContext_v0_3(Map.of("sessionId", sessionId), Map.of()); + ClientCallContext_v0_3 context = + new ClientCallContext_v0_3(Map.of("sessionId", sessionId), Map.of()); String retrievedCredential = credentialStore.getCredential(schemeName, context); assertEquals(credential, retrievedCredential); // wrong session ID - ClientCallContext_v0_3 wrongContext = new ClientCallContext_v0_3(Map.of("sessionId", "wrong-session"), Map.of()); + ClientCallContext_v0_3 wrongContext = + new ClientCallContext_v0_3(Map.of("sessionId", "wrong-session"), Map.of()); retrievedCredential = credentialStore.getCredential(schemeName, wrongContext); assertNull(retrievedCredential); @@ -365,13 +370,8 @@ void testCustomInterceptor() { Map payload = Map.of("test", "payload"); Map headers = Map.of(); - PayloadAndHeaders_v0_3 result = interceptor.intercept( - "message/send", - payload, - headers, - null, - null - ); + PayloadAndHeaders_v0_3 result = + interceptor.intercept("message/send", payload, headers, null, null); assertEquals(payload, result.getPayload()); assertEquals(headerValue, result.getHeaders().get(headerName)); @@ -386,30 +386,28 @@ void testAvailableSecuritySchemeNotInAgentCardSecuritySchemes() { credentialStore.setCredential(sessionId, schemeName, credential); // Create agent card with security requirement but no scheme definition - AgentCard_v0_3 agentCard = new AgentCard_v0_3.Builder() - .name("missing") - .description("Uses missing scheme definition") - .url("http://agent.com/rpc") - .version("1.0") - .capabilities(new AgentCapabilities_v0_3.Builder().build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .security(List.of(Map.of(schemeName, List.of()))) - .securitySchemes(Map.of()) // no security schemes - .build(); + AgentCard_v0_3 agentCard = + new AgentCard_v0_3.Builder() + .name("missing") + .description("Uses missing scheme definition") + .url("http://agent.com/rpc") + .version("1.0") + .capabilities(new AgentCapabilities_v0_3.Builder().build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .security(List.of(Map.of(schemeName, List.of()))) + .securitySchemes(Map.of()) // no security schemes + .build(); Map requestPayload = Map.of("foo", "bar"); Map headers = Map.of("fizz", "buzz"); - ClientCallContext_v0_3 context = new ClientCallContext_v0_3(Map.of("sessionId", sessionId), Map.of()); + ClientCallContext_v0_3 context = + new ClientCallContext_v0_3(Map.of("sessionId", sessionId), Map.of()); - PayloadAndHeaders_v0_3 result = authInterceptor.intercept( - "message/send", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders_v0_3 result = + authInterceptor.intercept( + "message/send", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); assertEquals(headers, result.getHeaders()); @@ -418,20 +416,18 @@ void testAvailableSecuritySchemeNotInAgentCardSecuritySchemes() { @Test void testNoCredentialAvailable() { String schemeName = "apikey"; - SecurityScheme_v0_3 securityScheme = new APIKeySecurityScheme_v0_3("header", "X-API-Key", "API Key authentication"); + SecurityScheme_v0_3 securityScheme = + new APIKeySecurityScheme_v0_3("header", "X-API-Key", "API Key authentication"); AgentCard_v0_3 agentCard = createAgentCard(schemeName, securityScheme); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); - ClientCallContext_v0_3 context = new ClientCallContext_v0_3(Map.of("sessionId", "session-id"), Map.of()); + ClientCallContext_v0_3 context = + new ClientCallContext_v0_3(Map.of("sessionId", "session-id"), Map.of()); - PayloadAndHeaders_v0_3 result = authInterceptor.intercept( - "message/send", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders_v0_3 result = + authInterceptor.intercept( + "message/send", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); assertEquals(headers, result.getHeaders()); // headers should be unchanged @@ -440,29 +436,27 @@ void testNoCredentialAvailable() { @Test void testNoAgentCardSecuritySpecified() { // Arrange - AgentCard_v0_3 agentCard = new AgentCard_v0_3.Builder() - .name("nosecuritybot") - .description("A bot with no security requirements") - .url("http://agent.com/rpc") - .version("1.0") - .capabilities(new AgentCapabilities_v0_3.Builder().build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .security(null) // no security info - .build(); + AgentCard_v0_3 agentCard = + new AgentCard_v0_3.Builder() + .name("nosecuritybot") + .description("A bot with no security requirements") + .url("http://agent.com/rpc") + .version("1.0") + .capabilities(new AgentCapabilities_v0_3.Builder().build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .security(null) // no security info + .build(); Map requestPayload = Map.of("test", "payload"); Map headers = Map.of(); - ClientCallContext_v0_3 context = new ClientCallContext_v0_3(Map.of("sessionId", "session-id"), Map.of()); + ClientCallContext_v0_3 context = + new ClientCallContext_v0_3(Map.of("sessionId", "session-id"), Map.of()); - PayloadAndHeaders_v0_3 result = authInterceptor.intercept( - "message/send", - requestPayload, - headers, - agentCard, - context - ); + PayloadAndHeaders_v0_3 result = + authInterceptor.intercept( + "message/send", requestPayload, headers, agentCard, context); assertEquals(requestPayload, result.getPayload()); assertEquals(headers, result.getHeaders()); @@ -473,16 +467,16 @@ void testNoAgentCardSecuritySpecified() { */ private AgentCard_v0_3 createAgentCard(String schemeName, SecurityScheme_v0_3 securityScheme) { return new AgentCard_v0_3.Builder() - .name(schemeName + "bot") - .description("A bot that uses " + schemeName) - .url("http://agent.com/rpc") - .version("1.0") - .capabilities(new AgentCapabilities_v0_3.Builder().build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .security(List.of(Map.of(schemeName, List.of()))) - .securitySchemes(Map.of(schemeName, securityScheme)) - .build(); + .name(schemeName + "bot") + .description("A bot that uses " + schemeName) + .url("http://agent.com/rpc") + .version("1.0") + .capabilities(new AgentCapabilities_v0_3.Builder().build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .security(List.of(Map.of(schemeName, List.of()))) + .securitySchemes(Map.of(schemeName, securityScheme)) + .build(); } } diff --git a/compat-0.3/pom.xml b/compat-0.3/pom.xml index 87b7c1ca4..33e1fe7f2 100644 --- a/compat-0.3/pom.xml +++ b/compat-0.3/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -16,6 +14,38 @@ Java SDK A2A Compat 0.3 Parent A2A Protocol v0.3 backward compatibility layer for v1.0 SDK + + + spec + spec-grpc + + + server-conversion + + + tests/server-common + + + client/base + client/transport/spi + client/transport/jsonrpc + client/transport/grpc + client/transport/rest + + + transport/jsonrpc + transport/grpc + transport/rest + + + reference/jsonrpc + reference/grpc + reference/rest + + + tck + + @@ -102,8 +132,8 @@ ${project.groupId} a2a-java-sdk-compat-0.3-server-conversion - test-jar ${project.version} + test-jar ${project.groupId} @@ -113,36 +143,4 @@ - - - spec - spec-grpc - - - server-conversion - - - tests/server-common - - - client/base - client/transport/spi - client/transport/jsonrpc - client/transport/grpc - client/transport/rest - - - transport/jsonrpc - transport/grpc - transport/rest - - - reference/jsonrpc - reference/grpc - reference/rest - - - tck - - diff --git a/compat-0.3/reference/grpc/pom.xml b/compat-0.3/reference/grpc/pom.xml index 95ac53dec..315d12c4a 100644 --- a/compat-0.3/reference/grpc/pom.xml +++ b/compat-0.3/reference/grpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 org.a2aproject.sdk @@ -136,4 +134,4 @@ - \ No newline at end of file + diff --git a/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/A2AExtensionsInterceptor_v0_3.java b/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/A2AExtensionsInterceptor_v0_3.java index 5d3ae805c..121ccf85c 100644 --- a/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/A2AExtensionsInterceptor_v0_3.java +++ b/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/A2AExtensionsInterceptor_v0_3.java @@ -1,12 +1,12 @@ package org.a2aproject.sdk.compat03.server.grpc.quarkus; -import jakarta.enterprise.context.ApplicationScoped; import io.grpc.Context; import io.grpc.Contexts; import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; +import jakarta.enterprise.context.ApplicationScoped; import org.a2aproject.sdk.compat03.common.A2AHeaders_v0_3; import org.a2aproject.sdk.compat03.transport.grpc.context.GrpcContextKeys_v0_3; @@ -23,7 +23,6 @@ @ApplicationScoped public class A2AExtensionsInterceptor_v0_3 implements ServerInterceptor { - @Override public ServerCall.Listener interceptCall( ServerCall serverCall, @@ -32,17 +31,20 @@ public ServerCall.Listener interceptCall( // Extract A2A extensions header Metadata.Key extensionsKey = - Metadata.Key.of(A2AHeaders_v0_3.X_A2A_EXTENSIONS, Metadata.ASCII_STRING_MARSHALLER); + Metadata.Key.of(A2AHeaders_v0_3.X_A2A_EXTENSIONS, Metadata.ASCII_STRING_MARSHALLER); String extensions = metadata.get(extensionsKey); // Create enhanced context with rich information (equivalent to Python's ServicerContext) - Context context = Context.current() - // Store complete metadata for full header access - .withValue(GrpcContextKeys_v0_3.METADATA_KEY, metadata) - // Store method name (equivalent to Python's context.method()) - .withValue(GrpcContextKeys_v0_3.METHOD_NAME_KEY, serverCall.getMethodDescriptor().getFullMethodName()) - // Store peer information for client connection details - .withValue(GrpcContextKeys_v0_3.PEER_INFO_KEY, getPeerInfo(serverCall)); + Context context = + Context.current() + // Store complete metadata for full header access + .withValue(GrpcContextKeys_v0_3.METADATA_KEY, metadata) + // Store method name (equivalent to Python's context.method()) + .withValue( + GrpcContextKeys_v0_3.METHOD_NAME_KEY, + serverCall.getMethodDescriptor().getFullMethodName()) + // Store peer information for client connection details + .withValue(GrpcContextKeys_v0_3.PEER_INFO_KEY, getPeerInfo(serverCall)); // Store A2A extensions if present if (extensions != null) { @@ -61,7 +63,8 @@ public ServerCall.Listener interceptCall( */ private String getPeerInfo(ServerCall serverCall) { try { - Object remoteAddr = serverCall.getAttributes().get(io.grpc.Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + Object remoteAddr = + serverCall.getAttributes().get(io.grpc.Grpc.TRANSPORT_ATTR_REMOTE_ADDR); return remoteAddr != null ? remoteAddr.toString() : "unknown"; } catch (Exception e) { return "unknown"; diff --git a/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/BlockingOffloadInterceptor_v0_3.java b/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/BlockingOffloadInterceptor_v0_3.java index 3c79f8474..42c4a1d19 100644 --- a/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/BlockingOffloadInterceptor_v0_3.java +++ b/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/BlockingOffloadInterceptor_v0_3.java @@ -1,11 +1,5 @@ package org.a2aproject.sdk.compat03.server.grpc.quarkus; -import java.util.concurrent.Executor; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; - -import org.a2aproject.sdk.server.util.async.Internal; import io.grpc.Context; import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; import io.grpc.Metadata; @@ -14,6 +8,10 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.util.concurrent.Executor; +import org.a2aproject.sdk.server.util.async.Internal; /** * v0.3 variant of {@code BlockingOffloadInterceptor} in the reference/grpc module. @@ -30,9 +28,7 @@ public BlockingOffloadInterceptor_v0_3(@Internal Executor executor) { @Override public ServerCall.Listener interceptCall( - ServerCall call, - Metadata headers, - ServerCallHandler next) { + ServerCall call, Metadata headers, ServerCallHandler next) { MethodDescriptor.MethodType type = call.getMethodDescriptor().getType(); if (type == MethodDescriptor.MethodType.CLIENT_STREAMING @@ -47,18 +43,25 @@ public ServerCall.Listener interceptCall( public void onHalfClose() { Context grpcContext = Context.current().fork(); try { - executor.execute(() -> { - Context previous = grpcContext.attach(); - try { - super.onHalfClose(); - } catch (Exception e) { - call.close(Status.INTERNAL.withDescription("Error during execution: " + e.getMessage()), new Metadata()); - } finally { - grpcContext.detach(previous); - } - }); + executor.execute( + () -> { + Context previous = grpcContext.attach(); + try { + super.onHalfClose(); + } catch (Exception e) { + call.close( + Status.INTERNAL.withDescription( + "Error during execution: " + e.getMessage()), + new Metadata()); + } finally { + grpcContext.detach(previous); + } + }); } catch (Exception e) { - call.close(Status.INTERNAL.withDescription("Failed to offload to worker thread: " + e.getMessage()), new Metadata()); + call.close( + Status.INTERNAL.withDescription( + "Failed to offload to worker thread: " + e.getMessage()), + new Metadata()); } } }; diff --git a/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusCallContextFactory_v0_3.java b/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusCallContextFactory_v0_3.java index 8db172a5e..19396807c 100644 --- a/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusCallContextFactory_v0_3.java +++ b/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusCallContextFactory_v0_3.java @@ -2,18 +2,16 @@ import static org.a2aproject.sdk.server.ServerCallContext.TRANSPORT_KEY; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; - import io.grpc.Context; import io.grpc.Metadata; import io.grpc.stub.StreamObserver; import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; import org.a2aproject.sdk.compat03.conversion.A2AProtocol_v0_3; import org.a2aproject.sdk.compat03.transport.grpc.context.GrpcContextKeys_v0_3; import org.a2aproject.sdk.compat03.transport.grpc.handler.CallContextFactory_v0_3; @@ -26,8 +24,7 @@ @ApplicationScoped public class QuarkusCallContextFactory_v0_3 implements CallContextFactory_v0_3 { - @Inject - private Instance securityIdentityInstance; + @Inject private Instance securityIdentityInstance; @Override public ServerCallContext create(StreamObserver responseObserver) { @@ -58,7 +55,10 @@ public ServerCallContext create(StreamObserver responseObserver) { if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) { continue; } - headers.put(key, grpcMetadata.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER))); + headers.put( + key, + grpcMetadata.get( + Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER))); } state.put("headers", headers); } @@ -72,6 +72,7 @@ public ServerCallContext create(StreamObserver responseObserver) { } } - return new ServerCallContext(user, state, new HashSet<>(), A2AProtocol_v0_3.PROTOCOL_VERSION); + return new ServerCallContext( + user, state, new HashSet<>(), A2AProtocol_v0_3.PROTOCOL_VERSION); } } diff --git a/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusGrpcHandler_v0_3.java b/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusGrpcHandler_v0_3.java index d79871387..267a53252 100644 --- a/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusGrpcHandler_v0_3.java +++ b/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusGrpcHandler_v0_3.java @@ -1,14 +1,12 @@ package org.a2aproject.sdk.compat03.server.grpc.quarkus; -import java.util.concurrent.Executor; - -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; - import io.quarkus.grpc.GrpcService; import io.quarkus.grpc.RegisterInterceptor; import io.quarkus.security.Authenticated; import io.smallrye.common.annotation.Blocking; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import java.util.concurrent.Executor; import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.transport.grpc.handler.CallContextFactory_v0_3; @@ -35,17 +33,21 @@ public QuarkusGrpcHandler_v0_3() { this.executor = null; } - public QuarkusGrpcHandler_v0_3(AgentCard_v0_3 agentCard, Instance callContextFactoryInstance, Executor executor) { + public QuarkusGrpcHandler_v0_3( + AgentCard_v0_3 agentCard, + Instance callContextFactoryInstance, + Executor executor) { this.agentCard = agentCard; this.callContextFactoryInstance = callContextFactoryInstance; this.executor = executor; } @Inject - public QuarkusGrpcHandler_v0_3(@PublicAgentCard AgentCard_v0_3 agentCard, - Convert_v0_3_To10RequestHandler requestHandler, - Instance callContextFactoryInstance, - @Internal Executor executor) { + public QuarkusGrpcHandler_v0_3( + @PublicAgentCard AgentCard_v0_3 agentCard, + Convert_v0_3_To10RequestHandler requestHandler, + Instance callContextFactoryInstance, + @Internal Executor executor) { this.agentCard = agentCard; this.callContextFactoryInstance = callContextFactoryInstance; this.executor = executor; diff --git a/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusGrpcTransportMetadata_v0_3.java b/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusGrpcTransportMetadata_v0_3.java index 5cb9d813a..2307231e1 100644 --- a/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusGrpcTransportMetadata_v0_3.java +++ b/compat-0.3/reference/grpc/src/main/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusGrpcTransportMetadata_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.server.grpc.quarkus; -import org.a2aproject.sdk.server.TransportMetadata; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; +import org.a2aproject.sdk.server.TransportMetadata; public class QuarkusGrpcTransportMetadata_v0_3 implements TransportMetadata { @Override diff --git a/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/A2ATestResource_v0_3.java b/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/A2ATestResource_v0_3.java index 971d09afe..b01dfea5e 100644 --- a/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/A2ATestResource_v0_3.java +++ b/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/A2ATestResource_v0_3.java @@ -2,8 +2,6 @@ import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN; -import java.util.concurrent.atomic.AtomicInteger; - import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -17,14 +15,14 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; - +import java.util.concurrent.atomic.AtomicInteger; import org.a2aproject.sdk.compat03.conversion.TestUtilsBean_v0_3; +import org.a2aproject.sdk.compat03.transport.grpc.handler.GrpcHandler_v0_3; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskArtifactUpdateEvent; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.a2aproject.sdk.spec.TaskStatusUpdateEvent; -import org.a2aproject.sdk.compat03.transport.grpc.handler.GrpcHandler_v0_3; /** * Exposes the {@link TestUtilsBean_v0_3} via JAX-RS REST endpoints for testing the gRPC server. @@ -33,8 +31,7 @@ @Path("/test") @ApplicationScoped public class A2ATestResource_v0_3 { - @Inject - TestUtilsBean_v0_3 testUtilsBean; + @Inject TestUtilsBean_v0_3 testUtilsBean; private final AtomicInteger streamingSubscribedCount = new AtomicInteger(0); @@ -43,7 +40,6 @@ public void init() { GrpcHandler_v0_3.setStreamingSubscribedRunnable(streamingSubscribedCount::incrementAndGet); } - @POST @Path("/task") @Consumes(MediaType.APPLICATION_JSON) @@ -74,9 +70,7 @@ public Response deleteTask(@PathParam("taskId") String taskId) { return Response.status(404).build(); } testUtilsBean.deleteTask(taskId); - return Response.ok() - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .build(); + return Response.ok().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).build(); } @POST @@ -88,7 +82,8 @@ public Response ensureQueue(@PathParam("taskId") String taskId) { @POST @Path("/queue/enqueueTaskStatusUpdateEvent/{taskId}") - public Response enqueueTaskStatusUpdateEvent(@PathParam("taskId") String taskId, String body) throws Exception { + public Response enqueueTaskStatusUpdateEvent(@PathParam("taskId") String taskId, String body) + throws Exception { TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); return Response.ok().build(); @@ -96,7 +91,8 @@ public Response enqueueTaskStatusUpdateEvent(@PathParam("taskId") String taskId, @POST @Path("/queue/enqueueTaskArtifactUpdateEvent/{taskId}") - public Response enqueueTaskArtifactUpdateEvent(@PathParam("taskId") String taskId, String body) throws Exception { + public Response enqueueTaskArtifactUpdateEvent(@PathParam("taskId") String taskId, String body) + throws Exception { TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); return Response.ok().build(); @@ -119,22 +115,23 @@ public Response getChildQueueCount(@PathParam("taskId") String taskId) { @DELETE @Path("/task/{taskId}/config/{configId}") - public Response deleteTaskPushNotificationConfig(@PathParam("taskId") String taskId, @PathParam("configId") String configId) { + public Response deleteTaskPushNotificationConfig( + @PathParam("taskId") String taskId, @PathParam("configId") String configId) { Task task = testUtilsBean.getTask(taskId); if (task == null) { return Response.status(404).build(); } testUtilsBean.deleteTaskPushNotificationConfig(taskId, configId); - return Response.ok() - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .build(); + return Response.ok().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).build(); } @POST @Path("/task/{taskId}") @Consumes(MediaType.APPLICATION_JSON) - public Response savePushNotificationConfigInStore(@PathParam("taskId") String taskId, String body) throws Exception { - TaskPushNotificationConfig notificationConfig = JsonUtil.fromJson(body, TaskPushNotificationConfig.class); + public Response savePushNotificationConfigInStore( + @PathParam("taskId") String taskId, String body) throws Exception { + TaskPushNotificationConfig notificationConfig = + JsonUtil.fromJson(body, TaskPushNotificationConfig.class); if (notificationConfig == null) { return Response.status(404).build(); } diff --git a/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_Test.java b/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_Test.java index c5e362c6b..4bbe8a52e 100644 --- a/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_Test.java +++ b/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_Test.java @@ -1,16 +1,14 @@ package org.a2aproject.sdk.compat03.server.grpc.quarkus; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.quarkus.test.junit.QuarkusTest; import java.util.concurrent.TimeUnit; - import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerServerTest_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.quarkus.test.junit.QuarkusTest; - import org.junit.jupiter.api.AfterAll; @QuarkusTest @@ -35,10 +33,17 @@ protected String getTransportUrl() { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3().channelFactory(target -> { - channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); - return channel; - })); + builder.withTransport( + GrpcTransport_v0_3.class, + new GrpcTransportConfigBuilder_v0_3() + .channelFactory( + target -> { + channel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + return channel; + })); } @AfterAll diff --git a/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_WithAuthTest.java b/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_WithAuthTest.java index bb23598e9..56b701ff6 100644 --- a/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_WithAuthTest.java +++ b/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_WithAuthTest.java @@ -1,18 +1,17 @@ package org.a2aproject.sdk.compat03.server.grpc.quarkus; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; import java.util.concurrent.TimeUnit; - import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithAuthTest_v0_3; import org.a2aproject.sdk.compat03.conversion.AuthTestProfile_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -40,30 +39,40 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); - builder.withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3() - .channelFactory(target -> { - authenticatedChannel = ManagedChannelBuilder - .forTarget(target) - .usePlaintext() - .build(); - return authenticatedChannel; - }) - .addInterceptor(authInterceptor)); + builder.withTransport( + GrpcTransport_v0_3.class, + new GrpcTransportConfigBuilder_v0_3() + .channelFactory( + target -> { + authenticatedChannel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + return authenticatedChannel; + }) + .addInterceptor(authInterceptor)); } @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3().channelFactory(target -> { - unauthenticatedChannel = ManagedChannelBuilder - .forTarget(target) - .usePlaintext() - .build(); - return unauthenticatedChannel; - })); + builder.withTransport( + GrpcTransport_v0_3.class, + new GrpcTransportConfigBuilder_v0_3() + .channelFactory( + target -> { + unauthenticatedChannel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + return unauthenticatedChannel; + })); } @AfterAll diff --git a/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_WithTaskAuthorizationTest.java b/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_WithTaskAuthorizationTest.java index 8bb82b83a..3c4f3f688 100644 --- a/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_WithTaskAuthorizationTest.java +++ b/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/QuarkusA2AGrpc_v0_3_WithTaskAuthorizationTest.java @@ -1,16 +1,15 @@ package org.a2aproject.sdk.compat03.server.grpc.quarkus; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithTaskAuthorizationTest_v0_3; import org.a2aproject.sdk.compat03.conversion.TaskAuthorizationTestProfile_v0_3; @@ -19,7 +18,8 @@ @QuarkusTest @TestProfile(TaskAuthorizationTestProfile_v0_3.class) -public class QuarkusA2AGrpc_v0_3_WithTaskAuthorizationTest extends AbstractA2AServerWithTaskAuthorizationTest_v0_3 { +public class QuarkusA2AGrpc_v0_3_WithTaskAuthorizationTest + extends AbstractA2AServerWithTaskAuthorizationTest_v0_3 { private static final Map channels = new ConcurrentHashMap<>(); @@ -38,28 +38,40 @@ protected String getTransportUrl() { } @Override - protected void configureTransportWithCredentials(ClientBuilder_v0_3 builder, String username, String password) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) - ? getEncodedCredentials(username, password) : null); - builder.withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3() - .channelFactory(target -> { - ManagedChannel channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); - channels.put(username, channel); - return channel; - }) - .addInterceptor(authInterceptor)); + protected void configureTransportWithCredentials( + ClientBuilder_v0_3 builder, String username, String password) { + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials(username, password) + : null); + builder.withTransport( + GrpcTransport_v0_3.class, + new GrpcTransportConfigBuilder_v0_3() + .channelFactory( + target -> { + ManagedChannel channel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + channels.put(username, channel); + return channel; + }) + .addInterceptor(authInterceptor)); } @AfterAll static void closeChannels() { - channels.values().forEach(ch -> { - ch.shutdownNow(); - try { - ch.awaitTermination(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); + channels.values() + .forEach( + ch -> { + ch.shutdownNow(); + try { + ch.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); } } diff --git a/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/TestAuthorizationController_v0_3.java b/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/TestAuthorizationController_v0_3.java index f01ddd848..f0c5dd45a 100644 --- a/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/TestAuthorizationController_v0_3.java +++ b/compat-0.3/reference/grpc/src/test/java/org/a2aproject/sdk/compat03/server/grpc/quarkus/TestAuthorizationController_v0_3.java @@ -1,14 +1,12 @@ package org.a2aproject.sdk.compat03.server.grpc.quarkus; +import io.quarkus.security.spi.runtime.AuthorizationController; import jakarta.annotation.Priority; import jakarta.enterprise.inject.Alternative; import jakarta.inject.Singleton; import jakarta.interceptor.Interceptor; - import org.eclipse.microprofile.config.inject.ConfigProperty; -import io.quarkus.security.spi.runtime.AuthorizationController; - @Alternative @Priority(Interceptor.Priority.LIBRARY_AFTER + 1) @Singleton diff --git a/compat-0.3/reference/jsonrpc/pom.xml b/compat-0.3/reference/jsonrpc/pom.xml index a178e3e0f..be34f2ccd 100644 --- a/compat-0.3/reference/jsonrpc/pom.xml +++ b/compat-0.3/reference/jsonrpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2AServerRoutes_v0_3.java b/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2AServerRoutes_v0_3.java index bf2d1d66c..bca357274 100644 --- a/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2AServerRoutes_v0_3.java +++ b/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2AServerRoutes_v0_3.java @@ -5,18 +5,6 @@ import static org.a2aproject.sdk.compat03.transport.jsonrpc.context.JSONRPCContextKeys_v0_3.HEADERS_KEY; import static org.a2aproject.sdk.compat03.transport.jsonrpc.context.JSONRPCContextKeys_v0_3.METHOD_NAME_KEY; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Flow; -import java.util.concurrent.atomic.AtomicLong; - -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; - import com.google.gson.JsonSyntaxException; import io.quarkus.security.Authenticated; import io.quarkus.security.ForbiddenException; @@ -25,6 +13,16 @@ import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicLong; import org.a2aproject.sdk.compat03.common.A2AHeaders_v0_3; import org.a2aproject.sdk.compat03.conversion.A2AProtocol_v0_3; import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; @@ -36,8 +34,8 @@ import org.a2aproject.sdk.compat03.spec.GetTaskRequest_v0_3; import org.a2aproject.sdk.compat03.spec.InternalError_v0_3; import org.a2aproject.sdk.compat03.spec.JSONParseError_v0_3; -import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3; import org.a2aproject.sdk.compat03.spec.JSONRPCErrorResponse_v0_3; +import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3; import org.a2aproject.sdk.compat03.spec.JSONRPCRequest_v0_3; import org.a2aproject.sdk.compat03.spec.JSONRPCResponse_v0_3; import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigRequest_v0_3; @@ -65,51 +63,54 @@ @Singleton public class A2AServerRoutes_v0_3 { - @Inject - JSONRPCHandler_v0_3 jsonRpcHandler; + @Inject JSONRPCHandler_v0_3 jsonRpcHandler; // Hook so testing can wait until the SSE subscriber is attached. // Without this we get intermittent failures private static volatile @Nullable Runnable streamingMultiSseSupportSubscribedRunnable; - @Inject - Instance callContextFactory; + @Inject Instance callContextFactory; - @Inject - VertxSecurityHelper vertxSecurityHelper; + @Inject VertxSecurityHelper vertxSecurityHelper; void setupRoutes(@Observes Router router) { // Main JSON-RPC endpoint: POST / router.post("/") - .consumes(APPLICATION_JSON) - .handler(BodyHandler.create()) - .blockingHandler(ctx -> { - try { - vertxSecurityHelper.runInRequestContextDeferred(ctx, () -> { - invokeJSONRPCHandler(ctx.body().asString(), ctx); - }); - } catch (UnauthorizedException | ForbiddenException e) { - vertxSecurityHelper.handleAuthError(ctx, e); - } catch (Exception e) { - VertxSecurityHelper.handleGenericError(ctx); - } - }, false); + .consumes(APPLICATION_JSON) + .handler(BodyHandler.create()) + .blockingHandler( + ctx -> { + try { + vertxSecurityHelper.runInRequestContextDeferred( + ctx, + () -> { + invokeJSONRPCHandler(ctx.body().asString(), ctx); + }); + } catch (UnauthorizedException | ForbiddenException e) { + vertxSecurityHelper.handleAuthError(ctx, e); + } catch (Exception e) { + VertxSecurityHelper.handleGenericError(ctx); + } + }, + false); // Only register v0.3 agent card if no user-provided v1.0 @PublicAgentCard bean exists. if (!hasNonDefaultV10AgentCard()) { router.get("/.well-known/agent-card.json") - .produces(APPLICATION_JSON) - .handler(ctx -> { - try { - String agentCard = JsonUtil_v0_3.toJson(jsonRpcHandler.getAgentCard()); - ctx.response() - .setStatusCode(200) - .putHeader(CONTENT_TYPE, APPLICATION_JSON) - .end(agentCard); - } catch (JsonProcessingException_v0_3 e) { - ctx.response().setStatusCode(500).end("Internal Server Error"); - } - }); + .produces(APPLICATION_JSON) + .handler( + ctx -> { + try { + String agentCard = + JsonUtil_v0_3.toJson(jsonRpcHandler.getAgentCard()); + ctx.response() + .setStatusCode(200) + .putHeader(CONTENT_TYPE, APPLICATION_JSON) + .end(agentCard); + } catch (JsonProcessingException_v0_3 e) { + ctx.response().setStatusCode(500).end("Internal Server Error"); + } + }); } } @@ -134,7 +135,8 @@ public void invokeJSONRPCHandler(@Nullable String body, RoutingContext rc) { JSONRPCErrorResponse_v0_3 error = null; Object requestId = null; try { - JSONRPCRequestDecoder_v0_3.DecodeResult result = JSONRPCRequestDecoder_v0_3.decode(body); + JSONRPCRequestDecoder_v0_3.DecodeResult result = + JSONRPCRequestDecoder_v0_3.decode(body); if (result instanceof JSONRPCRequestDecoder_v0_3.DecodeFailure failure) { requestId = failure.requestId(); if (failure.methodName() != null) { @@ -142,22 +144,28 @@ public void invokeJSONRPCHandler(@Nullable String body, RoutingContext rc) { } throw failure.error(); } - JSONRPCRequestDecoder_v0_3.DecodedRequest decoded = (JSONRPCRequestDecoder_v0_3.DecodedRequest) result; + JSONRPCRequestDecoder_v0_3.DecodedRequest decoded = + (JSONRPCRequestDecoder_v0_3.DecodedRequest) result; requestId = decoded.requestId(); context.getState().put(METHOD_NAME_KEY, decoded.methodName()); if (decoded.request() instanceof StreamingJSONRPCRequest_v0_3 request) { streaming = true; streamingResponse = processStreamingRequest(request, context); } else { - nonStreamingResponse = processNonStreamingRequest( - (NonStreamingJSONRPCRequest_v0_3) decoded.request(), context); + nonStreamingResponse = + processNonStreamingRequest( + (NonStreamingJSONRPCRequest_v0_3) decoded.request(), context); } } catch (JSONRPCError_v0_3 e) { error = new JSONRPCErrorResponse_v0_3(requestId, e); } catch (JsonSyntaxException e) { - error = new JSONRPCErrorResponse_v0_3(requestId, new JSONParseError_v0_3(e.getMessage())); + error = + new JSONRPCErrorResponse_v0_3( + requestId, new JSONParseError_v0_3(e.getMessage())); } catch (Throwable t) { - error = new JSONRPCErrorResponse_v0_3(requestId, new InternalError_v0_3(t.getMessage())); + error = + new JSONRPCErrorResponse_v0_3( + requestId, new InternalError_v0_3(t.getMessage())); } finally { if (error != null) { rc.response() @@ -166,9 +174,12 @@ public void invokeJSONRPCHandler(@Nullable String body, RoutingContext rc) { .end(Utils_v0_3.toJsonString(error)); } else if (streaming) { AtomicLong eventIdCounter = new AtomicLong(0); - Multi sseEvents = streamingResponse - .map(response -> formatSseEvent(response, eventIdCounter.getAndIncrement())); - SseResponseWriter.writeSseStrings(sseEvents, rc, context, streamingMultiSseSupportSubscribedRunnable); + Multi sseEvents = + streamingResponse.map( + response -> + formatSseEvent(response, eventIdCounter.getAndIncrement())); + SseResponseWriter.writeSseStrings( + sseEvents, rc, context, streamingMultiSseSupportSubscribedRunnable); } else { rc.response() .setStatusCode(200) @@ -213,12 +224,14 @@ private Multi> processStreamingRequest( } else if (request instanceof TaskResubscriptionRequest_v0_3 req) { publisher = jsonRpcHandler.onResubscribeToTask(req, context); } else { - return Multi.createFrom().item(generateErrorResponse(request, new UnsupportedOperationError_v0_3())); + return Multi.createFrom() + .item(generateErrorResponse(request, new UnsupportedOperationError_v0_3())); } return Multi.createFrom().publisher(publisher); } - private JSONRPCResponse_v0_3 generateErrorResponse(JSONRPCRequest_v0_3 request, JSONRPCError_v0_3 error) { + private JSONRPCResponse_v0_3 generateErrorResponse( + JSONRPCRequest_v0_3 request, JSONRPCError_v0_3 error) { return new JSONRPCErrorResponse_v0_3(request.getId(), error); } @@ -234,7 +247,10 @@ private ServerCallContext createCallContext(RoutingContext rc) { user = UnauthenticatedUser.INSTANCE; } else { String subject = rc.user().subject(); - user = subject != null ? new AuthenticatedUser(subject) : UnauthenticatedUser.INSTANCE; + user = + subject != null + ? new AuthenticatedUser(subject) + : UnauthenticatedUser.INSTANCE; } Map state = new HashMap<>(); @@ -244,14 +260,16 @@ private ServerCallContext createCallContext(RoutingContext rc) { state.put(HEADERS_KEY, headers); // Extract requested extensions from X-A2A-Extensions header - List extensionHeaderValues = rc.request().headers().getAll(A2AHeaders_v0_3.X_A2A_EXTENSIONS); - Set requestedExtensions = A2AExtensions.getRequestedExtensions(extensionHeaderValues); + List extensionHeaderValues = + rc.request().headers().getAll(A2AHeaders_v0_3.X_A2A_EXTENSIONS); + Set requestedExtensions = + A2AExtensions.getRequestedExtensions(extensionHeaderValues); - return new ServerCallContext(user, state, requestedExtensions, A2AProtocol_v0_3.PROTOCOL_VERSION); + return new ServerCallContext( + user, state, requestedExtensions, A2AProtocol_v0_3.PROTOCOL_VERSION); } else { CallContextFactory_v0_3 builder = callContextFactory.get(); return builder.build(rc); } } - } diff --git a/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/CallContextFactory_v0_3.java b/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/CallContextFactory_v0_3.java index b2bf3ef27..22b1dba7b 100644 --- a/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/CallContextFactory_v0_3.java +++ b/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/CallContextFactory_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.server.apps.quarkus; -import org.a2aproject.sdk.server.ServerCallContext; import io.vertx.ext.web.RoutingContext; +import org.a2aproject.sdk.server.ServerCallContext; /** * Factory interface for creating ServerCallContext from a Vert.x RoutingContext. diff --git a/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusJSONRPCTransportMetadata_v0_3.java b/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusJSONRPCTransportMetadata_v0_3.java index 6b98db4b2..db4a89d58 100644 --- a/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusJSONRPCTransportMetadata_v0_3.java +++ b/compat-0.3/reference/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusJSONRPCTransportMetadata_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.server.apps.quarkus; -import org.a2aproject.sdk.server.TransportMetadata; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; +import org.a2aproject.sdk.server.TransportMetadata; public class QuarkusJSONRPCTransportMetadata_v0_3 implements TransportMetadata { diff --git a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2AServerRoutes_v0_3_Test.java b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2AServerRoutes_v0_3_Test.java index 993bc99a9..c81209741 100644 --- a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2AServerRoutes_v0_3_Test.java +++ b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2AServerRoutes_v0_3_Test.java @@ -16,13 +16,6 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import java.util.HashMap; -import java.util.List; -import java.util.Set; -import java.util.stream.Stream; - -import jakarta.enterprise.inject.Instance; - import com.google.gson.JsonObject; import com.google.gson.JsonParser; import io.smallrye.mutiny.Multi; @@ -31,7 +24,11 @@ import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; -import org.jspecify.annotations.Nullable; +import jakarta.enterprise.inject.Instance; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; @@ -42,6 +39,7 @@ import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.auth.UnauthenticatedUser; import org.a2aproject.sdk.server.requesthandlers.RequestHandler; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -73,7 +71,8 @@ public void setUp() { when(request.headers()).thenReturn(MultiMap.caseInsensitiveMultiMap()); when(response.headers()).thenReturn(responseHeaders); when(response.setStatusCode(anyInt())).thenReturn(response); - when(response.putHeader(any(CharSequence.class), any(CharSequence.class))).thenReturn(response); + when(response.putHeader(any(CharSequence.class), any(CharSequence.class))) + .thenReturn(response); when(response.end(anyString())).thenReturn(Future.succeededFuture()); when(response.end()).thenReturn(Future.succeededFuture()); } @@ -150,7 +149,10 @@ public void testNullBodyIsJsonParseError() { @ParameterizedTest(name = "id={0}") @org.junit.jupiter.params.provider.ValueSource(strings = {"{}", "[]"}) public void testInvalidIdBeforeVersionAndMethod(String id) { - assertError(invoke("{\"id\":" + id + ",\"jsonrpc\":false,\"method\":null}"), null, -32600, + assertError( + invoke("{\"id\":" + id + ",\"jsonrpc\":false,\"method\":null}"), + null, + -32600, "Invalid JSON-RPC request: 'id' must be a string, number, or null"); verifyNoInteractions(transport); } @@ -159,7 +161,10 @@ public void testInvalidIdBeforeVersionAndMethod(String id) { @MethodSource("idCoercionCases") public void testExistingIdCoercion(@Nullable String rawId, @Nullable String responseId) { String id = rawId == null ? "" : "\"id\":" + rawId + ","; - assertError(invoke("{" + id + "\"jsonrpc\":\"wrong\"}"), responseId, -32600, + assertError( + invoke("{" + id + "\"jsonrpc\":\"wrong\"}"), + responseId, + -32600, "Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); verifyNoInteractions(transport); } @@ -178,20 +183,27 @@ private static Stream idCoercionCases() { @MethodSource("invalidVersions") public void testVersionFieldErrorRetainsId(@Nullable String version) { String field = version == null ? "" : ",\"jsonrpc\":" + version; - assertError(invoke("{\"id\":\"original\",\"method\":{}" + field + "}"), "\"original\"", -32600, + assertError( + invoke("{\"id\":\"original\",\"method\":{}" + field + "}"), + "\"original\"", + -32600, "Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); verifyNoInteractions(transport); } private static Stream invalidVersions() { - return Stream.of((String) null, "null", "{}", "[]", "true", "1", "\"1.0\"").map(Arguments::of); + return Stream.of((String) null, "null", "{}", "[]", "true", "1", "\"1.0\"") + .map(Arguments::of); } @ParameterizedTest(name = "method={0}") @MethodSource("invalidMethods") public void testMethodFieldErrorRetainsId(@Nullable String method) { String field = method == null ? "" : ",\"method\":" + method; - assertError(invoke("{\"jsonrpc\":\"2.0\",\"id\":17" + field + "}"), "17", -32600, + assertError( + invoke("{\"jsonrpc\":\"2.0\",\"id\":17" + field + "}"), + "17", + -32600, "Invalid JSON-RPC request: missing or invalid 'method' field"); verifyNoInteractions(transport); } @@ -203,16 +215,27 @@ private static Stream invalidMethods() { @ParameterizedTest(name = "method={0}") @org.junit.jupiter.params.provider.ValueSource(strings = {"\"unknown\"", "\"\""}) public void testUnknownMethodBeforeParams(String method) { - assertError(invoke("{\"jsonrpc\":\"2.0\",\"id\":\"unknown\",\"method\":" + method - + ",\"params\":[]}"), "\"unknown\"", -32601, "Method not found"); + assertError( + invoke( + "{\"jsonrpc\":\"2.0\",\"id\":\"unknown\",\"method\":" + + method + + ",\"params\":[]}"), + "\"unknown\"", + -32601, + "Method not found"); verifyNoInteractions(transport); } @ParameterizedTest(name = "method={0}") @org.junit.jupiter.params.provider.ValueSource(strings = {"true", "17"}) public void testNonStringMethodBeforeParams(String method) { - assertError(invoke("{\"jsonrpc\":\"2.0\",\"id\":\"unknown\",\"method\":" + method - + ",\"params\":[]}"), "\"unknown\"", -32600, + assertError( + invoke( + "{\"jsonrpc\":\"2.0\",\"id\":\"unknown\",\"method\":" + + method + + ",\"params\":[]}"), + "\"unknown\"", + -32600, "Invalid JSON-RPC request: missing or invalid 'method' field"); verifyNoInteractions(transport); } @@ -221,13 +244,18 @@ public void testNonStringMethodBeforeParams(String method) { @MethodSource("invalidGetParams") public void testParamsErrorRetainsId(@Nullable String params) { JsonObject actual = invoke(request("tasks/get", params)); - assertError(actual, "\"original\"", -32602, + assertError( + actual, + "\"original\"", + -32602, "{\"historyLength\":-1}".equals(params) ? null : "Failed to parse JSON"); verifyNoInteractions(transport); } private static Stream invalidGetParams() { - return Stream.of(Arguments.of("[]"), Arguments.of("\"bad\""), + return Stream.of( + Arguments.of("[]"), + Arguments.of("\"bad\""), Arguments.of("{\"historyLength\":-1}"), Arguments.of("{\"id\":\"task\",\"historyLength\":\"bad\"}")); } @@ -236,15 +264,20 @@ private static Stream invalidGetParams() { @MethodSource("emptyParams") public void testMissingMessageParamsIsInvalidParams(@Nullable String params) { useRealTransport(); - assertError(invoke(request("message/send", params)), "\"original\"", -32602, + assertError( + invoke(request("message/send", params)), + "\"original\"", + -32602, "Parameter 'params' may not be null"); } @ParameterizedTest(name = "params={0}") @MethodSource("emptyParams") public void testMissingGetParamsReachTransport(@Nullable String params) { - when(transport.onGetTask(any(), any())).thenThrow(new IllegalStateException("dispatch reached")); - assertError(invoke(request("tasks/get", params)), "\"original\"", -32603, "dispatch reached"); + when(transport.onGetTask(any(), any())) + .thenThrow(new IllegalStateException("dispatch reached")); + assertError( + invoke(request("tasks/get", params)), "\"original\"", -32603, "dispatch reached"); ArgumentCaptor decoded = ArgumentCaptor.forClass(org.a2aproject.sdk.compat03.spec.GetTaskRequest_v0_3.class); verify(transport).onGetTask(decoded.capture(), any()); @@ -258,19 +291,27 @@ private static Stream emptyParams() { @ParameterizedTest(name = "params={0}") @MethodSource("invalidMessageParams") public void testInvalidMessageParams(@Nullable String params) { - assertError(invoke(request("message/send", params)), "\"original\"", -32602, "Failed to parse JSON"); + assertError( + invoke(request("message/send", params)), + "\"original\"", + -32602, + "Failed to parse JSON"); verifyNoInteractions(transport); } private static Stream invalidMessageParams() { return Stream.of( - Arguments.of("{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"invalid\",\"parts\":[{\"kind\":\"text\",\"text\":\"hello\"}]}}"), - Arguments.of("{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{}]}}"), - Arguments.of("{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{\"kind\":\"file\",\"file\":{}}]}}")); + Arguments.of( + "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"invalid\",\"parts\":[{\"kind\":\"text\",\"text\":\"hello\"}]}}"), + Arguments.of( + "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{}]}}"), + Arguments.of( + "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{\"kind\":\"file\",\"file\":{}}]}}")); } @ParameterizedTest(name = "method={0}") - @org.junit.jupiter.params.provider.ValueSource(strings = {"message/stream", "tasks/resubscribe"}) + @org.junit.jupiter.params.provider.ValueSource( + strings = {"message/stream", "tasks/resubscribe"}) public void testStreamingDecodeFailureIsHttpJson(String method) { assertError(invoke(request(method, "[]")), "\"original\"", -32602, "Failed to parse JSON"); verifyNoInteractions(transport); @@ -281,23 +322,35 @@ public void testStreamingDecodeFailureWithNullParamsIsHttpJson() { useRealTransport(); routes.invokeJSONRPCHandler(request("message/stream", "null"), rc); assertEquals("text/event-stream", responseHeaders.get(CONTENT_TYPE)); - ArgumentCaptor event = ArgumentCaptor.forClass(io.vertx.core.buffer.Buffer.class); + ArgumentCaptor event = + ArgumentCaptor.forClass(io.vertx.core.buffer.Buffer.class); verify(response).write(event.capture(), any()); String eventText = event.getValue().toString(); assertTrue(eventText.startsWith(": SSE stream started\n\ndata: ")); assertTrue(eventText.endsWith("\nid: 0\n\n")); - assertError(JsonParser.parseString(eventText.substring(eventText.indexOf("data: ") + 6, - eventText.indexOf("\nid: "))).getAsJsonObject(), "\"original\"", -32603, + assertError( + JsonParser.parseString( + eventText.substring( + eventText.indexOf("data: ") + 6, + eventText.indexOf("\nid: "))) + .getAsJsonObject(), + "\"original\"", + -32603, "Parameter 'params' may not be null"); } @ParameterizedTest(name = "method={0}") @MethodSource("supportedMethods") - public void testSupportedMethodsDecode(String method, @Nullable String params, String dispatchMethod) { - // A transport failure after decoding makes dispatch observable without implementing its dependencies. - transport = mock(JSONRPCHandler_v0_3.class, invocation -> { - throw new IllegalStateException("dispatch reached"); - }); + public void testSupportedMethodsDecode( + String method, @Nullable String params, String dispatchMethod) { + // A transport failure after decoding makes dispatch observable without implementing its + // dependencies. + transport = + mock( + JSONRPCHandler_v0_3.class, + invocation -> { + throw new IllegalStateException("dispatch reached"); + }); routes.jsonRpcHandler = transport; assertError(invoke(request(method, params)), "\"original\"", -32603, "dispatch reached"); var invocation = mockingDetails(transport).getInvocations().iterator().next(); @@ -317,18 +370,40 @@ private static Stream supportedMethods() { return Stream.of( Arguments.of("tasks/get", "{\"id\":\"task\"}", "onGetTask"), Arguments.of("tasks/cancel", "{\"id\":\"task\"}", "onCancelTask"), - Arguments.of("message/send", "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{\"kind\":\"text\",\"text\":\"hello\"}]}}", "onMessageSend"), - Arguments.of("tasks/pushNotificationConfig/set", "{\"taskId\":\"task\",\"pushNotificationConfig\":{\"url\":\"http://localhost/callback\"}}", "setPushNotificationConfig"), - Arguments.of("tasks/pushNotificationConfig/get", "{\"id\":\"task\"}", "getPushNotificationConfig"), - Arguments.of("tasks/pushNotificationConfig/list", "{\"id\":\"task\"}", "listPushNotificationConfig"), - Arguments.of("tasks/pushNotificationConfig/delete", "{\"id\":\"task\",\"pushNotificationConfigId\":\"config\"}", "deletePushNotificationConfig"), - Arguments.of("agent/getAuthenticatedExtendedCard", null, "onGetAuthenticatedExtendedCardRequest")); + Arguments.of( + "message/send", + "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{\"kind\":\"text\",\"text\":\"hello\"}]}}", + "onMessageSend"), + Arguments.of( + "tasks/pushNotificationConfig/set", + "{\"taskId\":\"task\",\"pushNotificationConfig\":{\"url\":\"http://localhost/callback\"}}", + "setPushNotificationConfig"), + Arguments.of( + "tasks/pushNotificationConfig/get", + "{\"id\":\"task\"}", + "getPushNotificationConfig"), + Arguments.of( + "tasks/pushNotificationConfig/list", + "{\"id\":\"task\"}", + "listPushNotificationConfig"), + Arguments.of( + "tasks/pushNotificationConfig/delete", + "{\"id\":\"task\",\"pushNotificationConfigId\":\"config\"}", + "deletePushNotificationConfig"), + Arguments.of( + "agent/getAuthenticatedExtendedCard", + null, + "onGetAuthenticatedExtendedCardRequest")); } @ParameterizedTest(name = "method={0}") @MethodSource("streamingMethods") public void testSupportedStreamingMethodsDecode(String method, String params) { - var publisher = Multi.createFrom().item(new SendStreamingMessageResponse_v0_3("original", new TaskNotFoundError_v0_3())); + var publisher = + Multi.createFrom() + .item( + new SendStreamingMessageResponse_v0_3( + "original", new TaskNotFoundError_v0_3())); when(transport.onMessageSendStream(any(), any())).thenReturn(publisher); when(transport.onResubscribeToTask(any(), any())).thenReturn(publisher); routes.invokeJSONRPCHandler(request(method, params), rc); @@ -348,22 +423,33 @@ public void testSupportedStreamingMethodsDecode(String method, String params) { assertEquals("text/event-stream", responseHeaders.get(CONTENT_TYPE)); assertEquals("no-cache", responseHeaders.get("Cache-Control")); assertEquals("no", responseHeaders.get("X-Accel-Buffering")); - ArgumentCaptor event = ArgumentCaptor.forClass(io.vertx.core.buffer.Buffer.class); + ArgumentCaptor event = + ArgumentCaptor.forClass(io.vertx.core.buffer.Buffer.class); verify(response).write(event.capture(), any()); - assertEquals(": SSE stream started\n\ndata: {\"jsonrpc\":\"2.0\",\"id\":\"original\",\"error\":{\"code\":-32001,\"message\":\"Task not found\"}}\nid: 0\n\n", event.getValue().toString()); + assertEquals( + ": SSE stream started\n\n" + + "data:" + + " {\"jsonrpc\":\"2.0\",\"id\":\"original\",\"error\":{\"code\":-32001,\"message\":\"Task" + + " not found\"}}\n" + + "id: 0\n\n", + event.getValue().toString()); verify(response).end(); } private static Stream streamingMethods() { return Stream.of( - Arguments.of("message/stream", "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{\"kind\":\"text\",\"text\":\"hello\"}]}}"), + Arguments.of( + "message/stream", + "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{\"kind\":\"text\",\"text\":\"hello\"}]}}"), Arguments.of("tasks/resubscribe", "{\"id\":\"task\"}")); } @ParameterizedTest(name = "body={0}, recoveredMethod={1}") @MethodSource("contextRecoveryCases") - public void testMethodContextRecoveryOnDecodeFailure(String body, @Nullable String expectedMethod) { - ServerCallContext context = new ServerCallContext(UnauthenticatedUser.INSTANCE, new HashMap<>(), Set.of()); + public void testMethodContextRecoveryOnDecodeFailure( + String body, @Nullable String expectedMethod) { + ServerCallContext context = + new ServerCallContext(UnauthenticatedUser.INSTANCE, new HashMap<>(), Set.of()); CallContextFactory_v0_3 factory = mock(CallContextFactory_v0_3.class); when(routes.callContextFactory.isUnsatisfied()).thenReturn(false); when(routes.callContextFactory.get()).thenReturn(factory); @@ -390,9 +476,12 @@ private static Stream contextRecoveryCases() { @ParameterizedTest(name = "id={0}") @MethodSource("dispatchIds") - public void testDispatchFailureRetainsRecoveredId(String rawId, String responseId, Object decodedId) { - when(transport.onGetTask(any(), any())).thenThrow(new IllegalStateException("dispatch failed")); - JsonObject body = JsonParser.parseString(request("tasks/get", "{\"id\":\"task\"}")).getAsJsonObject(); + public void testDispatchFailureRetainsRecoveredId( + String rawId, String responseId, Object decodedId) { + when(transport.onGetTask(any(), any())) + .thenThrow(new IllegalStateException("dispatch failed")); + JsonObject body = + JsonParser.parseString(request("tasks/get", "{\"id\":\"task\"}")).getAsJsonObject(); body.add("id", JsonParser.parseString(rawId)); assertError(invoke(body.toString()), responseId, -32603, "dispatch failed"); ArgumentCaptor decoded = @@ -409,16 +498,30 @@ private static Stream dispatchIds() { } private static String request(String method, String params) { - return "{\"jsonrpc\":\"2.0\",\"id\":\"original\",\"method\":\"" + method + "\"" - + (params == null ? "" : ",\"params\":" + params) + "}"; + return "{\"jsonrpc\":\"2.0\",\"id\":\"original\",\"method\":\"" + + method + + "\"" + + (params == null ? "" : ",\"params\":" + params) + + "}"; } private void useRealTransport() { - AgentCard_v0_3 card = new AgentCard_v0_3.Builder().name("validation").description("Validation baseline") - .url("http://localhost:9999").version("1").protocolVersion("0.3") - .capabilities(new AgentCapabilities_v0_3(true, true, false, null)) - .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text")).skills(List.of()).build(); - routes.jsonRpcHandler = new JSONRPCHandler_v0_3(card, Runnable::run, - new Convert_v0_3_To10RequestHandler(mock(RequestHandler.class))); + AgentCard_v0_3 card = + new AgentCard_v0_3.Builder() + .name("validation") + .description("Validation baseline") + .url("http://localhost:9999") + .version("1") + .protocolVersion("0.3") + .capabilities(new AgentCapabilities_v0_3(true, true, false, null)) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + routes.jsonRpcHandler = + new JSONRPCHandler_v0_3( + card, + Runnable::run, + new Convert_v0_3_To10RequestHandler(mock(RequestHandler.class))); } } diff --git a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2ATestRoutes_v0_3.java b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2ATestRoutes_v0_3.java index ab460d8b1..bc500ebe9 100644 --- a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2ATestRoutes_v0_3.java +++ b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/A2ATestRoutes_v0_3.java @@ -4,131 +4,137 @@ import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN; -import java.util.concurrent.atomic.AtomicInteger; - +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; import jakarta.annotation.PostConstruct; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.inject.Singleton; - +import java.util.concurrent.atomic.AtomicInteger; import org.a2aproject.sdk.compat03.conversion.TestUtilsBean_v0_3; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskArtifactUpdateEvent; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.a2aproject.sdk.spec.TaskStatusUpdateEvent; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.BodyHandler; /** * Exposes the {@link TestUtilsBean_v0_3} via REST using the Vert.x Web Router */ @Singleton public class A2ATestRoutes_v0_3 { - @Inject - TestUtilsBean_v0_3 testUtilsBean; + @Inject TestUtilsBean_v0_3 testUtilsBean; - @Inject - A2AServerRoutes_v0_3 a2AServerRoutes; + @Inject A2AServerRoutes_v0_3 a2AServerRoutes; AtomicInteger streamingSubscribedCount = new AtomicInteger(0); @PostConstruct public void init() { - A2AServerRoutes_v0_3.setStreamingMultiSseSupportSubscribedRunnable(() -> streamingSubscribedCount.incrementAndGet()); + A2AServerRoutes_v0_3.setStreamingMultiSseSupportSubscribedRunnable( + () -> streamingSubscribedCount.incrementAndGet()); } void setupRoutes(@Observes Router router) { // Save task: POST /test/task router.post("/test/task") - .consumes(APPLICATION_JSON) - .handler(BodyHandler.create()) - .blockingHandler(ctx -> { - String body = ctx.body().asString(); - saveTask(body, ctx); - }); + .consumes(APPLICATION_JSON) + .handler(BodyHandler.create()) + .blockingHandler( + ctx -> { + String body = ctx.body().asString(); + saveTask(body, ctx); + }); // Get task: GET /test/task/:taskId router.get("/test/task/:taskId") - .produces(APPLICATION_JSON) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - getTask(taskId, ctx); - }); + .produces(APPLICATION_JSON) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + getTask(taskId, ctx); + }); // Delete task: DELETE /test/task/:taskId router.delete("/test/task/:taskId") - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - deleteTask(taskId, ctx); - }); + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + deleteTask(taskId, ctx); + }); // Ensure task queue: POST /test/queue/ensure/:taskId router.post("/test/queue/ensure/:taskId") - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - ensureTaskQueue(taskId, ctx); - }); + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + ensureTaskQueue(taskId, ctx); + }); // Enqueue task status update event: POST /test/queue/enqueueTaskStatusUpdateEvent/:taskId router.post("/test/queue/enqueueTaskStatusUpdateEvent/:taskId") - .handler(BodyHandler.create()) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - enqueueTaskStatusUpdateEvent(taskId, body, ctx); - }); - - // Enqueue task artifact update event: POST /test/queue/enqueueTaskArtifactUpdateEvent/:taskId + .handler(BodyHandler.create()) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + enqueueTaskStatusUpdateEvent(taskId, body, ctx); + }); + + // Enqueue task artifact update event: POST + // /test/queue/enqueueTaskArtifactUpdateEvent/:taskId router.post("/test/queue/enqueueTaskArtifactUpdateEvent/:taskId") - .handler(BodyHandler.create()) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - enqueueTaskArtifactUpdateEvent(taskId, body, ctx); - }); + .handler(BodyHandler.create()) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + enqueueTaskArtifactUpdateEvent(taskId, body, ctx); + }); // Get streaming subscribed count: GET /test/streamingSubscribedCount router.get("/test/streamingSubscribedCount") - .produces(TEXT_PLAIN) - .handler(ctx -> { - getStreamingSubscribedCount(ctx); - }); + .produces(TEXT_PLAIN) + .handler( + ctx -> { + getStreamingSubscribedCount(ctx); + }); // Get child queue count: GET /test/queue/childCount/:taskId router.get("/test/queue/childCount/:taskId") - .produces(TEXT_PLAIN) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - getChildQueueCount(taskId, ctx); - }); + .produces(TEXT_PLAIN) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + getChildQueueCount(taskId, ctx); + }); // Delete task push notification config: DELETE /test/task/:taskId/config/:configId router.delete("/test/task/:taskId/config/:configId") - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String configId = ctx.pathParam("configId"); - deleteTaskPushNotificationConfig(taskId, configId, ctx); - }); + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String configId = ctx.pathParam("configId"); + deleteTaskPushNotificationConfig(taskId, configId, ctx); + }); // Save task push notification config: POST /test/task/:taskId router.post("/test/task/:taskId") - .handler(BodyHandler.create()) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - saveTaskPushNotificationConfig(taskId, body, ctx); - }); + .handler(BodyHandler.create()) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + saveTaskPushNotificationConfig(taskId, body, ctx); + }); } public void saveTask(String body, RoutingContext rc) { try { Task task = JsonUtil.fromJson(body, Task.class); testUtilsBean.saveTask(task); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -138,9 +144,7 @@ public void getTask(String taskId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } rc.response() @@ -157,15 +161,11 @@ public void deleteTask(String taskId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } testUtilsBean.deleteTask(taskId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -174,9 +174,7 @@ public void deleteTask(String taskId, RoutingContext rc) { public void ensureTaskQueue(String taskId, RoutingContext rc) { try { testUtilsBean.ensureQueue(taskId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -187,9 +185,7 @@ public void enqueueTaskStatusUpdateEvent(String taskId, String body, RoutingCont try { TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -200,40 +196,31 @@ public void enqueueTaskArtifactUpdateEvent(String taskId, String body, RoutingCo try { TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } } public void getStreamingSubscribedCount(RoutingContext rc) { - rc.response() - .setStatusCode(200) - .end(String.valueOf(streamingSubscribedCount.get())); + rc.response().setStatusCode(200).end(String.valueOf(streamingSubscribedCount.get())); } public void getChildQueueCount(String taskId, RoutingContext rc) { int count = testUtilsBean.getChildQueueCount(taskId); - rc.response() - .setStatusCode(200) - .end(String.valueOf(count)); + rc.response().setStatusCode(200).end(String.valueOf(count)); } - public void deleteTaskPushNotificationConfig(String taskId, String configId, RoutingContext rc) { + public void deleteTaskPushNotificationConfig( + String taskId, String configId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } testUtilsBean.deleteTaskPushNotificationConfig(taskId, configId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -241,17 +228,14 @@ public void deleteTaskPushNotificationConfig(String taskId, String configId, Rou public void saveTaskPushNotificationConfig(String taskId, String body, RoutingContext rc) { try { - TaskPushNotificationConfig notificationConfig = JsonUtil.fromJson(body, TaskPushNotificationConfig.class); + TaskPushNotificationConfig notificationConfig = + JsonUtil.fromJson(body, TaskPushNotificationConfig.class); if (notificationConfig == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } testUtilsBean.saveTaskPushNotificationConfig(taskId, notificationConfig); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -259,10 +243,6 @@ public void saveTaskPushNotificationConfig(String taskId, String body, RoutingCo private void errorResponse(Throwable t, RoutingContext rc) { t.printStackTrace(); - rc.response() - .setStatusCode(500) - .putHeader(CONTENT_TYPE, TEXT_PLAIN) - .end(); + rc.response().setStatusCode(500).putHeader(CONTENT_TYPE, TEXT_PLAIN).end(); } - } diff --git a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_AndroidTest.java b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_AndroidTest.java index 827f7778e..abe6c1712 100644 --- a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_AndroidTest.java +++ b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_AndroidTest.java @@ -1,17 +1,18 @@ package org.a2aproject.sdk.compat03.server.apps.quarkus; +import io.quarkus.test.junit.QuarkusTest; import org.a2aproject.sdk.client.http.android.AndroidA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; -import io.quarkus.test.junit.QuarkusTest; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; @QuarkusTest public class QuarkusA2AJSONRPC_v0_3_AndroidTest extends QuarkusA2AJSONRPC_v0_3_Test { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(JSONRPCTransport_v0_3.class, + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3().httpClient(new AndroidA2AHttpClient())); } } diff --git a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_JdkTest.java b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_JdkTest.java index 6c096b069..8c0ca6eee 100644 --- a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_JdkTest.java +++ b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_JdkTest.java @@ -1,17 +1,18 @@ package org.a2aproject.sdk.compat03.server.apps.quarkus; +import io.quarkus.test.junit.QuarkusTest; import org.a2aproject.sdk.client.http.JdkA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; -import io.quarkus.test.junit.QuarkusTest; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; @QuarkusTest public class QuarkusA2AJSONRPC_v0_3_JdkTest extends QuarkusA2AJSONRPC_v0_3_Test { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(JSONRPCTransport_v0_3.class, + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3().httpClient(new JdkA2AHttpClient())); } } diff --git a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_VertxTest.java b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_VertxTest.java index 094cd6f60..5053d7af8 100644 --- a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_VertxTest.java +++ b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_VertxTest.java @@ -1,22 +1,22 @@ package org.a2aproject.sdk.compat03.server.apps.quarkus; -import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; -import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; import io.quarkus.test.junit.QuarkusTest; import io.vertx.core.Vertx; import jakarta.inject.Inject; +import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; +import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; @QuarkusTest public class QuarkusA2AJSONRPC_v0_3_VertxTest extends QuarkusA2AJSONRPC_v0_3_Test { - @Inject - Vertx vertx; + @Inject Vertx vertx; @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(JSONRPCTransport_v0_3.class, + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3().httpClient(new VertxA2AHttpClient(vertx))); } } diff --git a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthAndroidTest.java b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthAndroidTest.java index 66f4cdf18..0a052981f 100644 --- a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthAndroidTest.java +++ b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthAndroidTest.java @@ -1,15 +1,15 @@ package org.a2aproject.sdk.compat03.server.apps.quarkus; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; import org.a2aproject.sdk.client.http.android.AndroidA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithAuthTest_v0_3; import org.a2aproject.sdk.compat03.conversion.AuthTestProfile_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; @QuarkusTest @TestProfile(AuthTestProfile_v0_3.class) @@ -31,11 +31,15 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); - - builder.withTransport(JSONRPCTransport_v0_3.class, + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); + + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3() .httpClient(new AndroidA2AHttpClient()) .addInterceptor(authInterceptor)); @@ -43,7 +47,8 @@ protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(JSONRPCTransport_v0_3.class, + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3().httpClient(new AndroidA2AHttpClient())); } } diff --git a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthTest.java b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthTest.java index 2122bfff8..dc8428d27 100644 --- a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthTest.java +++ b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthTest.java @@ -1,15 +1,15 @@ package org.a2aproject.sdk.compat03.server.apps.quarkus; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; import org.a2aproject.sdk.client.http.JdkA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithAuthTest_v0_3; import org.a2aproject.sdk.compat03.conversion.AuthTestProfile_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; @QuarkusTest @TestProfile(AuthTestProfile_v0_3.class) @@ -31,11 +31,15 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); - - builder.withTransport(JSONRPCTransport_v0_3.class, + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); + + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3() .httpClient(new JdkA2AHttpClient()) .addInterceptor(authInterceptor)); @@ -43,7 +47,8 @@ protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(JSONRPCTransport_v0_3.class, + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3().httpClient(new JdkA2AHttpClient())); } } diff --git a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthVertxTest.java b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthVertxTest.java index 2319b4db1..a001ccc69 100644 --- a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthVertxTest.java +++ b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithAuthVertxTest.java @@ -1,24 +1,23 @@ package org.a2aproject.sdk.compat03.server.apps.quarkus; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.vertx.core.Vertx; +import jakarta.inject.Inject; import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithAuthTest_v0_3; import org.a2aproject.sdk.compat03.conversion.AuthTestProfile_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; -import io.vertx.core.Vertx; -import jakarta.inject.Inject; @QuarkusTest @TestProfile(AuthTestProfile_v0_3.class) public class QuarkusA2AJSONRPC_v0_3_WithAuthVertxTest extends AbstractA2AServerWithAuthTest_v0_3 { - @Inject - Vertx vertx; + @Inject Vertx vertx; public QuarkusA2AJSONRPC_v0_3_WithAuthVertxTest() { super(8081); @@ -36,11 +35,15 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); - - builder.withTransport(JSONRPCTransport_v0_3.class, + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); + + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3() .httpClient(new VertxA2AHttpClient(vertx)) .addInterceptor(authInterceptor)); @@ -48,7 +51,8 @@ protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(JSONRPCTransport_v0_3.class, + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3().httpClient(new VertxA2AHttpClient(vertx))); } } diff --git a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithTaskAuthorizationVertxTest.java b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithTaskAuthorizationVertxTest.java index 5b536f3e8..08ded6883 100644 --- a/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithTaskAuthorizationVertxTest.java +++ b/compat-0.3/reference/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/server/apps/quarkus/QuarkusA2AJSONRPC_v0_3_WithTaskAuthorizationVertxTest.java @@ -6,8 +6,8 @@ import jakarta.inject.Inject; import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithTaskAuthorizationTest_v0_3; import org.a2aproject.sdk.compat03.conversion.TaskAuthorizationTestProfile_v0_3; @@ -15,10 +15,10 @@ @QuarkusTest @TestProfile(TaskAuthorizationTestProfile_v0_3.class) -public class QuarkusA2AJSONRPC_v0_3_WithTaskAuthorizationVertxTest extends AbstractA2AServerWithTaskAuthorizationTest_v0_3 { +public class QuarkusA2AJSONRPC_v0_3_WithTaskAuthorizationVertxTest + extends AbstractA2AServerWithTaskAuthorizationTest_v0_3 { - @Inject - Vertx vertx; + @Inject Vertx vertx; public QuarkusA2AJSONRPC_v0_3_WithTaskAuthorizationVertxTest() { super(8081); @@ -35,11 +35,16 @@ protected String getTransportUrl() { } @Override - protected void configureTransportWithCredentials(ClientBuilder_v0_3 builder, String username, String password) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) - ? getEncodedCredentials(username, password) : null); - builder.withTransport(JSONRPCTransport_v0_3.class, + protected void configureTransportWithCredentials( + ClientBuilder_v0_3 builder, String username, String password) { + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials(username, password) + : null); + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3() .httpClient(new VertxA2AHttpClient(vertx)) .addInterceptor(authInterceptor)); diff --git a/compat-0.3/reference/rest/pom.xml b/compat-0.3/reference/rest/pom.xml index abf91a16e..4c6b2b397 100644 --- a/compat-0.3/reference/rest/pom.xml +++ b/compat-0.3/reference/rest/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2AServerRoutes_v0_3.java b/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2AServerRoutes_v0_3.java index 62261ca47..6fc8c3e43 100644 --- a/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2AServerRoutes_v0_3.java +++ b/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2AServerRoutes_v0_3.java @@ -5,19 +5,6 @@ import static org.a2aproject.sdk.compat03.transport.rest.context.RestContextKeys_v0_3.HEADERS_KEY; import static org.a2aproject.sdk.compat03.transport.rest.context.RestContextKeys_v0_3.METHOD_NAME_KEY; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; - -import jakarta.annotation.Priority; -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; - import io.quarkus.security.Authenticated; import io.quarkus.security.ForbiddenException; import io.quarkus.security.UnauthorizedException; @@ -26,6 +13,17 @@ import io.vertx.ext.web.Router; import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; +import jakarta.annotation.Priority; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; import org.a2aproject.sdk.compat03.common.A2AHeaders_v0_3; import org.a2aproject.sdk.compat03.conversion.A2AProtocol_v0_3; import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3; @@ -56,85 +54,91 @@ @Singleton public class A2AServerRoutes_v0_3 { - @Inject - RestHandler_v0_3 jsonRestHandler; + @Inject RestHandler_v0_3 jsonRestHandler; // Hook so testing can wait until the SSE subscriber is attached. // Without this we get intermittent failures private static volatile @Nullable Runnable streamingMultiSseSupportSubscribedRunnable; - @Inject - Instance callContextFactory; + @Inject Instance callContextFactory; - @Inject - VertxSecurityHelper vertxSecurityHelper; + @Inject VertxSecurityHelper vertxSecurityHelper; void setupRouter(@Observes @Priority(10) Router router) { // POST /v1/message:send router.postWithRegex("^\\/v1\\/message:send$") - .handler(BodyHandler.create()) - .blockingHandler(authenticated(ctx -> { - sendMessage(extractBody(ctx), ctx); - }), false); + .handler(BodyHandler.create()) + .blockingHandler( + authenticated( + ctx -> { + sendMessage(extractBody(ctx), ctx); + }), + false); // POST /v1/message:stream router.postWithRegex("^\\/v1\\/message:stream$") - .handler(BodyHandler.create()) - .blockingHandler(authenticatedStreaming(ctx -> { - sendMessageStreaming(extractBody(ctx), ctx); - }), false); + .handler(BodyHandler.create()) + .blockingHandler( + authenticatedStreaming( + ctx -> { + sendMessageStreaming(extractBody(ctx), ctx); + }), + false); // GET /v1/tasks/:id - router.get("/v1/tasks/:id") - .order(1) - .blockingHandler(authenticated(this::getTask), false); + router.get("/v1/tasks/:id").order(1).blockingHandler(authenticated(this::getTask), false); // POST /v1/tasks/{id}:cancel router.postWithRegex("^\\/v1\\/tasks\\/([^/]+):cancel$") - .order(1) - .blockingHandler(authenticated(this::cancelTask), false); + .order(1) + .blockingHandler(authenticated(this::cancelTask), false); // POST /v1/tasks/{id}:subscribe router.postWithRegex("^\\/v1\\/tasks\\/([^/]+):subscribe$") - .order(1) - .blockingHandler(authenticatedStreaming(this::resubscribeTask), false); + .order(1) + .blockingHandler(authenticatedStreaming(this::resubscribeTask), false); // POST /v1/tasks/:id/pushNotificationConfigs router.post("/v1/tasks/:id/pushNotificationConfigs") - .order(1) - .handler(BodyHandler.create()) - .blockingHandler(authenticated(ctx -> { - setTaskPushNotificationConfiguration(extractBody(ctx), ctx); - }), false); + .order(1) + .handler(BodyHandler.create()) + .blockingHandler( + authenticated( + ctx -> { + setTaskPushNotificationConfiguration(extractBody(ctx), ctx); + }), + false); // GET /v1/tasks/:id/pushNotificationConfigs/:configId router.get("/v1/tasks/:id/pushNotificationConfigs/:configId") - .order(1) - .blockingHandler(authenticated(this::getTaskPushNotificationConfiguration), false); + .order(1) + .blockingHandler(authenticated(this::getTaskPushNotificationConfiguration), false); // GET /v1/tasks/:id/pushNotificationConfigs router.get("/v1/tasks/:id/pushNotificationConfigs") - .order(2) - .blockingHandler(authenticated(this::listTaskPushNotificationConfigurations), false); + .order(2) + .blockingHandler( + authenticated(this::listTaskPushNotificationConfigurations), false); // DELETE /v1/tasks/:id/pushNotificationConfigs/:configId router.delete("/v1/tasks/:id/pushNotificationConfigs/:configId") - .order(1) - .blockingHandler(authenticated(this::deleteTaskPushNotificationConfiguration), false); + .order(1) + .blockingHandler( + authenticated(this::deleteTaskPushNotificationConfiguration), false); // Only register v0.3 agent card if no user-provided v1.0 @PublicAgentCard bean exists. if (!hasNonDefaultV10AgentCard()) { router.get("/.well-known/agent-card.json") - .order(1) - .produces(APPLICATION_JSON) - .handler(this::getAgentCard); + .order(1) + .produces(APPLICATION_JSON) + .handler(this::getAgentCard); } // GET /v1/card - authenticated router.get("/v1/card") - .order(1) - .produces(APPLICATION_JSON) - .blockingHandler(authenticated(this::getAuthenticatedExtendedCard), false); + .order(1) + .produces(APPLICATION_JSON) + .blockingHandler(authenticated(this::getAuthenticatedExtendedCard), false); } private Handler authenticated(Consumer action) { @@ -191,9 +195,15 @@ public void sendMessageStreaming(String body, RoutingContext rc) { sendResponse(rc, error); } else if (streamingResponse != null) { AtomicLong eventIdCounter = new AtomicLong(0); - Multi sseEvents = Multi.createFrom().publisher(streamingResponse.getPublisher()) - .map(json -> formatSseEvent(json, eventIdCounter.getAndIncrement())); - SseResponseWriter.writeSseStrings(sseEvents, rc, context, streamingMultiSseSupportSubscribedRunnable); + Multi sseEvents = + Multi.createFrom() + .publisher(streamingResponse.getPublisher()) + .map( + json -> + formatSseEvent( + json, eventIdCounter.getAndIncrement())); + SseResponseWriter.writeSseStrings( + sseEvents, rc, context, streamingMultiSseSupportSubscribedRunnable); } } } @@ -204,8 +214,12 @@ public void getTask(RoutingContext rc) { ServerCallContext context = createCallContext(rc, GetTaskRequest_v0_3.METHOD); HTTPRestResponse response = null; try { - response = jsonRestHandler.getTask(taskId, rc.request().params().get("history_length"), - rc.request().params().get("historyLength"), context); + response = + jsonRestHandler.getTask( + taskId, + rc.request().params().get("history_length"), + rc.request().params().get("historyLength"), + context); } catch (Throwable t) { response = jsonRestHandler.createErrorResponse(new InternalError_v0_3(t.getMessage())); } finally { @@ -224,7 +238,8 @@ public void cancelTask(RoutingContext rc) { if (t instanceof JSONRPCError_v0_3 error) { response = jsonRestHandler.createErrorResponse(error); } else { - response = jsonRestHandler.createErrorResponse(new InternalError_v0_3(t.getMessage())); + response = + jsonRestHandler.createErrorResponse(new InternalError_v0_3(t.getMessage())); } } finally { sendResponse(rc, response); @@ -233,9 +248,10 @@ public void cancelTask(RoutingContext rc) { private void sendResponse(RoutingContext rc, @Nullable HTTPRestResponse response) { if (response != null) { - var httpResponse = rc.response() - .setStatusCode(response.getStatusCode()) - .putHeader(CONTENT_TYPE, response.getContentType()); + var httpResponse = + rc.response() + .setStatusCode(response.getStatusCode()) + .putHeader(CONTENT_TYPE, response.getContentType()); response.getHeaders().forEach(httpResponse::putHeader); @@ -263,9 +279,15 @@ public void resubscribeTask(RoutingContext rc) { sendResponse(rc, error); } else if (streamingResponse != null) { AtomicLong eventIdCounter = new AtomicLong(0); - Multi sseEvents = Multi.createFrom().publisher(streamingResponse.getPublisher()) - .map(json -> formatSseEvent(json, eventIdCounter.getAndIncrement())); - SseResponseWriter.writeSseStrings(sseEvents, rc, context, streamingMultiSseSupportSubscribedRunnable); + Multi sseEvents = + Multi.createFrom() + .publisher(streamingResponse.getPublisher()) + .map( + json -> + formatSseEvent( + json, eventIdCounter.getAndIncrement())); + SseResponseWriter.writeSseStrings( + sseEvents, rc, context, streamingMultiSseSupportSubscribedRunnable); } } } @@ -273,7 +295,8 @@ public void resubscribeTask(RoutingContext rc) { @Authenticated public void setTaskPushNotificationConfiguration(String body, RoutingContext rc) { String taskId = rc.pathParam("id"); - ServerCallContext context = createCallContext(rc, SetTaskPushNotificationConfigRequest_v0_3.METHOD); + ServerCallContext context = + createCallContext(rc, SetTaskPushNotificationConfigRequest_v0_3.METHOD); HTTPRestResponse response = null; try { response = jsonRestHandler.setTaskPushNotificationConfiguration(taskId, body, context); @@ -288,10 +311,12 @@ public void setTaskPushNotificationConfiguration(String body, RoutingContext rc) public void getTaskPushNotificationConfiguration(RoutingContext rc) { String taskId = rc.pathParam("id"); String configId = rc.pathParam("configId"); - ServerCallContext context = createCallContext(rc, GetTaskPushNotificationConfigRequest_v0_3.METHOD); + ServerCallContext context = + createCallContext(rc, GetTaskPushNotificationConfigRequest_v0_3.METHOD); HTTPRestResponse response = null; try { - response = jsonRestHandler.getTaskPushNotificationConfiguration(taskId, configId, context); + response = + jsonRestHandler.getTaskPushNotificationConfiguration(taskId, configId, context); } catch (Throwable t) { response = jsonRestHandler.createErrorResponse(new InternalError_v0_3(t.getMessage())); } finally { @@ -302,7 +327,8 @@ public void getTaskPushNotificationConfiguration(RoutingContext rc) { @Authenticated public void listTaskPushNotificationConfigurations(RoutingContext rc) { String taskId = rc.pathParam("id"); - ServerCallContext context = createCallContext(rc, ListTaskPushNotificationConfigRequest_v0_3.METHOD); + ServerCallContext context = + createCallContext(rc, ListTaskPushNotificationConfigRequest_v0_3.METHOD); HTTPRestResponse response = null; try { response = jsonRestHandler.listTaskPushNotificationConfigurations(taskId, context); @@ -317,10 +343,13 @@ public void listTaskPushNotificationConfigurations(RoutingContext rc) { public void deleteTaskPushNotificationConfiguration(RoutingContext rc) { String taskId = rc.pathParam("id"); String configId = rc.pathParam("configId"); - ServerCallContext context = createCallContext(rc, DeleteTaskPushNotificationConfigRequest_v0_3.METHOD); + ServerCallContext context = + createCallContext(rc, DeleteTaskPushNotificationConfigRequest_v0_3.METHOD); HTTPRestResponse response = null; try { - response = jsonRestHandler.deleteTaskPushNotificationConfiguration(taskId, configId, context); + response = + jsonRestHandler.deleteTaskPushNotificationConfiguration( + taskId, configId, context); } catch (Throwable t) { response = jsonRestHandler.createErrorResponse(new InternalError_v0_3(t.getMessage())); } finally { @@ -359,7 +388,10 @@ private ServerCallContext createCallContext(RoutingContext rc, String jsonRpcMet user = UnauthenticatedUser.INSTANCE; } else { String subject = rc.user().subject(); - user = subject != null ? new AuthenticatedUser(subject) : UnauthenticatedUser.INSTANCE; + user = + subject != null + ? new AuthenticatedUser(subject) + : UnauthenticatedUser.INSTANCE; } Map state = new HashMap<>(); @@ -370,10 +402,13 @@ private ServerCallContext createCallContext(RoutingContext rc, String jsonRpcMet state.put(METHOD_NAME_KEY, jsonRpcMethodName); // Extract requested extensions from X-A2A-Extensions header (v0.3 header) - List extensionHeaderValues = rc.request().headers().getAll(A2AHeaders_v0_3.X_A2A_EXTENSIONS); - Set requestedExtensions = A2AExtensions.getRequestedExtensions(extensionHeaderValues); + List extensionHeaderValues = + rc.request().headers().getAll(A2AHeaders_v0_3.X_A2A_EXTENSIONS); + Set requestedExtensions = + A2AExtensions.getRequestedExtensions(extensionHeaderValues); - return new ServerCallContext(user, state, requestedExtensions, A2AProtocol_v0_3.PROTOCOL_VERSION); + return new ServerCallContext( + user, state, requestedExtensions, A2AProtocol_v0_3.PROTOCOL_VERSION); } else { CallContextFactory_v0_3 builder = callContextFactory.get(); return builder.build(rc); @@ -391,5 +426,4 @@ private static boolean hasNonDefaultV10AgentCard() { } return false; } - } diff --git a/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/CallContextFactory_v0_3.java b/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/CallContextFactory_v0_3.java index b9bc05697..d58b2e425 100644 --- a/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/CallContextFactory_v0_3.java +++ b/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/CallContextFactory_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.server.rest.quarkus; -import org.a2aproject.sdk.server.ServerCallContext; import io.vertx.ext.web.RoutingContext; +import org.a2aproject.sdk.server.ServerCallContext; /** * Factory interface for creating ServerCallContext from a Vert.x RoutingContext. diff --git a/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusRestTransportMetadata_v0_3.java b/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusRestTransportMetadata_v0_3.java index ea463eba9..24a7aeca8 100644 --- a/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusRestTransportMetadata_v0_3.java +++ b/compat-0.3/reference/rest/src/main/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusRestTransportMetadata_v0_3.java @@ -1,10 +1,10 @@ package org.a2aproject.sdk.compat03.server.rest.quarkus; // TODO: Uncomment when server-common is ported -// See: /Users/kabir/sourcecontrol/AI/a2a-java-0.3.x/reference/rest/src/main/java/io/a2a/server/rest/quarkus/QuarkusRestTransportMetadata.java +// See: +// /Users/kabir/sourcecontrol/AI/a2a-java-0.3.x/reference/rest/src/main/java/io/a2a/server/rest/quarkus/QuarkusRestTransportMetadata.java /** * Placeholder stub - awaiting server-common port. */ -public class QuarkusRestTransportMetadata_v0_3 { -} +public class QuarkusRestTransportMetadata_v0_3 {} diff --git a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2AServerRoutes_v0_3_Test.java b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2AServerRoutes_v0_3_Test.java index e9f18801b..2a9ca06a2 100644 --- a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2AServerRoutes_v0_3_Test.java +++ b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2AServerRoutes_v0_3_Test.java @@ -12,18 +12,15 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Stream; - -import jakarta.enterprise.inject.Instance; - import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; -import org.jspecify.annotations.Nullable; +import jakarta.enterprise.inject.Instance; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; @@ -32,6 +29,7 @@ import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams; import org.a2aproject.sdk.spec.InternalError; import org.a2aproject.sdk.spec.TaskQueryParams; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -49,11 +47,22 @@ public class A2AServerRoutes_v0_3_Test { private void fixture(boolean capabilities) { routes = new A2AServerRoutes_v0_3(); handler = mock(RequestHandler.class); - AgentCard_v0_3 card = new AgentCard_v0_3.Builder().name("validation").description("Validation baseline") - .url("http://localhost:9999").version("1").protocolVersion("0.3") - .capabilities(new AgentCapabilities_v0_3(capabilities, capabilities, false, null)) - .defaultInputModes(List.of("text")).defaultOutputModes(List.of("text")).skills(List.of()).build(); - routes.jsonRestHandler = new RestHandler_v0_3(card, Runnable::run, new Convert_v0_3_To10RequestHandler(handler)); + AgentCard_v0_3 card = + new AgentCard_v0_3.Builder() + .name("validation") + .description("Validation baseline") + .url("http://localhost:9999") + .version("1") + .protocolVersion("0.3") + .capabilities( + new AgentCapabilities_v0_3(capabilities, capabilities, false, null)) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + routes.jsonRestHandler = + new RestHandler_v0_3( + card, Runnable::run, new Convert_v0_3_To10RequestHandler(handler)); routes.callContextFactory = mock(Instance.class); when(routes.callContextFactory.isUnsatisfied()).thenReturn(true); rc = mock(RoutingContext.class); @@ -68,15 +77,21 @@ private void fixture(boolean capabilities) { when(request.params()).thenReturn(params); when(request.headers()).thenReturn(MultiMap.caseInsensitiveMultiMap()); when(response.setStatusCode(anyInt())).thenReturn(response); - when(response.putHeader(any(CharSequence.class), any(CharSequence.class))).thenReturn(response); + when(response.putHeader(any(CharSequence.class), any(CharSequence.class))) + .thenReturn(response); when(response.end(anyString())).thenReturn(Future.succeededFuture()); } private void assertError(int status, String type, String message) { verify(response).setStatusCode(status); verify(response).putHeader(CONTENT_TYPE, "application/json"); - verify(response).end("{\"error\": \"org.a2aproject.sdk.compat03.spec." + type - + "_v0_3\", \"message\": \"" + message + "\"}"); + verify(response) + .end( + "{\"error\": \"org.a2aproject.sdk.compat03.spec." + + type + + "_v0_3\", \"message\": \"" + + message + + "\"}"); verifyNoMoreInteractions(response); } @@ -110,7 +125,10 @@ public void testGetTaskRawAliasValidationOrder() { fixture(true); params.set("history_length", snake).set("historyLength", "bad"); routes.getTask(rc); - assertError(422, "InvalidParamsError", "Only one of 'history_length' or 'historyLength' may be specified"); + assertError( + 422, + "InvalidParamsError", + "Only one of 'history_length' or 'historyLength' may be specified"); verifyNoInteractions(handler); } for (String alias : List.of("history_length", "historyLength")) { @@ -130,7 +148,8 @@ public void testGetTaskRawAliasValidationOrder() { routes.getTask(rc); ArgumentCaptor query = ArgumentCaptor.forClass(TaskQueryParams.class); verify(handler).onGetTask(query.capture(), any()); - assertEquals(alias == null ? null : Integer.valueOf(2), query.getValue().historyLength()); + assertEquals( + alias == null ? null : Integer.valueOf(2), query.getValue().historyLength()); assertError(404, "TaskNotFoundError", "Task not found"); } } @@ -160,7 +179,8 @@ public void testGetPushConfigPathValidationOrder() { for (String configId : new String[] {null, ""}) { fixture(true); when(rc.pathParam("configId")).thenReturn(configId); - when(handler.onGetTaskPushNotificationConfig(any(), any())).thenThrow(new InternalError("delegated")); + when(handler.onGetTaskPushNotificationConfig(any(), any())) + .thenThrow(new InternalError("delegated")); routes.getTaskPushNotificationConfiguration(rc); ArgumentCaptor query = ArgumentCaptor.forClass(GetTaskPushNotificationConfigParams.class); @@ -191,8 +211,8 @@ public void testDeletePushConfigPathValidationOrder() { @ParameterizedTest(name = "taskId={0}, history_length={1}, historyLength={2}") @MethodSource("taskAndAliasValues") - public void testGetTaskForwardsRawPathAndQueryValues(@Nullable String taskId, @Nullable String snake, - @Nullable String camel) { + public void testGetTaskForwardsRawPathAndQueryValues( + @Nullable String taskId, @Nullable String snake, @Nullable String camel) { fixture(true); RestHandler_v0_3 transport = mock(RestHandler_v0_3.class); routes.jsonRestHandler = transport; @@ -256,7 +276,8 @@ public void testSetPushConfigForwardsRawPathValue(@Nullable String taskId) { routes.jsonRestHandler = transport; setTaskId(taskId); RestHandler_v0_3.HTTPRestResponse error = rawForwardingError(); - when(transport.setTaskPushNotificationConfiguration(eq(taskId), eq("{broken"), any())).thenReturn(error); + when(transport.setTaskPushNotificationConfiguration(eq(taskId), eq("{broken"), any())) + .thenReturn(error); routes.setTaskPushNotificationConfiguration("{broken", rc); @@ -267,14 +288,16 @@ public void testSetPushConfigForwardsRawPathValue(@Nullable String taskId) { @ParameterizedTest(name = "taskId={0}, configId={1}") @MethodSource("taskAndConfigIds") - public void testGetPushConfigForwardsRawPathValues(@Nullable String taskId, @Nullable String configId) { + public void testGetPushConfigForwardsRawPathValues( + @Nullable String taskId, @Nullable String configId) { fixture(true); RestHandler_v0_3 transport = mock(RestHandler_v0_3.class); routes.jsonRestHandler = transport; setTaskId(taskId); when(rc.pathParam("configId")).thenReturn(configId); RestHandler_v0_3.HTTPRestResponse error = rawForwardingError(); - when(transport.getTaskPushNotificationConfiguration(eq(taskId), eq(configId), any())).thenReturn(error); + when(transport.getTaskPushNotificationConfiguration(eq(taskId), eq(configId), any())) + .thenReturn(error); routes.getTaskPushNotificationConfiguration(rc); @@ -302,14 +325,16 @@ public void testListPushConfigsForwardsRawPathValue(@Nullable String taskId) { @ParameterizedTest(name = "taskId={0}, configId={1}") @MethodSource("taskAndConfigIds") - public void testDeletePushConfigForwardsRawPathValues(@Nullable String taskId, @Nullable String configId) { + public void testDeletePushConfigForwardsRawPathValues( + @Nullable String taskId, @Nullable String configId) { fixture(true); RestHandler_v0_3 transport = mock(RestHandler_v0_3.class); routes.jsonRestHandler = transport; setTaskId(taskId); when(rc.pathParam("configId")).thenReturn(configId); RestHandler_v0_3.HTTPRestResponse error = rawForwardingError(); - when(transport.deleteTaskPushNotificationConfiguration(eq(taskId), eq(configId), any())).thenReturn(error); + when(transport.deleteTaskPushNotificationConfiguration(eq(taskId), eq(configId), any())) + .thenReturn(error); routes.deleteTaskPushNotificationConfiguration(rc); @@ -324,16 +349,28 @@ private static Stream taskIds() { private static Stream taskAndConfigIds() { return Stream.of((String) null, "", "task") - .flatMap(taskId -> Stream.of((String) null, "", "config") - .map(configId -> Arguments.of(taskId, configId))); + .flatMap( + taskId -> + Stream.of((String) null, "", "config") + .map(configId -> Arguments.of(taskId, configId))); } private static Stream taskAndAliasValues() { - String[][] aliases = {{null, null}, {"", null}, {null, ""}, {"bad", null}, {null, "bad"}, - {"2", null}, {null, "2"}, {"", "bad"}}; + String[][] aliases = { + {null, null}, + {"", null}, + {null, ""}, + {"bad", null}, + {null, "bad"}, + {"2", null}, + {null, "2"}, + {"", "bad"} + }; return Stream.of((String) null, "", "task") - .flatMap(taskId -> Stream.of(aliases) - .map(alias -> Arguments.of(taskId, alias[0], alias[1]))); + .flatMap( + taskId -> + Stream.of(aliases) + .map(alias -> Arguments.of(taskId, alias[0], alias[1]))); } private void setTaskId(@Nullable String taskId) { @@ -342,7 +379,8 @@ private void setTaskId(@Nullable String taskId) { } private RestHandler_v0_3.HTTPRestResponse rawForwardingError() { - return new RestHandler_v0_3.HTTPRestResponse(422, "application/json", "{\"transportError\":true}"); + return new RestHandler_v0_3.HTTPRestResponse( + 422, "application/json", "{\"transportError\":true}"); } private void assertRawForwardingResponse(RestHandler_v0_3.HTTPRestResponse error) { diff --git a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2ATestRoutes_v0_3.java b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2ATestRoutes_v0_3.java index 753cca76f..4d342b5ab 100644 --- a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2ATestRoutes_v0_3.java +++ b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/A2ATestRoutes_v0_3.java @@ -4,131 +4,137 @@ import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN; -import java.util.concurrent.atomic.AtomicInteger; - +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; import jakarta.annotation.PostConstruct; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.inject.Singleton; - +import java.util.concurrent.atomic.AtomicInteger; import org.a2aproject.sdk.compat03.conversion.TestUtilsBean_v0_3; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskArtifactUpdateEvent; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.a2aproject.sdk.spec.TaskStatusUpdateEvent; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.BodyHandler; /** * Exposes the {@link TestUtilsBean_v0_3} via REST using the Vert.x Web Router */ @Singleton public class A2ATestRoutes_v0_3 { - @Inject - TestUtilsBean_v0_3 testUtilsBean; + @Inject TestUtilsBean_v0_3 testUtilsBean; - @Inject - A2AServerRoutes_v0_3 a2AServerRoutes; + @Inject A2AServerRoutes_v0_3 a2AServerRoutes; AtomicInteger streamingSubscribedCount = new AtomicInteger(0); @PostConstruct public void init() { - A2AServerRoutes_v0_3.setStreamingMultiSseSupportSubscribedRunnable(() -> streamingSubscribedCount.incrementAndGet()); + A2AServerRoutes_v0_3.setStreamingMultiSseSupportSubscribedRunnable( + () -> streamingSubscribedCount.incrementAndGet()); } void setupRoutes(@Observes Router router) { // Save task: POST /test/task router.post("/test/task") - .consumes(APPLICATION_JSON) - .handler(BodyHandler.create()) - .blockingHandler(ctx -> { - String body = ctx.body().asString(); - saveTask(body, ctx); - }); + .consumes(APPLICATION_JSON) + .handler(BodyHandler.create()) + .blockingHandler( + ctx -> { + String body = ctx.body().asString(); + saveTask(body, ctx); + }); // Get task: GET /test/task/:taskId router.get("/test/task/:taskId") - .produces(APPLICATION_JSON) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - getTask(taskId, ctx); - }); + .produces(APPLICATION_JSON) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + getTask(taskId, ctx); + }); // Delete task: DELETE /test/task/:taskId router.delete("/test/task/:taskId") - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - deleteTask(taskId, ctx); - }); + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + deleteTask(taskId, ctx); + }); // Ensure task queue: POST /test/queue/ensure/:taskId router.post("/test/queue/ensure/:taskId") - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - ensureTaskQueue(taskId, ctx); - }); + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + ensureTaskQueue(taskId, ctx); + }); // Enqueue task status update event: POST /test/queue/enqueueTaskStatusUpdateEvent/:taskId router.post("/test/queue/enqueueTaskStatusUpdateEvent/:taskId") - .handler(BodyHandler.create()) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - enqueueTaskStatusUpdateEvent(taskId, body, ctx); - }); - - // Enqueue task artifact update event: POST /test/queue/enqueueTaskArtifactUpdateEvent/:taskId + .handler(BodyHandler.create()) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + enqueueTaskStatusUpdateEvent(taskId, body, ctx); + }); + + // Enqueue task artifact update event: POST + // /test/queue/enqueueTaskArtifactUpdateEvent/:taskId router.post("/test/queue/enqueueTaskArtifactUpdateEvent/:taskId") - .handler(BodyHandler.create()) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - enqueueTaskArtifactUpdateEvent(taskId, body, ctx); - }); + .handler(BodyHandler.create()) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + enqueueTaskArtifactUpdateEvent(taskId, body, ctx); + }); // Get streaming subscribed count: GET /test/streamingSubscribedCount router.get("/test/streamingSubscribedCount") - .produces(TEXT_PLAIN) - .handler(ctx -> { - getStreamingSubscribedCount(ctx); - }); + .produces(TEXT_PLAIN) + .handler( + ctx -> { + getStreamingSubscribedCount(ctx); + }); // Get child queue count: GET /test/queue/childCount/:taskId router.get("/test/queue/childCount/:taskId") - .produces(TEXT_PLAIN) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - getChildQueueCount(taskId, ctx); - }); + .produces(TEXT_PLAIN) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + getChildQueueCount(taskId, ctx); + }); // Delete task push notification config: DELETE /test/task/:taskId/config/:configId router.delete("/test/task/:taskId/config/:configId") - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String configId = ctx.pathParam("configId"); - deleteTaskPushNotificationConfig(taskId, configId, ctx); - }); + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String configId = ctx.pathParam("configId"); + deleteTaskPushNotificationConfig(taskId, configId, ctx); + }); // Save task push notification config: POST /test/task/:taskId router.post("/test/task/:taskId") - .handler(BodyHandler.create()) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - saveTaskPushNotificationConfig(taskId, body, ctx); - }); + .handler(BodyHandler.create()) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + saveTaskPushNotificationConfig(taskId, body, ctx); + }); } public void saveTask(String body, RoutingContext rc) { try { Task task = JsonUtil.fromJson(body, Task.class); testUtilsBean.saveTask(task); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -138,9 +144,7 @@ public void getTask(String taskId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } rc.response() @@ -157,15 +161,11 @@ public void deleteTask(String taskId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } testUtilsBean.deleteTask(taskId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -174,9 +174,7 @@ public void deleteTask(String taskId, RoutingContext rc) { public void ensureTaskQueue(String taskId, RoutingContext rc) { try { testUtilsBean.ensureQueue(taskId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -187,9 +185,7 @@ public void enqueueTaskStatusUpdateEvent(String taskId, String body, RoutingCont try { TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -200,40 +196,31 @@ public void enqueueTaskArtifactUpdateEvent(String taskId, String body, RoutingCo try { TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } } public void getStreamingSubscribedCount(RoutingContext rc) { - rc.response() - .setStatusCode(200) - .end(String.valueOf(streamingSubscribedCount.get())); + rc.response().setStatusCode(200).end(String.valueOf(streamingSubscribedCount.get())); } public void getChildQueueCount(String taskId, RoutingContext rc) { int count = testUtilsBean.getChildQueueCount(taskId); - rc.response() - .setStatusCode(200) - .end(String.valueOf(count)); + rc.response().setStatusCode(200).end(String.valueOf(count)); } - public void deleteTaskPushNotificationConfig(String taskId, String configId, RoutingContext rc) { + public void deleteTaskPushNotificationConfig( + String taskId, String configId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } testUtilsBean.deleteTaskPushNotificationConfig(taskId, configId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -241,17 +228,14 @@ public void deleteTaskPushNotificationConfig(String taskId, String configId, Rou public void saveTaskPushNotificationConfig(String taskId, String body, RoutingContext rc) { try { - TaskPushNotificationConfig notificationConfig = JsonUtil.fromJson(body, TaskPushNotificationConfig.class); + TaskPushNotificationConfig notificationConfig = + JsonUtil.fromJson(body, TaskPushNotificationConfig.class); if (notificationConfig == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } testUtilsBean.saveTaskPushNotificationConfig(taskId, notificationConfig); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -259,10 +243,6 @@ public void saveTaskPushNotificationConfig(String taskId, String body, RoutingCo private void errorResponse(Throwable t, RoutingContext rc) { t.printStackTrace(); - rc.response() - .setStatusCode(500) - .putHeader(CONTENT_TYPE, TEXT_PLAIN) - .end(); + rc.response().setStatusCode(500).putHeader(CONTENT_TYPE, TEXT_PLAIN).end(); } - } diff --git a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_AndroidTest.java b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_AndroidTest.java index a8e584a45..035fff4f9 100644 --- a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_AndroidTest.java +++ b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_AndroidTest.java @@ -1,17 +1,18 @@ package org.a2aproject.sdk.compat03.server.rest.quarkus; +import io.quarkus.test.junit.QuarkusTest; import org.a2aproject.sdk.client.http.android.AndroidA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; -import io.quarkus.test.junit.QuarkusTest; +import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; @QuarkusTest public class QuarkusA2ARest_v0_3_AndroidTest extends QuarkusA2ARest_v0_3_Test { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(RestTransport_v0_3.class, + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3().httpClient(new AndroidA2AHttpClient())); } } diff --git a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_JdkTest.java b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_JdkTest.java index fc6c578a2..d734a9edc 100644 --- a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_JdkTest.java +++ b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_JdkTest.java @@ -1,17 +1,18 @@ package org.a2aproject.sdk.compat03.server.rest.quarkus; +import io.quarkus.test.junit.QuarkusTest; import org.a2aproject.sdk.client.http.JdkA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; -import io.quarkus.test.junit.QuarkusTest; +import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; @QuarkusTest public class QuarkusA2ARest_v0_3_JdkTest extends QuarkusA2ARest_v0_3_Test { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(RestTransport_v0_3.class, + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3().httpClient(new JdkA2AHttpClient())); } } diff --git a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_Test.java b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_Test.java index 9c9dff961..c7ddd5af5 100644 --- a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_Test.java +++ b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_Test.java @@ -6,7 +6,6 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; - import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerServerTest_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; @@ -34,49 +33,89 @@ protected String getTransportUrl() { @Test public void testMethodNotFound() throws Exception { // Create the client - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); // Create the request - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/v1/message:send")) - .PUT(HttpRequest.BodyPublishers.ofString("test")) - .header("Content-Type", APPLICATION_JSON); - HttpResponse response = client.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + HttpRequest.Builder builder = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/v1/message:send")) + .PUT(HttpRequest.BodyPublishers.ofString("test")) + .header("Content-Type", APPLICATION_JSON); + HttpResponse response = + client.send(builder.build(), HttpResponse.BodyHandlers.ofString()); assertEquals(405, response.statusCode()); - builder = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/v1/message:send")) - .DELETE() - .header("Content-Type", APPLICATION_JSON); + builder = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/v1/message:send")) + .DELETE() + .header("Content-Type", APPLICATION_JSON); response = client.send(builder.build(), HttpResponse.BodyHandlers.ofString()); assertEquals(405, response.statusCode()); } + @Test public void testHistoryAliasWireValidation() throws Exception { String[][] cases = { - {"", "404", "TaskNotFoundError", "Task not found"}, - {"history_length=", "422", "InvalidParamsError", "bad history_length or historyLength"}, - {"historyLength=", "422", "InvalidParamsError", "bad history_length or historyLength"}, - {"history_length=&historyLength=bad", "422", "InvalidParamsError", - "Only one of 'history_length' or 'historyLength' may be specified"}, - {"history_length=-1", "422", "InvalidParamsError", "bad history_length or historyLength"}, - {"history_length=2", "404", "TaskNotFoundError", "Task not found"}, - {"historyLength=2", "404", "TaskNotFoundError", "Task not found"}, - {"history_length=2&historyLength=bad", "422", "InvalidParamsError", - "Only one of 'history_length' or 'historyLength' may be specified"}, - {"history_length=bad", "422", "InvalidParamsError", "bad history_length or historyLength"}, - {"historyLength=bad", "422", "InvalidParamsError", "bad history_length or historyLength"} + {"", "404", "TaskNotFoundError", "Task not found"}, + {"history_length=", "422", "InvalidParamsError", "bad history_length or historyLength"}, + {"historyLength=", "422", "InvalidParamsError", "bad history_length or historyLength"}, + { + "history_length=&historyLength=bad", + "422", + "InvalidParamsError", + "Only one of 'history_length' or 'historyLength' may be specified" + }, + { + "history_length=-1", + "422", + "InvalidParamsError", + "bad history_length or historyLength" + }, + {"history_length=2", "404", "TaskNotFoundError", "Task not found"}, + {"historyLength=2", "404", "TaskNotFoundError", "Task not found"}, + { + "history_length=2&historyLength=bad", + "422", + "InvalidParamsError", + "Only one of 'history_length' or 'historyLength' may be specified" + }, + { + "history_length=bad", + "422", + "InvalidParamsError", + "bad history_length or historyLength" + }, + { + "historyLength=bad", + "422", + "InvalidParamsError", + "bad history_length or historyLength" + } }; HttpClient client = HttpClient.newHttpClient(); for (String[] entry : cases) { - HttpResponse response = client.send(HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/v1/tasks/missing-alias-task?" + entry[0])) - .GET().build(), HttpResponse.BodyHandlers.ofString()); + HttpResponse response = + client.send( + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/v1/tasks/missing-alias-task?" + + entry[0])) + .GET() + .build(), + HttpResponse.BodyHandlers.ofString()); assertEquals(Integer.parseInt(entry[1]), response.statusCode(), response.body()); - assertEquals("application/json", response.headers().firstValue("Content-Type").orElseThrow()); - assertEquals("{\"error\": \"org.a2aproject.sdk.compat03.spec." + entry[2] - + "_v0_3\", \"message\": \"" + entry[3] + "\"}", response.body()); + assertEquals( + "application/json", + response.headers().firstValue("Content-Type").orElseThrow()); + assertEquals( + "{\"error\": \"org.a2aproject.sdk.compat03.spec." + + entry[2] + + "_v0_3\", \"message\": \"" + + entry[3] + + "\"}", + response.body()); } } - } diff --git a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_VertxTest.java b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_VertxTest.java index 3b8a86cd9..268f0ece9 100644 --- a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_VertxTest.java +++ b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_VertxTest.java @@ -1,22 +1,22 @@ package org.a2aproject.sdk.compat03.server.rest.quarkus; -import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; -import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; -import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; import io.quarkus.test.junit.QuarkusTest; import io.vertx.core.Vertx; import jakarta.inject.Inject; +import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; +import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; @QuarkusTest public class QuarkusA2ARest_v0_3_VertxTest extends QuarkusA2ARest_v0_3_Test { - @Inject - Vertx vertx; + @Inject Vertx vertx; @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(RestTransport_v0_3.class, + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3().httpClient(new VertxA2AHttpClient(vertx))); } } diff --git a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthAndroidTest.java b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthAndroidTest.java index 7fa61a9a3..fedf670ea 100644 --- a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthAndroidTest.java +++ b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthAndroidTest.java @@ -1,15 +1,15 @@ package org.a2aproject.sdk.compat03.server.rest.quarkus; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; import org.a2aproject.sdk.client.http.android.AndroidA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithAuthTest_v0_3; import org.a2aproject.sdk.compat03.conversion.AuthTestProfile_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; import org.junit.jupiter.api.Test; @QuarkusTest @@ -32,11 +32,15 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); - builder.withTransport(RestTransport_v0_3.class, + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3() .httpClient(new AndroidA2AHttpClient()) .addInterceptor(authInterceptor)); @@ -44,7 +48,8 @@ protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(RestTransport_v0_3.class, + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3().httpClient(new AndroidA2AHttpClient())); } @@ -53,10 +58,7 @@ protected void configureTransport(ClientBuilder_v0_3 builder) { public void testBasicAuthWorksViaHttp() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); - givenAuthenticated() - .get("/v1/tasks/" + MINIMAL_TASK.id()) - .then() - .statusCode(200); + givenAuthenticated().get("/v1/tasks/" + MINIMAL_TASK.id()).then().statusCode(200); deleteTaskInTaskStore(MINIMAL_TASK.id()); } diff --git a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthTest.java b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthTest.java index 64feaddf2..000166727 100644 --- a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthTest.java +++ b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthTest.java @@ -1,15 +1,15 @@ package org.a2aproject.sdk.compat03.server.rest.quarkus; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; import org.a2aproject.sdk.client.http.JdkA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithAuthTest_v0_3; import org.a2aproject.sdk.compat03.conversion.AuthTestProfile_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; import org.junit.jupiter.api.Test; @QuarkusTest @@ -32,11 +32,15 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); - builder.withTransport(RestTransport_v0_3.class, + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3() .httpClient(new JdkA2AHttpClient()) .addInterceptor(authInterceptor)); @@ -44,7 +48,8 @@ protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(RestTransport_v0_3.class, + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3().httpClient(new JdkA2AHttpClient())); } @@ -53,10 +58,7 @@ protected void configureTransport(ClientBuilder_v0_3 builder) { public void testBasicAuthWorksViaHttp() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); - givenAuthenticated() - .get("/v1/tasks/" + MINIMAL_TASK.id()) - .then() - .statusCode(200); + givenAuthenticated().get("/v1/tasks/" + MINIMAL_TASK.id()).then().statusCode(200); deleteTaskInTaskStore(MINIMAL_TASK.id()); } diff --git a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthVertxTest.java b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthVertxTest.java index d2997e514..cd9dcaeca 100644 --- a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthVertxTest.java +++ b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithAuthVertxTest.java @@ -1,25 +1,24 @@ package org.a2aproject.sdk.compat03.server.rest.quarkus; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import io.vertx.core.Vertx; +import jakarta.inject.Inject; import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithAuthTest_v0_3; import org.a2aproject.sdk.compat03.conversion.AuthTestProfile_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; -import io.vertx.core.Vertx; -import jakarta.inject.Inject; import org.junit.jupiter.api.Test; @QuarkusTest @TestProfile(AuthTestProfile_v0_3.class) public class QuarkusA2ARest_v0_3_WithAuthVertxTest extends AbstractA2AServerWithAuthTest_v0_3 { - @Inject - Vertx vertx; + @Inject Vertx vertx; public QuarkusA2ARest_v0_3_WithAuthVertxTest() { super(8081); @@ -37,11 +36,15 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); - builder.withTransport(RestTransport_v0_3.class, + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3() .httpClient(new VertxA2AHttpClient(vertx)) .addInterceptor(authInterceptor)); @@ -49,7 +52,8 @@ protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(RestTransport_v0_3.class, + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3().httpClient(new VertxA2AHttpClient(vertx))); } @@ -58,10 +62,7 @@ protected void configureTransport(ClientBuilder_v0_3 builder) { public void testBasicAuthWorksViaHttp() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); - givenAuthenticated() - .get("/v1/tasks/" + MINIMAL_TASK.id()) - .then() - .statusCode(200); + givenAuthenticated().get("/v1/tasks/" + MINIMAL_TASK.id()).then().statusCode(200); deleteTaskInTaskStore(MINIMAL_TASK.id()); } diff --git a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithTaskAuthorizationVertxTest.java b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithTaskAuthorizationVertxTest.java index 9e32ecbc7..618c8c787 100644 --- a/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithTaskAuthorizationVertxTest.java +++ b/compat-0.3/reference/rest/src/test/java/org/a2aproject/sdk/compat03/server/rest/quarkus/QuarkusA2ARest_v0_3_WithTaskAuthorizationVertxTest.java @@ -6,8 +6,8 @@ import jakarta.inject.Inject; import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithTaskAuthorizationTest_v0_3; import org.a2aproject.sdk.compat03.conversion.TaskAuthorizationTestProfile_v0_3; @@ -15,10 +15,10 @@ @QuarkusTest @TestProfile(TaskAuthorizationTestProfile_v0_3.class) -public class QuarkusA2ARest_v0_3_WithTaskAuthorizationVertxTest extends AbstractA2AServerWithTaskAuthorizationTest_v0_3 { +public class QuarkusA2ARest_v0_3_WithTaskAuthorizationVertxTest + extends AbstractA2AServerWithTaskAuthorizationTest_v0_3 { - @Inject - Vertx vertx; + @Inject Vertx vertx; public QuarkusA2ARest_v0_3_WithTaskAuthorizationVertxTest() { super(8081); @@ -35,11 +35,16 @@ protected String getTransportUrl() { } @Override - protected void configureTransportWithCredentials(ClientBuilder_v0_3 builder, String username, String password) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) - ? getEncodedCredentials(username, password) : null); - builder.withTransport(RestTransport_v0_3.class, + protected void configureTransportWithCredentials( + ClientBuilder_v0_3 builder, String username, String password) { + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials(username, password) + : null); + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3() .httpClient(new VertxA2AHttpClient(vertx)) .addInterceptor(authInterceptor)); diff --git a/compat-0.3/server-conversion/pom.xml b/compat-0.3/server-conversion/pom.xml index 80acaa031..cb33dfff7 100644 --- a/compat-0.3/server-conversion/pom.xml +++ b/compat-0.3/server-conversion/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/A2AProtocol_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/A2AProtocol_v0_3.java index d7c61edf5..2240d31e9 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/A2AProtocol_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/A2AProtocol_v0_3.java @@ -4,6 +4,5 @@ public final class A2AProtocol_v0_3 { public static final String PROTOCOL_VERSION = "0.3"; - private A2AProtocol_v0_3() { - } + private A2AProtocol_v0_3() {} } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/Convert_v0_3_To10RequestHandler.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/Convert_v0_3_To10RequestHandler.java index 7e7c5b11a..81e483cd2 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/Convert_v0_3_To10RequestHandler.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/Convert_v0_3_To10RequestHandler.java @@ -1,11 +1,9 @@ package org.a2aproject.sdk.compat03.conversion; -import java.util.List; -import java.util.concurrent.Flow; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; - +import java.util.List; +import java.util.concurrent.Flow; import org.a2aproject.sdk.compat03.conversion.mappers.domain.EventKindMapper_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.domain.StreamingEventKindMapper_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskMapper_v0_3; @@ -79,7 +77,8 @@ public Convert_v0_3_To10RequestHandler() { } @Inject - public Convert_v0_3_To10RequestHandler(org.a2aproject.sdk.server.requesthandlers.RequestHandler v10Handler) { + public Convert_v0_3_To10RequestHandler( + org.a2aproject.sdk.server.requesthandlers.RequestHandler v10Handler) { this.v10Handler = v10Handler; } @@ -93,9 +92,8 @@ public Convert_v0_3_To10RequestHandler(org.a2aproject.sdk.server.requesthandlers * @return the v0.3 task * @throws A2AError if an error occurs */ - public Task_v0_3 onGetTask( - TaskQueryParams_v0_3 v03Params, - ServerCallContext context) throws A2AError { + public Task_v0_3 onGetTask(TaskQueryParams_v0_3 v03Params, ServerCallContext context) + throws A2AError { // Convert v0.3 params → v1.0 params TaskQueryParams v10Params = TaskQueryParamsMapper_v0_3.INSTANCE.toV10(v03Params); @@ -117,9 +115,8 @@ public Task_v0_3 onGetTask( * @return the v0.3 task * @throws A2AError if an error occurs */ - public Task_v0_3 onCancelTask( - TaskIdParams_v0_3 v03Params, - ServerCallContext context) throws A2AError { + public Task_v0_3 onCancelTask(TaskIdParams_v0_3 v03Params, ServerCallContext context) + throws A2AError { // Convert v0.3 TaskIdParams → v1.0 CancelTaskParams CancelTaskParams v10Params = CancelTaskParamsMapper_v0_3.INSTANCE.toV10(v03Params); @@ -141,9 +138,8 @@ public Task_v0_3 onCancelTask( * @return the v0.3 event kind (Task or Message) * @throws A2AError if an error occurs */ - public EventKind_v0_3 onMessageSend( - MessageSendParams_v0_3 v03Params, - ServerCallContext context) throws A2AError { + public EventKind_v0_3 onMessageSend(MessageSendParams_v0_3 v03Params, ServerCallContext context) + throws A2AError { // Convert v0.3 params → v1.0 params MessageSendParams v10Params = MessageSendParamsMapper_v0_3.INSTANCE.toV10(v03Params); @@ -170,8 +166,7 @@ public EventKind_v0_3 onMessageSend( * @throws A2AError if an error occurs */ public Flow.Publisher onMessageSendStream( - MessageSendParams_v0_3 v03Params, - ServerCallContext context) throws A2AError { + MessageSendParams_v0_3 v03Params, ServerCallContext context) throws A2AError { // Convert v0.3 params → v1.0 params MessageSendParams v10Params = MessageSendParamsMapper_v0_3.INSTANCE.toV10(v03Params); @@ -182,7 +177,7 @@ public Flow.Publisher onMessageSendStream( // Get v1.0 publisher Flow.Publisher v10Publisher = - v10Handler.onMessageSendStream(v10Params, context); + v10Handler.onMessageSendStream(v10Params, context); // Convert each event using a mapping processor return convertPublisher(v10Publisher, StreamingEventKindMapper_v0_3.INSTANCE::fromV10); @@ -200,16 +195,15 @@ public Flow.Publisher onMessageSendStream( * @throws A2AError if an error occurs */ public TaskPushNotificationConfig_v0_3 onSetTaskPushNotificationConfig( - TaskPushNotificationConfig_v0_3 v03Config, - ServerCallContext context) throws A2AError { + TaskPushNotificationConfig_v0_3 v03Config, ServerCallContext context) throws A2AError { // Convert v0.3 config → v1.0 config TaskPushNotificationConfig v10Config = - TaskPushNotificationConfigMapper_v0_3.INSTANCE.toV10(v03Config); + TaskPushNotificationConfigMapper_v0_3.INSTANCE.toV10(v03Config); // Call v1.0 handler TaskPushNotificationConfig v10Result = - v10Handler.onCreateTaskPushNotificationConfig(v10Config, context); + v10Handler.onCreateTaskPushNotificationConfig(v10Config, context); // Convert v1.0 result → v0.3 result return TaskPushNotificationConfigMapper_v0_3.INSTANCE.fromV10(v10Result); @@ -229,18 +223,19 @@ public TaskPushNotificationConfig_v0_3 onSetTaskPushNotificationConfig( * @throws A2AError if an error occurs */ public TaskPushNotificationConfig_v0_3 onGetTaskPushNotificationConfig( - GetTaskPushNotificationConfigParams_v0_3 v03Params, - ServerCallContext context) throws A2AError { + GetTaskPushNotificationConfigParams_v0_3 v03Params, ServerCallContext context) + throws A2AError { // Convert v0.3 params → v1.0 params // v0.3: id = taskId, pushNotificationConfigId = optional config id // v1.0: taskId = taskId, id = config id (defaults to taskId if not specified) - String configId = v03Params.pushNotificationConfigId() != null - ? v03Params.pushNotificationConfigId() - : v03Params.id(); // Default to taskId when config id not specified + String configId = + v03Params.pushNotificationConfigId() != null + ? v03Params.pushNotificationConfigId() + : v03Params.id(); // Default to taskId when config id not specified GetTaskPushNotificationConfigParams v10Params = - new GetTaskPushNotificationConfigParams(v03Params.id(), configId); + new GetTaskPushNotificationConfigParams(v03Params.id(), configId); TaskPushNotificationConfig v10Result; try { @@ -251,9 +246,9 @@ public TaskPushNotificationConfig_v0_3 onGetTaskPushNotificationConfig( // This matches v0.3 DefaultRequestHandler.getPushNotificationConfig() behavior if (v03Params.pushNotificationConfigId() != null) { ListTaskPushNotificationConfigsParams listParams = - new ListTaskPushNotificationConfigsParams(v03Params.id()); + new ListTaskPushNotificationConfigsParams(v03Params.id()); ListTaskPushNotificationConfigsResult listResult = - v10Handler.onListTaskPushNotificationConfigs(listParams, context); + v10Handler.onListTaskPushNotificationConfigs(listParams, context); if (listResult.configs().isEmpty()) { throw e; // Re-throw if no configs exist at all @@ -281,15 +276,14 @@ public TaskPushNotificationConfig_v0_3 onGetTaskPushNotificationConfig( * @throws A2AError if an error occurs */ public Flow.Publisher onResubscribeToTask( - TaskIdParams_v0_3 v03Params, - ServerCallContext context) throws A2AError { + TaskIdParams_v0_3 v03Params, ServerCallContext context) throws A2AError { // Convert v0.3 params → v1.0 params TaskIdParams v10Params = TaskIdParamsMapper_v0_3.INSTANCE.toV10(v03Params); // Get v1.0 publisher Flow.Publisher v10Publisher = - v10Handler.onSubscribeToTask(v10Params, context); + v10Handler.onSubscribeToTask(v10Params, context); // Convert each event using a mapping processor return convertPublisher(v10Publisher, StreamingEventKindMapper_v0_3.INSTANCE::fromV10); @@ -306,22 +300,23 @@ public Flow.Publisher onResubscribeToTask( * @throws A2AError if an error occurs */ public List onListTaskPushNotificationConfig( - ListTaskPushNotificationConfigParams_v0_3 v03Params, - ServerCallContext context) throws A2AError { + ListTaskPushNotificationConfigParams_v0_3 v03Params, ServerCallContext context) + throws A2AError { // Convert v0.3 params → v1.0 params - // ListTaskPushNotificationConfigParams has different structure - v0.3 has id, v1.0 has more fields + // ListTaskPushNotificationConfigParams has different structure - v0.3 has id, v1.0 has more + // fields ListTaskPushNotificationConfigsParams v10Params = - new ListTaskPushNotificationConfigsParams( - v03Params.id(), - 0, // No pageSize in v0.3 - use 0 (will use default) - "", // No pageToken in v0.3 - use empty string - "" // Default tenant - ); + new ListTaskPushNotificationConfigsParams( + v03Params.id(), + 0, // No pageSize in v0.3 - use 0 (will use default) + "", // No pageToken in v0.3 - use empty string + "" // Default tenant + ); // Call v1.0 handler ListTaskPushNotificationConfigsResult v10Result = - v10Handler.onListTaskPushNotificationConfigs(v10Params, context); + v10Handler.onListTaskPushNotificationConfigs(v10Params, context); // Convert v1.0 result → v0.3 result (extract list from result wrapper) return ListTaskPushNotificationConfigsResultMapper_v0_3.INSTANCE.fromV10(v10Result); @@ -337,16 +332,14 @@ public List onListTaskPushNotificationConfig( * @throws A2AError if an error occurs */ public void onDeleteTaskPushNotificationConfig( - DeleteTaskPushNotificationConfigParams_v0_3 v03Params, - ServerCallContext context) throws A2AError { + DeleteTaskPushNotificationConfigParams_v0_3 v03Params, ServerCallContext context) + throws A2AError { // Convert v0.3 params → v1.0 params (add tenant field) DeleteTaskPushNotificationConfigParams v10Params = - new DeleteTaskPushNotificationConfigParams( - v03Params.id(), - v03Params.pushNotificationConfigId(), - "" // Default tenant - ); + new DeleteTaskPushNotificationConfigParams( + v03Params.id(), v03Params.pushNotificationConfigId(), "" // Default tenant + ); // Call v1.0 handler v10Handler.onDeleteTaskPushNotificationConfig(v10Params, context); @@ -362,34 +355,35 @@ public void onDeleteTaskPushNotificationConfig( * @return publisher of v0.3 elements */ private Flow.Publisher convertPublisher( - Flow.Publisher v10Publisher, - java.util.function.Function mapper) { - - return subscriber -> v10Publisher.subscribe(new Flow.Subscriber() { - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscriber.onSubscribe(subscription); - } - - @Override - public void onNext(V10 v10Item) { - try { - V03 v03Item = mapper.apply(v10Item); - subscriber.onNext(v03Item); - } catch (Exception e) { - subscriber.onError(e); - } - } - - @Override - public void onError(Throwable throwable) { - subscriber.onError(throwable); - } - - @Override - public void onComplete() { - subscriber.onComplete(); - } - }); + Flow.Publisher v10Publisher, java.util.function.Function mapper) { + + return subscriber -> + v10Publisher.subscribe( + new Flow.Subscriber() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscriber.onSubscribe(subscription); + } + + @Override + public void onNext(V10 v10Item) { + try { + V03 v03Item = mapper.apply(v10Item); + subscriber.onNext(v03Item); + } catch (Exception e) { + subscriber.onError(e); + } + } + + @Override + public void onError(Throwable throwable) { + subscriber.onError(throwable); + } + + @Override + public void onComplete() { + subscriber.onComplete(); + } + }); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/ErrorConverter_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/ErrorConverter_v0_3.java index 4e5c51a2c..f4a9e4914 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/ErrorConverter_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/ErrorConverter_v0_3.java @@ -59,33 +59,44 @@ public static JSONRPCError_v0_3 convertA2AError(A2AError v10Error) { // Preserve specific error types by mapping v1.0 errors to v0.3 equivalents if (v10Error instanceof TaskNotFoundError) { - return new TaskNotFoundError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new TaskNotFoundError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } else if (v10Error instanceof UnsupportedOperationError) { - return new UnsupportedOperationError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new UnsupportedOperationError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } else if (v10Error instanceof TaskNotCancelableError) { - return new TaskNotCancelableError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new TaskNotCancelableError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } else if (v10Error instanceof InvalidParamsError) { - return new InvalidParamsError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new InvalidParamsError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } else if (v10Error instanceof InvalidRequestError) { - return new InvalidRequestError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new InvalidRequestError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } else if (v10Error instanceof InternalError) { return new InternalError_v0_3(v10Error.getMessage()); } else if (v10Error instanceof InvalidAgentResponseError) { - return new InvalidAgentResponseError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new InvalidAgentResponseError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } else if (v10Error instanceof ContentTypeNotSupportedError) { - return new ContentTypeNotSupportedError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new ContentTypeNotSupportedError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } else if (v10Error instanceof PushNotificationNotSupportedError) { - return new PushNotificationNotSupportedError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new PushNotificationNotSupportedError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } else if (v10Error instanceof MethodNotFoundError) { - return new MethodNotFoundError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new MethodNotFoundError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } else if (v10Error instanceof JSONParseError) { - return new JSONParseError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new JSONParseError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } else if (v10Error instanceof ExtendedAgentCardNotConfiguredError) { return new AuthenticatedExtendedCardNotConfiguredError_v0_3( v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } // Fallback to generic JSONRPCError for unmapped types - return new JSONRPCError_v0_3(v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); + return new JSONRPCError_v0_3( + v10Error.getCode(), v10Error.getMessage(), v10Error.getDetails()); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/PushNotificationPayloadFormatter_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/PushNotificationPayloadFormatter_v0_3.java index 3c91c0e92..8f41636b2 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/PushNotificationPayloadFormatter_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/PushNotificationPayloadFormatter_v0_3.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.compat03.conversion; import jakarta.enterprise.context.ApplicationScoped; - import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskMapper_v0_3; import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; @@ -17,7 +16,8 @@ @ApplicationScoped public class PushNotificationPayloadFormatter_v0_3 implements PushNotificationPayloadFormatter { - private static final Logger LOGGER = LoggerFactory.getLogger(PushNotificationPayloadFormatter_v0_3.class); + private static final Logger LOGGER = + LoggerFactory.getLogger(PushNotificationPayloadFormatter_v0_3.class); @Override public String targetVersion() { diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A03ToV10MapperConfig.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A03ToV10MapperConfig.java index 4067ef9f2..f8a2dc996 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A03ToV10MapperConfig.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A03ToV10MapperConfig.java @@ -21,10 +21,9 @@ * @see A2AMappers_v0_3 */ @MapperConfig( - unmappedTargetPolicy = ReportingPolicy.ERROR, - componentModel = "default", - nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE -) + unmappedTargetPolicy = ReportingPolicy.ERROR, + componentModel = "default", + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) public interface A03ToV10MapperConfig { /** diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A2AMappers_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A2AMappers_v0_3.java index 2bdb85967..125a04a9a 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A2AMappers_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/config/A2AMappers_v0_3.java @@ -55,15 +55,19 @@ private A2AMappers_v0_3() { */ @SuppressWarnings("unchecked") public static T getMapper(Class mapperInterface) { - return (T) MAPPERS.computeIfAbsent(mapperInterface, clazz -> { - try { - // MapStruct generates implementation with "Impl" suffix - String implName = clazz.getName() + "Impl"; - Class implClass = Class.forName(implName); - return implClass.getDeclaredConstructor().newInstance(); - } catch (Exception e) { - throw new RuntimeException("Failed to load mapper: " + clazz.getName(), e); - } - }); + return (T) + MAPPERS.computeIfAbsent( + mapperInterface, + clazz -> { + try { + // MapStruct generates implementation with "Impl" suffix + String implName = clazz.getName() + "Impl"; + Class implClass = Class.forName(implName); + return implClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException( + "Failed to load mapper: " + clazz.getName(), e); + } + }); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/ArtifactMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/ArtifactMapper_v0_3.java index 166842cbb..449204b95 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/ArtifactMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/ArtifactMapper_v0_3.java @@ -2,9 +2,8 @@ import java.util.List; import java.util.stream.Collectors; - -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.Artifact_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3; import org.a2aproject.sdk.spec.Artifact; @@ -19,7 +18,9 @@ *

* The conversion primarily involves converting the nested {@link Part} list using {@link PartMapper_v0_3}. */ -@Mapper(config = A03ToV10MapperConfig.class, uses = {PartMapper_v0_3.class}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = {PartMapper_v0_3.class}) public interface ArtifactMapper_v0_3 { /** @@ -40,18 +41,18 @@ default Artifact toV10(Artifact_v0_3 v03) { return null; } - List> parts = v03.parts().stream() - .map(PartMapper_v0_3.INSTANCE::toV10) - .collect(Collectors.toList()); + List> parts = + v03.parts().stream() + .map(PartMapper_v0_3.INSTANCE::toV10) + .collect(Collectors.toList()); return new Artifact( - v03.artifactId(), - v03.name(), - v03.description(), - parts, - v03.metadata(), - v03.extensions() - ); + v03.artifactId(), + v03.name(), + v03.description(), + parts, + v03.metadata(), + v03.extensions()); } /** @@ -67,17 +68,17 @@ default Artifact_v0_3 fromV10(Artifact v10) { return null; } - List> parts = v10.parts().stream() - .map(PartMapper_v0_3.INSTANCE::fromV10) - .collect(Collectors.toList()); + List> parts = + v10.parts().stream() + .map(PartMapper_v0_3.INSTANCE::fromV10) + .collect(Collectors.toList()); return new Artifact_v0_3( - v10.artifactId(), - v10.name(), - v10.description(), - parts, - v10.metadata(), - v10.extensions() - ); + v10.artifactId(), + v10.name(), + v10.description(), + parts, + v10.metadata(), + v10.extensions()); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AuthenticationInfoMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AuthenticationInfoMapper_v0_3.java index f0c1106a3..6a320a4c9 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AuthenticationInfoMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/AuthenticationInfoMapper_v0_3.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; import java.util.List; - -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.AuthenticationInfo_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationAuthenticationInfo_v0_3; import org.a2aproject.sdk.spec.AuthenticationInfo; @@ -33,7 +32,8 @@ public interface AuthenticationInfoMapper_v0_3 { /** * Singleton instance accessed via {@link A2AMappers_v0_3} factory. */ - AuthenticationInfoMapper_v0_3 INSTANCE = A2AMappers_v0_3.getMapper(AuthenticationInfoMapper_v0_3.class); + AuthenticationInfoMapper_v0_3 INSTANCE = + A2AMappers_v0_3.getMapper(AuthenticationInfoMapper_v0_3.class); /** * Converts v0.3 AuthenticationInfo to v1.0 AuthenticationInfo. @@ -49,9 +49,8 @@ default AuthenticationInfo toV10(AuthenticationInfo_v0_3 v03) { return null; } - String scheme = (v03.schemes() != null && !v03.schemes().isEmpty()) - ? v03.schemes().get(0) - : ""; + String scheme = + (v03.schemes() != null && !v03.schemes().isEmpty()) ? v03.schemes().get(0) : ""; return new AuthenticationInfo(scheme, v03.credentials()); } @@ -71,9 +70,8 @@ default AuthenticationInfo toV10FromPushNotification( return null; } - String scheme = (v03.schemes() != null && !v03.schemes().isEmpty()) - ? v03.schemes().get(0) - : ""; + String scheme = + (v03.schemes() != null && !v03.schemes().isEmpty()) ? v03.schemes().get(0) : ""; return new AuthenticationInfo(scheme, v03.credentials()); } @@ -91,10 +89,7 @@ default AuthenticationInfo_v0_3 fromV10(AuthenticationInfo v10) { return null; } - return new AuthenticationInfo_v0_3( - List.of(v10.scheme()), - v10.credentials() - ); + return new AuthenticationInfo_v0_3(List.of(v10.scheme()), v10.credentials()); } /** @@ -112,8 +107,6 @@ default PushNotificationAuthenticationInfo_v0_3 fromV10ToPushNotification( } return new PushNotificationAuthenticationInfo_v0_3( - List.of(v10.scheme()), - v10.credentials() - ); + List.of(v10.scheme()), v10.credentials()); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/EventKindMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/EventKindMapper_v0_3.java index e8fffde04..4082f2497 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/EventKindMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/EventKindMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.EventKind_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; @@ -28,12 +28,14 @@ *

* Uses instanceof dispatch to determine the concrete type and delegates to the appropriate mapper. */ -@Mapper(config = A03ToV10MapperConfig.class, uses = { - TaskMapper_v0_3.class, - MessageMapper_v0_3.class, - TaskStatusUpdateEventMapper_v0_3.class, - TaskArtifactUpdateEventMapper_v0_3.class -}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = { + TaskMapper_v0_3.class, + MessageMapper_v0_3.class, + TaskStatusUpdateEventMapper_v0_3.class, + TaskArtifactUpdateEventMapper_v0_3.class + }) public interface EventKindMapper_v0_3 { /** @@ -65,7 +67,8 @@ default EventKind toV10(EventKind_v0_3 v03) { return TaskArtifactUpdateEventMapper_v0_3.INSTANCE.toV10(v03ArtifactUpdate); } - throw new InvalidRequestError(null, "Unrecognized EventKind type: " + v03.getClass().getName(), null); + throw new InvalidRequestError( + null, "Unrecognized EventKind type: " + v03.getClass().getName(), null); } /** @@ -92,6 +95,7 @@ default EventKind_v0_3 fromV10(EventKind v10) { return TaskArtifactUpdateEventMapper_v0_3.INSTANCE.fromV10(v10ArtifactUpdate); } - throw new InvalidRequestError(null, "Unrecognized EventKind type: " + v10.getClass().getName(), null); + throw new InvalidRequestError( + null, "Unrecognized EventKind type: " + v10.getClass().getName(), null); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3.java index a39f840cf..97730e7b3 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.FileContent_v0_3; import org.a2aproject.sdk.compat03.spec.FileWithBytes_v0_3; import org.a2aproject.sdk.compat03.spec.FileWithUri_v0_3; @@ -58,7 +58,8 @@ default FileContent toV10(FileContent_v0_3 v03) { return new FileWithUri(v03Uri.mimeType(), name, v03Uri.uri()); } - throw new InvalidRequestError(null, "Unrecognized FileContent type: " + v03.getClass().getName(), null); + throw new InvalidRequestError( + null, "Unrecognized FileContent type: " + v03.getClass().getName(), null); } /** @@ -76,19 +77,12 @@ default FileContent_v0_3 fromV10(FileContent v10) { } if (v10 instanceof FileWithBytes v10Bytes) { - return new FileWithBytes_v0_3( - v10Bytes.mimeType(), - v10Bytes.name(), - v10Bytes.bytes() - ); + return new FileWithBytes_v0_3(v10Bytes.mimeType(), v10Bytes.name(), v10Bytes.bytes()); } else if (v10 instanceof FileWithUri v10Uri) { - return new FileWithUri_v0_3( - v10Uri.mimeType(), - v10Uri.name(), - v10Uri.uri() - ); + return new FileWithUri_v0_3(v10Uri.mimeType(), v10Uri.name(), v10Uri.uri()); } - throw new InvalidRequestError(null, "Unrecognized FileContent type: " + v10.getClass().getName(), null); + throw new InvalidRequestError( + null, "Unrecognized FileContent type: " + v10.getClass().getName(), null); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/MessageMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/MessageMapper_v0_3.java index b9973bc3f..25402d09d 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/MessageMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/MessageMapper_v0_3.java @@ -2,9 +2,8 @@ import java.util.List; import java.util.stream.Collectors; - -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3; import org.a2aproject.sdk.spec.Message; @@ -24,7 +23,9 @@ *

* Uses {@link RoleMapper_v0_3} and {@link PartMapper_v0_3} for nested conversions. */ -@Mapper(config = A03ToV10MapperConfig.class, uses = {RoleMapper_v0_3.class, PartMapper_v0_3.class}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = {RoleMapper_v0_3.class, PartMapper_v0_3.class}) public interface MessageMapper_v0_3 { /** @@ -47,20 +48,20 @@ default Message toV10(Message_v0_3 v03) { } Message.Role role = RoleMapper_v0_3.INSTANCE.toV10(v03.role()); - List> parts = v03.parts().stream() - .map(PartMapper_v0_3.INSTANCE::toV10) - .collect(Collectors.toList()); + List> parts = + v03.parts().stream() + .map(PartMapper_v0_3.INSTANCE::toV10) + .collect(Collectors.toList()); return new Message( - role, - parts, - v03.messageId(), - v03.contextId(), - v03.taskId(), - v03.referenceTaskIds(), - v03.metadata(), - v03.extensions() - ); + role, + parts, + v03.messageId(), + v03.contextId(), + v03.taskId(), + v03.referenceTaskIds(), + v03.metadata(), + v03.extensions()); } /** @@ -78,19 +79,19 @@ default Message_v0_3 fromV10(Message v10) { } Message_v0_3.Role role = RoleMapper_v0_3.INSTANCE.fromV10(v10.role()); - List> parts = v10.parts().stream() - .map(PartMapper_v0_3.INSTANCE::fromV10) - .collect(Collectors.toList()); + List> parts = + v10.parts().stream() + .map(PartMapper_v0_3.INSTANCE::fromV10) + .collect(Collectors.toList()); return new Message_v0_3( - role, - parts, - v10.messageId(), - v10.contextId(), - v10.taskId(), - v10.referenceTaskIds(), - v10.metadata(), - v10.extensions() - ); + role, + parts, + v10.messageId(), + v10.contextId(), + v10.taskId(), + v10.referenceTaskIds(), + v10.metadata(), + v10.extensions()); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/PartMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/PartMapper_v0_3.java index 3cfa66a69..aa401aa09 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/PartMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/PartMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.DataPart_v0_3; import org.a2aproject.sdk.compat03.spec.FilePart_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3; @@ -57,14 +57,13 @@ default Part toV10(Part_v0_3 v03) { return new TextPart(v03Text.text(), v03Text.metadata()); } else if (v03 instanceof FilePart_v0_3 v03File) { return new FilePart( - FileContentMapper_v0_3.INSTANCE.toV10(v03File.file()), - v03File.metadata() - ); + FileContentMapper_v0_3.INSTANCE.toV10(v03File.file()), v03File.metadata()); } else if (v03 instanceof DataPart_v0_3 v03Data) { return new DataPart(v03Data.data(), v03Data.metadata()); } - throw new InvalidRequestError(null, "Unrecognized Part type: " + v03.getClass().getName(), null); + throw new InvalidRequestError( + null, "Unrecognized Part type: " + v03.getClass().getName(), null); } /** @@ -85,20 +84,20 @@ default Part_v0_3 fromV10(Part v10) { return new TextPart_v0_3(v10Text.text(), v10Text.metadata()); } else if (v10 instanceof FilePart v10File) { return new FilePart_v0_3( - FileContentMapper_v0_3.INSTANCE.fromV10(v10File.file()), - v10File.metadata() - ); + FileContentMapper_v0_3.INSTANCE.fromV10(v10File.file()), v10File.metadata()); } else if (v10 instanceof DataPart v10Data) { // v1.0 DataPart.data() returns Object, but v0.3 expects Map Object data = v10Data.data(); if (!(data instanceof java.util.Map)) { - throw new InvalidRequestError(null, "DataPart data must be a Map for v0.3 compatibility", null); + throw new InvalidRequestError( + null, "DataPart data must be a Map for v0.3 compatibility", null); } @SuppressWarnings("unchecked") java.util.Map dataMap = (java.util.Map) data; return new DataPart_v0_3(dataMap, v10Data.metadata()); } - throw new InvalidRequestError(null, "Unrecognized Part type: " + v10.getClass().getName(), null); + throw new InvalidRequestError( + null, "Unrecognized Part type: " + v10.getClass().getName(), null); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/RoleMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/RoleMapper_v0_3.java index 2ae2dc733..3420dde9b 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/RoleMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/RoleMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.spec.Message; import org.mapstruct.Mapper; diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/StreamingEventKindMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/StreamingEventKindMapper_v0_3.java index 8a3fe90df..a743472ca 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/StreamingEventKindMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/StreamingEventKindMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; @@ -31,18 +31,21 @@ * Note: The same types implement both {@link org.a2aproject.sdk.spec.EventKind} and * {@link StreamingEventKind}, so this mapper uses the same delegation logic as {@link EventKindMapper_v0_3}. */ -@Mapper(config = A03ToV10MapperConfig.class, uses = { - TaskMapper_v0_3.class, - MessageMapper_v0_3.class, - TaskStatusUpdateEventMapper_v0_3.class, - TaskArtifactUpdateEventMapper_v0_3.class -}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = { + TaskMapper_v0_3.class, + MessageMapper_v0_3.class, + TaskStatusUpdateEventMapper_v0_3.class, + TaskArtifactUpdateEventMapper_v0_3.class + }) public interface StreamingEventKindMapper_v0_3 { /** * Singleton instance accessed via {@link A2AMappers_v0_3} factory. */ - StreamingEventKindMapper_v0_3 INSTANCE = A2AMappers_v0_3.getMapper(StreamingEventKindMapper_v0_3.class); + StreamingEventKindMapper_v0_3 INSTANCE = + A2AMappers_v0_3.getMapper(StreamingEventKindMapper_v0_3.class); /** * Converts v0.3 StreamingEventKind to v1.0 StreamingEventKind. @@ -68,7 +71,8 @@ default StreamingEventKind toV10(StreamingEventKind_v0_3 v03) { return TaskArtifactUpdateEventMapper_v0_3.INSTANCE.toV10(v03ArtifactUpdate); } - throw new InvalidRequestError(null, "Unrecognized StreamingEventKind type: " + v03.getClass().getName(), null); + throw new InvalidRequestError( + null, "Unrecognized StreamingEventKind type: " + v03.getClass().getName(), null); } /** @@ -95,6 +99,7 @@ default StreamingEventKind_v0_3 fromV10(StreamingEventKind v10) { return TaskArtifactUpdateEventMapper_v0_3.INSTANCE.fromV10(v10ArtifactUpdate); } - throw new InvalidRequestError(null, "Unrecognized StreamingEventKind type: " + v10.getClass().getName(), null); + throw new InvalidRequestError( + null, "Unrecognized StreamingEventKind type: " + v10.getClass().getName(), null); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskArtifactUpdateEventMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskArtifactUpdateEventMapper_v0_3.java index c4df3c83c..d944892bd 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskArtifactUpdateEventMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskArtifactUpdateEventMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; import org.a2aproject.sdk.spec.TaskArtifactUpdateEvent; import org.mapstruct.Mapper; @@ -18,13 +18,16 @@ * Both versions have the same structure: * {@code TaskArtifactUpdateEvent(taskId, artifact, contextId, append, lastChunk, metadata)}. */ -@Mapper(config = A03ToV10MapperConfig.class, uses = {ArtifactMapper_v0_3.class}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = {ArtifactMapper_v0_3.class}) public interface TaskArtifactUpdateEventMapper_v0_3 { /** * Singleton instance accessed via {@link A2AMappers_v0_3} factory. */ - TaskArtifactUpdateEventMapper_v0_3 INSTANCE = A2AMappers_v0_3.getMapper(TaskArtifactUpdateEventMapper_v0_3.class); + TaskArtifactUpdateEventMapper_v0_3 INSTANCE = + A2AMappers_v0_3.getMapper(TaskArtifactUpdateEventMapper_v0_3.class); /** * Converts v0.3 TaskArtifactUpdateEvent to v1.0 TaskArtifactUpdateEvent. @@ -40,13 +43,12 @@ default TaskArtifactUpdateEvent toV10(TaskArtifactUpdateEvent_v0_3 v03) { } return new TaskArtifactUpdateEvent( - v03.taskId(), - ArtifactMapper_v0_3.INSTANCE.toV10(v03.artifact()), - v03.contextId(), - v03.append(), - v03.lastChunk(), - v03.metadata() - ); + v03.taskId(), + ArtifactMapper_v0_3.INSTANCE.toV10(v03.artifact()), + v03.contextId(), + v03.append(), + v03.lastChunk(), + v03.metadata()); } /** @@ -63,12 +65,11 @@ default TaskArtifactUpdateEvent_v0_3 fromV10(TaskArtifactUpdateEvent v10) { } return new TaskArtifactUpdateEvent_v0_3( - v10.taskId(), - ArtifactMapper_v0_3.INSTANCE.fromV10(v10.artifact()), - v10.contextId(), - v10.append(), - v10.lastChunk(), - v10.metadata() - ); + v10.taskId(), + ArtifactMapper_v0_3.INSTANCE.fromV10(v10.artifact()), + v10.contextId(), + v10.append(), + v10.lastChunk(), + v10.metadata()); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3.java index 6c9dd259f..7f8183b6e 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3.java @@ -2,9 +2,8 @@ import java.util.List; import java.util.stream.Collectors; - -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.Artifact_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.Task_v0_3; @@ -31,7 +30,9 @@ *

  • {@link Message} history list via {@link MessageMapper_v0_3}
  • * */ -@Mapper(config = A03ToV10MapperConfig.class, uses = {TaskStatusMapper_v0_3.class, ArtifactMapper_v0_3.class, MessageMapper_v0_3.class}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = {TaskStatusMapper_v0_3.class, ArtifactMapper_v0_3.class, MessageMapper_v0_3.class}) public interface TaskMapper_v0_3 { /** @@ -52,26 +53,27 @@ default Task toV10(Task_v0_3 v03) { return null; } - List artifacts = v03.artifacts() != null - ? v03.artifacts().stream() - .map(ArtifactMapper_v0_3.INSTANCE::toV10) - .collect(Collectors.toList()) - : null; + List artifacts = + v03.artifacts() != null + ? v03.artifacts().stream() + .map(ArtifactMapper_v0_3.INSTANCE::toV10) + .collect(Collectors.toList()) + : null; - List history = v03.history() != null - ? v03.history().stream() - .map(MessageMapper_v0_3.INSTANCE::toV10) - .collect(Collectors.toList()) - : null; + List history = + v03.history() != null + ? v03.history().stream() + .map(MessageMapper_v0_3.INSTANCE::toV10) + .collect(Collectors.toList()) + : null; return new Task( - v03.id(), - v03.contextId(), - TaskStatusMapper_v0_3.INSTANCE.toV10(v03.status()), - artifacts, - history, - v03.metadata() - ); + v03.id(), + v03.contextId(), + TaskStatusMapper_v0_3.INSTANCE.toV10(v03.status()), + artifacts, + history, + v03.metadata()); } /** @@ -87,25 +89,26 @@ default Task_v0_3 fromV10(Task v10) { return null; } - List artifacts = v10.artifacts() != null - ? v10.artifacts().stream() - .map(ArtifactMapper_v0_3.INSTANCE::fromV10) - .collect(Collectors.toList()) - : null; + List artifacts = + v10.artifacts() != null + ? v10.artifacts().stream() + .map(ArtifactMapper_v0_3.INSTANCE::fromV10) + .collect(Collectors.toList()) + : null; - List history = v10.history() != null - ? v10.history().stream() - .map(MessageMapper_v0_3.INSTANCE::fromV10) - .collect(Collectors.toList()) - : null; + List history = + v10.history() != null + ? v10.history().stream() + .map(MessageMapper_v0_3.INSTANCE::fromV10) + .collect(Collectors.toList()) + : null; return new Task_v0_3( - v10.id(), - v10.contextId(), - TaskStatusMapper_v0_3.INSTANCE.fromV10(v10.status()), - artifacts, - history, - v10.metadata() - ); + v10.id(), + v10.contextId(), + TaskStatusMapper_v0_3.INSTANCE.fromV10(v10.status()), + artifacts, + history, + v10.metadata()); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskPushNotificationConfigMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskPushNotificationConfigMapper_v0_3.java index 0b9534a30..4744eed35 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskPushNotificationConfigMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskPushNotificationConfigMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; @@ -22,13 +22,16 @@ *
  • v1.0 → v0.3: Nest url/token/authentication/id into {@code PushNotificationConfig}, drop tenant field
  • * */ -@Mapper(config = A03ToV10MapperConfig.class, uses = {AuthenticationInfoMapper_v0_3.class}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = {AuthenticationInfoMapper_v0_3.class}) public interface TaskPushNotificationConfigMapper_v0_3 { /** * Singleton instance accessed via {@link A2AMappers_v0_3} factory. */ - TaskPushNotificationConfigMapper_v0_3 INSTANCE = A2AMappers_v0_3.getMapper(TaskPushNotificationConfigMapper_v0_3.class); + TaskPushNotificationConfigMapper_v0_3 INSTANCE = + A2AMappers_v0_3.getMapper(TaskPushNotificationConfigMapper_v0_3.class); /** * Converts v0.3 TaskPushNotificationConfig to v1.0 TaskPushNotificationConfig. @@ -38,8 +41,7 @@ public interface TaskPushNotificationConfigMapper_v0_3 { * @param v03 the v0.3 task push notification config * @return the equivalent v1.0 task push notification config */ - default TaskPushNotificationConfig toV10( - TaskPushNotificationConfig_v0_3 v03) { + default TaskPushNotificationConfig toV10(TaskPushNotificationConfig_v0_3 v03) { if (v03 == null) { return null; } @@ -50,13 +52,14 @@ default TaskPushNotificationConfig toV10( String id = pushConfig.id() != null ? pushConfig.id() : ""; return new TaskPushNotificationConfig( - id, - v03.taskId(), - pushConfig.url(), - pushConfig.token(), - AuthenticationInfoMapper_v0_3.INSTANCE.toV10FromPushNotification(pushConfig.authentication()), - "" // Default tenant - ); + id, + v03.taskId(), + pushConfig.url(), + pushConfig.token(), + AuthenticationInfoMapper_v0_3.INSTANCE.toV10FromPushNotification( + pushConfig.authentication()), + "" // Default tenant + ); } /** @@ -67,23 +70,19 @@ default TaskPushNotificationConfig toV10( * @param v10 the v1.0 task push notification config * @return the equivalent v0.3 task push notification config */ - default TaskPushNotificationConfig_v0_3 fromV10( - TaskPushNotificationConfig v10) { + default TaskPushNotificationConfig_v0_3 fromV10(TaskPushNotificationConfig v10) { if (v10 == null) { return null; } PushNotificationConfig_v0_3 pushConfig = - new PushNotificationConfig_v0_3( - v10.url(), - v10.token(), - AuthenticationInfoMapper_v0_3.INSTANCE.fromV10ToPushNotification(v10.authentication()), - v10.id() - ); + new PushNotificationConfig_v0_3( + v10.url(), + v10.token(), + AuthenticationInfoMapper_v0_3.INSTANCE.fromV10ToPushNotification( + v10.authentication()), + v10.id()); - return new TaskPushNotificationConfig_v0_3( - v10.taskId(), - pushConfig - ); + return new TaskPushNotificationConfig_v0_3(v10.taskId(), pushConfig); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStateMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStateMapper_v0_3.java index 82a5faf4b..e02c50e89 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStateMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStateMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; import org.a2aproject.sdk.spec.TaskState; import org.mapstruct.Mapper; diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusMapper_v0_3.java index f72aafcf7..b66c27b15 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; @@ -21,7 +21,9 @@ *
  • Preserving the timestamp field (same type in both versions)
  • * */ -@Mapper(config = A03ToV10MapperConfig.class, uses = {TaskStateMapper_v0_3.class, MessageMapper_v0_3.class}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = {TaskStateMapper_v0_3.class, MessageMapper_v0_3.class}) public interface TaskStatusMapper_v0_3 { /** @@ -43,10 +45,9 @@ default TaskStatus toV10(TaskStatus_v0_3 v03) { } return new TaskStatus( - TaskStateMapper_v0_3.INSTANCE.toV10(v03.state()), - MessageMapper_v0_3.INSTANCE.toV10(v03.message()), - v03.timestamp() - ); + TaskStateMapper_v0_3.INSTANCE.toV10(v03.state()), + MessageMapper_v0_3.INSTANCE.toV10(v03.message()), + v03.timestamp()); } /** @@ -63,9 +64,8 @@ default TaskStatus_v0_3 fromV10(TaskStatus v10) { } return new TaskStatus_v0_3( - TaskStateMapper_v0_3.INSTANCE.fromV10(v10.state()), - MessageMapper_v0_3.INSTANCE.fromV10(v10.message()), - v10.timestamp() - ); + TaskStateMapper_v0_3.INSTANCE.fromV10(v10.state()), + MessageMapper_v0_3.INSTANCE.fromV10(v10.message()), + v10.timestamp()); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3.java index 92c6172ba..3aa1862d6 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; import org.a2aproject.sdk.spec.TaskStatusUpdateEvent; import org.mapstruct.Mapper; @@ -18,13 +18,16 @@ * Both versions have the same structure: * {@code TaskStatusUpdateEvent(taskId, status, contextId, isFinal, metadata)}. */ -@Mapper(config = A03ToV10MapperConfig.class, uses = {TaskStatusMapper_v0_3.class}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = {TaskStatusMapper_v0_3.class}) public interface TaskStatusUpdateEventMapper_v0_3 { /** * Singleton instance accessed via {@link A2AMappers_v0_3} factory. */ - TaskStatusUpdateEventMapper_v0_3 INSTANCE = A2AMappers_v0_3.getMapper(TaskStatusUpdateEventMapper_v0_3.class); + TaskStatusUpdateEventMapper_v0_3 INSTANCE = + A2AMappers_v0_3.getMapper(TaskStatusUpdateEventMapper_v0_3.class); /** * Converts v0.3 TaskStatusUpdateEvent to v1.0 TaskStatusUpdateEvent. @@ -40,11 +43,10 @@ default TaskStatusUpdateEvent toV10(TaskStatusUpdateEvent_v0_3 v03) { } return new TaskStatusUpdateEvent( - v03.taskId(), - TaskStatusMapper_v0_3.INSTANCE.toV10(v03.status()), - v03.contextId(), - v03.metadata() - ); + v03.taskId(), + TaskStatusMapper_v0_3.INSTANCE.toV10(v03.status()), + v03.contextId(), + v03.metadata()); } /** @@ -61,11 +63,10 @@ default TaskStatusUpdateEvent_v0_3 fromV10(TaskStatusUpdateEvent v10) { } return new TaskStatusUpdateEvent_v0_3( - v10.taskId(), - TaskStatusMapper_v0_3.INSTANCE.fromV10(v10.status()), - v10.contextId(), - v10.isFinal(), - v10.metadata() - ); + v10.taskId(), + TaskStatusMapper_v0_3.INSTANCE.fromV10(v10.status()), + v10.contextId(), + v10.isFinal(), + v10.metadata()); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/CancelTaskParamsMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/CancelTaskParamsMapper_v0_3.java index 67ee7a597..ac26d17ea 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/CancelTaskParamsMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/CancelTaskParamsMapper_v0_3.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.compat03.conversion.mappers.params; import java.util.Collections; - -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.spec.CancelTaskParams; import org.mapstruct.Mapper; @@ -32,7 +31,8 @@ public interface CancelTaskParamsMapper_v0_3 { /** * Singleton instance accessed via {@link A2AMappers_v0_3} factory. */ - CancelTaskParamsMapper_v0_3 INSTANCE = A2AMappers_v0_3.getMapper(CancelTaskParamsMapper_v0_3.class); + CancelTaskParamsMapper_v0_3 INSTANCE = + A2AMappers_v0_3.getMapper(CancelTaskParamsMapper_v0_3.class); /** * Converts v0.3 {@code TaskIdParams} to v1.0 {@code CancelTaskParams}. @@ -48,10 +48,9 @@ default CancelTaskParams toV10(TaskIdParams_v0_3 v03) { return null; } return new CancelTaskParams( - v03.id(), - "", // Default tenant - v03.metadata() != null ? v03.metadata() : Collections.emptyMap() - ); + v03.id(), + "", // Default tenant + v03.metadata() != null ? v03.metadata() : Collections.emptyMap()); } /** diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java index c1228087e..c1d4b5d54 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendConfigurationMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.params; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskPushNotificationConfigMapper_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; @@ -25,13 +25,16 @@ *
  • {@code PushNotificationConfig} wraps to {@code TaskPushNotificationConfig} with empty taskId
  • * */ -@Mapper(config = A03ToV10MapperConfig.class, uses = {TaskPushNotificationConfigMapper_v0_3.class}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = {TaskPushNotificationConfigMapper_v0_3.class}) public interface MessageSendConfigurationMapper_v0_3 { /** * Singleton instance accessed via {@link A2AMappers_v0_3} factory. */ - MessageSendConfigurationMapper_v0_3 INSTANCE = A2AMappers_v0_3.getMapper(MessageSendConfigurationMapper_v0_3.class); + MessageSendConfigurationMapper_v0_3 INSTANCE = + A2AMappers_v0_3.getMapper(MessageSendConfigurationMapper_v0_3.class); /** * Converts v0.3 MessageSendConfiguration to v1.0 MessageSendConfiguration. @@ -42,8 +45,7 @@ public interface MessageSendConfigurationMapper_v0_3 { * @param v03 the v0.3 message send configuration * @return the equivalent v1.0 message send configuration */ - default MessageSendConfiguration toV10( - MessageSendConfiguration_v0_3 v03) { + default MessageSendConfiguration toV10(MessageSendConfiguration_v0_3 v03) { if (v03 == null) { return null; } @@ -53,10 +55,9 @@ default MessageSendConfiguration toV10( if (v03.pushNotificationConfig() != null) { // Wrap the push notification config with an empty taskId (will be set by the server) TaskPushNotificationConfig_v0_3 v03TaskConfig = - new TaskPushNotificationConfig_v0_3( - "", // Empty taskId - will be populated by server - v03.pushNotificationConfig() - ); + new TaskPushNotificationConfig_v0_3( + "", // Empty taskId - will be populated by server + v03.pushNotificationConfig()); taskPushConfig = TaskPushNotificationConfigMapper_v0_3.INSTANCE.toV10(v03TaskConfig); } @@ -64,11 +65,7 @@ default MessageSendConfiguration toV10( Boolean returnImmediately = v03.blocking() != null ? !v03.blocking() : null; return new MessageSendConfiguration( - v03.acceptedOutputModes(), - v03.historyLength(), - taskPushConfig, - returnImmediately - ); + v03.acceptedOutputModes(), v03.historyLength(), taskPushConfig, returnImmediately); } /** @@ -80,8 +77,7 @@ default MessageSendConfiguration toV10( * @param v10 the v1.0 message send configuration * @return the equivalent v0.3 message send configuration */ - default MessageSendConfiguration_v0_3 fromV10( - MessageSendConfiguration v10) { + default MessageSendConfiguration_v0_3 fromV10(MessageSendConfiguration v10) { if (v10 == null) { return null; } @@ -90,7 +86,8 @@ default MessageSendConfiguration_v0_3 fromV10( PushNotificationConfig_v0_3 pushConfig = null; if (v10.taskPushNotificationConfig() != null) { TaskPushNotificationConfig_v0_3 v03TaskConfig = - TaskPushNotificationConfigMapper_v0_3.INSTANCE.fromV10(v10.taskPushNotificationConfig()); + TaskPushNotificationConfigMapper_v0_3.INSTANCE.fromV10( + v10.taskPushNotificationConfig()); pushConfig = v03TaskConfig.pushNotificationConfig(); } @@ -98,10 +95,6 @@ default MessageSendConfiguration_v0_3 fromV10( Boolean blocking = v10.returnImmediately() != null ? !v10.returnImmediately() : null; return new MessageSendConfiguration_v0_3( - v10.acceptedOutputModes(), - v10.historyLength(), - pushConfig, - blocking - ); + v10.acceptedOutputModes(), v10.historyLength(), pushConfig, blocking); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3.java index 862b4e0c8..4ee5ba155 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/MessageSendParamsMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.params; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.domain.MessageMapper_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; import org.a2aproject.sdk.spec.MessageSendParams; @@ -24,13 +24,16 @@ *

    * Uses {@link MessageMapper_v0_3} and {@link MessageSendConfigurationMapper_v0_3} for nested conversions. */ -@Mapper(config = A03ToV10MapperConfig.class, uses = {MessageMapper_v0_3.class, MessageSendConfigurationMapper_v0_3.class}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = {MessageMapper_v0_3.class, MessageSendConfigurationMapper_v0_3.class}) public interface MessageSendParamsMapper_v0_3 { /** * Singleton instance accessed via {@link A2AMappers_v0_3} factory. */ - MessageSendParamsMapper_v0_3 INSTANCE = A2AMappers_v0_3.getMapper(MessageSendParamsMapper_v0_3.class); + MessageSendParamsMapper_v0_3 INSTANCE = + A2AMappers_v0_3.getMapper(MessageSendParamsMapper_v0_3.class); /** * Converts v0.3 MessageSendParams to v1.0 MessageSendParams. @@ -46,11 +49,11 @@ default MessageSendParams toV10(MessageSendParams_v0_3 v03) { } return new MessageSendParams( - MessageMapper_v0_3.INSTANCE.toV10(v03.message()), - MessageSendConfigurationMapper_v0_3.INSTANCE.toV10(v03.configuration()), - v03.metadata(), - "" // Default tenant - ); + MessageMapper_v0_3.INSTANCE.toV10(v03.message()), + MessageSendConfigurationMapper_v0_3.INSTANCE.toV10(v03.configuration()), + v03.metadata(), + "" // Default tenant + ); } /** @@ -67,9 +70,8 @@ default MessageSendParams_v0_3 fromV10(MessageSendParams v10) { } return new MessageSendParams_v0_3( - MessageMapper_v0_3.INSTANCE.fromV10(v10.message()), - MessageSendConfigurationMapper_v0_3.INSTANCE.fromV10(v10.configuration()), - v10.metadata() - ); + MessageMapper_v0_3.INSTANCE.fromV10(v10.message()), + MessageSendConfigurationMapper_v0_3.INSTANCE.fromV10(v10.configuration()), + v10.metadata()); } } diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskIdParamsMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskIdParamsMapper_v0_3.java index b8ae86dec..912c88c83 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskIdParamsMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskIdParamsMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.params; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.spec.TaskIdParams; import org.mapstruct.Mapper; diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskQueryParamsMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskQueryParamsMapper_v0_3.java index 31357b42e..b4ef9387d 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskQueryParamsMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/params/TaskQueryParamsMapper_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.conversion.mappers.params; -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.spec.TaskQueryParams; import org.mapstruct.Mapper; @@ -42,7 +42,8 @@ public interface TaskQueryParamsMapper_v0_3 { /** * Singleton instance accessed via {@link A2AMappers_v0_3} factory. */ - TaskQueryParamsMapper_v0_3 INSTANCE = A2AMappers_v0_3.getMapper(TaskQueryParamsMapper_v0_3.class); + TaskQueryParamsMapper_v0_3 INSTANCE = + A2AMappers_v0_3.getMapper(TaskQueryParamsMapper_v0_3.class); /** * Converts v0.3 {@code TaskQueryParams} to v1.0 {@code TaskQueryParams}. diff --git a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/result/ListTaskPushNotificationConfigsResultMapper_v0_3.java b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/result/ListTaskPushNotificationConfigsResultMapper_v0_3.java index 95320f8c8..d0f5d9df6 100644 --- a/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/result/ListTaskPushNotificationConfigsResultMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/main/java/org/a2aproject/sdk/compat03/conversion/mappers/result/ListTaskPushNotificationConfigsResultMapper_v0_3.java @@ -2,9 +2,8 @@ import java.util.List; import java.util.stream.Collectors; - -import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.config.A03ToV10MapperConfig; +import org.a2aproject.sdk.compat03.conversion.mappers.config.A2AMappers_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskPushNotificationConfigMapper_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsResult; @@ -27,13 +26,16 @@ *

  • v1.0 → v0.3: Extract the configs list (discard nextPageToken as 0.3 doesn't support pagination)
  • * */ -@Mapper(config = A03ToV10MapperConfig.class, uses = {TaskPushNotificationConfigMapper_v0_3.class}) +@Mapper( + config = A03ToV10MapperConfig.class, + uses = {TaskPushNotificationConfigMapper_v0_3.class}) public interface ListTaskPushNotificationConfigsResultMapper_v0_3 { /** * Singleton instance accessed via {@link A2AMappers_v0_3} factory. */ - ListTaskPushNotificationConfigsResultMapper_v0_3 INSTANCE = A2AMappers_v0_3.getMapper(ListTaskPushNotificationConfigsResultMapper_v0_3.class); + ListTaskPushNotificationConfigsResultMapper_v0_3 INSTANCE = + A2AMappers_v0_3.getMapper(ListTaskPushNotificationConfigsResultMapper_v0_3.class); /** * Converts v0.3 {@code List} to v1.0 {@link ListTaskPushNotificationConfigsResult}. @@ -50,9 +52,10 @@ default ListTaskPushNotificationConfigsResult toV10( return null; } - List v10Configs = v03List.stream() - .map(TaskPushNotificationConfigMapper_v0_3.INSTANCE::toV10) - .collect(Collectors.toList()); + List v10Configs = + v03List.stream() + .map(TaskPushNotificationConfigMapper_v0_3.INSTANCE::toV10) + .collect(Collectors.toList()); return new ListTaskPushNotificationConfigsResult(v10Configs, null); } @@ -73,7 +76,7 @@ default List fromV10( } return v10Result.configs().stream() - .map(TaskPushNotificationConfigMapper_v0_3.INSTANCE::fromV10) - .collect(Collectors.toList()); + .map(TaskPushNotificationConfigMapper_v0_3.INSTANCE::fromV10) + .collect(Collectors.toList()); } } diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2ARequestHandlerTest_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2ARequestHandlerTest_v0_3.java index 4c4e8320c..52410869c 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2ARequestHandlerTest_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2ARequestHandlerTest_v0_3.java @@ -1,5 +1,7 @@ package org.a2aproject.sdk.compat03.conversion; +import io.quarkus.arc.profile.IfBuildProfile; +import jakarta.enterprise.context.Dependent; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -10,12 +12,16 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.function.Consumer; - -import jakarta.enterprise.context.Dependent; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.client.http.ServerSentEvent; +import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; +import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; +import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; +import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; +import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; @@ -36,16 +42,6 @@ import org.a2aproject.sdk.spec.StreamingEventKind; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import io.quarkus.arc.profile.IfBuildProfile; - -// V0.3 imports for test fixtures -import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; -import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; -import org.a2aproject.sdk.compat03.spec.Message_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; -import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; /** * Base test class for v0.3 transport handler tests. @@ -72,17 +68,19 @@ public abstract class AbstractA2ARequestHandlerTest_v0_3 { // V0.3 test fixtures (client perspective) protected static final AgentCard_v0_3 CARD = createAgentCard(true, true, true); - protected static final Task_v0_3 MINIMAL_TASK = new Task_v0_3.Builder() - .id("task-123") - .contextId("session-xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .build(); + protected static final Task_v0_3 MINIMAL_TASK = + new Task_v0_3.Builder() + .id("task-123") + .contextId("session-xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .build(); - protected static final Message_v0_3 MESSAGE = new Message_v0_3.Builder() - .messageId("111") - .role(Message_v0_3.Role.AGENT) - .parts(new TextPart_v0_3("test message")) - .build(); + protected static final Message_v0_3 MESSAGE = + new Message_v0_3.Builder() + .messageId("111") + .role(Message_v0_3.Role.AGENT) + .parts(new TextPart_v0_3("test message")) + .build(); // V1.0 backend infrastructure protected AgentExecutor agentExecutor; @@ -104,21 +102,24 @@ public abstract class AbstractA2ARequestHandlerTest_v0_3 { @BeforeEach public void init() { // Create AgentExecutor with lambda injection (v1.0 interface) - agentExecutor = new AgentExecutor() { - @Override - public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2AError { - if (agentExecutorExecute != null) { - agentExecutorExecute.invoke(context, agentEmitter); - } - } + agentExecutor = + new AgentExecutor() { + @Override + public void execute(RequestContext context, AgentEmitter agentEmitter) + throws A2AError { + if (agentExecutorExecute != null) { + agentExecutorExecute.invoke(context, agentEmitter); + } + } - @Override - public void cancel(RequestContext context, AgentEmitter agentEmitter) throws A2AError { - if (agentExecutorCancel != null) { - agentExecutorCancel.invoke(context, agentEmitter); - } - } - }; + @Override + public void cancel(RequestContext context, AgentEmitter agentEmitter) + throws A2AError { + if (agentExecutorCancel != null) { + agentExecutorCancel.invoke(context, agentEmitter); + } + } + }; // Set up v1.0 backend components InMemoryTaskStore inMemoryTaskStore = new InMemoryTaskStore(); @@ -127,12 +128,15 @@ public void cancel(RequestContext context, AgentEmitter agentEmitter) throws A2A // Create push notification components BEFORE MainEventBusProcessor httpClient = new TestHttpClient(); PushNotificationConfigStore pushConfigStore = new InMemoryPushNotificationConfigStore(); - BasePushNotificationSender pushSender = new BasePushNotificationSender(pushConfigStore, httpClient, PushNotificationUrlValidator.ALLOW_ALL); + BasePushNotificationSender pushSender = + new BasePushNotificationSender( + pushConfigStore, httpClient, PushNotificationUrlValidator.ALLOW_ALL); // Create MainEventBus and MainEventBusProcessor (production code path) mainEventBus = new MainEventBus(); queueManager = new InMemoryQueueManager(inMemoryTaskStore, mainEventBus); - mainEventBusProcessor = new MainEventBusProcessor(mainEventBus, taskStore, pushSender, queueManager); + mainEventBusProcessor = + new MainEventBusProcessor(mainEventBus, taskStore, pushSender, queueManager); EventQueueUtil.start(mainEventBusProcessor); // Create v1.0 DefaultRequestHandler @@ -172,19 +176,20 @@ public void cleanup() { * @param stateTransitionHistory whether state transition history is supported * @return configured AgentCard */ - protected static AgentCard_v0_3 createAgentCard(boolean streaming, boolean pushNotifications, - boolean stateTransitionHistory) { + protected static AgentCard_v0_3 createAgentCard( + boolean streaming, boolean pushNotifications, boolean stateTransitionHistory) { return new AgentCard_v0_3.Builder() .name("test-card") .description("A test agent card") .url("http://example.com") .version("1.0") .documentationUrl("http://example.com/docs") - .capabilities(new AgentCapabilities_v0_3.Builder() - .streaming(streaming) - .pushNotifications(pushNotifications) - .stateTransitionHistory(stateTransitionHistory) - .build()) + .capabilities( + new AgentCapabilities_v0_3.Builder() + .streaming(streaming) + .pushNotifications(pushNotifications) + .stateTransitionHistory(stateTransitionHistory) + .build()) .defaultInputModes(new ArrayList<>()) .defaultOutputModes(new ArrayList<>()) .preferredTransport("jsonrpc") @@ -207,7 +212,8 @@ protected interface AgentExecutorMethod { @Dependent @IfBuildProfile("test") protected static class TestHttpClient implements A2AHttpClient { - public final List events = Collections.synchronizedList(new ArrayList<>()); + public final List events = + Collections.synchronizedList(new ArrayList<>()); public volatile CountDownLatch latch; @Override @@ -238,7 +244,8 @@ public PostBuilder body(String body) { public A2AHttpResponse post() throws IOException, InterruptedException { try { // Parse StreamResponse format to extract the streaming event - // The body contains a wrapper with one of: task, message, statusUpdate, artifactUpdate + // The body contains a wrapper with one of: task, message, statusUpdate, + // artifactUpdate StreamingEventKind event = JsonUtil.fromJson(body, StreamingEventKind.class); events.add(event); return new A2AHttpResponse() { @@ -267,9 +274,10 @@ public String body() { } @Override - public CompletableFuture postAsyncSSE(Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) + public CompletableFuture postAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) throws IOException, InterruptedException { return null; } diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerServerTest_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerServerTest_v0_3.java index 6cf582458..3abed8f52 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerServerTest_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerServerTest_v0_3.java @@ -9,6 +9,10 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import io.restassured.RestAssured; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.specification.RequestSpecification; +import jakarta.ws.rs.core.MediaType; import java.io.EOFException; import java.io.IOException; import java.net.URI; @@ -27,15 +31,9 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Stream; - -import jakarta.ws.rs.core.MediaType; - -import io.restassured.RestAssured; -import io.restassured.config.ObjectMapperConfig; -import io.restassured.specification.RequestSpecification; -import org.a2aproject.sdk.compat03.client.Client_v0_3; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; import org.a2aproject.sdk.compat03.client.ClientEvent_v0_3; +import org.a2aproject.sdk.compat03.client.Client_v0_3; import org.a2aproject.sdk.compat03.client.MessageEvent_v0_3; import org.a2aproject.sdk.compat03.client.TaskEvent_v0_3; import org.a2aproject.sdk.compat03.client.TaskUpdateEvent_v0_3; @@ -60,23 +58,23 @@ import org.a2aproject.sdk.compat03.spec.JSONParseError_v0_3; import org.a2aproject.sdk.compat03.spec.JSONRPCErrorResponse_v0_3; import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3; -import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.MethodNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest_v0_3; import org.a2aproject.sdk.compat03.spec.SendStreamingMessageResponse_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingJSONRPCRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; +import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3; @@ -101,35 +99,40 @@ */ public abstract class AbstractA2AServerServerTest_v0_3 { - protected static final Task_v0_3 MINIMAL_TASK = new Task_v0_3.Builder() - .id("task-123") - .contextId("session-xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .build(); - - private static final Task_v0_3 CANCEL_TASK = new Task_v0_3.Builder() - .id("cancel-task-123") - .contextId("session-xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .build(); - - private static final Task_v0_3 CANCEL_TASK_NOT_SUPPORTED = new Task_v0_3.Builder() - .id("cancel-task-not-supported-123") - .contextId("session-xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .build(); - - private static final Task_v0_3 SEND_MESSAGE_NOT_SUPPORTED = new Task_v0_3.Builder() - .id("task-not-supported-123") - .contextId("session-xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .build(); - - protected static final Message_v0_3 MESSAGE = new Message_v0_3.Builder() - .messageId("111") - .role(Message_v0_3.Role.AGENT) - .parts(new TextPart_v0_3("test message")) - .build(); + protected static final Task_v0_3 MINIMAL_TASK = + new Task_v0_3.Builder() + .id("task-123") + .contextId("session-xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .build(); + + private static final Task_v0_3 CANCEL_TASK = + new Task_v0_3.Builder() + .id("cancel-task-123") + .contextId("session-xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .build(); + + private static final Task_v0_3 CANCEL_TASK_NOT_SUPPORTED = + new Task_v0_3.Builder() + .id("cancel-task-not-supported-123") + .contextId("session-xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .build(); + + private static final Task_v0_3 SEND_MESSAGE_NOT_SUPPORTED = + new Task_v0_3.Builder() + .id("task-not-supported-123") + .contextId("session-xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .build(); + + protected static final Message_v0_3 MESSAGE = + new Message_v0_3.Builder() + .messageId("111") + .role(Message_v0_3.Role.AGENT) + .parts(new TextPart_v0_3("test message")) + .build(); public static final String APPLICATION_JSON = "application/json"; @@ -139,8 +142,10 @@ public abstract class AbstractA2AServerServerTest_v0_3 { */ public static RequestSpecification given() { return RestAssured.given() - .config(RestAssured.config() - .objectMapperConfig(new ObjectMapperConfig(V10GsonObjectMapper_v0_3.INSTANCE))); + .config( + RestAssured.config() + .objectMapperConfig( + new ObjectMapperConfig(V10GsonObjectMapper_v0_3.INSTANCE))); } /** @@ -149,8 +154,10 @@ public static RequestSpecification given() { */ public static RequestSpecification givenV03() { return RestAssured.given() - .config(RestAssured.config() - .objectMapperConfig(new ObjectMapperConfig(GsonObjectMapper_v0_3.INSTANCE))); + .config( + RestAssured.config() + .objectMapperConfig( + new ObjectMapperConfig(GsonObjectMapper_v0_3.INSTANCE))); } protected final int serverPort; @@ -268,24 +275,24 @@ public void testCancelTaskNotFound() { @Test public void testSendMessageNewMessageSuccess() throws Exception { - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .build(); + Message_v0_3 message = new Message_v0_3.Builder(MESSAGE).build(); CountDownLatch latch = new CountDownLatch(1); AtomicReference receivedMessage = new AtomicReference<>(); AtomicBoolean wasUnexpectedEvent = new AtomicBoolean(false); - BiConsumer consumer = (event, agentCard) -> { - if (event instanceof MessageEvent_v0_3 messageEvent) { - if (latch.getCount() > 0) { - receivedMessage.set(messageEvent.getMessage()); - latch.countDown(); - } else { - wasUnexpectedEvent.set(true); - } - } else { - wasUnexpectedEvent.set(true); - } - }; + BiConsumer consumer = + (event, agentCard) -> { + if (event instanceof MessageEvent_v0_3 messageEvent) { + if (latch.getCount() > 0) { + receivedMessage.set(messageEvent.getMessage()); + latch.countDown(); + } else { + wasUnexpectedEvent.set(true); + } + } else { + wasUnexpectedEvent.set(true); + } + }; // testing the non-streaming send message getNonStreamingClient().sendMessage(message, List.of(consumer), null); @@ -303,27 +310,34 @@ public void testSendMessageNewMessageSuccess() throws Exception { @Test public void testRequestScopedBeanAvailableOnAgentExecutorThread() throws Exception { - Message_v0_3 message = new Message_v0_3.Builder() - .messageId("request-scoped-test") - .role(Message_v0_3.Role.USER) - .parts(new TextPart_v0_3("request-scoped:test")) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .messageId("request-scoped-test") + .role(Message_v0_3.Role.USER) + .parts(new TextPart_v0_3("request-scoped:test")) + .build(); CountDownLatch latch = new CountDownLatch(1); AtomicReference receivedTask = new AtomicReference<>(); AtomicReference errorRef = new AtomicReference<>(); - getNonStreamingClient().sendMessage(message, List.of((event, agentCard) -> { - if (event instanceof TaskEvent_v0_3 te) { - receivedTask.set(te.getTask()); - if (te.getTask().status().state() == TaskState_v0_3.COMPLETED) { - latch.countDown(); - } - } - }), error -> { - errorRef.set(error); - latch.countDown(); - }); + getNonStreamingClient() + .sendMessage( + message, + List.of( + (event, agentCard) -> { + if (event instanceof TaskEvent_v0_3 te) { + receivedTask.set(te.getTask()); + if (te.getTask().status().state() + == TaskState_v0_3.COMPLETED) { + latch.countDown(); + } + } + }), + error -> { + errorRef.set(error); + latch.countDown(); + }); assertTrue(latch.await(10, TimeUnit.SECONDS), "Request should complete within timeout"); assertNull(errorRef.get(), "Should not have received an error"); @@ -343,26 +357,28 @@ public void testRequestScopedBeanAvailableOnAgentExecutorThread() throws Excepti public void testSendMessageExistingTaskSuccess() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); CountDownLatch latch = new CountDownLatch(1); AtomicReference receivedMessage = new AtomicReference<>(); AtomicBoolean wasUnexpectedEvent = new AtomicBoolean(false); - BiConsumer consumer = (event, agentCard) -> { - if (event instanceof MessageEvent_v0_3 messageEvent) { - if (latch.getCount() > 0) { - receivedMessage.set(messageEvent.getMessage()); - latch.countDown(); - } else { - wasUnexpectedEvent.set(true); - } - } else { - wasUnexpectedEvent.set(true); - } - }; + BiConsumer consumer = + (event, agentCard) -> { + if (event instanceof MessageEvent_v0_3 messageEvent) { + if (latch.getCount() > 0) { + receivedMessage.set(messageEvent.getMessage()); + latch.countDown(); + } else { + wasUnexpectedEvent.set(true); + } + } else { + wasUnexpectedEvent.set(true); + } + }; // testing the non-streaming send message getNonStreamingClient().sendMessage(message, List.of(consumer), null); @@ -388,8 +404,12 @@ public void testSetPushNotificationSuccess() throws Exception { try { TaskPushNotificationConfig_v0_3 taskPushConfig = new TaskPushNotificationConfig_v0_3( - MINIMAL_TASK.id(), new PushNotificationConfig_v0_3.Builder().url("http://example.com").build()); - TaskPushNotificationConfig_v0_3 config = getClient().setTaskPushNotificationConfiguration(taskPushConfig); + MINIMAL_TASK.id(), + new PushNotificationConfig_v0_3.Builder() + .url("http://example.com") + .build()); + TaskPushNotificationConfig_v0_3 config = + getClient().setTaskPushNotificationConfiguration(taskPushConfig); assertEquals(MINIMAL_TASK.id(), config.taskId()); assertEquals("http://example.com", config.pushNotificationConfig().url()); } catch (A2AClientException_v0_3 e) { @@ -406,13 +426,20 @@ public void testGetPushNotificationSuccess() throws Exception { try { TaskPushNotificationConfig_v0_3 taskPushConfig = new TaskPushNotificationConfig_v0_3( - MINIMAL_TASK.id(), new PushNotificationConfig_v0_3.Builder().url("http://example.com").build()); + MINIMAL_TASK.id(), + new PushNotificationConfig_v0_3.Builder() + .url("http://example.com") + .build()); - TaskPushNotificationConfig_v0_3 setResult = getClient().setTaskPushNotificationConfiguration(taskPushConfig); + TaskPushNotificationConfig_v0_3 setResult = + getClient().setTaskPushNotificationConfiguration(taskPushConfig); assertNotNull(setResult); - TaskPushNotificationConfig_v0_3 config = getClient().getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id())); + TaskPushNotificationConfig_v0_3 config = + getClient() + .getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id())); assertEquals(MINIMAL_TASK.id(), config.taskId()); assertEquals("http://example.com", config.pushNotificationConfig().url()); } catch (A2AClientException_v0_3 e) { @@ -427,10 +454,11 @@ public void testGetPushNotificationSuccess() throws Exception { public void testError() throws Exception { saveTaskInTaskStore(SEND_MESSAGE_NOT_SUPPORTED); try { - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .taskId(SEND_MESSAGE_NOT_SUPPORTED.id()) - .contextId(SEND_MESSAGE_NOT_SUPPORTED.contextId()) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE) + .taskId(SEND_MESSAGE_NOT_SUPPORTED.id()) + .contextId(SEND_MESSAGE_NOT_SUPPORTED.contextId()) + .build(); try { getNonStreamingClient().sendMessage(message); @@ -477,48 +505,58 @@ public void testSendMessageStreamExistingTaskSuccess() throws Exception { public void testResubscribeExistingTaskSuccess() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - // attempting to send a streaming message instead of explicitly calling queueManager#createOrTap - // does not work because after the message is sent, the queue becomes null but task resubscription + // attempting to send a streaming message instead of explicitly calling + // queueManager#createOrTap + // does not work because after the message is sent, the queue becomes null but task + // resubscription // requires the queue to still be active ensureQueueForTask(MINIMAL_TASK.id()); CountDownLatch eventLatch = new CountDownLatch(2); - AtomicReference artifactUpdateEvent = new AtomicReference<>(); + AtomicReference artifactUpdateEvent = + new AtomicReference<>(); AtomicReference statusUpdateEvent = new AtomicReference<>(); AtomicBoolean wasUnexpectedEvent = new AtomicBoolean(false); AtomicReference errorRef = new AtomicReference<>(); AtomicBoolean receivedInitialSnapshot = new AtomicBoolean(false); // Create consumer to handle resubscribed events - BiConsumer consumer = (event, agentCard) -> { - if (event instanceof TaskUpdateEvent_v0_3 taskUpdateEvent) { - if (taskUpdateEvent.getUpdateEvent() instanceof TaskArtifactUpdateEvent_v0_3 artifactEvent) { - artifactUpdateEvent.set(artifactEvent); - eventLatch.countDown(); - } else if (taskUpdateEvent.getUpdateEvent() instanceof TaskStatusUpdateEvent_v0_3 statusEvent) { - statusUpdateEvent.set(statusEvent); - eventLatch.countDown(); - } else { - wasUnexpectedEvent.set(true); - } - } else if (event instanceof TaskEvent_v0_3) { - // v0.3 spec confirms task snapshot as first event on resubscribe is valid behavior - // See: https://a2a-protocol.org/v0.3.0/specification/#721-sendstreamingmessageresponse-object - if (!receivedInitialSnapshot.compareAndSet(false, true)) { - wasUnexpectedEvent.set(true); // TaskEvent received after first event - } - } else { - wasUnexpectedEvent.set(true); - } - }; + BiConsumer consumer = + (event, agentCard) -> { + if (event instanceof TaskUpdateEvent_v0_3 taskUpdateEvent) { + if (taskUpdateEvent.getUpdateEvent() + instanceof TaskArtifactUpdateEvent_v0_3 artifactEvent) { + artifactUpdateEvent.set(artifactEvent); + eventLatch.countDown(); + } else if (taskUpdateEvent.getUpdateEvent() + instanceof TaskStatusUpdateEvent_v0_3 statusEvent) { + statusUpdateEvent.set(statusEvent); + eventLatch.countDown(); + } else { + wasUnexpectedEvent.set(true); + } + } else if (event instanceof TaskEvent_v0_3) { + // v0.3 spec confirms task snapshot as first event on resubscribe is + // valid behavior + // See: + // https://a2a-protocol.org/v0.3.0/specification/#721-sendstreamingmessageresponse-object + if (!receivedInitialSnapshot.compareAndSet(false, true)) { + wasUnexpectedEvent.set( + true); // TaskEvent received after first event + } + } else { + wasUnexpectedEvent.set(true); + } + }; // Create error handler - Consumer errorHandler = error -> { - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - eventLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + eventLatch.countDown(); + }; // Count down when the streaming subscription is established CountDownLatch subscriptionLatch = new CountDownLatch(1); @@ -526,27 +564,33 @@ public void testResubscribeExistingTaskSuccess() throws Exception { .whenComplete((unused, throwable) -> subscriptionLatch.countDown()); // Resubscribe to the task with specific consumer and error handler - getClient().resubscribe(new TaskIdParams_v0_3(MINIMAL_TASK.id()), List.of(consumer), errorHandler); + getClient() + .resubscribe( + new TaskIdParams_v0_3(MINIMAL_TASK.id()), + List.of(consumer), + errorHandler); // Wait for subscription to be established assertTrue(subscriptionLatch.await(15, TimeUnit.SECONDS)); // Enqueue events on the server - List events = List.of( - new TaskArtifactUpdateEvent_v0_3.Builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(new Artifact_v0_3.Builder() - .artifactId("11") - .parts(new TextPart_v0_3("text")) - .build()) - .build(), - new TaskStatusUpdateEvent_v0_3.Builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) - .isFinal(true) - .build()); + List events = + List.of( + new TaskArtifactUpdateEvent_v0_3.Builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + new Artifact_v0_3.Builder() + .artifactId("11") + .parts(new TextPart_v0_3("text")) + .build()) + .build(), + new TaskStatusUpdateEvent_v0_3.Builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) + .isFinal(true) + .build()); for (Event_v0_3 event : events) { enqueueEventOnServer(event); @@ -584,19 +628,22 @@ public void testResubscribeNoExistingTaskError() throws Exception { AtomicReference errorRef = new AtomicReference<>(); // Create error handler to capture the TaskNotFoundError - Consumer errorHandler = error -> { - if (error == null) { - // Stream completed successfully - ignore, we're waiting for an error - return; - } - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - errorLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (error == null) { + // Stream completed successfully - ignore, we're waiting for an error + return; + } + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + errorLatch.countDown(); + }; try { - getClient().resubscribe(new TaskIdParams_v0_3("non-existent-task"), List.of(), errorHandler); + getClient() + .resubscribe( + new TaskIdParams_v0_3("non-existent-task"), List.of(), errorHandler); // Wait for error to be captured (may come via error handler for streaming) boolean errorReceived = errorLatch.await(10, TimeUnit.SECONDS); @@ -606,7 +653,9 @@ public void testResubscribeNoExistingTaskError() throws Exception { Throwable error = errorRef.get(); assertNotNull(error); if (error instanceof A2AClientException_v0_3) { - assertInstanceOf(TaskNotFoundError_v0_3.class, ((A2AClientException_v0_3) error).getCause()); + assertInstanceOf( + TaskNotFoundError_v0_3.class, + ((A2AClientException_v0_3) error).getCause()); } else { // Check if it's directly a TaskNotFoundError or walk the cause chain Throwable cause = error; @@ -649,57 +698,73 @@ public void testMainQueueReferenceCountingWithMultipleConsumers() throws Excepti // 2. First consumer subscribes and receives initial event CountDownLatch firstConsumerLatch = new CountDownLatch(1); - AtomicReference firstConsumerEvent = new AtomicReference<>(); + AtomicReference firstConsumerEvent = + new AtomicReference<>(); AtomicBoolean firstUnexpectedEvent = new AtomicBoolean(false); AtomicReference firstErrorRef = new AtomicReference<>(); AtomicBoolean firstReceivedInitialSnapshot = new AtomicBoolean(false); - BiConsumer firstConsumer = (event, agentCard) -> { - if (event instanceof TaskUpdateEvent_v0_3 tue && tue.getUpdateEvent() instanceof TaskArtifactUpdateEvent_v0_3 artifact) { - firstConsumerEvent.set(artifact); - firstConsumerLatch.countDown(); - } else if (event instanceof TaskEvent_v0_3) { - // v0.3 spec confirms task snapshot as first event on resubscribe is valid behavior - // See: https://a2a-protocol.org/v0.3.0/specification/#721-sendstreamingmessageresponse-object - if (!firstReceivedInitialSnapshot.compareAndSet(false, true)) { - firstUnexpectedEvent.set(true); // TaskEvent received after first event - } - } else if (!(event instanceof TaskUpdateEvent_v0_3)) { - firstUnexpectedEvent.set(true); - } - }; + BiConsumer firstConsumer = + (event, agentCard) -> { + if (event instanceof TaskUpdateEvent_v0_3 tue + && tue.getUpdateEvent() + instanceof TaskArtifactUpdateEvent_v0_3 artifact) { + firstConsumerEvent.set(artifact); + firstConsumerLatch.countDown(); + } else if (event instanceof TaskEvent_v0_3) { + // v0.3 spec confirms task snapshot as first event on resubscribe is + // valid behavior + // See: + // https://a2a-protocol.org/v0.3.0/specification/#721-sendstreamingmessageresponse-object + if (!firstReceivedInitialSnapshot.compareAndSet(false, true)) { + firstUnexpectedEvent.set( + true); // TaskEvent received after first event + } + } else if (!(event instanceof TaskUpdateEvent_v0_3)) { + firstUnexpectedEvent.set(true); + } + }; - Consumer firstErrorHandler = error -> { - if (!isStreamClosedError(error)) { - firstErrorRef.set(error); - } - firstConsumerLatch.countDown(); - }; + Consumer firstErrorHandler = + error -> { + if (!isStreamClosedError(error)) { + firstErrorRef.set(error); + } + firstConsumerLatch.countDown(); + }; // Wait for first subscription to be established CountDownLatch firstSubscriptionLatch = new CountDownLatch(1); awaitStreamingSubscription() .whenComplete((unused, throwable) -> firstSubscriptionLatch.countDown()); - getClient().resubscribe(new TaskIdParams_v0_3(MINIMAL_TASK.id()), - List.of(firstConsumer), - firstErrorHandler); + getClient() + .resubscribe( + new TaskIdParams_v0_3(MINIMAL_TASK.id()), + List.of(firstConsumer), + firstErrorHandler); - assertTrue(firstSubscriptionLatch.await(15, TimeUnit.SECONDS), "First subscription should be established"); + assertTrue( + firstSubscriptionLatch.await(15, TimeUnit.SECONDS), + "First subscription should be established"); // Enqueue first event - TaskArtifactUpdateEvent_v0_3 event1 = new TaskArtifactUpdateEvent_v0_3.Builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(new Artifact_v0_3.Builder() - .artifactId("artifact-1") - .parts(new TextPart_v0_3("First artifact")) - .build()) - .build(); + TaskArtifactUpdateEvent_v0_3 event1 = + new TaskArtifactUpdateEvent_v0_3.Builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + new Artifact_v0_3.Builder() + .artifactId("artifact-1") + .parts(new TextPart_v0_3("First artifact")) + .build()) + .build(); enqueueEventOnServer(event1); // Wait for first consumer to receive event - assertTrue(firstConsumerLatch.await(15, TimeUnit.SECONDS), "First consumer should receive event"); + assertTrue( + firstConsumerLatch.await(15, TimeUnit.SECONDS), + "First consumer should receive event"); assertFalse(firstUnexpectedEvent.get()); assertNull(firstErrorRef.get()); assertNotNull(firstConsumerEvent.get()); @@ -713,32 +778,40 @@ public void testMainQueueReferenceCountingWithMultipleConsumers() throws Excepti // while other consumers are still active. Without reference counting, the MainQueue // might close when the ensureQueue ChildQueue closes, preventing this resubscription. CountDownLatch secondConsumerLatch = new CountDownLatch(1); - AtomicReference secondConsumerEvent = new AtomicReference<>(); + AtomicReference secondConsumerEvent = + new AtomicReference<>(); AtomicBoolean secondUnexpectedEvent = new AtomicBoolean(false); AtomicReference secondErrorRef = new AtomicReference<>(); AtomicBoolean secondReceivedInitialSnapshot = new AtomicBoolean(false); - BiConsumer secondConsumer = (event, agentCard) -> { - if (event instanceof TaskUpdateEvent_v0_3 tue && tue.getUpdateEvent() instanceof TaskArtifactUpdateEvent_v0_3 artifact) { - secondConsumerEvent.set(artifact); - secondConsumerLatch.countDown(); - } else if (event instanceof TaskEvent_v0_3) { - // v0.3 spec confirms task snapshot as first event on resubscribe is valid behavior - // See: https://a2a-protocol.org/v0.3.0/specification/#721-sendstreamingmessageresponse-object - if (!secondReceivedInitialSnapshot.compareAndSet(false, true)) { - secondUnexpectedEvent.set(true); // TaskEvent received after first event - } - } else if (!(event instanceof TaskUpdateEvent_v0_3)) { - secondUnexpectedEvent.set(true); - } - }; + BiConsumer secondConsumer = + (event, agentCard) -> { + if (event instanceof TaskUpdateEvent_v0_3 tue + && tue.getUpdateEvent() + instanceof TaskArtifactUpdateEvent_v0_3 artifact) { + secondConsumerEvent.set(artifact); + secondConsumerLatch.countDown(); + } else if (event instanceof TaskEvent_v0_3) { + // v0.3 spec confirms task snapshot as first event on resubscribe is + // valid behavior + // See: + // https://a2a-protocol.org/v0.3.0/specification/#721-sendstreamingmessageresponse-object + if (!secondReceivedInitialSnapshot.compareAndSet(false, true)) { + secondUnexpectedEvent.set( + true); // TaskEvent received after first event + } + } else if (!(event instanceof TaskUpdateEvent_v0_3)) { + secondUnexpectedEvent.set(true); + } + }; - Consumer secondErrorHandler = error -> { - if (!isStreamClosedError(error)) { - secondErrorRef.set(error); - } - secondConsumerLatch.countDown(); - }; + Consumer secondErrorHandler = + error -> { + if (!isStreamClosedError(error)) { + secondErrorRef.set(error); + } + secondConsumerLatch.countDown(); + }; // Wait for second subscription to be established CountDownLatch secondSubscriptionLatch = new CountDownLatch(1); @@ -747,38 +820,51 @@ public void testMainQueueReferenceCountingWithMultipleConsumers() throws Excepti // This should succeed with reference counting because MainQueue stays alive // while first consumer's ChildQueue exists - getClient().resubscribe(new TaskIdParams_v0_3(MINIMAL_TASK.id()), - List.of(secondConsumer), - secondErrorHandler); + getClient() + .resubscribe( + new TaskIdParams_v0_3(MINIMAL_TASK.id()), + List.of(secondConsumer), + secondErrorHandler); - assertTrue(secondSubscriptionLatch.await(15, TimeUnit.SECONDS), "Second subscription should be established"); + assertTrue( + secondSubscriptionLatch.await(15, TimeUnit.SECONDS), + "Second subscription should be established"); // Verify child queue count increased (now ensureQueue + first + second) int childCountAfterSecond = getChildQueueCount(MINIMAL_TASK.id()); - assertTrue(childCountAfterSecond > childCountBeforeSecond, + assertTrue( + childCountAfterSecond > childCountBeforeSecond, "Child queue count should increase after second resubscription"); // 4. Enqueue second event - both consumers should receive it - TaskArtifactUpdateEvent_v0_3 event2 = new TaskArtifactUpdateEvent_v0_3.Builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(new Artifact_v0_3.Builder() - .artifactId("artifact-2") - .parts(new TextPart_v0_3("Second artifact")) - .build()) - .build(); + TaskArtifactUpdateEvent_v0_3 event2 = + new TaskArtifactUpdateEvent_v0_3.Builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + new Artifact_v0_3.Builder() + .artifactId("artifact-2") + .parts(new TextPart_v0_3("Second artifact")) + .build()) + .build(); enqueueEventOnServer(event2); // Both consumers should receive the event - assertTrue(secondConsumerLatch.await(15, TimeUnit.SECONDS), "Second consumer should receive event"); + assertTrue( + secondConsumerLatch.await(15, TimeUnit.SECONDS), + "Second consumer should receive event"); assertFalse(secondUnexpectedEvent.get()); - assertNull(secondErrorRef.get(), - "Resubscription should succeed with reference counting (MainQueue stays alive)"); + assertNull( + secondErrorRef.get(), + "Resubscription should succeed with reference counting (MainQueue stays" + + " alive)"); TaskArtifactUpdateEvent_v0_3 receivedEvent = secondConsumerEvent.get(); assertNotNull(receivedEvent); assertEquals("artifact-2", receivedEvent.artifact().artifactId()); - assertEquals("Second artifact", ((TextPart_v0_3) receivedEvent.artifact().parts().get(0)).text()); + assertEquals( + "Second artifact", + ((TextPart_v0_3) receivedEvent.artifact().parts().get(0)).text()); } finally { deleteTaskInTaskStore(MINIMAL_TASK.id()); @@ -827,11 +913,18 @@ public void testListPushNotificationConfigWithConfigId() throws Exception { savePushNotificationConfigInStore(MINIMAL_TASK.id(), notificationConfig2); try { - List result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id())); + List result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id())); assertEquals(2, result.size()); - assertEquals(new TaskPushNotificationConfig_v0_3(MINIMAL_TASK.id(), notificationConfig1), result.get(0)); - assertEquals(new TaskPushNotificationConfig_v0_3(MINIMAL_TASK.id(), notificationConfig2), result.get(1)); + assertEquals( + new TaskPushNotificationConfig_v0_3(MINIMAL_TASK.id(), notificationConfig1), + result.get(0)); + assertEquals( + new TaskPushNotificationConfig_v0_3(MINIMAL_TASK.id(), notificationConfig2), + result.get(1)); } catch (Exception e) { fail(); } finally { @@ -845,27 +938,29 @@ public void testListPushNotificationConfigWithConfigId() throws Exception { public void testListPushNotificationConfigWithoutConfigId() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); PushNotificationConfig_v0_3 notificationConfig1 = - new PushNotificationConfig_v0_3.Builder() - .url("http://1.example.com") - .build(); + new PushNotificationConfig_v0_3.Builder().url("http://1.example.com").build(); PushNotificationConfig_v0_3 notificationConfig2 = - new PushNotificationConfig_v0_3.Builder() - .url("http://2.example.com") - .build(); + new PushNotificationConfig_v0_3.Builder().url("http://2.example.com").build(); savePushNotificationConfigInStore(MINIMAL_TASK.id(), notificationConfig1); // will overwrite the previous one savePushNotificationConfigInStore(MINIMAL_TASK.id(), notificationConfig2); try { - List result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id())); + List result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id())); assertEquals(1, result.size()); - PushNotificationConfig_v0_3 expectedNotificationConfig = new PushNotificationConfig_v0_3.Builder() - .url("http://2.example.com") - .id(MINIMAL_TASK.id()) - .build(); - assertEquals(new TaskPushNotificationConfig_v0_3(MINIMAL_TASK.id(), expectedNotificationConfig), + PushNotificationConfig_v0_3 expectedNotificationConfig = + new PushNotificationConfig_v0_3.Builder() + .url("http://2.example.com") + .id(MINIMAL_TASK.id()) + .build(); + assertEquals( + new TaskPushNotificationConfig_v0_3( + MINIMAL_TASK.id(), expectedNotificationConfig), result.get(0)); } catch (Exception e) { fail(); @@ -878,8 +973,11 @@ public void testListPushNotificationConfigWithoutConfigId() throws Exception { @Test public void testListPushNotificationConfigTaskNotFound() { try { - List result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams_v0_3("non-existent-task")); + List result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams_v0_3( + "non-existent-task")); fail(); } catch (A2AClientException_v0_3 e) { assertInstanceOf(TaskNotFoundError_v0_3.class, e.getCause()); @@ -890,8 +988,11 @@ public void testListPushNotificationConfigTaskNotFound() { public void testListPushNotificationConfigEmptyList() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - List result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id())); + List result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id())); assertEquals(0, result.size()); } catch (Exception e) { fail(e.getMessage()); @@ -903,11 +1004,12 @@ public void testListPushNotificationConfigEmptyList() throws Exception { @Test public void testDeletePushNotificationConfigWithValidConfigId() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); - saveTaskInTaskStore(new Task_v0_3.Builder() - .id("task-456") - .contextId("session-xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .build()); + saveTaskInTaskStore( + new Task_v0_3.Builder() + .id("task-456") + .contextId("session-xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .build()); PushNotificationConfig_v0_3 notificationConfig1 = new PushNotificationConfig_v0_3.Builder() @@ -925,17 +1027,24 @@ public void testDeletePushNotificationConfigWithValidConfigId() throws Exception try { // specify the config ID to delete - getClient().deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id(), "config1")); + getClient() + .deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id(), "config1")); // should now be 1 left - List result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id())); + List result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id())); assertEquals(1, result.size()); // should remain unchanged, this is a different task - result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams_v0_3("task-456")); + result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams_v0_3("task-456")); assertEquals(1, result.size()); } catch (Exception e) { fail(e.getMessage()); @@ -965,12 +1074,17 @@ public void testDeletePushNotificationConfigWithNonExistingConfigId() throws Exc savePushNotificationConfigInStore(MINIMAL_TASK.id(), notificationConfig2); try { - getClient().deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id(), "non-existent-config-id")); + getClient() + .deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id(), "non-existent-config-id")); // should remain unchanged - List result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id())); + List result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id())); assertEquals(2, result.size()); } catch (Exception e) { fail(); @@ -984,9 +1098,10 @@ public void testDeletePushNotificationConfigWithNonExistingConfigId() throws Exc @Test public void testDeletePushNotificationConfigTaskNotFound() { try { - getClient().deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams_v0_3("non-existent-task", - "non-existent-config-id")); + getClient() + .deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams_v0_3( + "non-existent-task", "non-existent-config-id")); fail(); } catch (A2AClientException_v0_3 e) { assertInstanceOf(TaskNotFoundError_v0_3.class, e.getCause()); @@ -997,25 +1112,27 @@ public void testDeletePushNotificationConfigTaskNotFound() { public void testDeletePushNotificationConfigSetWithoutConfigId() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); PushNotificationConfig_v0_3 notificationConfig1 = - new PushNotificationConfig_v0_3.Builder() - .url("http://1.example.com") - .build(); + new PushNotificationConfig_v0_3.Builder().url("http://1.example.com").build(); PushNotificationConfig_v0_3 notificationConfig2 = - new PushNotificationConfig_v0_3.Builder() - .url("http://2.example.com") - .build(); + new PushNotificationConfig_v0_3.Builder().url("http://2.example.com").build(); savePushNotificationConfigInStore(MINIMAL_TASK.id(), notificationConfig1); // this one will overwrite the previous one savePushNotificationConfigInStore(MINIMAL_TASK.id(), notificationConfig2); try { - getClient().deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id(), MINIMAL_TASK.id())); + getClient() + .deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id(), MINIMAL_TASK.id())); // should now be 0 - List result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id()), null); + List result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id()), + null); assertEquals(0, result.size()); } catch (Exception e) { fail(); @@ -1030,162 +1147,194 @@ public void testDeletePushNotificationConfigSetWithoutConfigId() throws Exceptio public void testNonBlockingWithMultipleMessages() throws Exception { AtomicReference generatedTaskIdRef = new AtomicReference<>(); try { - // 1. Send first non-blocking message without taskId - server generates one - // Routing is by message content prefix "multi-event:first" - Message_v0_3 message1 = new Message_v0_3.Builder(MESSAGE) - .parts(new TextPart_v0_3("multi-event:first")) - .build(); - - AtomicReference taskIdRef = new AtomicReference<>(); - CountDownLatch firstTaskLatch = new CountDownLatch(1); + // 1. Send first non-blocking message without taskId - server generates one + // Routing is by message content prefix "multi-event:first" + Message_v0_3 message1 = + new Message_v0_3.Builder(MESSAGE) + .parts(new TextPart_v0_3("multi-event:first")) + .build(); + + AtomicReference taskIdRef = new AtomicReference<>(); + CountDownLatch firstTaskLatch = new CountDownLatch(1); + + BiConsumer firstMessageConsumer = + (event, agentCard) -> { + if (event instanceof TaskEvent_v0_3 te) { + taskIdRef.set(te.getTask().id()); + firstTaskLatch.countDown(); + } else if (event instanceof TaskUpdateEvent_v0_3 tue + && tue.getUpdateEvent() + instanceof TaskStatusUpdateEvent_v0_3 status) { + taskIdRef.set(status.taskId()); + firstTaskLatch.countDown(); + } + }; - BiConsumer firstMessageConsumer = (event, agentCard) -> { - if (event instanceof TaskEvent_v0_3 te) { - taskIdRef.set(te.getTask().id()); - firstTaskLatch.countDown(); - } else if (event instanceof TaskUpdateEvent_v0_3 tue && tue.getUpdateEvent() instanceof TaskStatusUpdateEvent_v0_3 status) { - taskIdRef.set(status.taskId()); - firstTaskLatch.countDown(); - } - }; - - // Non-blocking message creates task in WORKING state and returns immediately - // Queue stays open because task is not in final state - getPollingClient().sendMessage(message1, List.of(firstMessageConsumer), null); - - assertTrue(firstTaskLatch.await(10, TimeUnit.SECONDS)); - String taskId = taskIdRef.get(); - assertNotNull(taskId); - generatedTaskIdRef.set(taskId); - - // 2. Resubscribe to task (queue should still be open) - CountDownLatch resubEventLatch = new CountDownLatch(2); // artifact-2 + completion - List resubReceivedEvents = new CopyOnWriteArrayList<>(); - AtomicBoolean resubUnexpectedEvent = new AtomicBoolean(false); - AtomicReference resubErrorRef = new AtomicReference<>(); - AtomicBoolean resubReceivedInitialSnapshot = new AtomicBoolean(false); - - BiConsumer resubConsumer = (event, agentCard) -> { - if (event instanceof TaskUpdateEvent_v0_3 tue) { - resubReceivedEvents.add(tue.getUpdateEvent()); - resubEventLatch.countDown(); - } else if (event instanceof TaskEvent_v0_3) { - // v0.3 spec confirms task snapshot as first event on resubscribe is valid behavior - // See: https://a2a-protocol.org/v0.3.0/specification/#721-sendstreamingmessageresponse-object - if (!resubReceivedInitialSnapshot.compareAndSet(false, true)) { - System.err.println("testNonBlockingWithMultipleMessages: TaskEvent received after first event"); - resubUnexpectedEvent.set(true); // TaskEvent received after first event - } - } else { - System.err.println("testNonBlockingWithMultipleMessages: Unexpected event type in resubConsumer: " + event.getClass().getName()); - resubUnexpectedEvent.set(true); - } - }; + // Non-blocking message creates task in WORKING state and returns immediately + // Queue stays open because task is not in final state + getPollingClient().sendMessage(message1, List.of(firstMessageConsumer), null); - Consumer resubErrorHandler = error -> { - if (!isStreamClosedError(error)) { - resubErrorRef.set(error); - } - }; + assertTrue(firstTaskLatch.await(10, TimeUnit.SECONDS)); + String taskId = taskIdRef.get(); + assertNotNull(taskId); + generatedTaskIdRef.set(taskId); - // Wait for subscription to be active - CountDownLatch subscriptionLatch = new CountDownLatch(1); - awaitStreamingSubscription() - .whenComplete((unused, throwable) -> subscriptionLatch.countDown()); + // 2. Resubscribe to task (queue should still be open) + CountDownLatch resubEventLatch = new CountDownLatch(2); // artifact-2 + completion + List resubReceivedEvents = new CopyOnWriteArrayList<>(); + AtomicBoolean resubUnexpectedEvent = new AtomicBoolean(false); + AtomicReference resubErrorRef = new AtomicReference<>(); + AtomicBoolean resubReceivedInitialSnapshot = new AtomicBoolean(false); + + BiConsumer resubConsumer = + (event, agentCard) -> { + if (event instanceof TaskUpdateEvent_v0_3 tue) { + resubReceivedEvents.add(tue.getUpdateEvent()); + resubEventLatch.countDown(); + } else if (event instanceof TaskEvent_v0_3) { + // v0.3 spec confirms task snapshot as first event on resubscribe is + // valid behavior + // See: + // https://a2a-protocol.org/v0.3.0/specification/#721-sendstreamingmessageresponse-object + if (!resubReceivedInitialSnapshot.compareAndSet(false, true)) { + System.err.println( + "testNonBlockingWithMultipleMessages: TaskEvent received" + + " after first event"); + resubUnexpectedEvent.set( + true); // TaskEvent received after first event + } + } else { + System.err.println( + "testNonBlockingWithMultipleMessages: Unexpected event type in" + + " resubConsumer: " + + event.getClass().getName()); + resubUnexpectedEvent.set(true); + } + }; - getClient().resubscribe(new TaskIdParams_v0_3(taskId), - List.of(resubConsumer), - resubErrorHandler); + Consumer resubErrorHandler = + error -> { + if (!isStreamClosedError(error)) { + resubErrorRef.set(error); + } + }; - assertTrue(subscriptionLatch.await(15, TimeUnit.SECONDS)); + // Wait for subscription to be active + CountDownLatch subscriptionLatch = new CountDownLatch(1); + awaitStreamingSubscription() + .whenComplete((unused, throwable) -> subscriptionLatch.countDown()); - // 3. Send second streaming message to same taskId - Message_v0_3 message2 = new Message_v0_3.Builder(MESSAGE) - .taskId(taskId) - .parts(new TextPart_v0_3("multi-event:second")) - .build(); + getClient() + .resubscribe( + new TaskIdParams_v0_3(taskId), + List.of(resubConsumer), + resubErrorHandler); - CountDownLatch streamEventLatch = new CountDownLatch(2); // artifact-2 + completion - List streamReceivedEvents = new CopyOnWriteArrayList<>(); - AtomicBoolean streamUnexpectedEvent = new AtomicBoolean(false); + assertTrue(subscriptionLatch.await(15, TimeUnit.SECONDS)); - BiConsumer streamConsumer = (event, agentCard) -> { - if (event instanceof TaskUpdateEvent_v0_3 tue) { - streamReceivedEvents.add(tue.getUpdateEvent()); - streamEventLatch.countDown(); - } else { - streamUnexpectedEvent.set(true); - } - }; - - // Wait for the server to register the new SSE subscription before the agent emits events. - // Without this guard there is a race: the server can finish processing message2 and emit - // events to the task queue before the streaming subscriber from sendMessage is registered, - // causing those events to be silently dropped (sendMessage streaming starts from the - // current queue position, unlike resubscribe which replays from an earlier point). - CountDownLatch streamSubscriptionLatch = new CountDownLatch(1); - awaitStreamingSubscription() - .whenComplete((unused, throwable) -> streamSubscriptionLatch.countDown()); - - // Streaming message adds artifact-2 and completes task - getClient().sendMessage(message2, List.of(streamConsumer), null); - - assertTrue(streamSubscriptionLatch.await(15, TimeUnit.SECONDS), - "Stream subscription should be established before agent emits events"); - - // 4. Verify both consumers received artifact-2 and completion - assertTrue(resubEventLatch.await(10, TimeUnit.SECONDS)); - assertTrue(streamEventLatch.await(10, TimeUnit.SECONDS)); - - assertFalse(resubUnexpectedEvent.get()); - assertFalse(streamUnexpectedEvent.get()); - assertNull(resubErrorRef.get()); - - // Both should have received 2 events: artifact-2 and completion - assertEquals(2, resubReceivedEvents.size()); - assertEquals(2, streamReceivedEvents.size()); - - // Verify resubscription events - long resubArtifactCount = resubReceivedEvents.stream() - .filter(e -> e instanceof TaskArtifactUpdateEvent_v0_3) - .count(); - assertEquals(1, resubArtifactCount); - - long resubCompletionCount = resubReceivedEvents.stream() - .filter(e -> e instanceof TaskStatusUpdateEvent_v0_3) - .filter(e -> ((TaskStatusUpdateEvent_v0_3) e).isFinal()) - .count(); - assertEquals(1, resubCompletionCount); - - // Verify streaming events - long streamArtifactCount = streamReceivedEvents.stream() - .filter(e -> e instanceof TaskArtifactUpdateEvent_v0_3) - .count(); - assertEquals(1, streamArtifactCount); - - long streamCompletionCount = streamReceivedEvents.stream() - .filter(e -> e instanceof TaskStatusUpdateEvent_v0_3) - .filter(e -> ((TaskStatusUpdateEvent_v0_3) e).isFinal()) - .count(); - assertEquals(1, streamCompletionCount); - - // Verify artifact-2 details from resubscription - TaskArtifactUpdateEvent_v0_3 resubArtifact = (TaskArtifactUpdateEvent_v0_3) resubReceivedEvents.stream() - .filter(e -> e instanceof TaskArtifactUpdateEvent_v0_3) - .findFirst() - .orElseThrow(); - assertEquals("artifact-2", resubArtifact.artifact().artifactId()); - assertEquals("Second message artifact", - ((TextPart_v0_3) resubArtifact.artifact().parts().get(0)).text()); - - // Verify artifact-2 details from streaming - TaskArtifactUpdateEvent_v0_3 streamArtifact = (TaskArtifactUpdateEvent_v0_3) streamReceivedEvents.stream() - .filter(e -> e instanceof TaskArtifactUpdateEvent_v0_3) - .findFirst() - .orElseThrow(); - assertEquals("artifact-2", streamArtifact.artifact().artifactId()); - assertEquals("Second message artifact", - ((TextPart_v0_3) streamArtifact.artifact().parts().get(0)).text()); + // 3. Send second streaming message to same taskId + Message_v0_3 message2 = + new Message_v0_3.Builder(MESSAGE) + .taskId(taskId) + .parts(new TextPart_v0_3("multi-event:second")) + .build(); + + CountDownLatch streamEventLatch = new CountDownLatch(2); // artifact-2 + completion + List streamReceivedEvents = new CopyOnWriteArrayList<>(); + AtomicBoolean streamUnexpectedEvent = new AtomicBoolean(false); + + BiConsumer streamConsumer = + (event, agentCard) -> { + if (event instanceof TaskUpdateEvent_v0_3 tue) { + streamReceivedEvents.add(tue.getUpdateEvent()); + streamEventLatch.countDown(); + } else { + streamUnexpectedEvent.set(true); + } + }; + + // Wait for the server to register the new SSE subscription before the agent emits + // events. + // Without this guard there is a race: the server can finish processing message2 and + // emit + // events to the task queue before the streaming subscriber from sendMessage is + // registered, + // causing those events to be silently dropped (sendMessage streaming starts from the + // current queue position, unlike resubscribe which replays from an earlier point). + CountDownLatch streamSubscriptionLatch = new CountDownLatch(1); + awaitStreamingSubscription() + .whenComplete((unused, throwable) -> streamSubscriptionLatch.countDown()); + + // Streaming message adds artifact-2 and completes task + getClient().sendMessage(message2, List.of(streamConsumer), null); + + assertTrue( + streamSubscriptionLatch.await(15, TimeUnit.SECONDS), + "Stream subscription should be established before agent emits events"); + + // 4. Verify both consumers received artifact-2 and completion + assertTrue(resubEventLatch.await(10, TimeUnit.SECONDS)); + assertTrue(streamEventLatch.await(10, TimeUnit.SECONDS)); + + assertFalse(resubUnexpectedEvent.get()); + assertFalse(streamUnexpectedEvent.get()); + assertNull(resubErrorRef.get()); + + // Both should have received 2 events: artifact-2 and completion + assertEquals(2, resubReceivedEvents.size()); + assertEquals(2, streamReceivedEvents.size()); + + // Verify resubscription events + long resubArtifactCount = + resubReceivedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent_v0_3) + .count(); + assertEquals(1, resubArtifactCount); + + long resubCompletionCount = + resubReceivedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent_v0_3) + .filter(e -> ((TaskStatusUpdateEvent_v0_3) e).isFinal()) + .count(); + assertEquals(1, resubCompletionCount); + + // Verify streaming events + long streamArtifactCount = + streamReceivedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent_v0_3) + .count(); + assertEquals(1, streamArtifactCount); + + long streamCompletionCount = + streamReceivedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent_v0_3) + .filter(e -> ((TaskStatusUpdateEvent_v0_3) e).isFinal()) + .count(); + assertEquals(1, streamCompletionCount); + + // Verify artifact-2 details from resubscription + TaskArtifactUpdateEvent_v0_3 resubArtifact = + (TaskArtifactUpdateEvent_v0_3) + resubReceivedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent_v0_3) + .findFirst() + .orElseThrow(); + assertEquals("artifact-2", resubArtifact.artifact().artifactId()); + assertEquals( + "Second message artifact", + ((TextPart_v0_3) resubArtifact.artifact().parts().get(0)).text()); + + // Verify artifact-2 details from streaming + TaskArtifactUpdateEvent_v0_3 streamArtifact = + (TaskArtifactUpdateEvent_v0_3) + streamReceivedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent_v0_3) + .findFirst() + .orElseThrow(); + assertEquals("artifact-2", streamArtifact.artifact().artifactId()); + assertEquals( + "Second message artifact", + ((TextPart_v0_3) streamArtifact.artifact().parts().get(0)).text()); } finally { String taskId = generatedTaskIdRef.get(); if (taskId != null) { @@ -1197,20 +1346,24 @@ public void testNonBlockingWithMultipleMessages() throws Exception { @Test public void testMalformedJSONRPCRequest() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); // missing closing bracket - String malformedRequest = "{\"jsonrpc\": \"2.0\", \"method\": \"message/send\", \"params\": {\"foo\": \"bar\"}"; - JSONRPCErrorResponse_v0_3 response = givenV03() - .contentType(MediaType.APPLICATION_JSON) - .body(malformedRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(JSONRPCErrorResponse_v0_3.class); + String malformedRequest = + "{\"jsonrpc\": \"2.0\", \"method\": \"message/send\", \"params\": {\"foo\":" + + " \"bar\"}"; + JSONRPCErrorResponse_v0_3 response = + givenV03() + .contentType(MediaType.APPLICATION_JSON) + .body(malformedRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(JSONRPCErrorResponse_v0_3.class); assertNotNull(response.getError()); assertEquals(new JSONParseError_v0_3().getCode(), response.getError().getCode()); } @@ -1218,30 +1371,34 @@ public void testMalformedJSONRPCRequest() { @Test public void testInvalidParamsJSONRPCRequest() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); - String invalidParamsRequest = """ + String invalidParamsRequest = + """ {"jsonrpc": "2.0", "method": "message/send", "params": "not_a_dict", "id": "1"} """; testInvalidParams(invalidParamsRequest); - invalidParamsRequest = """ + invalidParamsRequest = + """ {"jsonrpc": "2.0", "method": "message/send", "params": {"message": {"parts": "invalid"}}, "id": "1"} """; testInvalidParams(invalidParamsRequest); } private void testInvalidParams(String invalidParamsRequest) { - JSONRPCErrorResponse_v0_3 response = givenV03() - .contentType(MediaType.APPLICATION_JSON) - .body(invalidParamsRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(JSONRPCErrorResponse_v0_3.class); + JSONRPCErrorResponse_v0_3 response = + givenV03() + .contentType(MediaType.APPLICATION_JSON) + .body(invalidParamsRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(JSONRPCErrorResponse_v0_3.class); assertNotNull(response.getError()); assertEquals(new InvalidParamsError_v0_3().getCode(), response.getError().getCode()); assertEquals("1", response.getId()); @@ -1250,24 +1407,27 @@ private void testInvalidParams(String invalidParamsRequest) { @Test public void testInvalidJSONRPCRequestMissingJsonrpc() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); - String invalidRequest = """ + String invalidRequest = + """ { "method": "message/send", "params": {} } """; - JSONRPCErrorResponse_v0_3 response = givenV03() - .contentType(MediaType.APPLICATION_JSON) - .body(invalidRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(JSONRPCErrorResponse_v0_3.class); + JSONRPCErrorResponse_v0_3 response = + givenV03() + .contentType(MediaType.APPLICATION_JSON) + .body(invalidRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(JSONRPCErrorResponse_v0_3.class); assertNotNull(response.getError()); assertEquals(new InvalidRequestError_v0_3().getCode(), response.getError().getCode()); } @@ -1275,21 +1435,24 @@ public void testInvalidJSONRPCRequestMissingJsonrpc() { @Test public void testInvalidJSONRPCRequestMissingMethod() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); - String invalidRequest = """ + String invalidRequest = + """ {"jsonrpc": "2.0", "params": {}} """; - JSONRPCErrorResponse_v0_3 response = givenV03() - .contentType(MediaType.APPLICATION_JSON) - .body(invalidRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(JSONRPCErrorResponse_v0_3.class); + JSONRPCErrorResponse_v0_3 response = + givenV03() + .contentType(MediaType.APPLICATION_JSON) + .body(invalidRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(JSONRPCErrorResponse_v0_3.class); assertNotNull(response.getError()); assertEquals(new InvalidRequestError_v0_3().getCode(), response.getError().getCode()); } @@ -1297,21 +1460,24 @@ public void testInvalidJSONRPCRequestMissingMethod() { @Test public void testInvalidJSONRPCRequestInvalidId() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); - String invalidRequest = """ + String invalidRequest = + """ {"jsonrpc": "2.0", "method": "message/send", "params": {}, "id": {"bad": "type"}} """; - JSONRPCErrorResponse_v0_3 response = givenV03() - .contentType(MediaType.APPLICATION_JSON) - .body(invalidRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(JSONRPCErrorResponse_v0_3.class); + JSONRPCErrorResponse_v0_3 response = + givenV03() + .contentType(MediaType.APPLICATION_JSON) + .body(invalidRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(JSONRPCErrorResponse_v0_3.class); assertNotNull(response.getError()); assertEquals(new InvalidRequestError_v0_3().getCode(), response.getError().getCode()); } @@ -1319,21 +1485,24 @@ public void testInvalidJSONRPCRequestInvalidId() { @Test public void testInvalidJSONRPCRequestNonExistentMethod() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); - String invalidRequest = """ + String invalidRequest = + """ {"jsonrpc": "2.0", "method" : "nonexistent/method", "params": {}} """; - JSONRPCErrorResponse_v0_3 response = givenV03() - .contentType(MediaType.APPLICATION_JSON) - .body(invalidRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(JSONRPCErrorResponse_v0_3.class); + JSONRPCErrorResponse_v0_3 response = + givenV03() + .contentType(MediaType.APPLICATION_JSON) + .body(invalidRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(JSONRPCErrorResponse_v0_3.class); assertNotNull(response.getError()); assertEquals(new MethodNotFoundError_v0_3().getCode(), response.getError().getCode()); } @@ -1341,7 +1510,8 @@ public void testInvalidJSONRPCRequestNonExistentMethod() { @Test public void testNonStreamingMethodWithAcceptHeader() throws Exception { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); testGetTask(MediaType.APPLICATION_JSON); } @@ -1349,7 +1519,8 @@ public void testNonStreamingMethodWithAcceptHeader() throws Exception { @Test public void testStreamingMethodWithAcceptHeader() throws Exception { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); testSendStreamingMessageWithHttpClient(MediaType.SERVER_SENT_EVENTS); @@ -1358,7 +1529,8 @@ public void testStreamingMethodWithAcceptHeader() throws Exception { @Test public void testStreamingMethodWithoutAcceptHeader() throws Exception { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol_v0_3.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); testSendStreamingMessageWithHttpClient(null); @@ -1367,50 +1539,74 @@ public void testStreamingMethodWithoutAcceptHeader() throws Exception { private void testSendStreamingMessageWithHttpClient(String mediaType) throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendStreamingMessageRequest_v0_3 request = new SendStreamingMessageRequest_v0_3( - "1", new MessageSendParams_v0_3(message, null, null)); - - CompletableFuture>> responseFuture = initialiseStreamingRequest(request, mediaType); + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendStreamingMessageRequest_v0_3 request = + new SendStreamingMessageRequest_v0_3( + "1", new MessageSendParams_v0_3(message, null, null)); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference errorRef = new AtomicReference<>(); + CompletableFuture>> responseFuture = + initialiseStreamingRequest(request, mediaType); - responseFuture.thenAccept(response -> { - if (response.statusCode() != 200) { - throw new IllegalStateException("Status code was " + response.statusCode()); - } - response.body().forEach(line -> { - try { - SendStreamingMessageResponse_v0_3 jsonResponse = extractJsonResponseFromSseLine(line); - if (jsonResponse != null) { - assertNull(jsonResponse.getError()); - Message_v0_3 messageResponse = (Message_v0_3) jsonResponse.getResult(); - assertEquals(MESSAGE.messageId(), messageResponse.messageId()); - assertEquals(MESSAGE.role(), messageResponse.role()); - Part_v0_3 part = messageResponse.parts().get(0); - assertInstanceOf(TextPart_v0_3.class, part); - assertEquals("test message", ((TextPart_v0_3) part).text()); - latch.countDown(); - } - } catch (JsonProcessingException_v0_3 e) { - throw new RuntimeException(e); - } - }); - }).exceptionally(t -> { - if (!isStreamClosedError(t)) { - errorRef.set(t); - } - latch.countDown(); - return null; - }); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference errorRef = new AtomicReference<>(); - boolean dataRead = latch.await(20, TimeUnit.SECONDS); - Assertions.assertTrue(dataRead); - Assertions.assertNull(errorRef.get()); + responseFuture + .thenAccept( + response -> { + if (response.statusCode() != 200) { + throw new IllegalStateException( + "Status code was " + response.statusCode()); + } + response.body() + .forEach( + line -> { + try { + SendStreamingMessageResponse_v0_3 + jsonResponse = + extractJsonResponseFromSseLine( + line); + if (jsonResponse != null) { + assertNull(jsonResponse.getError()); + Message_v0_3 messageResponse = + (Message_v0_3) + jsonResponse + .getResult(); + assertEquals( + MESSAGE.messageId(), + messageResponse.messageId()); + assertEquals( + MESSAGE.role(), + messageResponse.role()); + Part_v0_3 part = + messageResponse.parts().get(0); + assertInstanceOf( + TextPart_v0_3.class, part); + assertEquals( + "test message", + ((TextPart_v0_3) part).text()); + latch.countDown(); + } + } catch (JsonProcessingException_v0_3 e) { + throw new RuntimeException(e); + } + }); + }) + .exceptionally( + t -> { + if (!isStreamClosedError(t)) { + errorRef.set(t); + } + latch.countDown(); + return null; + }); + + boolean dataRead = latch.await(20, TimeUnit.SECONDS); + Assertions.assertTrue(dataRead); + Assertions.assertNull(errorRef.get()); } finally { deleteTaskInTaskStore(MINIMAL_TASK.id()); @@ -1433,23 +1629,25 @@ public void testSendStreamingMessage(boolean createTask) throws Exception { AtomicBoolean wasUnexpectedEvent = new AtomicBoolean(false); AtomicReference errorRef = new AtomicReference<>(); - BiConsumer consumer = (event, agentCard) -> { - if (event instanceof MessageEvent_v0_3 messageEvent) { - if (latch.getCount() > 0) { - receivedMessage.set(messageEvent.getMessage()); - latch.countDown(); - } else { - wasUnexpectedEvent.set(true); - } - } else { - wasUnexpectedEvent.set(true); - } - }; + BiConsumer consumer = + (event, agentCard) -> { + if (event instanceof MessageEvent_v0_3 messageEvent) { + if (latch.getCount() > 0) { + receivedMessage.set(messageEvent.getMessage()); + latch.countDown(); + } else { + wasUnexpectedEvent.set(true); + } + } else { + wasUnexpectedEvent.set(true); + } + }; - Consumer errorHandler = error -> { - errorRef.set(error); - latch.countDown(); - }; + Consumer errorHandler = + error -> { + errorRef.set(error); + latch.countDown(); + }; // testing the streaming send message getClient().sendMessage(message, List.of(consumer), errorHandler); @@ -1478,15 +1676,14 @@ private CompletableFuture>> initialiseStreamingReque StreamingJSONRPCRequest_v0_3 request, String mediaType) throws Exception { // Create the client - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); // Create the request - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/")) - .POST(HttpRequest.BodyPublishers.ofString(JsonUtil_v0_3.toJson(request))) - .header("Content-Type", APPLICATION_JSON); + HttpRequest.Builder builder = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/")) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil_v0_3.toJson(request))) + .header("Content-Type", APPLICATION_JSON); if (mediaType != null) { builder.header("Accept", mediaType); } @@ -1496,7 +1693,8 @@ private CompletableFuture>> initialiseStreamingReque return client.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines()); } - private SendStreamingMessageResponse_v0_3 extractJsonResponseFromSseLine(String line) throws JsonProcessingException_v0_3 { + private SendStreamingMessageResponse_v0_3 extractJsonResponseFromSseLine(String line) + throws JsonProcessingException_v0_3 { line = extractSseData(line); if (line != null) { return JsonUtil_v0_3.fromJson(line, SendStreamingMessageResponse_v0_3.class); @@ -1520,7 +1718,8 @@ protected boolean isStreamClosedError(Throwable throwable) { if (cause instanceof EOFException) { return true; } - if (cause instanceof IOException && cause.getMessage() != null + if (cause instanceof IOException + && cause.getMessage() != null && cause.getMessage().contains("cancelled")) { // stream is closed upon cancellation return true; @@ -1538,18 +1737,24 @@ protected void saveTaskInTaskStore(Task_v0_3 task) throws Exception { // Convert v0.3 → v1.0 org.a2aproject.sdk.spec.Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(task); - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/task")) - .POST(HttpRequest.BodyPublishers.ofString(org.a2aproject.sdk.jsonrpc.common.json.JsonUtil.toJson(v10Task))) - .header("Content-Type", APPLICATION_JSON) - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task")) + .POST( + HttpRequest.BodyPublishers.ofString( + org.a2aproject.sdk.jsonrpc.common.json.JsonUtil.toJson( + v10Task))) + .header("Content-Type", APPLICATION_JSON) + .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(String.format("Saving task failed! Status: %d, Body: %s", response.statusCode(), response.body())); + throw new RuntimeException( + String.format( + "Saving task failed! Status: %d, Body: %s", + response.statusCode(), response.body())); } } @@ -1558,53 +1763,71 @@ protected void saveTaskInTaskStore(Task_v0_3 task) throws Exception { * Converts v1.0 → v0.3 using TaskMapper. */ protected Task_v0_3 getTaskFromTaskStore(String taskId) throws Exception { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) - .GET() - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) + .GET() + .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() == 404) { return null; } if (response.statusCode() != 200) { - throw new RuntimeException(String.format("Getting task failed! Status: %d, Body: %s", response.statusCode(), response.body())); + throw new RuntimeException( + String.format( + "Getting task failed! Status: %d, Body: %s", + response.statusCode(), response.body())); } // Convert v1.0 → v0.3 - org.a2aproject.sdk.spec.Task v10Task = org.a2aproject.sdk.jsonrpc.common.json.JsonUtil.fromJson( - response.body(), org.a2aproject.sdk.spec.Task.class); + org.a2aproject.sdk.spec.Task v10Task = + org.a2aproject.sdk.jsonrpc.common.json.JsonUtil.fromJson( + response.body(), org.a2aproject.sdk.spec.Task.class); return TaskMapper_v0_3.INSTANCE.fromV10(v10Task); } protected void deleteTaskInTaskStore(String taskId) throws Exception { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(("http://localhost:" + serverPort + "/test/task/" + taskId))) - .DELETE() - .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + ("http://localhost:" + + serverPort + + "/test/task/" + + taskId))) + .DELETE() + .build(); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(response.statusCode() + ": Deleting task failed!" + response.body()); + throw new RuntimeException( + response.statusCode() + ": Deleting task failed!" + response.body()); } } protected void ensureQueueForTask(String taskId) throws Exception { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/queue/ensure/" + taskId)) - .POST(HttpRequest.BodyPublishers.noBody()) - .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/test/queue/ensure/" + + taskId)) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(String.format("Ensuring queue failed! Status: %d, Body: %s", response.statusCode(), response.body())); + throw new RuntimeException( + String.format( + "Ensuring queue failed! Status: %d, Body: %s", + response.statusCode(), response.body())); } } @@ -1623,22 +1846,29 @@ protected void enqueueEventOnServer(Event_v0_3 event) throws Exception { path = "test/queue/enqueueTaskStatusUpdateEvent/" + e.taskId(); v10Event = TaskStatusUpdateEventMapper_v0_3.INSTANCE.toV10(e); } else { - throw new RuntimeException("Unknown event type " + event.getClass() + ". If you need the ability to" + - " handle more types, please add the REST endpoints."); + throw new RuntimeException( + "Unknown event type " + + event.getClass() + + ". If you need the ability to" + + " handle more types, please add the REST endpoints."); } - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/" + path)) - .header("Content-Type", APPLICATION_JSON) - .POST(HttpRequest.BodyPublishers.ofString(org.a2aproject.sdk.jsonrpc.common.json.JsonUtil.toJson(v10Event))) - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/" + path)) + .header("Content-Type", APPLICATION_JSON) + .POST( + HttpRequest.BodyPublishers.ofString( + org.a2aproject.sdk.jsonrpc.common.json.JsonUtil.toJson( + v10Event))) + .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(response.statusCode() + ": Queueing event failed!" + response.body()); + throw new RuntimeException( + response.statusCode() + ": Queueing event failed!" + response.body()); } } @@ -1646,38 +1876,44 @@ private CompletableFuture awaitStreamingSubscription() { int cnt = getStreamingSubscribedCount(); AtomicInteger initialCount = new AtomicInteger(cnt); - return CompletableFuture.runAsync(() -> { - try { - boolean done = false; - long end = System.currentTimeMillis() + 15000; - while (System.currentTimeMillis() < end) { - int count = getStreamingSubscribedCount(); - if (count > initialCount.get()) { - done = true; - break; + return CompletableFuture.runAsync( + () -> { + try { + boolean done = false; + long end = System.currentTimeMillis() + 15000; + while (System.currentTimeMillis() < end) { + int count = getStreamingSubscribedCount(); + if (count > initialCount.get()) { + done = true; + break; + } + Thread.sleep(500); + } + if (!done) { + throw new RuntimeException("Timed out waiting for subscription"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted"); } - Thread.sleep(500); - } - if (!done) { - throw new RuntimeException("Timed out waiting for subscription"); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted"); - } - }); + }); } private int getStreamingSubscribedCount() { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/streamingSubscribedCount")) - .GET() - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/test/streamingSubscribedCount")) + .GET() + .build(); try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send( + request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); String body = response.body().trim(); return Integer.parseInt(body); } catch (IOException | InterruptedException e) { @@ -1686,15 +1922,21 @@ private int getStreamingSubscribedCount() { } protected int getChildQueueCount(String taskId) { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/queue/childCount/" + taskId)) - .GET() - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/test/queue/childCount/" + + taskId)) + .GET() + .build(); try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send( + request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); String body = response.body().trim(); return Integer.parseInt(body); } catch (IOException | InterruptedException e) { @@ -1705,17 +1947,26 @@ protected int getChildQueueCount(String taskId) { /** * Delete a push notification config from the v1.0 store. */ - protected void deletePushNotificationConfigInStore(String taskId, String configId) throws Exception { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(("http://localhost:" + serverPort + "/test/task/" + taskId + "/config/" + configId))) - .DELETE() - .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + protected void deletePushNotificationConfigInStore(String taskId, String configId) + throws Exception { + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + ("http://localhost:" + + serverPort + + "/test/task/" + + taskId + + "/config/" + + configId))) + .DELETE() + .build(); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(response.statusCode() + ": Deleting task failed!" + response.body()); + throw new RuntimeException( + response.statusCode() + ": Deleting task failed!" + response.body()); } } @@ -1723,7 +1974,8 @@ protected void deletePushNotificationConfigInStore(String taskId, String configI * Save a v0.3 push notification config to the v1.0 store. * Converts v0.3 → v1.0 using mappers. */ - protected void savePushNotificationConfigInStore(String taskId, PushNotificationConfig_v0_3 notificationConfig) throws Exception { + protected void savePushNotificationConfigInStore( + String taskId, PushNotificationConfig_v0_3 notificationConfig) throws Exception { // Create v0.3 TaskPushNotificationConfig wrapper TaskPushNotificationConfig_v0_3 v03Config = new TaskPushNotificationConfig_v0_3(taskId, notificationConfig); @@ -1733,19 +1985,25 @@ protected void savePushNotificationConfigInStore(String taskId, PushNotification TaskPushNotificationConfigMapper_v0_3.INSTANCE.toV10(v03Config); // Send to v1.0 server using v1.0 JSON serialization - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) - .POST(HttpRequest.BodyPublishers.ofString(org.a2aproject.sdk.jsonrpc.common.json.JsonUtil.toJson(v10Config))) - .header("Content-Type", APPLICATION_JSON) - .header("A2A-Version", A2AProtocol_v0_3.PROTOCOL_VERSION) - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) + .POST( + HttpRequest.BodyPublishers.ofString( + org.a2aproject.sdk.jsonrpc.common.json.JsonUtil.toJson( + v10Config))) + .header("Content-Type", APPLICATION_JSON) + .header("A2A-Version", A2AProtocol_v0_3.PROTOCOL_VERSION) + .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(response.statusCode() + ": Creating task push notification config failed! " + response.body()); + throw new RuntimeException( + response.statusCode() + + ": Creating task push notification config failed! " + + response.body()); } } @@ -1786,9 +2044,8 @@ private Client_v0_3 createClient(boolean streaming) throws A2AClientException_v0 AgentCard_v0_3 agentCard = createTestAgentCard(); ClientConfig_v0_3 clientConfig = createClientConfig(streaming); - ClientBuilder_v0_3 clientBuilder = Client_v0_3 - .builder(agentCard) - .clientConfig(clientConfig); + ClientBuilder_v0_3 clientBuilder = + Client_v0_3.builder(agentCard).clientConfig(clientConfig); configureTransport(clientBuilder); @@ -1806,15 +2063,17 @@ private AgentCard_v0_3 createTestAgentCard() { .version("1.0") .documentationUrl("http://example.com/docs") .preferredTransport(getTransportProtocol()) - .capabilities(new AgentCapabilities_v0_3.Builder() - .streaming(true) - .pushNotifications(true) - .stateTransitionHistory(true) - .build()) + .capabilities( + new AgentCapabilities_v0_3.Builder() + .streaming(true) + .pushNotifications(true) + .stateTransitionHistory(true) + .build()) .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) .skills(List.of()) - .additionalInterfaces(List.of(new AgentInterface_v0_3(getTransportProtocol(), getTransportUrl()))) + .additionalInterfaces( + List.of(new AgentInterface_v0_3(getTransportProtocol(), getTransportUrl()))) .protocolVersion("0.2.5") .build(); } @@ -1823,9 +2082,7 @@ private AgentCard_v0_3 createTestAgentCard() { * Create client configuration with transport-specific settings. */ private ClientConfig_v0_3 createClientConfig(boolean streaming) { - return new ClientConfig_v0_3.Builder() - .setStreaming(streaming) - .build(); + return new ClientConfig_v0_3.Builder().setStreaming(streaming).build(); } /** @@ -1833,14 +2090,14 @@ private ClientConfig_v0_3 createClientConfig(boolean streaming) { */ private Client_v0_3 createPollingClient() throws A2AClientException_v0_3 { AgentCard_v0_3 agentCard = createTestAgentCard(); - ClientConfig_v0_3 clientConfig = new ClientConfig_v0_3.Builder() - .setStreaming(false) // Non-streaming - .setPolling(true) // Polling mode (translates to blocking=false on server) - .build(); + ClientConfig_v0_3 clientConfig = + new ClientConfig_v0_3.Builder() + .setStreaming(false) // Non-streaming + .setPolling(true) // Polling mode (translates to blocking=false on server) + .build(); - ClientBuilder_v0_3 clientBuilder = Client_v0_3 - .builder(agentCard) - .clientConfig(clientConfig); + ClientBuilder_v0_3 clientBuilder = + Client_v0_3.builder(agentCard).clientConfig(clientConfig); configureTransport(clientBuilder); @@ -1864,11 +2121,12 @@ public void testMainQueueStaysOpenForNonFinalTasks() throws Exception { String contextId = "fire-ctx"; // Create task in WORKING state (non-final) - Task_v0_3 workingTask = new Task_v0_3.Builder() - .id(taskId) - .contextId(contextId) - .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING)) - .build(); + Task_v0_3 workingTask = + new Task_v0_3.Builder() + .id(taskId) + .contextId(contextId) + .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING)) + .build(); saveTaskInTaskStore(workingTask); try { @@ -1876,28 +2134,31 @@ public void testMainQueueStaysOpenForNonFinalTasks() throws Exception { ensureQueueForTask(taskId); // Send a message that will leave task in WORKING state (fire-and-forget pattern) - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .taskId(taskId) - .contextId(contextId) - .parts(new TextPart_v0_3("fire and forget")) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE) + .taskId(taskId) + .contextId(contextId) + .parts(new TextPart_v0_3("fire and forget")) + .build(); CountDownLatch firstEventLatch = new CountDownLatch(1); AtomicReference errorRef = new AtomicReference<>(); - BiConsumer consumer = (event, agentCard) -> { - // Receive any event (Message) to know agent processed the request - if (event instanceof MessageEvent_v0_3) { - firstEventLatch.countDown(); - } - }; + BiConsumer consumer = + (event, agentCard) -> { + // Receive any event (Message) to know agent processed the request + if (event instanceof MessageEvent_v0_3) { + firstEventLatch.countDown(); + } + }; - Consumer errorHandler = error -> { - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - firstEventLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + firstEventLatch.countDown(); + }; // Start streaming subscription CountDownLatch subscriptionLatch = new CountDownLatch(1); @@ -1907,12 +2168,13 @@ public void testMainQueueStaysOpenForNonFinalTasks() throws Exception { getClient().sendMessage(message, List.of(consumer), errorHandler); // Wait for subscription to be established - assertTrue(subscriptionLatch.await(15, TimeUnit.SECONDS), + assertTrue( + subscriptionLatch.await(15, TimeUnit.SECONDS), "Subscription should be established"); // Wait for agent to respond (test agent sends Message, not WORKING status) - assertTrue(firstEventLatch.await(15, TimeUnit.SECONDS), - "Should receive agent response"); + assertTrue( + firstEventLatch.await(15, TimeUnit.SECONDS), "Should receive agent response"); assertNull(errorRef.get()); // Give agent time to finish (task remains in WORKING state - non-final) @@ -1925,33 +2187,39 @@ public void testMainQueueStaysOpenForNonFinalTasks() throws Exception { CountDownLatch resubLatch = new CountDownLatch(1); AtomicReference resubErrorRef = new AtomicReference<>(); - BiConsumer resubConsumer = (event, agentCard) -> { - // We might not receive events immediately, but subscription should succeed - resubLatch.countDown(); - }; + BiConsumer resubConsumer = + (event, agentCard) -> { + // We might not receive events immediately, but subscription should succeed + resubLatch.countDown(); + }; - Consumer resubErrorHandler = error -> { - if (!isStreamClosedError(error)) { - resubErrorRef.set(error); - } - resubLatch.countDown(); - }; + Consumer resubErrorHandler = + error -> { + if (!isStreamClosedError(error)) { + resubErrorRef.set(error); + } + resubLatch.countDown(); + }; // This should succeed - MainQueue is still open for non-final task CountDownLatch resubSubscriptionLatch = new CountDownLatch(1); awaitStreamingSubscription() .whenComplete((unused, throwable) -> resubSubscriptionLatch.countDown()); - getClient().resubscribe(new TaskIdParams_v0_3(taskId), - List.of(resubConsumer), - resubErrorHandler); + getClient() + .resubscribe( + new TaskIdParams_v0_3(taskId), + List.of(resubConsumer), + resubErrorHandler); // Wait for resubscription to be established - assertTrue(resubSubscriptionLatch.await(15, TimeUnit.SECONDS), + assertTrue( + resubSubscriptionLatch.await(15, TimeUnit.SECONDS), "Resubscription should succeed - MainQueue stayed open for non-final task"); // Verify no errors during resubscription - assertNull(resubErrorRef.get(), + assertNull( + resubErrorRef.get(), "Resubscription should not error - validates THE BIG IDEA works end-to-end"); } finally { @@ -1974,47 +2242,47 @@ public void testMainQueueStaysOpenForNonFinalTasks() throws Exception { @Timeout(value = 2, unit = TimeUnit.MINUTES) public void testMainQueueClosesForFinalizedTasks() throws Exception { // Send a message without taskId - server generates one - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .parts(new TextPart_v0_3("complete task")) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE).parts(new TextPart_v0_3("complete task")).build(); CountDownLatch completionLatch = new CountDownLatch(1); AtomicReference errorRef = new AtomicReference<>(); AtomicReference generatedTaskId = new AtomicReference<>(); - BiConsumer consumer = (event, agentCard) -> { - if (event instanceof TaskEvent_v0_3 te) { - generatedTaskId.compareAndSet(null, te.getTask().id()); - // Might get Task with final state - if (te.getTask().status().state().isFinal()) { - completionLatch.countDown(); - } - } else if (event instanceof MessageEvent_v0_3 me) { - // Message is considered a final event - capture taskId from the message - generatedTaskId.compareAndSet(null, me.getMessage().taskId()); - completionLatch.countDown(); - } else if (event instanceof TaskUpdateEvent_v0_3 tue && - tue.getUpdateEvent() instanceof TaskStatusUpdateEvent_v0_3 status) { - generatedTaskId.compareAndSet(null, status.taskId()); - if (status.isFinal()) { - completionLatch.countDown(); - } - } - }; + BiConsumer consumer = + (event, agentCard) -> { + if (event instanceof TaskEvent_v0_3 te) { + generatedTaskId.compareAndSet(null, te.getTask().id()); + // Might get Task with final state + if (te.getTask().status().state().isFinal()) { + completionLatch.countDown(); + } + } else if (event instanceof MessageEvent_v0_3 me) { + // Message is considered a final event - capture taskId from the message + generatedTaskId.compareAndSet(null, me.getMessage().taskId()); + completionLatch.countDown(); + } else if (event instanceof TaskUpdateEvent_v0_3 tue + && tue.getUpdateEvent() instanceof TaskStatusUpdateEvent_v0_3 status) { + generatedTaskId.compareAndSet(null, status.taskId()); + if (status.isFinal()) { + completionLatch.countDown(); + } + } + }; - Consumer errorHandler = error -> { - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - completionLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + completionLatch.countDown(); + }; try { // Send message and wait for completion getClient().sendMessage(message, List.of(consumer), errorHandler); - assertTrue(completionLatch.await(15, TimeUnit.SECONDS), - "Should receive final event"); + assertTrue(completionLatch.await(15, TimeUnit.SECONDS), "Should receive final event"); assertNull(errorRef.get(), "Should not have errors during message send"); String taskId = generatedTaskId.get(); @@ -2027,25 +2295,26 @@ public void testMainQueueClosesForFinalizedTasks() throws Exception { CountDownLatch errorLatch = new CountDownLatch(1); AtomicReference resubErrorRef = new AtomicReference<>(); - Consumer resubErrorHandler = error -> { - if (error == null) { - // Stream completed successfully - ignore, we're waiting for an error - return; - } - if (!isStreamClosedError(error)) { - resubErrorRef.set(error); - } - errorLatch.countDown(); - }; + Consumer resubErrorHandler = + error -> { + if (error == null) { + // Stream completed successfully - ignore, we're waiting for an error + return; + } + if (!isStreamClosedError(error)) { + resubErrorRef.set(error); + } + errorLatch.countDown(); + }; // Attempt resubscription try { - getClient().resubscribe(new TaskIdParams_v0_3(taskId), - List.of(), - resubErrorHandler); + getClient() + .resubscribe(new TaskIdParams_v0_3(taskId), List.of(), resubErrorHandler); // Wait for error - assertTrue(errorLatch.await(15, TimeUnit.SECONDS), + assertTrue( + errorLatch.await(15, TimeUnit.SECONDS), "Should receive error for finalized task"); Throwable error = resubErrorRef.get(); @@ -2055,20 +2324,25 @@ public void testMainQueueClosesForFinalizedTasks() throws Exception { Throwable cause = error; boolean foundTaskNotFound = false; while (cause != null && !foundTaskNotFound) { - if (cause instanceof TaskNotFoundError_v0_3 || - (cause instanceof A2AClientException_v0_3 && - ((A2AClientException_v0_3) cause).getCause() instanceof TaskNotFoundError_v0_3)) { + if (cause instanceof TaskNotFoundError_v0_3 + || (cause instanceof A2AClientException_v0_3 + && ((A2AClientException_v0_3) cause).getCause() + instanceof TaskNotFoundError_v0_3)) { foundTaskNotFound = true; } cause = cause.getCause(); } - assertTrue(foundTaskNotFound, + assertTrue( + foundTaskNotFound, "Should receive TaskNotFoundError - MainQueue closed for finalized task"); } catch (A2AClientException_v0_3 e) { // Exception might be thrown immediately instead of via error handler - assertInstanceOf(TaskNotFoundError_v0_3.class, e.getCause(), - "Should fail with TaskNotFoundError - MainQueue cleaned up for finalized task"); + assertInstanceOf( + TaskNotFoundError_v0_3.class, + e.getCause(), + "Should fail with TaskNotFoundError - MainQueue cleaned up for finalized" + + " task"); } } finally { diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerWithAuthTest_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerWithAuthTest_v0_3.java index 525dbb798..21710e96b 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerWithAuthTest_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerWithAuthTest_v0_3.java @@ -5,6 +5,10 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.restassured.RestAssured; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.specification.RequestSpecification; +import jakarta.ws.rs.core.MediaType; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -13,13 +17,8 @@ import java.util.Base64; import java.util.List; import java.util.Map; - -import io.restassured.RestAssured; -import io.restassured.config.ObjectMapperConfig; -import io.restassured.specification.RequestSpecification; -import jakarta.ws.rs.core.MediaType; -import org.a2aproject.sdk.compat03.client.Client_v0_3; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.Client_v0_3; import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskMapper_v0_3; import org.a2aproject.sdk.compat03.spec.A2AClientException_v0_3; @@ -27,10 +26,10 @@ import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3; import org.a2aproject.sdk.compat03.spec.HTTPAuthSecurityScheme_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.junit.jupiter.api.Test; @@ -51,11 +50,12 @@ protected static String getEncodedCredentials() { return Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); } - protected static final Task_v0_3 MINIMAL_TASK = new Task_v0_3.Builder() - .id("task-123") - .contextId("session-xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .build(); + protected static final Task_v0_3 MINIMAL_TASK = + new Task_v0_3.Builder() + .id("task-123") + .contextId("session-xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .build(); protected final int serverPort; private Client_v0_3 authenticatedClient; @@ -89,12 +89,11 @@ protected Client_v0_3 getUnauthenticatedClient() throws A2AClientException_v0_3 private Client_v0_3 createAuthenticatedClient() throws A2AClientException_v0_3 { AgentCard_v0_3 agentCard = createTestAgentCard(); - ClientConfig_v0_3 clientConfig = new ClientConfig_v0_3.Builder() - .setStreaming(false) - .build(); + ClientConfig_v0_3 clientConfig = + new ClientConfig_v0_3.Builder().setStreaming(false).build(); - ClientBuilder_v0_3 clientBuilder = Client_v0_3.builder(agentCard) - .clientConfig(clientConfig); + ClientBuilder_v0_3 clientBuilder = + Client_v0_3.builder(agentCard).clientConfig(clientConfig); configureTransportWithAuth(clientBuilder); @@ -103,12 +102,11 @@ private Client_v0_3 createAuthenticatedClient() throws A2AClientException_v0_3 { private Client_v0_3 createUnauthenticatedClient() throws A2AClientException_v0_3 { AgentCard_v0_3 agentCard = createTestAgentCard(); - ClientConfig_v0_3 clientConfig = new ClientConfig_v0_3.Builder() - .setStreaming(false) - .build(); + ClientConfig_v0_3 clientConfig = + new ClientConfig_v0_3.Builder().setStreaming(false).build(); - ClientBuilder_v0_3 clientBuilder = Client_v0_3.builder(agentCard) - .clientConfig(clientConfig); + ClientBuilder_v0_3 clientBuilder = + Client_v0_3.builder(agentCard).clientConfig(clientConfig); configureTransport(clientBuilder); @@ -122,34 +120,38 @@ private AgentCard_v0_3 createTestAgentCard() { .url(getTransportUrl()) .version("1.0") .preferredTransport(getTransportProtocol()) - .capabilities(new AgentCapabilities_v0_3.Builder() - .streaming(false) - .pushNotifications(false) - .stateTransitionHistory(false) - .build()) + .capabilities( + new AgentCapabilities_v0_3.Builder() + .streaming(false) + .pushNotifications(false) + .stateTransitionHistory(false) + .build()) .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) .skills(List.of()) - .additionalInterfaces(List.of(new AgentInterface_v0_3(getTransportProtocol(), getTransportUrl()))) - .securitySchemes(Map.of( - BASIC_AUTH_SCHEME_NAME, - new HTTPAuthSecurityScheme_v0_3.Builder() - .scheme("basic") - .description("HTTP Basic authentication") - .build())) + .additionalInterfaces( + List.of(new AgentInterface_v0_3(getTransportProtocol(), getTransportUrl()))) + .securitySchemes( + Map.of( + BASIC_AUTH_SCHEME_NAME, + new HTTPAuthSecurityScheme_v0_3.Builder() + .scheme("basic") + .description("HTTP Basic authentication") + .build())) .security(List.of(Map.of(BASIC_AUTH_SCHEME_NAME, List.of()))) .build(); } protected static RequestSpecification given() { return RestAssured.given() - .config(RestAssured.config() - .objectMapperConfig(new ObjectMapperConfig(V10GsonObjectMapper_v0_3.INSTANCE))); + .config( + RestAssured.config() + .objectMapperConfig( + new ObjectMapperConfig(V10GsonObjectMapper_v0_3.INSTANCE))); } protected RequestSpecification givenAuthenticated() { - return given() - .auth().basic(TEST_USERNAME, TEST_PASSWORD); + return given().auth().basic(TEST_USERNAME, TEST_PASSWORD); } protected RequestSpecification givenUnauthenticated() { @@ -159,33 +161,38 @@ protected RequestSpecification givenUnauthenticated() { protected void saveTaskInTaskStore(Task_v0_3 task) throws Exception { org.a2aproject.sdk.spec.Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(task); - HttpClient httpClient = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); + HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/task")) - .header("Content-Type", MediaType.APPLICATION_JSON) - .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(v10Task))) - .build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task")) + .header("Content-Type", MediaType.APPLICATION_JSON) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(v10Task))) + .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + httpClient.send( + request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200 && response.statusCode() != 204) { - throw new RuntimeException("Failed to save task: " + response.statusCode() + " " + response.body()); + throw new RuntimeException( + "Failed to save task: " + response.statusCode() + " " + response.body()); } } protected void deleteTaskInTaskStore(String taskId) throws Exception { HttpClient httpClient = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) - .DELETE() - .build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) + .DELETE() + .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200 && response.statusCode() != 204) { - throw new RuntimeException("Failed to delete task: " + response.statusCode() + " " + response.body()); + throw new RuntimeException( + "Failed to delete task: " + response.statusCode() + " " + response.body()); } } @@ -194,12 +201,16 @@ public void testGetTaskRequiresAuthenticationUnauthenticated() throws Exception saveTaskInTaskStore(MINIMAL_TASK); Client_v0_3 unauthClient = getUnauthenticatedClient(); - A2AClientException_v0_3 error = assertThrows(A2AClientException_v0_3.class, () -> { - unauthClient.getTask(new TaskQueryParams_v0_3(MINIMAL_TASK.id())); - }); - assertTrue(error.getMessage().contains("Authentication failed") || - error.getMessage().contains("401") || - error.getMessage().contains("Unauthorized"), + A2AClientException_v0_3 error = + assertThrows( + A2AClientException_v0_3.class, + () -> { + unauthClient.getTask(new TaskQueryParams_v0_3(MINIMAL_TASK.id())); + }); + assertTrue( + error.getMessage().contains("Authentication failed") + || error.getMessage().contains("401") + || error.getMessage().contains("Unauthorized"), "Expected authentication error, got: " + error.getMessage()); deleteTaskInTaskStore(MINIMAL_TASK.id()); @@ -219,10 +230,7 @@ public void testGetTaskWithAuthentication() throws Exception { @Test public void testGetAgentCardIsPublic() { - givenUnauthenticated() - .get("/.well-known/agent-card.json") - .then() - .statusCode(200); + givenUnauthenticated().get("/.well-known/agent-card.json").then().statusCode(200); } @Test @@ -231,7 +239,10 @@ public void testBasicAuthWorksViaHttp() throws Exception { givenAuthenticated() .contentType("application/json") - .body("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tasks/get\",\"params\":{\"id\":\"" + MINIMAL_TASK.id() + "\"}}") + .body( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tasks/get\",\"params\":{\"id\":\"" + + MINIMAL_TASK.id() + + "\"}}") .post("/") .then() .statusCode(200); diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerWithTaskAuthorizationTest_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerWithTaskAuthorizationTest_v0_3.java index 154701fc8..9e214472a 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerWithTaskAuthorizationTest_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AbstractA2AServerWithTaskAuthorizationTest_v0_3.java @@ -16,9 +16,8 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; - -import org.a2aproject.sdk.compat03.client.Client_v0_3; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.Client_v0_3; import org.a2aproject.sdk.compat03.client.TaskEvent_v0_3; import org.a2aproject.sdk.compat03.client.TaskUpdateEvent_v0_3; import org.a2aproject.sdk.compat03.client.config.ClientConfig_v0_3; @@ -28,12 +27,12 @@ import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3; import org.a2aproject.sdk.compat03.spec.HTTPAuthSecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; import org.junit.jupiter.api.Test; @@ -66,12 +65,16 @@ protected AbstractA2AServerWithTaskAuthorizationTest_v0_3(int serverPort) { protected abstract String getTransportUrl(); - protected abstract void configureTransportWithCredentials(ClientBuilder_v0_3 builder, String username, String password); + protected abstract void configureTransportWithCredentials( + ClientBuilder_v0_3 builder, String username, String password); - protected Client_v0_3 createClient(String username, String password) throws A2AClientException_v0_3 { + protected Client_v0_3 createClient(String username, String password) + throws A2AClientException_v0_3 { AgentCard_v0_3 agentCard = createTestAgentCard(); - ClientConfig_v0_3 clientConfig = new ClientConfig_v0_3.Builder().setStreaming(false).build(); - ClientBuilder_v0_3 clientBuilder = Client_v0_3.builder(agentCard).clientConfig(clientConfig); + ClientConfig_v0_3 clientConfig = + new ClientConfig_v0_3.Builder().setStreaming(false).build(); + ClientBuilder_v0_3 clientBuilder = + Client_v0_3.builder(agentCard).clientConfig(clientConfig); configureTransportWithCredentials(clientBuilder, username, password); return clientBuilder.build(); } @@ -83,56 +86,66 @@ private AgentCard_v0_3 createTestAgentCard() { .url(getTransportUrl()) .version("1.0") .preferredTransport(getTransportProtocol()) - .capabilities(new AgentCapabilities_v0_3.Builder() - .streaming(false) - .pushNotifications(false) - .stateTransitionHistory(false) - .build()) + .capabilities( + new AgentCapabilities_v0_3.Builder() + .streaming(false) + .pushNotifications(false) + .stateTransitionHistory(false) + .build()) .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) .skills(List.of()) - .additionalInterfaces(List.of(new AgentInterface_v0_3(getTransportProtocol(), getTransportUrl()))) - .securitySchemes(Map.of( - BASIC_AUTH_SCHEME_NAME, - new HTTPAuthSecurityScheme_v0_3.Builder() - .scheme("basic") - .description("HTTP Basic authentication") - .build())) + .additionalInterfaces( + List.of(new AgentInterface_v0_3(getTransportProtocol(), getTransportUrl()))) + .securitySchemes( + Map.of( + BASIC_AUTH_SCHEME_NAME, + new HTTPAuthSecurityScheme_v0_3.Builder() + .scheme("basic") + .description("HTTP Basic authentication") + .build())) .security(List.of(Map.of(BASIC_AUTH_SCHEME_NAME, List.of()))) .build(); } protected static String getEncodedCredentials(String username, String password) { - return Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder() + .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); } - protected Task_v0_3 sendMessageAndGetTask(Client_v0_3 client, String messageText) throws Exception { - Message_v0_3 message = new Message_v0_3.Builder() - .messageId(UUID.randomUUID().toString()) - .role(Message_v0_3.Role.USER) - .parts(new TextPart_v0_3("a2a-local:" + messageText)) - .build(); + protected Task_v0_3 sendMessageAndGetTask(Client_v0_3 client, String messageText) + throws Exception { + Message_v0_3 message = + new Message_v0_3.Builder() + .messageId(UUID.randomUUID().toString()) + .role(Message_v0_3.Role.USER) + .parts(new TextPart_v0_3("a2a-local:" + messageText)) + .build(); CountDownLatch latch = new CountDownLatch(1); AtomicReference receivedTask = new AtomicReference<>(); AtomicReference errorRef = new AtomicReference<>(); - client.sendMessage(message, List.of((event, agentCard) -> { - if (event instanceof TaskEvent_v0_3 te) { - receivedTask.set(te.getTask()); - if (te.getTask().status().state() == TaskState_v0_3.COMPLETED) { + client.sendMessage( + message, + List.of( + (event, agentCard) -> { + if (event instanceof TaskEvent_v0_3 te) { + receivedTask.set(te.getTask()); + if (te.getTask().status().state() == TaskState_v0_3.COMPLETED) { + latch.countDown(); + } + } else if (event instanceof TaskUpdateEvent_v0_3 tue) { + receivedTask.set(tue.getTask()); + if (tue.getTask().status().state() == TaskState_v0_3.COMPLETED) { + latch.countDown(); + } + } + }), + error -> { + errorRef.set(error); latch.countDown(); - } - } else if (event instanceof TaskUpdateEvent_v0_3 tue) { - receivedTask.set(tue.getTask()); - if (tue.getTask().status().state() == TaskState_v0_3.COMPLETED) { - latch.countDown(); - } - } - }), error -> { - errorRef.set(error); - latch.countDown(); - }); + }); assertTrue(latch.await(10, TimeUnit.SECONDS), "Task should complete within timeout"); assertNull(errorRef.get(), "Should not have received an error: " + errorRef.get()); @@ -159,8 +172,10 @@ public void testNonOwnerCannotGetTask() throws Exception { Task_v0_3 task = sendMessageAndGetTask(clientA, "non-owner-get-test"); Client_v0_3 clientB = createClient(USER_B, USER_B_PASSWORD); - A2AClientException_v0_3 error = assertThrows(A2AClientException_v0_3.class, () -> - clientB.getTask(new TaskQueryParams_v0_3(task.id()))); + A2AClientException_v0_3 error = + assertThrows( + A2AClientException_v0_3.class, + () -> clientB.getTask(new TaskQueryParams_v0_3(task.id()))); assertInstanceOf(TaskNotFoundError_v0_3.class, error.getCause()); } @@ -185,8 +200,10 @@ public void testNonOwnerCannotCancelTask() throws Exception { Task_v0_3 task = sendMessageAndGetTask(clientA, "non-owner-cancel-test"); Client_v0_3 clientB = createClient(USER_B, USER_B_PASSWORD); - A2AClientException_v0_3 error = assertThrows(A2AClientException_v0_3.class, () -> - clientB.cancelTask(new TaskIdParams_v0_3(task.id()))); + A2AClientException_v0_3 error = + assertThrows( + A2AClientException_v0_3.class, + () -> clientB.cancelTask(new TaskIdParams_v0_3(task.id()))); assertInstanceOf(TaskNotFoundError_v0_3.class, error.getCause()); } @@ -197,10 +214,16 @@ public void testUnauthorizedLooksLikeNotFound() throws Exception { Client_v0_3 clientB = createClient(USER_B, USER_B_PASSWORD); - A2AClientException_v0_3 unauthorizedError = assertThrows(A2AClientException_v0_3.class, () -> - clientB.getTask(new TaskQueryParams_v0_3(task.id()))); - A2AClientException_v0_3 notFoundError = assertThrows(A2AClientException_v0_3.class, () -> - clientB.getTask(new TaskQueryParams_v0_3(UUID.randomUUID().toString()))); + A2AClientException_v0_3 unauthorizedError = + assertThrows( + A2AClientException_v0_3.class, + () -> clientB.getTask(new TaskQueryParams_v0_3(task.id()))); + A2AClientException_v0_3 notFoundError = + assertThrows( + A2AClientException_v0_3.class, + () -> + clientB.getTask( + new TaskQueryParams_v0_3(UUID.randomUUID().toString()))); assertInstanceOf(TaskNotFoundError_v0_3.class, unauthorizedError.getCause()); assertInstanceOf(TaskNotFoundError_v0_3.class, notFoundError.getCause()); diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AgentCardProducer_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AgentCardProducer_v0_3.java index 576fdae06..22c4b5fe9 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AgentCardProducer_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AgentCardProducer_v0_3.java @@ -1,5 +1,10 @@ package org.a2aproject.sdk.compat03.conversion; +import io.quarkus.arc.DefaultBean; +import io.quarkus.arc.profile.IfBuildProfile; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -7,20 +12,12 @@ import java.util.List; import java.util.Map; import java.util.Properties; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Inject; - import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.HTTPAuthSecurityScheme_v0_3; import org.a2aproject.sdk.server.PublicAgentCard; import org.eclipse.microprofile.config.inject.ConfigProperty; -import io.quarkus.arc.DefaultBean; -import io.quarkus.arc.profile.IfBuildProfile; - /** * Produces v0.3 AgentCard for compat layer tests. * Duplicated from v1.0 tests/server-common to avoid dependency on v1.0 test infrastructure. @@ -45,30 +42,33 @@ public AgentCard_v0_3 createTestAgentCard() { String preferredTransport = loadPreferredTransportFromProperties(); // v0.3 uses 'url' field for primary endpoint - String url = "grpc".equalsIgnoreCase(preferredTransport) - ? "localhost:" + port - : "http://localhost:" + port; + String url = + "grpc".equalsIgnoreCase(preferredTransport) + ? "localhost:" + port + : "http://localhost:" + port; - AgentCard_v0_3.Builder builder = new AgentCard_v0_3.Builder() - .name("compat-0.3-test-agent") - .description("Test agent for v0.3 compatibility layer") - .url(url) - .version("1.0.0") - .preferredTransport(preferredTransport) - .capabilities(new AgentCapabilities_v0_3(true, true, true, List.of())) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .additionalInterfaces(new ArrayList<>()); + AgentCard_v0_3.Builder builder = + new AgentCard_v0_3.Builder() + .name("compat-0.3-test-agent") + .description("Test agent for v0.3 compatibility layer") + .url(url) + .version("1.0.0") + .preferredTransport(preferredTransport) + .capabilities(new AgentCapabilities_v0_3(true, true, true, List.of())) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .additionalInterfaces(new ArrayList<>()); if (securityEnabled) { - builder.securitySchemes(Map.of( - BASIC_AUTH_SCHEME_NAME, - new HTTPAuthSecurityScheme_v0_3.Builder() - .scheme("basic") - .description("HTTP Basic authentication") - .build())) - .security(List.of(Map.of(BASIC_AUTH_SCHEME_NAME, List.of()))); + builder.securitySchemes( + Map.of( + BASIC_AUTH_SCHEME_NAME, + new HTTPAuthSecurityScheme_v0_3.Builder() + .scheme("basic") + .description("HTTP Basic authentication") + .build())) + .security(List.of(Map.of(BASIC_AUTH_SCHEME_NAME, List.of()))); } return builder.build(); diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AuthTestProfile_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AuthTestProfile_v0_3.java index 6e8e181d1..b26ce1990 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AuthTestProfile_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/AuthTestProfile_v0_3.java @@ -1,8 +1,7 @@ package org.a2aproject.sdk.compat03.conversion; -import java.util.Map; - import io.quarkus.test.junit.QuarkusTestProfile; +import java.util.Map; public class AuthTestProfile_v0_3 implements QuarkusTestProfile { @@ -31,8 +30,7 @@ public Map getConfigOverrides() { Map.entry("quarkus.http.auth.basic", "true"), // Enable proactive authentication - authenticate at HTTP layer before route handler - Map.entry("quarkus.http.auth.proactive", "true") - ); + Map.entry("quarkus.http.auth.proactive", "true")); } @Override diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/GsonObjectMapper_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/GsonObjectMapper_v0_3.java index bc1c6cfbf..3f68dfe19 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/GsonObjectMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/GsonObjectMapper_v0_3.java @@ -16,13 +16,13 @@ public class GsonObjectMapper_v0_3 implements ObjectMapper { public static final GsonObjectMapper_v0_3 INSTANCE = new GsonObjectMapper_v0_3(); - private GsonObjectMapper_v0_3() { - } + private GsonObjectMapper_v0_3() {} @Override public Object deserialize(ObjectMapperDeserializationContext context) { try { - return JsonUtil_v0_3.fromJson(context.getDataToDeserialize().asString(), context.getType()); + return JsonUtil_v0_3.fromJson( + context.getDataToDeserialize().asString(), context.getType()); } catch (JsonProcessingException_v0_3 ex) { throw new RuntimeException(ex); } diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/PushNotificationPayloadFormatter_v0_3_Test.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/PushNotificationPayloadFormatter_v0_3_Test.java index 22bb542dc..e4663bef3 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/PushNotificationPayloadFormatter_v0_3_Test.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/PushNotificationPayloadFormatter_v0_3_Test.java @@ -30,10 +30,12 @@ void targetVersionIs03() { @Test void formatsTaskEventAsV03Task() { - Task task = Task.builder() - .id("t1").contextId("c1") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + Task task = + Task.builder() + .id("t1") + .contextId("c1") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); String payload = formatter.formatPayload(task, task); @@ -45,15 +47,19 @@ void formatsTaskEventAsV03Task() { @Test void formatsStatusUpdateUsingTaskSnapshot() { - Task snapshot = Task.builder() - .id("t1").contextId("c1") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId("t1").contextId("c1") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + Task snapshot = + Task.builder() + .id("t1") + .contextId("c1") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId("t1") + .contextId("c1") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); String payload = formatter.formatPayload(event, snapshot); @@ -64,16 +70,19 @@ void formatsStatusUpdateUsingTaskSnapshot() { @Test void skipsMessageEvents() { - Task snapshot = Task.builder() - .id("t1").contextId("c1") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - - Message message = Message.builder() - .messageId("m1") - .role(Message.Role.ROLE_AGENT) - .parts(new TextPart("hello")) - .build(); + Task snapshot = + Task.builder() + .id("t1") + .contextId("c1") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + + Message message = + Message.builder() + .messageId("m1") + .role(Message.Role.ROLE_AGENT) + .parts(new TextPart("hello")) + .build(); String payload = formatter.formatPayload(message, snapshot); @@ -82,10 +91,12 @@ void skipsMessageEvents() { @Test void returnsNullWhenSnapshotIsNull() { - Task task = Task.builder() - .id("t1").contextId("c1") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + Task task = + Task.builder() + .id("t1") + .contextId("c1") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); String payload = formatter.formatPayload(task, null); diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TaskAuthorizationTestProfile_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TaskAuthorizationTestProfile_v0_3.java index 79f8784fc..9f777ca80 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TaskAuthorizationTestProfile_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TaskAuthorizationTestProfile_v0_3.java @@ -3,8 +3,6 @@ import java.util.HashMap; import java.util.Map; -import io.quarkus.test.junit.QuarkusTestProfile; - public class TaskAuthorizationTestProfile_v0_3 extends AuthTestProfile_v0_3 { @Override diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestHttpClient_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestHttpClient_v0_3.java index a99f49da2..308737001 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestHttpClient_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestHttpClient_v0_3.java @@ -1,5 +1,8 @@ package org.a2aproject.sdk.compat03.conversion; +import io.quarkus.arc.profile.IfBuildProfile; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Alternative; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -8,10 +11,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; - -import jakarta.enterprise.context.Dependent; -import jakarta.enterprise.inject.Alternative; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.client.http.ServerSentEvent; @@ -19,8 +18,6 @@ import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.spec.Task; -import io.quarkus.arc.profile.IfBuildProfile; - /** * Test implementation of A2AHttpClient for push notification testing. * Duplicated from v1.0 tests/server-common to avoid dependency on v1.0 test infrastructure. @@ -49,6 +46,7 @@ public DeleteBuilder createDelete() { class TestPostBuilder implements A2AHttpClient.PostBuilder { private volatile String body; + @Override public PostBuilder body(String body) { this.body = body; @@ -85,7 +83,11 @@ public String body() { } @Override - public CompletableFuture postAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { + public CompletableFuture postAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) + throws IOException, InterruptedException { return null; } diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestIdentityProvider_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestIdentityProvider_v0_3.java index ad582aa6c..a7d4f0e24 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestIdentityProvider_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestIdentityProvider_v0_3.java @@ -1,7 +1,5 @@ package org.a2aproject.sdk.compat03.conversion; -import jakarta.enterprise.context.ApplicationScoped; - import io.quarkus.arc.Unremovable; import io.quarkus.arc.properties.IfBuildProperty; import io.quarkus.security.identity.AuthenticationRequestContext; @@ -9,6 +7,7 @@ import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped @Unremovable @@ -16,13 +15,16 @@ public class TestIdentityProvider_v0_3 implements SecurityIdentityAugmentor { @Override - public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context) { + public Uni augment( + SecurityIdentity identity, AuthenticationRequestContext context) { if (identity.isAnonymous()) { - return Uni.createFrom().item(QuarkusSecurityIdentity.builder() - .setPrincipal(() -> "testuser") - .addRole("user") - .setAnonymous(false) - .build()); + return Uni.createFrom() + .item( + QuarkusSecurityIdentity.builder() + .setPrincipal(() -> "testuser") + .addRole("user") + .setAnonymous(false) + .build()); } return Uni.createFrom().item(identity); } diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestUtilsBean_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestUtilsBean_v0_3.java index 3800880d4..a5c381e3e 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestUtilsBean_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/TestUtilsBean_v0_3.java @@ -1,16 +1,14 @@ package org.a2aproject.sdk.compat03.conversion; +import io.quarkus.arc.profile.IfBuildProfile; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; - import org.a2aproject.sdk.server.events.QueueManager; import org.a2aproject.sdk.server.tasks.PushNotificationConfigStore; import org.a2aproject.sdk.server.tasks.TaskStore; import org.a2aproject.sdk.spec.Event; -import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.a2aproject.sdk.spec.Task; - -import io.quarkus.arc.profile.IfBuildProfile; +import org.a2aproject.sdk.spec.TaskPushNotificationConfig; /** * Contains utilities to interact with the server side for the v0.3 compat tests. @@ -26,14 +24,11 @@ @IfBuildProfile("test") public class TestUtilsBean_v0_3 { - @Inject - TaskStore taskStore; + @Inject TaskStore taskStore; - @Inject - QueueManager queueManager; + @Inject QueueManager queueManager; - @Inject - PushNotificationConfigStore pushNotificationConfigStore; + @Inject PushNotificationConfigStore pushNotificationConfigStore; public void saveTask(Task task) { taskStore.save(task, false); @@ -63,9 +58,11 @@ public void deleteTaskPushNotificationConfig(String taskId, String configId) { pushNotificationConfigStore.deleteInfo(taskId, configId); } - public void saveTaskPushNotificationConfig(String taskId, TaskPushNotificationConfig notificationConfig) { + public void saveTaskPushNotificationConfig( + String taskId, TaskPushNotificationConfig notificationConfig) { pushNotificationConfigStore.setInfo( - TaskPushNotificationConfig.builder(notificationConfig).taskId(taskId).build(), "0.3"); + TaskPushNotificationConfig.builder(notificationConfig).taskId(taskId).build(), + "0.3"); } /** @@ -96,10 +93,12 @@ public void awaitQueuePollerStart(String taskId) throws InterruptedException { * @return true if the count stabilized at the expected value, false if timeout occurred * @throws InterruptedException if interrupted while waiting */ - public boolean awaitChildQueueCountStable(String taskId, int expectedCount, long timeoutMs) throws InterruptedException { + public boolean awaitChildQueueCountStable(String taskId, int expectedCount, long timeoutMs) + throws InterruptedException { long endTime = System.currentTimeMillis() + timeoutMs; int consecutiveMatches = 0; - final int requiredMatches = 3; // Count must match 3 times in a row (150ms) to be considered stable + final int requiredMatches = + 3; // Count must match 3 times in a row (150ms) to be considered stable while (System.currentTimeMillis() < endTime) { int count = queueManager.getActiveChildQueueCount(taskId); diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/V10GsonObjectMapper_v0_3.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/V10GsonObjectMapper_v0_3.java index 04439ab28..734070f0d 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/V10GsonObjectMapper_v0_3.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/V10GsonObjectMapper_v0_3.java @@ -16,8 +16,7 @@ public class V10GsonObjectMapper_v0_3 implements ObjectMapper { public static final V10GsonObjectMapper_v0_3 INSTANCE = new V10GsonObjectMapper_v0_3(); - private V10GsonObjectMapper_v0_3() { - } + private V10GsonObjectMapper_v0_3() {} @Override public Object deserialize(ObjectMapperDeserializationContext context) { diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3_Test.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3_Test.java index 2b562ccc0..49b90c618 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3_Test.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/FileContentMapper_v0_3_Test.java @@ -1,5 +1,8 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + import org.a2aproject.sdk.compat03.spec.FileWithBytes_v0_3; import org.a2aproject.sdk.compat03.spec.FileWithUri_v0_3; import org.a2aproject.sdk.spec.FileContent; @@ -7,9 +10,6 @@ import org.a2aproject.sdk.spec.FileWithUri; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - class FileContentMapper_v0_3_Test { @Test @@ -24,7 +24,8 @@ void fileWithBytesToV10PreservesName() { @Test void fileWithUriToV10PreservesName() { - FileWithUri_v0_3 v03 = new FileWithUri_v0_3("text/plain", "readme.txt", "file:///path/to/readme.txt"); + FileWithUri_v0_3 v03 = + new FileWithUri_v0_3("text/plain", "readme.txt", "file:///path/to/readme.txt"); FileContent v10 = FileContentMapper_v0_3.INSTANCE.toV10(v03); FileWithUri result = assertInstanceOf(FileWithUri.class, v10); assertEquals("text/plain", result.mimeType()); @@ -35,7 +36,9 @@ void fileWithUriToV10PreservesName() { @Test void fileWithBytesFromV10PreservesName() { FileWithBytes v10 = new FileWithBytes("image/png", "photo.png", "aGVsbG8="); - FileWithBytes_v0_3 result = assertInstanceOf(FileWithBytes_v0_3.class, FileContentMapper_v0_3.INSTANCE.fromV10(v10)); + FileWithBytes_v0_3 result = + assertInstanceOf( + FileWithBytes_v0_3.class, FileContentMapper_v0_3.INSTANCE.fromV10(v10)); assertEquals("image/png", result.mimeType()); assertEquals("photo.png", result.name()); assertEquals("aGVsbG8=", result.bytes()); @@ -44,7 +47,9 @@ void fileWithBytesFromV10PreservesName() { @Test void fileWithUriFromV10PreservesName() { FileWithUri v10 = new FileWithUri("text/plain", "readme.txt", "file:///path/to/readme.txt"); - FileWithUri_v0_3 result = assertInstanceOf(FileWithUri_v0_3.class, FileContentMapper_v0_3.INSTANCE.fromV10(v10)); + FileWithUri_v0_3 result = + assertInstanceOf( + FileWithUri_v0_3.class, FileContentMapper_v0_3.INSTANCE.fromV10(v10)); assertEquals("text/plain", result.mimeType()); assertEquals("readme.txt", result.name()); assertEquals("file:///path/to/readme.txt", result.uri()); @@ -53,7 +58,8 @@ void fileWithUriFromV10PreservesName() { @Test void nullNameInV03BecomesEmptyStringInV10() { FileWithBytes_v0_3 v03 = new FileWithBytes_v0_3("image/png", null, "aGVsbG8="); - FileWithBytes result = assertInstanceOf(FileWithBytes.class, FileContentMapper_v0_3.INSTANCE.toV10(v03)); + FileWithBytes result = + assertInstanceOf(FileWithBytes.class, FileContentMapper_v0_3.INSTANCE.toV10(v03)); assertEquals("", result.name()); } } diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3_Test.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3_Test.java index 1512f80d9..27d72c587 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3_Test.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskMapper_v0_3_Test.java @@ -1,10 +1,13 @@ package org.a2aproject.sdk.compat03.conversion.mappers.domain; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.compat03.spec.Artifact_v0_3; import org.a2aproject.sdk.compat03.spec.DataPart_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; @@ -15,10 +18,6 @@ import org.a2aproject.sdk.spec.Task; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - /** * Test for {@link TaskMapper_v0_3} to verify conversion of fully populated Task objects * between v0.3 and v1.0 protocol versions. @@ -30,54 +29,49 @@ void testFullyPopulatedTaskConversion() { // Create a fully populated v0.3 Task OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC); - Message_v0_3 v03Message = new Message_v0_3( - Message_v0_3.Role.USER, - List.of(new TextPart_v0_3("Hello, agent!")), - "msg-001", - "ctx-001", - "task-001", - List.of("ref-task-001"), - Map.of("key", "value"), - List.of("ext1") - ); - - TaskStatus_v0_3 v03Status = new TaskStatus_v0_3( - TaskState_v0_3.WORKING, - v03Message, - now - ); - - Artifact_v0_3 v03Artifact = new Artifact_v0_3( - "artifact-001", - "Test Artifact", - "A test artifact", - List.of( - new TextPart_v0_3("Response text"), - new DataPart_v0_3(Map.of("result", "success")) - ), - Map.of("artifactMeta", "value"), - List.of("artifactExt") - ); - - Message_v0_3 v03HistoryMessage = new Message_v0_3( - Message_v0_3.Role.AGENT, - List.of(new TextPart_v0_3("Agent response")), - "msg-002", - "ctx-001", - "task-001", - null, - null, - null - ); - - Task_v0_3 v03Task = new Task_v0_3( - "task-001", - "ctx-001", - v03Status, - List.of(v03Artifact), - List.of(v03HistoryMessage), - Map.of("taskMeta", "taskValue") - ); + Message_v0_3 v03Message = + new Message_v0_3( + Message_v0_3.Role.USER, + List.of(new TextPart_v0_3("Hello, agent!")), + "msg-001", + "ctx-001", + "task-001", + List.of("ref-task-001"), + Map.of("key", "value"), + List.of("ext1")); + + TaskStatus_v0_3 v03Status = new TaskStatus_v0_3(TaskState_v0_3.WORKING, v03Message, now); + + Artifact_v0_3 v03Artifact = + new Artifact_v0_3( + "artifact-001", + "Test Artifact", + "A test artifact", + List.of( + new TextPart_v0_3("Response text"), + new DataPart_v0_3(Map.of("result", "success"))), + Map.of("artifactMeta", "value"), + List.of("artifactExt")); + + Message_v0_3 v03HistoryMessage = + new Message_v0_3( + Message_v0_3.Role.AGENT, + List.of(new TextPart_v0_3("Agent response")), + "msg-002", + "ctx-001", + "task-001", + null, + null, + null); + + Task_v0_3 v03Task = + new Task_v0_3( + "task-001", + "ctx-001", + v03Status, + List.of(v03Artifact), + List.of(v03HistoryMessage), + Map.of("taskMeta", "taskValue")); // Convert v0.3 → v1.0 Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(v03Task); @@ -90,17 +84,22 @@ void testFullyPopulatedTaskConversion() { // Verify status conversion assertNotNull(v10Task.status()); - assertEquals(org.a2aproject.sdk.spec.TaskState.TASK_STATE_WORKING, v10Task.status().state()); + assertEquals( + org.a2aproject.sdk.spec.TaskState.TASK_STATE_WORKING, v10Task.status().state()); assertEquals(now, v10Task.status().timestamp()); // Verify status message conversion assertNotNull(v10Task.status().message()); - assertEquals(org.a2aproject.sdk.spec.Message.Role.ROLE_USER, v10Task.status().message().role()); + assertEquals( + org.a2aproject.sdk.spec.Message.Role.ROLE_USER, v10Task.status().message().role()); assertEquals("msg-001", v10Task.status().message().messageId()); assertEquals("ctx-001", v10Task.status().message().contextId()); assertEquals("task-001", v10Task.status().message().taskId()); assertEquals(1, v10Task.status().message().parts().size()); - assertEquals("Hello, agent!", ((org.a2aproject.sdk.spec.TextPart) v10Task.status().message().parts().get(0)).text()); + assertEquals( + "Hello, agent!", + ((org.a2aproject.sdk.spec.TextPart) v10Task.status().message().parts().get(0)) + .text()); // Verify artifacts conversion assertNotNull(v10Task.artifacts()); @@ -110,7 +109,9 @@ void testFullyPopulatedTaskConversion() { assertEquals("Test Artifact", v10Artifact.name()); assertEquals("A test artifact", v10Artifact.description()); assertEquals(2, v10Artifact.parts().size()); - assertEquals("Response text", ((org.a2aproject.sdk.spec.TextPart) v10Artifact.parts().get(0)).text()); + assertEquals( + "Response text", + ((org.a2aproject.sdk.spec.TextPart) v10Artifact.parts().get(0)).text()); // Verify history conversion assertNotNull(v10Task.history()); @@ -118,7 +119,9 @@ void testFullyPopulatedTaskConversion() { org.a2aproject.sdk.spec.Message v10HistoryMsg = v10Task.history().get(0); assertEquals(org.a2aproject.sdk.spec.Message.Role.ROLE_AGENT, v10HistoryMsg.role()); assertEquals("msg-002", v10HistoryMsg.messageId()); - assertEquals("Agent response", ((org.a2aproject.sdk.spec.TextPart) v10HistoryMsg.parts().get(0)).text()); + assertEquals( + "Agent response", + ((org.a2aproject.sdk.spec.TextPart) v10HistoryMsg.parts().get(0)).text()); // Convert v1.0 → v0.3 (round trip) Task_v0_3 v03TaskRoundTrip = TaskMapper_v0_3.INSTANCE.fromV10(v10Task); @@ -140,18 +143,10 @@ void testMinimalTaskConversion() { // Test with minimal Task (no artifacts, no history, no metadata) OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC); - TaskStatus_v0_3 v03Status = new TaskStatus_v0_3( - TaskState_v0_3.SUBMITTED - ); + TaskStatus_v0_3 v03Status = new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED); - Task_v0_3 v03Task = new Task_v0_3( - "task-minimal", - "ctx-minimal", - v03Status, - null, - null, - null - ); + Task_v0_3 v03Task = + new Task_v0_3("task-minimal", "ctx-minimal", v03Status, null, null, null); // Convert v0.3 → v1.0 Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(v03Task); @@ -160,7 +155,8 @@ void testMinimalTaskConversion() { assertNotNull(v10Task); assertEquals("task-minimal", v10Task.id()); assertEquals("ctx-minimal", v10Task.contextId()); - assertEquals(org.a2aproject.sdk.spec.TaskState.TASK_STATE_SUBMITTED, v10Task.status().state()); + assertEquals( + org.a2aproject.sdk.spec.TaskState.TASK_STATE_SUBMITTED, v10Task.status().state()); assertNull(v10Task.status().message()); // v1.0 Task compact constructor converts null to empty list diff --git a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3_Test.java b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3_Test.java index b72cec3f2..0522c6a83 100644 --- a/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3_Test.java +++ b/compat-0.3/server-conversion/src/test/java/org/a2aproject/sdk/compat03/conversion/mappers/domain/TaskStatusUpdateEventMapper_v0_3_Test.java @@ -5,7 +5,6 @@ import java.util.HashMap; import java.util.Map; - import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; @@ -21,13 +20,14 @@ void toV10_preservesMetadataAndDefensiveCopy() { Map mutableMetadata = new HashMap<>(); mutableMetadata.put("source", "sensor"); - TaskStatusUpdateEvent_v0_3 v03 = new TaskStatusUpdateEvent_v0_3.Builder() - .taskId("task-123") - .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING)) - .contextId("context-456") - .isFinal(false) - .metadata(mutableMetadata) - .build(); + TaskStatusUpdateEvent_v0_3 v03 = + new TaskStatusUpdateEvent_v0_3.Builder() + .taskId("task-123") + .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING)) + .contextId("context-456") + .isFinal(false) + .metadata(mutableMetadata) + .build(); TaskStatusUpdateEvent v10 = TaskStatusUpdateEventMapper_v0_3.INSTANCE.toV10(v03); mutableMetadata.put("write", "should-not-leak"); @@ -36,16 +36,18 @@ void toV10_preservesMetadataAndDefensiveCopy() { assertEquals(TaskState.TASK_STATE_WORKING, v10.status().state()); assertEquals("context-456", v10.contextId()); assertEquals(Map.of("source", "sensor"), v10.metadata()); - assertThrows(UnsupportedOperationException.class, () -> v10.metadata().put("another", "value")); + assertThrows( + UnsupportedOperationException.class, () -> v10.metadata().put("another", "value")); } @Test void fromV10_preservesMetadata() { - TaskStatusUpdateEvent v10 = new TaskStatusUpdateEvent( - "task-123", - new TaskStatus(TaskState.TASK_STATE_WORKING), - "context-456", - Map.of("source", "sensor")); + TaskStatusUpdateEvent v10 = + new TaskStatusUpdateEvent( + "task-123", + new TaskStatus(TaskState.TASK_STATE_WORKING), + "context-456", + Map.of("source", "sensor")); TaskStatusUpdateEvent_v0_3 v03 = TaskStatusUpdateEventMapper_v0_3.INSTANCE.fromV10(v10); diff --git a/compat-0.3/spec-grpc/pom.xml b/compat-0.3/spec-grpc/pom.xml index 0b97a5874..8bb78f0a0 100644 --- a/compat-0.3/spec-grpc/pom.xml +++ b/compat-0.3/spec-grpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -94,65 +92,65 @@ proto-compile - - - - org.apache.maven.plugins - maven-clean-plugin - ${maven-clean-plugin.version} - - - remove-generated-files - initialize - - clean - - - true - - - ${project.basedir}/src/main/java/io/a2a/grpc - false - - *.java - - - - - - - - - - io.github.ascopes - protobuf-maven-plugin - ${protobuf-maven-plugin.version} - - ${skip.protobuf.generate} - true - - ${protobuf-protoc.version} - - ${project.basedir}/src/main/java/ - - - io.grpc - protoc-gen-grpc-java - ${grpc.version} - - - - - - generate-sources - - generate - - - - - - + + + + + io.github.ascopes + protobuf-maven-plugin + ${protobuf-maven-plugin.version} + + ${skip.protobuf.generate} + true + + ${protobuf-protoc.version} + + ${project.basedir}/src/main/java/ + + + io.grpc + protoc-gen-grpc-java + ${grpc.version} + + + + + + + generate + + generate-sources + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + remove-generated-files + + clean + + initialize + + true + + + ${project.basedir}/src/main/java/io/a2a/grpc + false + + *.java + + + + + + + + + diff --git a/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoJsonUtils_v0_3.java b/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoJsonUtils_v0_3.java index 28327117a..91b9fb822 100644 --- a/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoJsonUtils_v0_3.java +++ b/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoJsonUtils_v0_3.java @@ -19,8 +19,7 @@ */ public final class ProtoJsonUtils_v0_3 { - private ProtoJsonUtils_v0_3() { - } + private ProtoJsonUtils_v0_3() {} /** * Serializes a protobuf message to JSON using the supplied printer, @@ -32,7 +31,8 @@ private ProtoJsonUtils_v0_3() { * @param proto the protobuf message to serialize, or {@code null} * @return JSON string without HTML-escaped characters, or empty string if proto is null */ - public static String toJson(JsonFormat.Printer printer, @Nullable MessageOrBuilder proto) throws InvalidProtocolBufferException { + public static String toJson(JsonFormat.Printer printer, @Nullable MessageOrBuilder proto) + throws InvalidProtocolBufferException { Assert.checkNotNullParam("printer", printer); if (proto == null) { return ""; diff --git a/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoUtils_v0_3.java b/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoUtils_v0_3.java index 7a4e6f284..387e9e739 100644 --- a/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoUtils_v0_3.java +++ b/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoUtils_v0_3.java @@ -1,6 +1,8 @@ package org.a2aproject.sdk.compat03.grpc.utils; - +import com.google.protobuf.ByteString; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.OffsetDateTime; @@ -9,15 +11,11 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; - -import com.google.protobuf.ByteString; -import com.google.protobuf.Struct; -import com.google.protobuf.Value; import org.a2aproject.sdk.compat03.grpc.StreamResponse; import org.a2aproject.sdk.compat03.spec.APIKeySecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; -import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCardSignature_v0_3; +import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.AgentExtension_v0_3; import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3; import org.a2aproject.sdk.compat03.spec.AgentProvider_v0_3; @@ -37,9 +35,9 @@ import org.a2aproject.sdk.compat03.spec.ImplicitOAuthFlow_v0_3; import org.a2aproject.sdk.compat03.spec.InvalidRequestError_v0_3; import org.a2aproject.sdk.compat03.spec.ListTaskPushNotificationConfigParams_v0_3; -import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.MutualTLSSecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.OAuth2SecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.OAuthFlows_v0_3; @@ -50,14 +48,14 @@ import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.SecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; +import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; import org.jspecify.annotations.Nullable; @@ -68,8 +66,10 @@ public class ProtoUtils_v0_3 { public static class ToProto { - public static org.a2aproject.sdk.compat03.grpc.AgentCard agentCard(AgentCard_v0_3 agentCard) { - org.a2aproject.sdk.compat03.grpc.AgentCard.Builder builder = org.a2aproject.sdk.compat03.grpc.AgentCard.newBuilder(); + public static org.a2aproject.sdk.compat03.grpc.AgentCard agentCard( + AgentCard_v0_3 agentCard) { + org.a2aproject.sdk.compat03.grpc.AgentCard.Builder builder = + org.a2aproject.sdk.compat03.grpc.AgentCard.newBuilder(); if (agentCard.protocolVersion() != null) { builder.setProtocolVersion(agentCard.protocolVersion()); } @@ -86,7 +86,10 @@ public static org.a2aproject.sdk.compat03.grpc.AgentCard agentCard(AgentCard_v0_ builder.setPreferredTransport(agentCard.preferredTransport()); } if (agentCard.additionalInterfaces() != null) { - builder.addAllAdditionalInterfaces(agentCard.additionalInterfaces().stream().map(item -> agentInterface(item)).collect(Collectors.toList())); + builder.addAllAdditionalInterfaces( + agentCard.additionalInterfaces().stream() + .map(item -> agentInterface(item)) + .collect(Collectors.toList())); } if (agentCard.provider() != null) { builder.setProvider(agentProvider(agentCard.provider())); @@ -103,19 +106,35 @@ public static org.a2aproject.sdk.compat03.grpc.AgentCard agentCard(AgentCard_v0_ if (agentCard.securitySchemes() != null) { builder.putAllSecuritySchemes( agentCard.securitySchemes().entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> securityScheme(e.getValue()))) - ); + .collect( + Collectors.toMap( + Map.Entry::getKey, + e -> securityScheme(e.getValue())))); } if (agentCard.security() != null) { - builder.addAllSecurity(agentCard.security().stream().map(s -> { - org.a2aproject.sdk.compat03.grpc.Security.Builder securityBuilder = org.a2aproject.sdk.compat03.grpc.Security.newBuilder(); - s.forEach((key, value) -> { - org.a2aproject.sdk.compat03.grpc.StringList.Builder stringListBuilder = org.a2aproject.sdk.compat03.grpc.StringList.newBuilder(); - stringListBuilder.addAllList(value); - securityBuilder.putSchemes(key, stringListBuilder.build()); - }); - return securityBuilder.build(); - }).collect(Collectors.toList())); + builder.addAllSecurity( + agentCard.security().stream() + .map( + s -> { + org.a2aproject.sdk.compat03.grpc.Security.Builder + securityBuilder = + org.a2aproject.sdk.compat03.grpc + .Security.newBuilder(); + s.forEach( + (key, value) -> { + org.a2aproject.sdk.compat03.grpc.StringList + .Builder + stringListBuilder = + org.a2aproject.sdk.compat03 + .grpc.StringList + .newBuilder(); + stringListBuilder.addAllList(value); + securityBuilder.putSchemes( + key, stringListBuilder.build()); + }); + return securityBuilder.build(); + }) + .collect(Collectors.toList())); } if (agentCard.defaultInputModes() != null) { builder.addAllDefaultInputModes(agentCard.defaultInputModes()); @@ -124,32 +143,45 @@ public static org.a2aproject.sdk.compat03.grpc.AgentCard agentCard(AgentCard_v0_ builder.addAllDefaultOutputModes(agentCard.defaultOutputModes()); } if (agentCard.skills() != null) { - builder.addAllSkills(agentCard.skills().stream().map(ToProto::agentSkill).collect(Collectors.toList())); + builder.addAllSkills( + agentCard.skills().stream() + .map(ToProto::agentSkill) + .collect(Collectors.toList())); } - builder.setSupportsAuthenticatedExtendedCard(agentCard.supportsAuthenticatedExtendedCard()); + builder.setSupportsAuthenticatedExtendedCard( + agentCard.supportsAuthenticatedExtendedCard()); if (agentCard.signatures() != null) { - builder.addAllSignatures(agentCard.signatures().stream().map(ToProto::agentCardSignature).collect(Collectors.toList())); + builder.addAllSignatures( + agentCard.signatures().stream() + .map(ToProto::agentCardSignature) + .collect(Collectors.toList())); } return builder.build(); } public static org.a2aproject.sdk.compat03.grpc.Task task(Task_v0_3 task) { - org.a2aproject.sdk.compat03.grpc.Task.Builder builder = org.a2aproject.sdk.compat03.grpc.Task.newBuilder(); + org.a2aproject.sdk.compat03.grpc.Task.Builder builder = + org.a2aproject.sdk.compat03.grpc.Task.newBuilder(); builder.setId(task.id()); builder.setContextId(task.contextId()); builder.setStatus(taskStatus(task.status())); if (task.artifacts() != null) { - builder.addAllArtifacts(task.artifacts().stream().map(ToProto::artifact).collect(Collectors.toList())); + builder.addAllArtifacts( + task.artifacts().stream() + .map(ToProto::artifact) + .collect(Collectors.toList())); } if (task.history() != null) { - builder.addAllHistory(task.history().stream().map(ToProto::message).collect(Collectors.toList())); + builder.addAllHistory( + task.history().stream().map(ToProto::message).collect(Collectors.toList())); } builder.setMetadata(struct(task.metadata())); return builder.build(); } public static org.a2aproject.sdk.compat03.grpc.Message message(Message_v0_3 message) { - org.a2aproject.sdk.compat03.grpc.Message.Builder builder = org.a2aproject.sdk.compat03.grpc.Message.newBuilder(); + org.a2aproject.sdk.compat03.grpc.Message.Builder builder = + org.a2aproject.sdk.compat03.grpc.Message.newBuilder(); if (message.messageId() != null) { builder.setMessageId(message.messageId()); } @@ -161,22 +193,32 @@ public static org.a2aproject.sdk.compat03.grpc.Message message(Message_v0_3 mess } builder.setRole(role(message.role())); if (message.parts() != null) { - builder.addAllContent(message.parts().stream().map(ToProto::part).collect(Collectors.toList())); + builder.addAllContent( + message.parts().stream().map(ToProto::part).collect(Collectors.toList())); } builder.setMetadata(struct(message.metadata())); return builder.build(); } - public static org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig taskPushNotificationConfig(TaskPushNotificationConfig_v0_3 config) { + public static org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig + taskPushNotificationConfig(TaskPushNotificationConfig_v0_3 config) { String id = config.pushNotificationConfig().id(); - org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.Builder builder = org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder(); - builder.setName("tasks/" + config.taskId() + "/pushNotificationConfigs" + (id == null ? "" : ('/' + id))); - builder.setPushNotificationConfig(pushNotificationConfig(config.pushNotificationConfig())); + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.Builder builder = + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder(); + builder.setName( + "tasks/" + + config.taskId() + + "/pushNotificationConfigs" + + (id == null ? "" : ('/' + id))); + builder.setPushNotificationConfig( + pushNotificationConfig(config.pushNotificationConfig())); return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.PushNotificationConfig pushNotificationConfig(PushNotificationConfig_v0_3 config) { - org.a2aproject.sdk.compat03.grpc.PushNotificationConfig.Builder builder = org.a2aproject.sdk.compat03.grpc.PushNotificationConfig.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.PushNotificationConfig + pushNotificationConfig(PushNotificationConfig_v0_3 config) { + org.a2aproject.sdk.compat03.grpc.PushNotificationConfig.Builder builder = + org.a2aproject.sdk.compat03.grpc.PushNotificationConfig.newBuilder(); if (config.url() != null) { builder.setUrl(config.url()); } @@ -192,8 +234,10 @@ private static org.a2aproject.sdk.compat03.grpc.PushNotificationConfig pushNotif return builder.build(); } - public static org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEvent taskArtifactUpdateEvent(TaskArtifactUpdateEvent_v0_3 event) { - org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEvent.Builder builder = org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEvent.newBuilder(); + public static org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEvent + taskArtifactUpdateEvent(TaskArtifactUpdateEvent_v0_3 event) { + org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEvent.Builder builder = + org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEvent.newBuilder(); builder.setTaskId(event.taskId()); builder.setContextId(event.contextId()); builder.setArtifact(artifact(event.artifact())); @@ -209,8 +253,10 @@ public static org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEvent taskArtif return builder.build(); } - public static org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEvent taskStatusUpdateEvent(TaskStatusUpdateEvent_v0_3 event) { - org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEvent.Builder builder = org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEvent.newBuilder(); + public static org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEvent taskStatusUpdateEvent( + TaskStatusUpdateEvent_v0_3 event) { + org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEvent.Builder builder = + org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEvent.newBuilder(); builder.setTaskId(event.taskId()); builder.setContextId(event.contextId()); builder.setStatus(taskStatus(event.status())); @@ -222,7 +268,8 @@ public static org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEvent taskStatusU } private static org.a2aproject.sdk.compat03.grpc.Artifact artifact(Artifact_v0_3 artifact) { - org.a2aproject.sdk.compat03.grpc.Artifact.Builder builder = org.a2aproject.sdk.compat03.grpc.Artifact.newBuilder(); + org.a2aproject.sdk.compat03.grpc.Artifact.Builder builder = + org.a2aproject.sdk.compat03.grpc.Artifact.newBuilder(); if (artifact.artifactId() != null) { builder.setArtifactId(artifact.artifactId()); } @@ -233,7 +280,8 @@ private static org.a2aproject.sdk.compat03.grpc.Artifact artifact(Artifact_v0_3 builder.setDescription(artifact.description()); } if (artifact.parts() != null) { - builder.addAllParts(artifact.parts().stream().map(ToProto::part).collect(Collectors.toList())); + builder.addAllParts( + artifact.parts().stream().map(ToProto::part).collect(Collectors.toList())); } if (artifact.metadata() != null) { builder.setMetadata(struct(artifact.metadata())); @@ -242,7 +290,8 @@ private static org.a2aproject.sdk.compat03.grpc.Artifact artifact(Artifact_v0_3 } static org.a2aproject.sdk.compat03.grpc.Part part(Part_v0_3 part) { - org.a2aproject.sdk.compat03.grpc.Part.Builder builder = org.a2aproject.sdk.compat03.grpc.Part.newBuilder(); + org.a2aproject.sdk.compat03.grpc.Part.Builder builder = + org.a2aproject.sdk.compat03.grpc.Part.newBuilder(); if (part instanceof TextPart_v0_3 textPart) { builder.setText(textPart.text()); if (!textPart.metadata().isEmpty()) { @@ -263,10 +312,14 @@ static org.a2aproject.sdk.compat03.grpc.Part part(Part_v0_3 part) { } private static org.a2aproject.sdk.compat03.grpc.FilePart filePart(FilePart_v0_3 filePart) { - org.a2aproject.sdk.compat03.grpc.FilePart.Builder builder = org.a2aproject.sdk.compat03.grpc.FilePart.newBuilder(); + org.a2aproject.sdk.compat03.grpc.FilePart.Builder builder = + org.a2aproject.sdk.compat03.grpc.FilePart.newBuilder(); FileContent_v0_3 fileContent = filePart.file(); if (fileContent instanceof FileWithBytes_v0_3) { - builder.setFileWithBytes(ByteString.copyFrom(((FileWithBytes_v0_3) fileContent).bytes(), StandardCharsets.UTF_8)); + builder.setFileWithBytes( + ByteString.copyFrom( + ((FileWithBytes_v0_3) fileContent).bytes(), + StandardCharsets.UTF_8)); } else if (fileContent instanceof FileWithUri_v0_3) { builder.setFileWithUri(((FileWithUri_v0_3) fileContent).uri()); } @@ -280,7 +333,8 @@ private static org.a2aproject.sdk.compat03.grpc.FilePart filePart(FilePart_v0_3 } private static org.a2aproject.sdk.compat03.grpc.DataPart dataPart(DataPart_v0_3 dataPart) { - org.a2aproject.sdk.compat03.grpc.DataPart.Builder builder = org.a2aproject.sdk.compat03.grpc.DataPart.newBuilder(); + org.a2aproject.sdk.compat03.grpc.DataPart.Builder builder = + org.a2aproject.sdk.compat03.grpc.DataPart.newBuilder(); if (dataPart.data() != null) { builder.setData(struct(dataPart.data())); } @@ -292,15 +346,15 @@ private static org.a2aproject.sdk.compat03.grpc.Role role(Message_v0_3.Role role return org.a2aproject.sdk.compat03.grpc.Role.ROLE_UNSPECIFIED; } return switch (role) { - case USER -> - org.a2aproject.sdk.compat03.grpc.Role.ROLE_USER; - case AGENT -> - org.a2aproject.sdk.compat03.grpc.Role.ROLE_AGENT; + case USER -> org.a2aproject.sdk.compat03.grpc.Role.ROLE_USER; + case AGENT -> org.a2aproject.sdk.compat03.grpc.Role.ROLE_AGENT; }; } - private static org.a2aproject.sdk.compat03.grpc.TaskStatus taskStatus(TaskStatus_v0_3 taskStatus) { - org.a2aproject.sdk.compat03.grpc.TaskStatus.Builder builder = org.a2aproject.sdk.compat03.grpc.TaskStatus.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.TaskStatus taskStatus( + TaskStatus_v0_3 taskStatus) { + org.a2aproject.sdk.compat03.grpc.TaskStatus.Builder builder = + org.a2aproject.sdk.compat03.grpc.TaskStatus.newBuilder(); if (taskStatus.state() != null) { builder.setState(taskState(taskStatus.state())); } @@ -309,39 +363,39 @@ private static org.a2aproject.sdk.compat03.grpc.TaskStatus taskStatus(TaskStatus } if (taskStatus.timestamp() != null) { Instant instant = taskStatus.timestamp().toInstant(); - builder.setTimestamp(com.google.protobuf.Timestamp.newBuilder().setSeconds(instant.getEpochSecond()).setNanos(instant.getNano()).build()); + builder.setTimestamp( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(instant.getEpochSecond()) + .setNanos(instant.getNano()) + .build()); } return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.TaskState taskState(TaskState_v0_3 taskState) { + private static org.a2aproject.sdk.compat03.grpc.TaskState taskState( + TaskState_v0_3 taskState) { if (taskState == null) { return org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_UNSPECIFIED; } return switch (taskState) { - case SUBMITTED -> - org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_SUBMITTED; - case WORKING -> - org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_WORKING; + case SUBMITTED -> org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_SUBMITTED; + case WORKING -> org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_WORKING; case INPUT_REQUIRED -> - org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_INPUT_REQUIRED; + org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_INPUT_REQUIRED; case AUTH_REQUIRED -> - org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_AUTH_REQUIRED; - case COMPLETED -> - org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_COMPLETED; - case CANCELED -> - org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_CANCELLED; - case FAILED -> - org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_FAILED; - case REJECTED -> - org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_REJECTED; - default -> - org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_UNSPECIFIED; + org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_AUTH_REQUIRED; + case COMPLETED -> org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_COMPLETED; + case CANCELED -> org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_CANCELLED; + case FAILED -> org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_FAILED; + case REJECTED -> org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_REJECTED; + default -> org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_UNSPECIFIED; }; } - private static org.a2aproject.sdk.compat03.grpc.AuthenticationInfo authenticationInfo(PushNotificationAuthenticationInfo_v0_3 pushNotificationAuthenticationInfo) { - org.a2aproject.sdk.compat03.grpc.AuthenticationInfo.Builder builder = org.a2aproject.sdk.compat03.grpc.AuthenticationInfo.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.AuthenticationInfo authenticationInfo( + PushNotificationAuthenticationInfo_v0_3 pushNotificationAuthenticationInfo) { + org.a2aproject.sdk.compat03.grpc.AuthenticationInfo.Builder builder = + org.a2aproject.sdk.compat03.grpc.AuthenticationInfo.newBuilder(); if (pushNotificationAuthenticationInfo.schemes() != null) { builder.addAllSchemes(pushNotificationAuthenticationInfo.schemes()); } @@ -351,8 +405,10 @@ private static org.a2aproject.sdk.compat03.grpc.AuthenticationInfo authenticatio return builder.build(); } - public static org.a2aproject.sdk.compat03.grpc.SendMessageConfiguration messageSendConfiguration(MessageSendConfiguration_v0_3 messageSendConfiguration) { - org.a2aproject.sdk.compat03.grpc.SendMessageConfiguration.Builder builder = org.a2aproject.sdk.compat03.grpc.SendMessageConfiguration.newBuilder(); + public static org.a2aproject.sdk.compat03.grpc.SendMessageConfiguration + messageSendConfiguration(MessageSendConfiguration_v0_3 messageSendConfiguration) { + org.a2aproject.sdk.compat03.grpc.SendMessageConfiguration.Builder builder = + org.a2aproject.sdk.compat03.grpc.SendMessageConfiguration.newBuilder(); if (messageSendConfiguration.acceptedOutputModes() != null) { builder.addAllAcceptedOutputModes(messageSendConfiguration.acceptedOutputModes()); } @@ -360,42 +416,55 @@ public static org.a2aproject.sdk.compat03.grpc.SendMessageConfiguration messageS builder.setHistoryLength(messageSendConfiguration.historyLength()); } if (messageSendConfiguration.pushNotificationConfig() != null) { - builder.setPushNotification(pushNotificationConfig(messageSendConfiguration.pushNotificationConfig())); + builder.setPushNotification( + pushNotificationConfig(messageSendConfiguration.pushNotificationConfig())); } builder.setBlocking(messageSendConfiguration.blocking()); return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.AgentProvider agentProvider(AgentProvider_v0_3 agentProvider) { - org.a2aproject.sdk.compat03.grpc.AgentProvider.Builder builder = org.a2aproject.sdk.compat03.grpc.AgentProvider.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.AgentProvider agentProvider( + AgentProvider_v0_3 agentProvider) { + org.a2aproject.sdk.compat03.grpc.AgentProvider.Builder builder = + org.a2aproject.sdk.compat03.grpc.AgentProvider.newBuilder(); builder.setOrganization(agentProvider.organization()); builder.setUrl(agentProvider.url()); return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.AgentCapabilities agentCapabilities(AgentCapabilities_v0_3 agentCapabilities) { - org.a2aproject.sdk.compat03.grpc.AgentCapabilities.Builder builder = org.a2aproject.sdk.compat03.grpc.AgentCapabilities.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.AgentCapabilities agentCapabilities( + AgentCapabilities_v0_3 agentCapabilities) { + org.a2aproject.sdk.compat03.grpc.AgentCapabilities.Builder builder = + org.a2aproject.sdk.compat03.grpc.AgentCapabilities.newBuilder(); builder.setStreaming(agentCapabilities.streaming()); builder.setPushNotifications(agentCapabilities.pushNotifications()); if (agentCapabilities.extensions() != null) { - builder.addAllExtensions(agentCapabilities.extensions().stream().map(ToProto::agentExtension).collect(Collectors.toList())); + builder.addAllExtensions( + agentCapabilities.extensions().stream() + .map(ToProto::agentExtension) + .collect(Collectors.toList())); } return builder.build(); } - public static org.a2aproject.sdk.compat03.grpc.SendMessageRequest sendMessageRequest(MessageSendParams_v0_3 request) { - org.a2aproject.sdk.compat03.grpc.SendMessageRequest.Builder builder = org.a2aproject.sdk.compat03.grpc.SendMessageRequest.newBuilder(); + public static org.a2aproject.sdk.compat03.grpc.SendMessageRequest sendMessageRequest( + MessageSendParams_v0_3 request) { + org.a2aproject.sdk.compat03.grpc.SendMessageRequest.Builder builder = + org.a2aproject.sdk.compat03.grpc.SendMessageRequest.newBuilder(); builder.setRequest(message(request.message())); if (request.configuration() != null) { builder.setConfiguration(messageSendConfiguration(request.configuration())); } - if (request.metadata() != null && ! request.metadata().isEmpty()) { + if (request.metadata() != null && !request.metadata().isEmpty()) { builder.setMetadata(struct(request.metadata())); } return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.AgentExtension agentExtension(AgentExtension_v0_3 agentExtension) { - org.a2aproject.sdk.compat03.grpc.AgentExtension.Builder builder = org.a2aproject.sdk.compat03.grpc.AgentExtension.newBuilder(); + + private static org.a2aproject.sdk.compat03.grpc.AgentExtension agentExtension( + AgentExtension_v0_3 agentExtension) { + org.a2aproject.sdk.compat03.grpc.AgentExtension.Builder builder = + org.a2aproject.sdk.compat03.grpc.AgentExtension.newBuilder(); if (agentExtension.description() != null) { builder.setDescription(agentExtension.description()); } @@ -409,8 +478,10 @@ private static org.a2aproject.sdk.compat03.grpc.AgentExtension agentExtension(Ag return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.AgentSkill agentSkill(AgentSkill_v0_3 agentSkill) { - org.a2aproject.sdk.compat03.grpc.AgentSkill.Builder builder = org.a2aproject.sdk.compat03.grpc.AgentSkill.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.AgentSkill agentSkill( + AgentSkill_v0_3 agentSkill) { + org.a2aproject.sdk.compat03.grpc.AgentSkill.Builder builder = + org.a2aproject.sdk.compat03.grpc.AgentSkill.newBuilder(); if (agentSkill.id() != null) { builder.setId(agentSkill.id()); } @@ -433,21 +504,37 @@ private static org.a2aproject.sdk.compat03.grpc.AgentSkill agentSkill(AgentSkill builder.addAllOutputModes(agentSkill.outputModes()); } if (agentSkill.security() != null) { - builder.addAllSecurity(agentSkill.security().stream().map(s -> { - org.a2aproject.sdk.compat03.grpc.Security.Builder securityBuilder = org.a2aproject.sdk.compat03.grpc.Security.newBuilder(); - s.forEach((key, value) -> { - org.a2aproject.sdk.compat03.grpc.StringList.Builder stringListBuilder = org.a2aproject.sdk.compat03.grpc.StringList.newBuilder(); - stringListBuilder.addAllList(value); - securityBuilder.putSchemes(key, stringListBuilder.build()); - }); - return securityBuilder.build(); - }).collect(Collectors.toList())); + builder.addAllSecurity( + agentSkill.security().stream() + .map( + s -> { + org.a2aproject.sdk.compat03.grpc.Security.Builder + securityBuilder = + org.a2aproject.sdk.compat03.grpc + .Security.newBuilder(); + s.forEach( + (key, value) -> { + org.a2aproject.sdk.compat03.grpc.StringList + .Builder + stringListBuilder = + org.a2aproject.sdk.compat03 + .grpc.StringList + .newBuilder(); + stringListBuilder.addAllList(value); + securityBuilder.putSchemes( + key, stringListBuilder.build()); + }); + return securityBuilder.build(); + }) + .collect(Collectors.toList())); } return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.AgentCardSignature agentCardSignature(AgentCardSignature_v0_3 agentCardSignature) { - org.a2aproject.sdk.compat03.grpc.AgentCardSignature.Builder builder = org.a2aproject.sdk.compat03.grpc.AgentCardSignature.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.AgentCardSignature agentCardSignature( + AgentCardSignature_v0_3 agentCardSignature) { + org.a2aproject.sdk.compat03.grpc.AgentCardSignature.Builder builder = + org.a2aproject.sdk.compat03.grpc.AgentCardSignature.newBuilder(); builder.setProtected(agentCardSignature.protectedHeader()); builder.setSignature(agentCardSignature.signature()); if (agentCardSignature.header() != null) { @@ -456,24 +543,34 @@ private static org.a2aproject.sdk.compat03.grpc.AgentCardSignature agentCardSign return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.SecurityScheme securityScheme(SecurityScheme_v0_3 securityScheme) { - org.a2aproject.sdk.compat03.grpc.SecurityScheme.Builder builder = org.a2aproject.sdk.compat03.grpc.SecurityScheme.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.SecurityScheme securityScheme( + SecurityScheme_v0_3 securityScheme) { + org.a2aproject.sdk.compat03.grpc.SecurityScheme.Builder builder = + org.a2aproject.sdk.compat03.grpc.SecurityScheme.newBuilder(); if (securityScheme instanceof APIKeySecurityScheme_v0_3) { - builder.setApiKeySecurityScheme(apiKeySecurityScheme((APIKeySecurityScheme_v0_3) securityScheme)); + builder.setApiKeySecurityScheme( + apiKeySecurityScheme((APIKeySecurityScheme_v0_3) securityScheme)); } else if (securityScheme instanceof HTTPAuthSecurityScheme_v0_3) { - builder.setHttpAuthSecurityScheme(httpAuthSecurityScheme((HTTPAuthSecurityScheme_v0_3) securityScheme)); + builder.setHttpAuthSecurityScheme( + httpAuthSecurityScheme((HTTPAuthSecurityScheme_v0_3) securityScheme)); } else if (securityScheme instanceof OAuth2SecurityScheme_v0_3) { - builder.setOauth2SecurityScheme(oauthSecurityScheme((OAuth2SecurityScheme_v0_3) securityScheme)); + builder.setOauth2SecurityScheme( + oauthSecurityScheme((OAuth2SecurityScheme_v0_3) securityScheme)); } else if (securityScheme instanceof OpenIdConnectSecurityScheme_v0_3) { - builder.setOpenIdConnectSecurityScheme(openIdConnectSecurityScheme((OpenIdConnectSecurityScheme_v0_3) securityScheme)); + builder.setOpenIdConnectSecurityScheme( + openIdConnectSecurityScheme( + (OpenIdConnectSecurityScheme_v0_3) securityScheme)); } else if (securityScheme instanceof MutualTLSSecurityScheme_v0_3) { - builder.setMtlsSecurityScheme(mutualTlsSecurityScheme((MutualTLSSecurityScheme_v0_3) securityScheme)); + builder.setMtlsSecurityScheme( + mutualTlsSecurityScheme((MutualTLSSecurityScheme_v0_3) securityScheme)); } return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.APIKeySecurityScheme apiKeySecurityScheme(APIKeySecurityScheme_v0_3 apiKeySecurityScheme) { - org.a2aproject.sdk.compat03.grpc.APIKeySecurityScheme.Builder builder = org.a2aproject.sdk.compat03.grpc.APIKeySecurityScheme.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.APIKeySecurityScheme apiKeySecurityScheme( + APIKeySecurityScheme_v0_3 apiKeySecurityScheme) { + org.a2aproject.sdk.compat03.grpc.APIKeySecurityScheme.Builder builder = + org.a2aproject.sdk.compat03.grpc.APIKeySecurityScheme.newBuilder(); if (apiKeySecurityScheme.description() != null) { builder.setDescription(apiKeySecurityScheme.description()); } @@ -486,8 +583,10 @@ private static org.a2aproject.sdk.compat03.grpc.APIKeySecurityScheme apiKeySecur return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.HTTPAuthSecurityScheme httpAuthSecurityScheme(HTTPAuthSecurityScheme_v0_3 httpAuthSecurityScheme) { - org.a2aproject.sdk.compat03.grpc.HTTPAuthSecurityScheme.Builder builder = org.a2aproject.sdk.compat03.grpc.HTTPAuthSecurityScheme.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.HTTPAuthSecurityScheme + httpAuthSecurityScheme(HTTPAuthSecurityScheme_v0_3 httpAuthSecurityScheme) { + org.a2aproject.sdk.compat03.grpc.HTTPAuthSecurityScheme.Builder builder = + org.a2aproject.sdk.compat03.grpc.HTTPAuthSecurityScheme.newBuilder(); if (httpAuthSecurityScheme.bearerFormat() != null) { builder.setBearerFormat(httpAuthSecurityScheme.bearerFormat()); } @@ -500,8 +599,10 @@ private static org.a2aproject.sdk.compat03.grpc.HTTPAuthSecurityScheme httpAuthS return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.OAuth2SecurityScheme oauthSecurityScheme(OAuth2SecurityScheme_v0_3 oauth2SecurityScheme) { - org.a2aproject.sdk.compat03.grpc.OAuth2SecurityScheme.Builder builder = org.a2aproject.sdk.compat03.grpc.OAuth2SecurityScheme.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.OAuth2SecurityScheme oauthSecurityScheme( + OAuth2SecurityScheme_v0_3 oauth2SecurityScheme) { + org.a2aproject.sdk.compat03.grpc.OAuth2SecurityScheme.Builder builder = + org.a2aproject.sdk.compat03.grpc.OAuth2SecurityScheme.newBuilder(); if (oauth2SecurityScheme.description() != null) { builder.setDescription(oauth2SecurityScheme.description()); } @@ -514,13 +615,17 @@ private static org.a2aproject.sdk.compat03.grpc.OAuth2SecurityScheme oauthSecuri return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.OAuthFlows oauthFlows(OAuthFlows_v0_3 oAuthFlows) { - org.a2aproject.sdk.compat03.grpc.OAuthFlows.Builder builder = org.a2aproject.sdk.compat03.grpc.OAuthFlows.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.OAuthFlows oauthFlows( + OAuthFlows_v0_3 oAuthFlows) { + org.a2aproject.sdk.compat03.grpc.OAuthFlows.Builder builder = + org.a2aproject.sdk.compat03.grpc.OAuthFlows.newBuilder(); if (oAuthFlows.authorizationCode() != null) { - builder.setAuthorizationCode(authorizationCodeOAuthFlow(oAuthFlows.authorizationCode())); + builder.setAuthorizationCode( + authorizationCodeOAuthFlow(oAuthFlows.authorizationCode())); } if (oAuthFlows.clientCredentials() != null) { - builder.setClientCredentials(clientCredentialsOAuthFlow(oAuthFlows.clientCredentials())); + builder.setClientCredentials( + clientCredentialsOAuthFlow(oAuthFlows.clientCredentials())); } if (oAuthFlows.implicit() != null) { builder.setImplicit(implicitOAuthFlow(oAuthFlows.implicit())); @@ -531,8 +636,11 @@ private static org.a2aproject.sdk.compat03.grpc.OAuthFlows oauthFlows(OAuthFlows return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.AuthorizationCodeOAuthFlow authorizationCodeOAuthFlow(AuthorizationCodeOAuthFlow_v0_3 authorizationCodeOAuthFlow) { - org.a2aproject.sdk.compat03.grpc.AuthorizationCodeOAuthFlow.Builder builder = org.a2aproject.sdk.compat03.grpc.AuthorizationCodeOAuthFlow.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.AuthorizationCodeOAuthFlow + authorizationCodeOAuthFlow( + AuthorizationCodeOAuthFlow_v0_3 authorizationCodeOAuthFlow) { + org.a2aproject.sdk.compat03.grpc.AuthorizationCodeOAuthFlow.Builder builder = + org.a2aproject.sdk.compat03.grpc.AuthorizationCodeOAuthFlow.newBuilder(); if (authorizationCodeOAuthFlow.authorizationUrl() != null) { builder.setAuthorizationUrl(authorizationCodeOAuthFlow.authorizationUrl()); } @@ -548,16 +656,25 @@ private static org.a2aproject.sdk.compat03.grpc.AuthorizationCodeOAuthFlow autho return builder.build(); } - public static org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse listTaskPushNotificationConfigResponse(List configs) { - List confs = new ArrayList<>(configs.size()); - for(TaskPushNotificationConfig_v0_3 config: configs) { + public static org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse + listTaskPushNotificationConfigResponse( + List configs) { + List confs = + new ArrayList<>(configs.size()); + for (TaskPushNotificationConfig_v0_3 config : configs) { confs.add(taskPushNotificationConfig(config)); } - return org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse.newBuilder().addAllConfigs(confs).build(); + return org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse + .newBuilder() + .addAllConfigs(confs) + .build(); } - private static org.a2aproject.sdk.compat03.grpc.ClientCredentialsOAuthFlow clientCredentialsOAuthFlow(ClientCredentialsOAuthFlow_v0_3 clientCredentialsOAuthFlow) { - org.a2aproject.sdk.compat03.grpc.ClientCredentialsOAuthFlow.Builder builder = org.a2aproject.sdk.compat03.grpc.ClientCredentialsOAuthFlow.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.ClientCredentialsOAuthFlow + clientCredentialsOAuthFlow( + ClientCredentialsOAuthFlow_v0_3 clientCredentialsOAuthFlow) { + org.a2aproject.sdk.compat03.grpc.ClientCredentialsOAuthFlow.Builder builder = + org.a2aproject.sdk.compat03.grpc.ClientCredentialsOAuthFlow.newBuilder(); if (clientCredentialsOAuthFlow.refreshUrl() != null) { builder.setRefreshUrl(clientCredentialsOAuthFlow.refreshUrl()); } @@ -570,8 +687,10 @@ private static org.a2aproject.sdk.compat03.grpc.ClientCredentialsOAuthFlow clien return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.ImplicitOAuthFlow implicitOAuthFlow(ImplicitOAuthFlow_v0_3 implicitOAuthFlow) { - org.a2aproject.sdk.compat03.grpc.ImplicitOAuthFlow.Builder builder = org.a2aproject.sdk.compat03.grpc.ImplicitOAuthFlow.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.ImplicitOAuthFlow implicitOAuthFlow( + ImplicitOAuthFlow_v0_3 implicitOAuthFlow) { + org.a2aproject.sdk.compat03.grpc.ImplicitOAuthFlow.Builder builder = + org.a2aproject.sdk.compat03.grpc.ImplicitOAuthFlow.newBuilder(); if (implicitOAuthFlow.authorizationUrl() != null) { builder.setAuthorizationUrl(implicitOAuthFlow.authorizationUrl()); } @@ -584,8 +703,10 @@ private static org.a2aproject.sdk.compat03.grpc.ImplicitOAuthFlow implicitOAuthF return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.PasswordOAuthFlow passwordOAuthFlow(PasswordOAuthFlow_v0_3 passwordOAuthFlow) { - org.a2aproject.sdk.compat03.grpc.PasswordOAuthFlow.Builder builder = org.a2aproject.sdk.compat03.grpc.PasswordOAuthFlow.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.PasswordOAuthFlow passwordOAuthFlow( + PasswordOAuthFlow_v0_3 passwordOAuthFlow) { + org.a2aproject.sdk.compat03.grpc.PasswordOAuthFlow.Builder builder = + org.a2aproject.sdk.compat03.grpc.PasswordOAuthFlow.newBuilder(); if (passwordOAuthFlow.refreshUrl() != null) { builder.setRefreshUrl(passwordOAuthFlow.refreshUrl()); } @@ -598,8 +719,11 @@ private static org.a2aproject.sdk.compat03.grpc.PasswordOAuthFlow passwordOAuthF return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.OpenIdConnectSecurityScheme openIdConnectSecurityScheme(OpenIdConnectSecurityScheme_v0_3 openIdConnectSecurityScheme) { - org.a2aproject.sdk.compat03.grpc.OpenIdConnectSecurityScheme.Builder builder = org.a2aproject.sdk.compat03.grpc.OpenIdConnectSecurityScheme.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.OpenIdConnectSecurityScheme + openIdConnectSecurityScheme( + OpenIdConnectSecurityScheme_v0_3 openIdConnectSecurityScheme) { + org.a2aproject.sdk.compat03.grpc.OpenIdConnectSecurityScheme.Builder builder = + org.a2aproject.sdk.compat03.grpc.OpenIdConnectSecurityScheme.newBuilder(); if (openIdConnectSecurityScheme.description() != null) { builder.setDescription(openIdConnectSecurityScheme.description()); } @@ -609,16 +733,20 @@ private static org.a2aproject.sdk.compat03.grpc.OpenIdConnectSecurityScheme open return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.MutualTlsSecurityScheme mutualTlsSecurityScheme(MutualTLSSecurityScheme_v0_3 mutualTlsSecurityScheme) { - org.a2aproject.sdk.compat03.grpc.MutualTlsSecurityScheme.Builder builder = org.a2aproject.sdk.compat03.grpc.MutualTlsSecurityScheme.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.MutualTlsSecurityScheme + mutualTlsSecurityScheme(MutualTLSSecurityScheme_v0_3 mutualTlsSecurityScheme) { + org.a2aproject.sdk.compat03.grpc.MutualTlsSecurityScheme.Builder builder = + org.a2aproject.sdk.compat03.grpc.MutualTlsSecurityScheme.newBuilder(); if (mutualTlsSecurityScheme.description() != null) { builder.setDescription(mutualTlsSecurityScheme.description()); } return builder.build(); } - private static org.a2aproject.sdk.compat03.grpc.AgentInterface agentInterface(AgentInterface_v0_3 agentInterface) { - org.a2aproject.sdk.compat03.grpc.AgentInterface.Builder builder = org.a2aproject.sdk.compat03.grpc.AgentInterface.newBuilder(); + private static org.a2aproject.sdk.compat03.grpc.AgentInterface agentInterface( + AgentInterface_v0_3 agentInterface) { + org.a2aproject.sdk.compat03.grpc.AgentInterface.Builder builder = + org.a2aproject.sdk.compat03.grpc.AgentInterface.newBuilder(); if (agentInterface.transport() != null) { builder.setTransport(agentInterface.transport()); } @@ -653,7 +781,8 @@ private static Value value(Object value) { } private static com.google.protobuf.ListValue listValue(List list) { - com.google.protobuf.ListValue.Builder listValueBuilder = com.google.protobuf.ListValue.newBuilder(); + com.google.protobuf.ListValue.Builder listValueBuilder = + com.google.protobuf.ListValue.newBuilder(); if (list != null) { list.forEach(o -> listValueBuilder.addValues(value(o))); } @@ -663,11 +792,15 @@ private static com.google.protobuf.ListValue listValue(List list) { public static StreamResponse streamResponse(StreamingEventKind_v0_3 streamingEventKind) { if (streamingEventKind instanceof TaskStatusUpdateEvent_v0_3) { return StreamResponse.newBuilder() - .setStatusUpdate(taskStatusUpdateEvent((TaskStatusUpdateEvent_v0_3) streamingEventKind)) + .setStatusUpdate( + taskStatusUpdateEvent( + (TaskStatusUpdateEvent_v0_3) streamingEventKind)) .build(); } else if (streamingEventKind instanceof TaskArtifactUpdateEvent_v0_3) { return StreamResponse.newBuilder() - .setArtifactUpdate(taskArtifactUpdateEvent((TaskArtifactUpdateEvent_v0_3) streamingEventKind)) + .setArtifactUpdate( + taskArtifactUpdateEvent( + (TaskArtifactUpdateEvent_v0_3) streamingEventKind)) .build(); } else if (streamingEventKind instanceof Message_v0_3) { return StreamResponse.newBuilder() @@ -682,7 +815,8 @@ public static StreamResponse streamResponse(StreamingEventKind_v0_3 streamingEve } } - public static org.a2aproject.sdk.compat03.grpc.SendMessageResponse taskOrMessage(EventKind_v0_3 eventKind) { + public static org.a2aproject.sdk.compat03.grpc.SendMessageResponse taskOrMessage( + EventKind_v0_3 eventKind) { if (eventKind instanceof Task_v0_3) { return org.a2aproject.sdk.compat03.grpc.SendMessageResponse.newBuilder() .setTask(task((Task_v0_3) eventKind)) @@ -696,7 +830,8 @@ public static org.a2aproject.sdk.compat03.grpc.SendMessageResponse taskOrMessage } } - public static org.a2aproject.sdk.compat03.grpc.StreamResponse taskOrMessageStream(StreamingEventKind_v0_3 eventKind) { + public static org.a2aproject.sdk.compat03.grpc.StreamResponse taskOrMessageStream( + StreamingEventKind_v0_3 eventKind) { if (eventKind instanceof Task_v0_3 task) { return org.a2aproject.sdk.compat03.grpc.StreamResponse.newBuilder() .setTask(task(task)) @@ -717,24 +852,26 @@ public static org.a2aproject.sdk.compat03.grpc.StreamResponse taskOrMessageStrea throw new IllegalArgumentException("Unsupported event type: " + eventKind); } } - } public static class FromProto { - public static TaskQueryParams_v0_3 taskQueryParams(org.a2aproject.sdk.compat03.grpc.GetTaskRequestOrBuilder request) { + public static TaskQueryParams_v0_3 taskQueryParams( + org.a2aproject.sdk.compat03.grpc.GetTaskRequestOrBuilder request) { String name = request.getName(); String id = name.substring(name.lastIndexOf('/') + 1); return new TaskQueryParams_v0_3(id, request.getHistoryLength()); } - public static TaskIdParams_v0_3 taskIdParams(org.a2aproject.sdk.compat03.grpc.CancelTaskRequestOrBuilder request) { + public static TaskIdParams_v0_3 taskIdParams( + org.a2aproject.sdk.compat03.grpc.CancelTaskRequestOrBuilder request) { String name = request.getName(); String id = name.substring(name.lastIndexOf('/') + 1); return new TaskIdParams_v0_3(id); } - public static MessageSendParams_v0_3 messageSendParams(org.a2aproject.sdk.compat03.grpc.SendMessageRequestOrBuilder request) { + public static MessageSendParams_v0_3 messageSendParams( + org.a2aproject.sdk.compat03.grpc.SendMessageRequestOrBuilder request) { MessageSendParams_v0_3.Builder builder = new MessageSendParams_v0_3.Builder(); builder.message(message(request.getRequest())); if (request.hasConfiguration()) { @@ -746,22 +883,28 @@ public static MessageSendParams_v0_3 messageSendParams(org.a2aproject.sdk.compat return builder.build(); } - public static TaskPushNotificationConfig_v0_3 taskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequestOrBuilder request) { + public static TaskPushNotificationConfig_v0_3 taskPushNotificationConfig( + org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequestOrBuilder + request) { return taskPushNotificationConfig(request.getConfig(), true); } - public static TaskPushNotificationConfig_v0_3 taskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfigOrBuilder config) { + public static TaskPushNotificationConfig_v0_3 taskPushNotificationConfig( + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfigOrBuilder config) { return taskPushNotificationConfig(config, false); } - private static TaskPushNotificationConfig_v0_3 taskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfigOrBuilder config, boolean create) { + private static TaskPushNotificationConfig_v0_3 taskPushNotificationConfig( + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfigOrBuilder config, + boolean create) { String name = config.getName(); // "tasks/{id}/pushNotificationConfigs/{push_id}" String[] parts = name.split("/"); String taskId = parts[1]; String configId = ""; if (create) { if (parts.length < 3) { - throw new IllegalArgumentException("Invalid name format for TaskPushNotificationConfig: " + name); + throw new IllegalArgumentException( + "Invalid name format for TaskPushNotificationConfig: " + name); } if (parts.length == 4) { configId = parts[3]; @@ -770,15 +913,19 @@ private static TaskPushNotificationConfig_v0_3 taskPushNotificationConfig(org.a2 } } else { if (parts.length < 4) { - throw new IllegalArgumentException("Invalid name format for TaskPushNotificationConfig: " + name); + throw new IllegalArgumentException( + "Invalid name format for TaskPushNotificationConfig: " + name); } configId = parts[3]; } - PushNotificationConfig_v0_3 pnc = pushNotification(config.getPushNotificationConfig(), configId); + PushNotificationConfig_v0_3 pnc = + pushNotification(config.getPushNotificationConfig(), configId); return new TaskPushNotificationConfig_v0_3(taskId, pnc); } - public static GetTaskPushNotificationConfigParams_v0_3 getTaskPushNotificationConfigParams(org.a2aproject.sdk.compat03.grpc.GetTaskPushNotificationConfigRequestOrBuilder request) { + public static GetTaskPushNotificationConfigParams_v0_3 getTaskPushNotificationConfigParams( + org.a2aproject.sdk.compat03.grpc.GetTaskPushNotificationConfigRequestOrBuilder + request) { String name = request.getName(); // "tasks/{id}/pushNotificationConfigs/{push_id}" String[] parts = name.split("/"); String taskId = parts[1]; @@ -786,85 +933,107 @@ public static GetTaskPushNotificationConfigParams_v0_3 getTaskPushNotificationCo if (parts.length == 2) { configId = taskId; } else if (parts.length < 4) { - throw new IllegalArgumentException("Invalid name format for GetTaskPushNotificationConfigRequest: " + name); + throw new IllegalArgumentException( + "Invalid name format for GetTaskPushNotificationConfigRequest: " + name); } else { configId = parts[3]; } return new GetTaskPushNotificationConfigParams_v0_3(taskId, configId); } - public static TaskIdParams_v0_3 taskIdParams(org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequestOrBuilder request) { + public static TaskIdParams_v0_3 taskIdParams( + org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequestOrBuilder request) { String name = request.getName(); String id = name.substring(name.lastIndexOf('/') + 1); return new TaskIdParams_v0_3(id); } - public static List listTaskPushNotificationConfigParams(org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponseOrBuilder response) { - List configs = response.getConfigsList(); + public static List listTaskPushNotificationConfigParams( + org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponseOrBuilder + response) { + List configs = + response.getConfigsList(); List result = new ArrayList<>(configs.size()); - for(org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig config : configs) { + for (org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig config : configs) { result.add(taskPushNotificationConfig(config, false)); } return result; } - public static ListTaskPushNotificationConfigParams_v0_3 listTaskPushNotificationConfigParams(org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigRequestOrBuilder request) { + public static ListTaskPushNotificationConfigParams_v0_3 + listTaskPushNotificationConfigParams( + org.a2aproject.sdk.compat03.grpc + .ListTaskPushNotificationConfigRequestOrBuilder + request) { String parent = request.getParent(); String id = parent.substring(parent.lastIndexOf('/') + 1); return new ListTaskPushNotificationConfigParams_v0_3(id); } - public static DeleteTaskPushNotificationConfigParams_v0_3 deleteTaskPushNotificationConfigParams(org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequestOrBuilder request) { + public static DeleteTaskPushNotificationConfigParams_v0_3 + deleteTaskPushNotificationConfigParams( + org.a2aproject.sdk.compat03.grpc + .DeleteTaskPushNotificationConfigRequestOrBuilder + request) { String name = request.getName(); // "tasks/{id}/pushNotificationConfigs/{push_id}" String[] parts = name.split("/"); if (parts.length < 4) { - throw new IllegalArgumentException("Invalid name format for DeleteTaskPushNotificationConfigRequest: " + name); + throw new IllegalArgumentException( + "Invalid name format for DeleteTaskPushNotificationConfigRequest: " + name); } String taskId = parts[1]; String configId = parts[3]; return new DeleteTaskPushNotificationConfigParams_v0_3(taskId, configId); } - private static AgentExtension_v0_3 agentExtension(org.a2aproject.sdk.compat03.grpc.AgentExtensionOrBuilder agentExtension) { + private static AgentExtension_v0_3 agentExtension( + org.a2aproject.sdk.compat03.grpc.AgentExtensionOrBuilder agentExtension) { return new AgentExtension_v0_3( agentExtension.getDescription(), struct(agentExtension.getParams()), agentExtension.getRequired(), - agentExtension.getUri() - ); + agentExtension.getUri()); } - private static MessageSendConfiguration_v0_3 messageSendConfiguration(org.a2aproject.sdk.compat03.grpc.SendMessageConfigurationOrBuilder sendMessageConfiguration) { + private static MessageSendConfiguration_v0_3 messageSendConfiguration( + org.a2aproject.sdk.compat03.grpc.SendMessageConfigurationOrBuilder + sendMessageConfiguration) { return new MessageSendConfiguration_v0_3( - sendMessageConfiguration.getAcceptedOutputModesList().isEmpty() ? null : - new ArrayList<>(sendMessageConfiguration.getAcceptedOutputModesList()), + sendMessageConfiguration.getAcceptedOutputModesList().isEmpty() + ? null + : new ArrayList<>( + sendMessageConfiguration.getAcceptedOutputModesList()), sendMessageConfiguration.getHistoryLength(), pushNotification(sendMessageConfiguration.getPushNotification()), - sendMessageConfiguration.getBlocking() - ); + sendMessageConfiguration.getBlocking()); } - private static @Nullable PushNotificationConfig_v0_3 pushNotification(org.a2aproject.sdk.compat03.grpc.PushNotificationConfigOrBuilder pushNotification, String configId) { - if(pushNotification == null || pushNotification.getDefaultInstanceForType().equals(pushNotification)) { + private static @Nullable PushNotificationConfig_v0_3 pushNotification( + org.a2aproject.sdk.compat03.grpc.PushNotificationConfigOrBuilder pushNotification, + String configId) { + if (pushNotification == null + || pushNotification.getDefaultInstanceForType().equals(pushNotification)) { return null; } return new PushNotificationConfig_v0_3( pushNotification.getUrl(), pushNotification.getToken().isEmpty() ? null : pushNotification.getToken(), - pushNotification.hasAuthentication() ? authenticationInfo(pushNotification.getAuthentication()) : null, - pushNotification.getId().isEmpty() ? configId : pushNotification.getId() - ); + pushNotification.hasAuthentication() + ? authenticationInfo(pushNotification.getAuthentication()) + : null, + pushNotification.getId().isEmpty() ? configId : pushNotification.getId()); } - private static @Nullable PushNotificationConfig_v0_3 pushNotification(org.a2aproject.sdk.compat03.grpc.PushNotificationConfigOrBuilder pushNotification) { + private static @Nullable PushNotificationConfig_v0_3 pushNotification( + org.a2aproject.sdk.compat03.grpc.PushNotificationConfigOrBuilder pushNotification) { return pushNotification(pushNotification, pushNotification.getId()); } - private static PushNotificationAuthenticationInfo_v0_3 authenticationInfo(org.a2aproject.sdk.compat03.grpc.AuthenticationInfoOrBuilder authenticationInfo) { + private static PushNotificationAuthenticationInfo_v0_3 authenticationInfo( + org.a2aproject.sdk.compat03.grpc.AuthenticationInfoOrBuilder authenticationInfo) { return new PushNotificationAuthenticationInfo_v0_3( new ArrayList<>(authenticationInfo.getSchemesList()), - authenticationInfo.getCredentials() - ); + authenticationInfo.getCredentials()); } public static Task_v0_3 task(org.a2aproject.sdk.compat03.grpc.TaskOrBuilder task) { @@ -872,26 +1041,33 @@ public static Task_v0_3 task(org.a2aproject.sdk.compat03.grpc.TaskOrBuilder task task.getId(), task.getContextId(), taskStatus(task.getStatus()), - task.getArtifactsList().stream().map(item -> artifact(item)).collect(Collectors.toList()), - task.getHistoryList().stream().map(item -> message(item)).collect(Collectors.toList()), - struct(task.getMetadata()) - ); + task.getArtifactsList().stream() + .map(item -> artifact(item)) + .collect(Collectors.toList()), + task.getHistoryList().stream() + .map(item -> message(item)) + .collect(Collectors.toList()), + struct(task.getMetadata())); } - public static Message_v0_3 message(org.a2aproject.sdk.compat03.grpc.MessageOrBuilder message) { + public static Message_v0_3 message( + org.a2aproject.sdk.compat03.grpc.MessageOrBuilder message) { return new Message_v0_3( role(message.getRole()), - message.getContentList().stream().map(item -> part(item)).collect(Collectors.toList()), - message.getMessageId().isEmpty() ? null : message.getMessageId(), - message.getContextId().isEmpty() ? null : message.getContextId(), - message.getTaskId().isEmpty() ? null : message.getTaskId(), + message.getContentList().stream() + .map(item -> part(item)) + .collect(Collectors.toList()), + message.getMessageId().isEmpty() ? null : message.getMessageId(), + message.getContextId().isEmpty() ? null : message.getContextId(), + message.getTaskId().isEmpty() ? null : message.getTaskId(), null, // referenceTaskIds is not in grpc message struct(message.getMetadata()), - message.getExtensionsList().isEmpty() ? null : message.getExtensionsList() - ); + message.getExtensionsList().isEmpty() ? null : message.getExtensionsList()); } - public static TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent(org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEventOrBuilder taskStatusUpdateEvent) { + public static TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent( + org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEventOrBuilder + taskStatusUpdateEvent) { return new TaskStatusUpdateEvent_v0_3.Builder() .taskId(taskStatusUpdateEvent.getTaskId()) .status(taskStatus(taskStatusUpdateEvent.getStatus())) @@ -901,7 +1077,9 @@ public static TaskStatusUpdateEvent_v0_3 taskStatusUpdateEvent(org.a2aproject.sd .build(); } - public static TaskArtifactUpdateEvent_v0_3 taskArtifactUpdateEvent(org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEventOrBuilder taskArtifactUpdateEvent) { + public static TaskArtifactUpdateEvent_v0_3 taskArtifactUpdateEvent( + org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEventOrBuilder + taskArtifactUpdateEvent) { return new TaskArtifactUpdateEvent_v0_3.Builder() .taskId(taskArtifactUpdateEvent.getTaskId()) .append(taskArtifactUpdateEvent.getAppend()) @@ -912,15 +1090,17 @@ public static TaskArtifactUpdateEvent_v0_3 taskArtifactUpdateEvent(org.a2aprojec .build(); } - private static Artifact_v0_3 artifact(org.a2aproject.sdk.compat03.grpc.ArtifactOrBuilder artifact) { + private static Artifact_v0_3 artifact( + org.a2aproject.sdk.compat03.grpc.ArtifactOrBuilder artifact) { return new Artifact_v0_3( artifact.getArtifactId(), artifact.getName(), artifact.getDescription(), - artifact.getPartsList().stream().map(item -> part(item)).collect(Collectors.toList()), + artifact.getPartsList().stream() + .map(item -> part(item)) + .collect(Collectors.toList()), struct(artifact.getMetadata()), - artifact.getExtensionsList().isEmpty() ? null : artifact.getExtensionsList() - ); + artifact.getExtensionsList().isEmpty() ? null : artifact.getExtensionsList()); } static Part_v0_3 part(org.a2aproject.sdk.compat03.grpc.PartOrBuilder part) { @@ -935,23 +1115,34 @@ static Part_v0_3 part(org.a2aproject.sdk.compat03.grpc.PartOrBuilder part) { throw new InvalidRequestError_v0_3(); } - private static FilePart_v0_3 filePart(org.a2aproject.sdk.compat03.grpc.FilePartOrBuilder filePart, - @Nullable Map metadata) { + private static FilePart_v0_3 filePart( + org.a2aproject.sdk.compat03.grpc.FilePartOrBuilder filePart, + @Nullable Map metadata) { String name = filePart.getName().isEmpty() ? null : filePart.getName(); if (filePart.hasFileWithBytes()) { - return new FilePart_v0_3(new FileWithBytes_v0_3(filePart.getMimeType(), name, filePart.getFileWithBytes().toStringUtf8()), metadata); + return new FilePart_v0_3( + new FileWithBytes_v0_3( + filePart.getMimeType(), + name, + filePart.getFileWithBytes().toStringUtf8()), + metadata); } else if (filePart.hasFileWithUri()) { - return new FilePart_v0_3(new FileWithUri_v0_3(filePart.getMimeType(), name, filePart.getFileWithUri()), metadata); + return new FilePart_v0_3( + new FileWithUri_v0_3( + filePart.getMimeType(), name, filePart.getFileWithUri()), + metadata); } throw new InvalidRequestError_v0_3(); } - private static DataPart_v0_3 dataPart(org.a2aproject.sdk.compat03.grpc.DataPartOrBuilder dataPart, - @Nullable Map metadata) { + private static DataPart_v0_3 dataPart( + org.a2aproject.sdk.compat03.grpc.DataPartOrBuilder dataPart, + @Nullable Map metadata) { return new DataPart_v0_3(struct(dataPart.getData()), metadata); } - private static @Nullable TaskStatus_v0_3 taskStatus(org.a2aproject.sdk.compat03.grpc.TaskStatusOrBuilder taskStatus) { + private static @Nullable TaskStatus_v0_3 taskStatus( + org.a2aproject.sdk.compat03.grpc.TaskStatusOrBuilder taskStatus) { TaskState_v0_3 state = taskState(taskStatus.getState()); if (state == null) { return null; @@ -959,49 +1150,41 @@ private static DataPart_v0_3 dataPart(org.a2aproject.sdk.compat03.grpc.DataPartO return new TaskStatus_v0_3( taskState(taskStatus.getState()), taskStatus.hasUpdate() ? message(taskStatus.getUpdateOrBuilder()) : null, - OffsetDateTime.ofInstant(Instant.ofEpochSecond(taskStatus.getTimestamp().getSeconds(), taskStatus.getTimestamp().getNanos()), ZoneOffset.UTC) - ); + OffsetDateTime.ofInstant( + Instant.ofEpochSecond( + taskStatus.getTimestamp().getSeconds(), + taskStatus.getTimestamp().getNanos()), + ZoneOffset.UTC)); } - private static Message_v0_3.@Nullable Role role(org.a2aproject.sdk.compat03.grpc.Role role) { + private static Message_v0_3.@Nullable Role role( + org.a2aproject.sdk.compat03.grpc.Role role) { if (role == null) { return null; } return switch (role) { - case ROLE_USER -> - Message_v0_3.Role.USER; - case ROLE_AGENT -> - Message_v0_3.Role.AGENT; - default -> - throw new InvalidRequestError_v0_3(); + case ROLE_USER -> Message_v0_3.Role.USER; + case ROLE_AGENT -> Message_v0_3.Role.AGENT; + default -> throw new InvalidRequestError_v0_3(); }; } - private static @Nullable TaskState_v0_3 taskState(org.a2aproject.sdk.compat03.grpc.TaskState taskState) { + private static @Nullable TaskState_v0_3 taskState( + org.a2aproject.sdk.compat03.grpc.TaskState taskState) { if (taskState == null) { return null; } return switch (taskState) { - case TASK_STATE_SUBMITTED -> - TaskState_v0_3.SUBMITTED; - case TASK_STATE_WORKING -> - TaskState_v0_3.WORKING; - case TASK_STATE_INPUT_REQUIRED -> - TaskState_v0_3.INPUT_REQUIRED; - case TASK_STATE_AUTH_REQUIRED -> - TaskState_v0_3.AUTH_REQUIRED; - case TASK_STATE_COMPLETED -> - TaskState_v0_3.COMPLETED; - case TASK_STATE_CANCELLED -> - TaskState_v0_3.CANCELED; - case TASK_STATE_FAILED -> - TaskState_v0_3.FAILED; - case TASK_STATE_REJECTED -> - TaskState_v0_3.REJECTED; - case TASK_STATE_UNSPECIFIED -> - null; - case UNRECOGNIZED -> - null; + case TASK_STATE_SUBMITTED -> TaskState_v0_3.SUBMITTED; + case TASK_STATE_WORKING -> TaskState_v0_3.WORKING; + case TASK_STATE_INPUT_REQUIRED -> TaskState_v0_3.INPUT_REQUIRED; + case TASK_STATE_AUTH_REQUIRED -> TaskState_v0_3.AUTH_REQUIRED; + case TASK_STATE_COMPLETED -> TaskState_v0_3.COMPLETED; + case TASK_STATE_CANCELLED -> TaskState_v0_3.CANCELED; + case TASK_STATE_FAILED -> TaskState_v0_3.FAILED; + case TASK_STATE_REJECTED -> TaskState_v0_3.REJECTED; + case TASK_STATE_UNSPECIFIED -> null; + case UNRECOGNIZED -> null; }; } @@ -1033,5 +1216,4 @@ private static DataPart_v0_3 dataPart(org.a2aproject.sdk.compat03.grpc.DataPartO } } } - } diff --git a/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/package-info.java b/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/package-info.java index bd0d8d936..e12648499 100644 --- a/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/package-info.java +++ b/compat-0.3/spec-grpc/src/main/java/org/a2aproject/sdk/compat03/grpc/utils/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.compat03.grpc.utils; import org.jspecify.annotations.NullMarked; - diff --git a/compat-0.3/spec-grpc/src/test/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoJsonUtils_v0_3_Test.java b/compat-0.3/spec-grpc/src/test/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoJsonUtils_v0_3_Test.java index 7766c57a2..b66b1eb67 100644 --- a/compat-0.3/spec-grpc/src/test/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoJsonUtils_v0_3_Test.java +++ b/compat-0.3/spec-grpc/src/test/java/org/a2aproject/sdk/compat03/grpc/utils/ProtoJsonUtils_v0_3_Test.java @@ -4,12 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.Collections; - import com.google.protobuf.util.JsonFormat; - -import org.a2aproject.sdk.compat03.spec.Message_v0_3; +import java.util.Collections; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; import org.junit.jupiter.api.Test; @@ -17,67 +15,80 @@ public class ProtoJsonUtils_v0_3_Test { @Test public void toJson_doesNotHtmlEscapeAngleBrackets() throws Exception { - MessageSendParams_v0_3 params = new MessageSendParams_v0_3( - new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3(""))) - .contextId("context-1234") - .messageId("message-1234") - .build(), - null, null - ); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3( + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts( + Collections.singletonList( + new TextPart_v0_3(""))) + .contextId("context-1234") + .messageId("message-1234") + .build(), + null, + null); var proto = ProtoUtils_v0_3.ToProto.sendMessageRequest(params); String json = ProtoJsonUtils_v0_3.toJson(JsonFormat.printer(), proto); - assertTrue(json.contains(""), - "JSON should preserve literal '' but got: " + json); - assertFalse(json.contains("\\u003c"), - "JSON must not contain HTML-escaped '<' (\\u003c) but got: " + json); - assertFalse(json.contains("\\u003e"), - "JSON must not contain HTML-escaped '>' (\\u003e) but got: " + json); + assertTrue( + json.contains(""), + "JSON should preserve literal '' but got: " + json); + assertFalse( + json.contains("\\u003c"), + "JSON must not contain HTML-escaped '<' (\\u003c) but got: " + json); + assertFalse( + json.contains("\\u003e"), + "JSON must not contain HTML-escaped '>' (\\u003e) but got: " + json); } @Test public void toJson_doesNotHtmlEscapeAmpersand() throws Exception { - MessageSendParams_v0_3 params = new MessageSendParams_v0_3( - new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("foo&bar"))) - .contextId("context-1234") - .messageId("message-1234") - .build(), - null, null - ); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3( + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("foo&bar"))) + .contextId("context-1234") + .messageId("message-1234") + .build(), + null, + null); var proto = ProtoUtils_v0_3.ToProto.sendMessageRequest(params); String json = ProtoJsonUtils_v0_3.toJson(JsonFormat.printer(), proto); - assertTrue(json.contains("foo&bar"), - "JSON should preserve literal '&' but got: " + json); - assertFalse(json.contains("\\u0026"), - "JSON must not contain HTML-escaped '&' (\\u0026) but got: " + json); + assertTrue(json.contains("foo&bar"), "JSON should preserve literal '&' but got: " + json); + assertFalse( + json.contains("\\u0026"), + "JSON must not contain HTML-escaped '&' (\\u0026) but got: " + json); } @Test public void toJson_respectsAlwaysPrintFieldsWithNoPresence() throws Exception { - MessageSendParams_v0_3 params = new MessageSendParams_v0_3( - new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("hello"))) - .contextId("context-1234") - .messageId("message-1234") - .build(), - null, null - ); + MessageSendParams_v0_3 params = + new MessageSendParams_v0_3( + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("hello"))) + .contextId("context-1234") + .messageId("message-1234") + .build(), + null, + null); var proto = ProtoUtils_v0_3.ToProto.sendMessageRequest(params); - String withDefaults = ProtoJsonUtils_v0_3.toJson( - JsonFormat.printer().alwaysPrintFieldsWithNoPresence(), proto); + String withDefaults = + ProtoJsonUtils_v0_3.toJson( + JsonFormat.printer().alwaysPrintFieldsWithNoPresence(), proto); String withoutDefaults = ProtoJsonUtils_v0_3.toJson(JsonFormat.printer(), proto); - assertTrue(withDefaults.length() > withoutDefaults.length(), - "alwaysPrintFieldsWithNoPresence should produce more fields, got:\n with: " + withDefaults + "\n without: " + withoutDefaults); + assertTrue( + withDefaults.length() > withoutDefaults.length(), + "alwaysPrintFieldsWithNoPresence should produce more fields, got:\n with: " + + withDefaults + + "\n without: " + + withoutDefaults); } @Test diff --git a/compat-0.3/spec-grpc/src/test/java/org/a2aproject/sdk/compat03/grpc/utils/ToProto_v0_3_Test.java b/compat-0.3/spec-grpc/src/test/java/org/a2aproject/sdk/compat03/grpc/utils/ToProto_v0_3_Test.java index 7a27827e4..b1c16c158 100644 --- a/compat-0.3/spec-grpc/src/test/java/org/a2aproject/sdk/compat03/grpc/utils/ToProto_v0_3_Test.java +++ b/compat-0.3/spec-grpc/src/test/java/org/a2aproject/sdk/compat03/grpc/utils/ToProto_v0_3_Test.java @@ -10,7 +10,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.compat03.grpc.SendMessageConfiguration; import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; @@ -20,53 +19,59 @@ import org.a2aproject.sdk.compat03.spec.FileWithBytes_v0_3; import org.a2aproject.sdk.compat03.spec.FileWithUri_v0_3; import org.a2aproject.sdk.compat03.spec.HTTPAuthSecurityScheme_v0_3; -import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendConfiguration_v0_3; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationAuthenticationInfo_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; +import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; import org.junit.jupiter.api.Test; public class ToProto_v0_3_Test { - private static final Message_v0_3 SIMPLE_MESSAGE = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); + private static final Message_v0_3 SIMPLE_MESSAGE = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); @Test public void convertAgentCard() { - AgentCard_v0_3 agentCard = new AgentCard_v0_3.Builder() - .name("Hello World Agent") - .description("Just a hello world agent") - .url("http://localhost:9999") - .version("1.0.0") - .documentationUrl("http://example.com/docs") - .capabilities(new AgentCapabilities_v0_3.Builder() - .streaming(true) - .pushNotifications(true) - .stateTransitionHistory(true) - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(new AgentSkill_v0_3.Builder() - .id("hello_world") - .name("Returns hello world") - .description("just returns hello world") - .tags(Collections.singletonList("hello world")) - .examples(List.of("hi", "hello world")) - .build())) - .protocolVersion("0.2.5") - .build(); - org.a2aproject.sdk.compat03.grpc.AgentCard result = ProtoUtils_v0_3.ToProto.agentCard(agentCard); + AgentCard_v0_3 agentCard = + new AgentCard_v0_3.Builder() + .name("Hello World Agent") + .description("Just a hello world agent") + .url("http://localhost:9999") + .version("1.0.0") + .documentationUrl("http://example.com/docs") + .capabilities( + new AgentCapabilities_v0_3.Builder() + .streaming(true) + .pushNotifications(true) + .stateTransitionHistory(true) + .build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills( + Collections.singletonList( + new AgentSkill_v0_3.Builder() + .id("hello_world") + .name("Returns hello world") + .description("just returns hello world") + .tags(Collections.singletonList("hello world")) + .examples(List.of("hi", "hello world")) + .build())) + .protocolVersion("0.2.5") + .build(); + org.a2aproject.sdk.compat03.grpc.AgentCard result = + ProtoUtils_v0_3.ToProto.agentCard(agentCard); assertEquals("Hello World Agent", result.getName()); assertEquals("Just a hello world agent", result.getDescription()); assertEquals("http://localhost:9999", result.getUrl()); @@ -77,33 +82,43 @@ public void convertAgentCard() { assertEquals(1, result.getDefaultOutputModesCount()); assertEquals("text", result.getDefaultOutputModes(0)); assertEquals("0.2.5", result.getProtocolVersion()); - agentCard = new AgentCard_v0_3.Builder() - .name("Hello World Agent") - .description("Just a hello world agent") - .url("http://localhost:9999") - .version("1.0.0") - .documentationUrl("http://example.com/docs") - .capabilities(new AgentCapabilities_v0_3.Builder() - .streaming(true) - .pushNotifications(true) - .stateTransitionHistory(true) - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(new AgentSkill_v0_3.Builder() - .id("hello_world") - .name("Returns hello world") - .description("just returns hello world") - .tags(Collections.singletonList("hello world")) - .examples(List.of("hi", "hello world")) - .build())) - .preferredTransport("HTTP+JSON") -// .iconUrl("http://example.com/icon.svg") - .securitySchemes(Map.of("basic", new HTTPAuthSecurityScheme_v0_3.Builder().scheme("basic").description("Basic Auth").build())) - .security(List.of(Map.of("oauth", List.of("read")))) - .protocolVersion("0.2.5") - .build(); - result = ProtoUtils_v0_3.ToProto.agentCard(agentCard); + agentCard = + new AgentCard_v0_3.Builder() + .name("Hello World Agent") + .description("Just a hello world agent") + .url("http://localhost:9999") + .version("1.0.0") + .documentationUrl("http://example.com/docs") + .capabilities( + new AgentCapabilities_v0_3.Builder() + .streaming(true) + .pushNotifications(true) + .stateTransitionHistory(true) + .build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills( + Collections.singletonList( + new AgentSkill_v0_3.Builder() + .id("hello_world") + .name("Returns hello world") + .description("just returns hello world") + .tags(Collections.singletonList("hello world")) + .examples(List.of("hi", "hello world")) + .build())) + .preferredTransport("HTTP+JSON") + // .iconUrl("http://example.com/icon.svg") + .securitySchemes( + Map.of( + "basic", + new HTTPAuthSecurityScheme_v0_3.Builder() + .scheme("basic") + .description("Basic Auth") + .build())) + .security(List.of(Map.of("oauth", List.of("read")))) + .protocolVersion("0.2.5") + .build(); + result = ProtoUtils_v0_3.ToProto.agentCard(agentCard); assertEquals("Hello World Agent", result.getName()); assertEquals("Just a hello world agent", result.getDescription()); assertEquals("http://localhost:9999", result.getUrl()); @@ -122,39 +137,69 @@ public void convertAgentCard() { assertEquals("read", result.getSecurity(0).getSchemesMap().get("oauth").getList(0)); assertEquals(1, result.getSecuritySchemesMap().size()); assertEquals(true, result.getSecuritySchemesMap().containsKey("basic")); - assertEquals(result.getSecuritySchemesMap().get("basic").getApiKeySecurityScheme().getDefaultInstanceForType(), result.getSecuritySchemesMap().get("basic").getApiKeySecurityScheme()); - assertEquals(result.getSecuritySchemesMap().get("basic").getOauth2SecurityScheme().getDefaultInstanceForType(), result.getSecuritySchemesMap().get("basic").getOauth2SecurityScheme()); - assertEquals("basic", result.getSecuritySchemesMap().get("basic").getHttpAuthSecurityScheme().getScheme()); - assertEquals("Basic Auth", result.getSecuritySchemesMap().get("basic").getHttpAuthSecurityScheme().getDescription()); + assertEquals( + result.getSecuritySchemesMap() + .get("basic") + .getApiKeySecurityScheme() + .getDefaultInstanceForType(), + result.getSecuritySchemesMap().get("basic").getApiKeySecurityScheme()); + assertEquals( + result.getSecuritySchemesMap() + .get("basic") + .getOauth2SecurityScheme() + .getDefaultInstanceForType(), + result.getSecuritySchemesMap().get("basic").getOauth2SecurityScheme()); + assertEquals( + "basic", + result.getSecuritySchemesMap() + .get("basic") + .getHttpAuthSecurityScheme() + .getScheme()); + assertEquals( + "Basic Auth", + result.getSecuritySchemesMap() + .get("basic") + .getHttpAuthSecurityScheme() + .getDescription()); } @Test public void convertTask() { - Task_v0_3 task = new Task_v0_3.Builder().id("cancel-task-123") - .contextId("session-xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .build(); + Task_v0_3 task = + new Task_v0_3.Builder() + .id("cancel-task-123") + .contextId("session-xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .build(); org.a2aproject.sdk.compat03.grpc.Task result = ProtoUtils_v0_3.ToProto.task(task); assertEquals("session-xyz", result.getContextId()); assertEquals("cancel-task-123", result.getId()); - assertEquals(org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_SUBMITTED, result.getStatus().getState()); + assertEquals( + org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_SUBMITTED, + result.getStatus().getState()); assertEquals(0, result.getArtifactsCount()); assertEquals(0, result.getHistoryCount()); - task = new Task_v0_3.Builder().id("cancel-task-123") - .contextId("session-xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .artifacts(List.of(new Artifact_v0_3.Builder() - .artifactId("11") - .name("artefact") - .parts(new TextPart_v0_3("text")) - .build())) - .history(List.of(SIMPLE_MESSAGE)) - .metadata(Collections.emptyMap()) - .build(); + task = + new Task_v0_3.Builder() + .id("cancel-task-123") + .contextId("session-xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .artifacts( + List.of( + new Artifact_v0_3.Builder() + .artifactId("11") + .name("artefact") + .parts(new TextPart_v0_3("text")) + .build())) + .history(List.of(SIMPLE_MESSAGE)) + .metadata(Collections.emptyMap()) + .build(); result = ProtoUtils_v0_3.ToProto.task(task); assertEquals("session-xyz", result.getContextId()); assertEquals("cancel-task-123", result.getId()); - assertEquals(org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_SUBMITTED, result.getStatus().getState()); + assertEquals( + org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_SUBMITTED, + result.getStatus().getState()); assertEquals(1, result.getArtifactsCount()); assertEquals("11", result.getArtifacts(0).getArtifactId()); assertEquals("artefact", result.getArtifacts(0).getName()); @@ -164,38 +209,52 @@ public void convertTask() { assertEquals(false, result.getArtifacts(0).getParts(0).hasData()); assertEquals("text", result.getArtifacts(0).getParts(0).getText()); assertEquals(1, result.getHistoryCount()); - assertEquals("context-1234", result.getHistory(0).getContextId()); + assertEquals("context-1234", result.getHistory(0).getContextId()); assertEquals("message-1234", result.getHistory(0).getMessageId()); assertEquals(ROLE_USER, result.getHistory(0).getRole()); assertEquals(1, result.getHistory(0).getContentCount()); assertEquals("tell me a joke", result.getHistory(0).getContent(0).getText()); - assertEquals(org.a2aproject.sdk.compat03.grpc.FilePart.getDefaultInstance(), result.getHistory(0).getContent(0).getFile()); - assertEquals(org.a2aproject.sdk.compat03.grpc.DataPart.getDefaultInstance(), result.getHistory(0).getContent(0).getData()); + assertEquals( + org.a2aproject.sdk.compat03.grpc.FilePart.getDefaultInstance(), + result.getHistory(0).getContent(0).getFile()); + assertEquals( + org.a2aproject.sdk.compat03.grpc.DataPart.getDefaultInstance(), + result.getHistory(0).getContent(0).getData()); } @Test public void convertMessage() { - org.a2aproject.sdk.compat03.grpc.Message result = ProtoUtils_v0_3.ToProto.message(SIMPLE_MESSAGE); + org.a2aproject.sdk.compat03.grpc.Message result = + ProtoUtils_v0_3.ToProto.message(SIMPLE_MESSAGE); assertEquals("context-1234", result.getContextId()); assertEquals("message-1234", result.getMessageId()); assertEquals(ROLE_USER, result.getRole()); assertEquals(1, result.getContentCount()); assertEquals("tell me a joke", result.getContent(0).getText()); - assertEquals(org.a2aproject.sdk.compat03.grpc.FilePart.getDefaultInstance(), result.getContent(0).getFile()); - assertEquals(org.a2aproject.sdk.compat03.grpc.DataPart.getDefaultInstance(), result.getContent(0).getData()); - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.AGENT) - .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) - .messageId("message-1234") - .build(); + assertEquals( + org.a2aproject.sdk.compat03.grpc.FilePart.getDefaultInstance(), + result.getContent(0).getFile()); + assertEquals( + org.a2aproject.sdk.compat03.grpc.DataPart.getDefaultInstance(), + result.getContent(0).getData()); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.AGENT) + .parts(Collections.singletonList(new TextPart_v0_3("tell me a joke"))) + .messageId("message-1234") + .build(); result = ProtoUtils_v0_3.ToProto.message(message); assertEquals("", result.getContextId()); assertEquals("message-1234", result.getMessageId()); assertEquals(ROLE_AGENT, result.getRole()); assertEquals(1, result.getContentCount()); assertEquals("tell me a joke", result.getContent(0).getText()); - assertEquals(org.a2aproject.sdk.compat03.grpc.FilePart.getDefaultInstance(), result.getContent(0).getFile()); - assertEquals(org.a2aproject.sdk.compat03.grpc.DataPart.getDefaultInstance(), result.getContent(0).getData()); + assertEquals( + org.a2aproject.sdk.compat03.grpc.FilePart.getDefaultInstance(), + result.getContent(0).getFile()); + assertEquals( + org.a2aproject.sdk.compat03.grpc.DataPart.getDefaultInstance(), + result.getContent(0).getData()); } @Test @@ -213,22 +272,28 @@ public void convertPartMetadata() { @Test public void convertTaskPushNotificationConfig() { - TaskPushNotificationConfig_v0_3 taskPushConfig = new TaskPushNotificationConfig_v0_3("push-task-123", - new PushNotificationConfig_v0_3.Builder() - .url("http://example.com") - .id("xyz") - .build()); - org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig result = ProtoUtils_v0_3.ToProto.taskPushNotificationConfig(taskPushConfig); + TaskPushNotificationConfig_v0_3 taskPushConfig = + new TaskPushNotificationConfig_v0_3( + "push-task-123", + new PushNotificationConfig_v0_3.Builder() + .url("http://example.com") + .id("xyz") + .build()); + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig result = + ProtoUtils_v0_3.ToProto.taskPushNotificationConfig(taskPushConfig); assertEquals("tasks/push-task-123/pushNotificationConfigs/xyz", result.getName()); assertNotNull(result.getPushNotificationConfig()); assertEquals("http://example.com", result.getPushNotificationConfig().getUrl()); assertEquals("xyz", result.getPushNotificationConfig().getId()); assertEquals(false, result.getPushNotificationConfig().hasAuthentication()); - taskPushConfig - = new TaskPushNotificationConfig_v0_3("push-task-123", + taskPushConfig = + new TaskPushNotificationConfig_v0_3( + "push-task-123", new PushNotificationConfig_v0_3.Builder() .token("AAAAAA") - .authenticationInfo(new PushNotificationAuthenticationInfo_v0_3(Collections.singletonList("jwt"), "credentials")) + .authenticationInfo( + new PushNotificationAuthenticationInfo_v0_3( + Collections.singletonList("jwt"), "credentials")) .url("http://example.com") .id("xyz") .build()); @@ -239,21 +304,27 @@ public void convertTaskPushNotificationConfig() { assertEquals("xyz", result.getPushNotificationConfig().getId()); assertEquals("AAAAAA", result.getPushNotificationConfig().getToken()); assertEquals(true, result.getPushNotificationConfig().hasAuthentication()); - assertEquals("credentials", result.getPushNotificationConfig().getAuthentication().getCredentials()); + assertEquals( + "credentials", + result.getPushNotificationConfig().getAuthentication().getCredentials()); assertEquals(1, result.getPushNotificationConfig().getAuthentication().getSchemesCount()); assertEquals("jwt", result.getPushNotificationConfig().getAuthentication().getSchemes(0)); } @Test public void convertTaskArtifactUpdateEvent() { - TaskArtifactUpdateEvent_v0_3 task = new TaskArtifactUpdateEvent_v0_3.Builder() - .taskId("task-123") - .contextId("session-123") - .artifact(new Artifact_v0_3.Builder() - .artifactId("11") - .parts(new TextPart_v0_3("text")) - .build()).build(); - org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEvent result = ProtoUtils_v0_3.ToProto.taskArtifactUpdateEvent(task); + TaskArtifactUpdateEvent_v0_3 task = + new TaskArtifactUpdateEvent_v0_3.Builder() + .taskId("task-123") + .contextId("session-123") + .artifact( + new Artifact_v0_3.Builder() + .artifactId("11") + .parts(new TextPart_v0_3("text")) + .build()) + .build(); + org.a2aproject.sdk.compat03.grpc.TaskArtifactUpdateEvent result = + ProtoUtils_v0_3.ToProto.taskArtifactUpdateEvent(task); assertEquals("task-123", result.getTaskId()); assertEquals("session-123", result.getContextId()); assertNotNull(result.getArtifact()); @@ -264,26 +335,32 @@ public void convertTaskArtifactUpdateEvent() { @Test public void convertTaskStatusUpdateEvent() { - TaskStatusUpdateEvent_v0_3 tsue = new TaskStatusUpdateEvent_v0_3.Builder() - .taskId("1234") - .contextId("xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) - .isFinal(true) - .build(); - org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEvent result = ProtoUtils_v0_3.ToProto.taskStatusUpdateEvent(tsue); + TaskStatusUpdateEvent_v0_3 tsue = + new TaskStatusUpdateEvent_v0_3.Builder() + .taskId("1234") + .contextId("xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) + .isFinal(true) + .build(); + org.a2aproject.sdk.compat03.grpc.TaskStatusUpdateEvent result = + ProtoUtils_v0_3.ToProto.taskStatusUpdateEvent(tsue); assertEquals("1234", result.getTaskId()); assertEquals("xyz", result.getContextId()); assertEquals(true, result.getFinal()); - assertEquals(org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_COMPLETED, result.getStatus().getState()); + assertEquals( + org.a2aproject.sdk.compat03.grpc.TaskState.TASK_STATE_COMPLETED, + result.getStatus().getState()); } @Test public void convertSendMessageConfiguration() { - MessageSendConfiguration_v0_3 configuration = new MessageSendConfiguration_v0_3.Builder() - .acceptedOutputModes(List.of("text")) - .blocking(false) - .build(); - SendMessageConfiguration result = ProtoUtils_v0_3.ToProto.messageSendConfiguration(configuration); + MessageSendConfiguration_v0_3 configuration = + new MessageSendConfiguration_v0_3.Builder() + .acceptedOutputModes(List.of("text")) + .blocking(false) + .build(); + SendMessageConfiguration result = + ProtoUtils_v0_3.ToProto.messageSendConfiguration(configuration); assertEquals(false, result.getBlocking()); assertEquals(1, result.getAcceptedOutputModesCount()); assertEquals("text", result.getAcceptedOutputModesBytes(0).toStringUtf8()); @@ -291,7 +368,8 @@ public void convertSendMessageConfiguration() { @Test public void convertFilePartWithBytesRoundTrip() { - FilePart_v0_3 filePart = new FilePart_v0_3(new FileWithBytes_v0_3("image/png", "photo.png", "aGVsbG8=")); + FilePart_v0_3 filePart = + new FilePart_v0_3(new FileWithBytes_v0_3("image/png", "photo.png", "aGVsbG8=")); org.a2aproject.sdk.compat03.grpc.Part proto = ProtoUtils_v0_3.ToProto.part(filePart); assertEquals(true, proto.hasFile()); assertEquals("image/png", proto.getFile().getMimeType()); @@ -306,7 +384,10 @@ public void convertFilePartWithBytesRoundTrip() { @Test public void convertFilePartWithUriRoundTrip() { - FilePart_v0_3 filePart = new FilePart_v0_3(new FileWithUri_v0_3("text/plain", "readme.txt", "file:///path/to/readme.txt")); + FilePart_v0_3 filePart = + new FilePart_v0_3( + new FileWithUri_v0_3( + "text/plain", "readme.txt", "file:///path/to/readme.txt")); org.a2aproject.sdk.compat03.grpc.Part proto = ProtoUtils_v0_3.ToProto.part(filePart); assertEquals(true, proto.hasFile()); assertEquals("text/plain", proto.getFile().getMimeType()); @@ -322,12 +403,14 @@ public void convertFilePartWithUriRoundTrip() { @Test public void convertTaskTimestampStatus() { OffsetDateTime expectedTimestamp = OffsetDateTime.parse("2024-10-05T12:34:56Z"); - TaskStatus_v0_3 testStatus = new TaskStatus_v0_3(TaskState_v0_3.COMPLETED, null, expectedTimestamp); - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(testStatus) - .build(); + TaskStatus_v0_3 testStatus = + new TaskStatus_v0_3(TaskState_v0_3.COMPLETED, null, expectedTimestamp); + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(testStatus) + .build(); org.a2aproject.sdk.compat03.grpc.Task grpcTask = ProtoUtils_v0_3.ToProto.task(task); task = ProtoUtils_v0_3.FromProto.task(grpcTask); diff --git a/compat-0.3/spec/pom.xml b/compat-0.3/spec/pom.xml index 1ead19893..7d4dffd75 100644 --- a/compat-0.3/spec/pom.xml +++ b/compat-0.3/spec/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -39,4 +37,4 @@ - \ No newline at end of file + diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/json/JsonMappingException_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/json/JsonMappingException_v0_3.java index c03399149..f9ebda4e9 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/json/JsonMappingException_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/json/JsonMappingException_v0_3.java @@ -60,7 +60,8 @@ public JsonMappingException_v0_3(@Nullable Object reference, String message) { * @param message the detail message explaining the mapping error * @param cause the underlying cause of the mapping error (may be null) */ - public JsonMappingException_v0_3(@Nullable Object reference, String message, @Nullable Throwable cause) { + public JsonMappingException_v0_3( + @Nullable Object reference, String message, @Nullable Throwable cause) { super(message, cause); this.reference = reference; } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/json/JsonUtil_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/json/JsonUtil_v0_3.java index 4239cd2e5..3ac8a58b2 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/json/JsonUtil_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/json/JsonUtil_v0_3.java @@ -22,49 +22,48 @@ import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import java.lang.reflect.Type; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; import org.a2aproject.sdk.compat03.spec.APIKeySecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; -import org.a2aproject.sdk.compat03.spec.EventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.JSONRPCResponse_v0_3; +import org.a2aproject.sdk.compat03.spec.AuthenticatedExtendedCardNotConfiguredError_v0_3; +import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3; import org.a2aproject.sdk.compat03.spec.DataPart_v0_3; +import org.a2aproject.sdk.compat03.spec.EventKind_v0_3; import org.a2aproject.sdk.compat03.spec.FileContent_v0_3; import org.a2aproject.sdk.compat03.spec.FilePart_v0_3; import org.a2aproject.sdk.compat03.spec.FileWithBytes_v0_3; import org.a2aproject.sdk.compat03.spec.FileWithUri_v0_3; import org.a2aproject.sdk.compat03.spec.HTTPAuthSecurityScheme_v0_3; +import org.a2aproject.sdk.compat03.spec.InternalError_v0_3; +import org.a2aproject.sdk.compat03.spec.InvalidAgentResponseError_v0_3; +import org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3; +import org.a2aproject.sdk.compat03.spec.InvalidRequestError_v0_3; +import org.a2aproject.sdk.compat03.spec.JSONParseError_v0_3; import org.a2aproject.sdk.compat03.spec.JSONRPCError_v0_3; +import org.a2aproject.sdk.compat03.spec.JSONRPCResponse_v0_3; import org.a2aproject.sdk.compat03.spec.Message_v0_3; +import org.a2aproject.sdk.compat03.spec.MethodNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.MutualTLSSecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.OAuth2SecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.OpenIdConnectSecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3; +import org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError_v0_3; import org.a2aproject.sdk.compat03.spec.SecurityScheme_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; +import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3; +import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; -import java.lang.reflect.Type; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.List; -import org.a2aproject.sdk.compat03.spec.AuthenticatedExtendedCardNotConfiguredError_v0_3; -import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3; -import org.a2aproject.sdk.compat03.spec.InternalError_v0_3; -import org.a2aproject.sdk.compat03.spec.InvalidAgentResponseError_v0_3; -import org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3; -import org.a2aproject.sdk.compat03.spec.InvalidRequestError_v0_3; -import org.a2aproject.sdk.compat03.spec.JSONParseError_v0_3; -import org.a2aproject.sdk.compat03.spec.MethodNotFoundError_v0_3; -import org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3; import org.jspecify.annotations.Nullable; - public class JsonUtil_v0_3 { private static final String THROWABLE_MARKER_FIELD = "__throwable"; @@ -81,11 +80,13 @@ private static GsonBuilder createBaseGsonBuilder() { .registerTypeAdapter(Message_v0_3.Role.class, new RoleTypeAdapter()) .registerTypeAdapter(Part_v0_3.Kind.class, new PartKindTypeAdapter()) .registerTypeHierarchyAdapter(FileContent_v0_3.class, new FileContentTypeAdapter()) - .registerTypeHierarchyAdapter(SecurityScheme_v0_3.class, new SecuritySchemeTypeAdapter()) + .registerTypeHierarchyAdapter( + SecurityScheme_v0_3.class, new SecuritySchemeTypeAdapter()) .registerTypeAdapter(void.class, new VoidTypeAdapter()) .registerTypeAdapter(Void.class, new VoidTypeAdapter()) .registerTypeAdapterFactory(new JSONRPCResponseTypeAdapterFactory()) - .registerTypeAdapter(AgentCapabilities_v0_3.class, new AgentCapabilitiesTypeAdapter()); + .registerTypeAdapter( + AgentCapabilities_v0_3.class, new AgentCapabilitiesTypeAdapter()); } /** @@ -96,13 +97,16 @@ private static GsonBuilder createBaseGsonBuilder() { *

    * Used throughout the SDK for consistent JSON serialization and deserialization. */ - public static final Gson OBJECT_MAPPER = createBaseGsonBuilder() - .registerTypeHierarchyAdapter(Part_v0_3.class, new PartTypeAdapter()) - .registerTypeHierarchyAdapter(StreamingEventKind_v0_3.class, new StreamingEventKindTypeAdapter()) - .registerTypeAdapter(EventKind_v0_3.class, new EventKindTypeAdapter()) - .create(); - - public static T fromJson(String json, Class classOfT) throws JsonProcessingException_v0_3 { + public static final Gson OBJECT_MAPPER = + createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part_v0_3.class, new PartTypeAdapter()) + .registerTypeHierarchyAdapter( + StreamingEventKind_v0_3.class, new StreamingEventKindTypeAdapter()) + .registerTypeAdapter(EventKind_v0_3.class, new EventKindTypeAdapter()) + .create(); + + public static T fromJson(String json, Class classOfT) + throws JsonProcessingException_v0_3 { try { return OBJECT_MAPPER.fromJson(json, classOfT); } catch (JsonSyntaxException e) { @@ -139,12 +143,16 @@ public static String toJson(Object data) throws JsonProcessingException_v0_3 { * Writes a JSON-RPC {@code id} field. Handles null, String, and Number values, * preserving fractional precision for non-integer numeric IDs. */ - public static void writeJsonRpcId(JsonWriter out, @Nullable Object id) throws java.io.IOException { + public static void writeJsonRpcId(JsonWriter out, @Nullable Object id) + throws java.io.IOException { out.name("id"); if (id == null) { out.nullValue(); } else if (id instanceof Number n) { - if (id instanceof Long || id instanceof Integer || id instanceof Short || id instanceof Byte) { + if (id instanceof Long + || id instanceof Integer + || id instanceof Short + || id instanceof Byte) { out.value(n.longValue()); } else { out.value(n); @@ -180,8 +188,7 @@ public void write(JsonWriter out, OffsetDateTime value) throws java.io.IOExcepti } @Override - public @Nullable - OffsetDateTime read(JsonReader in) throws java.io.IOException { + public @Nullable OffsetDateTime read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -190,7 +197,8 @@ OffsetDateTime read(JsonReader in) throws java.io.IOException { try { return OffsetDateTime.parse(dateTimeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); } catch (DateTimeParseException e) { - throw new JsonSyntaxException("Failed to parse OffsetDateTime: " + dateTimeString, e); + throw new JsonSyntaxException( + "Failed to parse OffsetDateTime: " + dateTimeString, e); } } } @@ -230,8 +238,7 @@ public void write(JsonWriter out, Throwable value) throws java.io.IOException { } @Override - public @Nullable - Throwable read(JsonReader in) throws java.io.IOException { + public @Nullable Throwable read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -244,12 +251,9 @@ Throwable read(JsonReader in) throws java.io.IOException { while (in.hasNext()) { String fieldName = in.nextName(); switch (fieldName) { - case "type" -> - type = in.nextString(); - case "message" -> - message = in.nextString(); - default -> - in.skipValue(); + case "type" -> type = in.nextString(); + case "message" -> message = in.nextString(); + default -> in.skipValue(); } } in.endObject(); @@ -363,14 +367,10 @@ public void write(JsonWriter out, JSONRPCError_v0_3 value) throws java.io.IOExce while (in.hasNext()) { String fieldName = in.nextName(); switch (fieldName) { - case CODE_FIELD -> - code = in.nextInt(); - case MESSAGE_FIELD -> - message = in.nextString(); - case DATA_FIELD -> - data = readDataValue(in); - default -> - in.skipValue(); + case CODE_FIELD -> code = in.nextInt(); + case MESSAGE_FIELD -> message = in.nextString(); + case DATA_FIELD -> data = readDataValue(in); + default -> in.skipValue(); } } in.endObject(); @@ -380,28 +380,27 @@ public void write(JsonWriter out, JSONRPCError_v0_3 value) throws java.io.IOExce private @Nullable Object readDataValue(JsonReader in) throws java.io.IOException { return switch (in.peek()) { - case STRING -> - in.nextString(); - case NUMBER -> - in.nextDouble(); - case BOOLEAN -> - in.nextBoolean(); + case STRING -> in.nextString(); + case NUMBER -> in.nextDouble(); + case BOOLEAN -> in.nextBoolean(); case NULL -> { in.nextNull(); yield null; } case BEGIN_OBJECT -> { - com.google.gson.JsonElement element = com.google.gson.JsonParser.parseReader(in); + com.google.gson.JsonElement element = + com.google.gson.JsonParser.parseReader(in); if (element.isJsonObject()) { com.google.gson.JsonObject obj = element.getAsJsonObject(); - if (obj.has(TYPE_FIELD) && obj.has(MESSAGE_FIELD) && obj.has(THROWABLE_MARKER_FIELD)) { + if (obj.has(TYPE_FIELD) + && obj.has(MESSAGE_FIELD) + && obj.has(THROWABLE_MARKER_FIELD)) { yield THROWABLE_ADAPTER.fromJsonTree(element); } } yield gson.fromJson(element, Object.class); } - case BEGIN_ARRAY -> - gson.fromJson(in, Object.class); + case BEGIN_ARRAY -> gson.fromJson(in, Object.class); default -> { in.skipValue(); yield null; @@ -409,44 +408,42 @@ public void write(JsonWriter out, JSONRPCError_v0_3 value) throws java.io.IOExce }; } - private static JSONRPCError_v0_3 createErrorInstance(@Nullable Integer code, @Nullable String message, @Nullable Object data) { + private static JSONRPCError_v0_3 createErrorInstance( + @Nullable Integer code, @Nullable String message, @Nullable Object data) { if (code == null) { throw new JsonSyntaxException("JSONRPCError must have a code field"); } return switch (code) { - case JSON_PARSE_ERROR_CODE -> - new JSONParseError_v0_3(code, message, data); + case JSON_PARSE_ERROR_CODE -> new JSONParseError_v0_3(code, message, data); case INVALID_REQUEST_ERROR_CODE -> - new InvalidRequestError_v0_3(code, message, data); + new InvalidRequestError_v0_3(code, message, data); case METHOD_NOT_FOUND_ERROR_CODE -> - new MethodNotFoundError_v0_3(code, message, data); + new MethodNotFoundError_v0_3(code, message, data); case INVALID_PARAMS_ERROR_CODE -> - new InvalidParamsError_v0_3(code, message, data); - case INTERNAL_ERROR_CODE -> - new InternalError_v0_3(code, message, data); + new InvalidParamsError_v0_3(code, message, data); + case INTERNAL_ERROR_CODE -> new InternalError_v0_3(code, message, data); case TASK_NOT_FOUND_ERROR_CODE -> - new TaskNotFoundError_v0_3(code, message, data); + new TaskNotFoundError_v0_3(code, message, data); case TASK_NOT_CANCELABLE_ERROR_CODE -> - new TaskNotCancelableError_v0_3(code, message, data); + new TaskNotCancelableError_v0_3(code, message, data); case PUSH_NOTIFICATION_NOT_SUPPORTED_ERROR_CODE -> - new PushNotificationNotSupportedError_v0_3(code, message, data); + new PushNotificationNotSupportedError_v0_3(code, message, data); case UNSUPPORTED_OPERATION_ERROR_CODE -> - new UnsupportedOperationError_v0_3(code, message, data); + new UnsupportedOperationError_v0_3(code, message, data); case CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE -> - new ContentTypeNotSupportedError_v0_3(code, message, data); + new ContentTypeNotSupportedError_v0_3(code, message, data); case INVALID_AGENT_RESPONSE_ERROR_CODE -> - new InvalidAgentResponseError_v0_3(code, message, data); + new InvalidAgentResponseError_v0_3(code, message, data); case AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED_ERROR_CODE -> - new AuthenticatedExtendedCardNotConfiguredError_v0_3(code, message, data); - default -> - new JSONRPCError_v0_3(code, message, data); + new AuthenticatedExtendedCardNotConfiguredError_v0_3( + code, message, data); + default -> new JSONRPCError_v0_3(code, message, data); }; } } } - /** * Gson TypeAdapter for serializing and deserializing {@link TaskState_v0_3} enum. *

    @@ -474,8 +471,7 @@ public void write(JsonWriter out, TaskState_v0_3 value) throws java.io.IOExcepti } @Override - public @Nullable - TaskState_v0_3 read(JsonReader in) throws java.io.IOException { + public @Nullable TaskState_v0_3 read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -522,12 +518,9 @@ public void write(JsonWriter out, Message_v0_3.Role value) throws java.io.IOExce String roleString = in.nextString(); try { return switch (roleString) { - case "user" -> - Message_v0_3.Role.USER; - case "agent" -> - Message_v0_3.Role.AGENT; - default -> - throw new IllegalArgumentException("Invalid Role: " + roleString); + case "user" -> Message_v0_3.Role.USER; + case "agent" -> Message_v0_3.Role.AGENT; + default -> throw new IllegalArgumentException("Invalid Role: " + roleString); }; } catch (IllegalArgumentException e) { throw new JsonSyntaxException("Invalid Message.Role: " + roleString, e); @@ -568,14 +561,11 @@ public void write(JsonWriter out, Part_v0_3.Kind value) throws java.io.IOExcepti String kindString = in.nextString(); try { return switch (kindString) { - case "text" -> - Part_v0_3.Kind.TEXT; - case "file" -> - Part_v0_3.Kind.FILE; - case "data" -> - Part_v0_3.Kind.DATA; + case "text" -> Part_v0_3.Kind.TEXT; + case "file" -> Part_v0_3.Kind.FILE; + case "data" -> Part_v0_3.Kind.DATA; default -> - throw new IllegalArgumentException("Invalid Part.Kind: " + kindString); + throw new IllegalArgumentException("Invalid Part.Kind: " + kindString); }; } catch (IllegalArgumentException e) { throw new JsonSyntaxException("Invalid Part.Kind: " + kindString, e); @@ -616,13 +606,13 @@ public void write(JsonWriter out, Part_v0_3 value) throws java.io.IOException } else if (value instanceof DataPart_v0_3 dataPart) { delegateGson.toJson(dataPart, DataPart_v0_3.class, out); } else { - throw new JsonSyntaxException("Unknown Part subclass: " + value.getClass().getName()); + throw new JsonSyntaxException( + "Unknown Part subclass: " + value.getClass().getName()); } } @Override - public @Nullable - Part_v0_3 read(JsonReader in) throws java.io.IOException { + public @Nullable Part_v0_3 read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -643,14 +633,10 @@ Part_v0_3 read(JsonReader in) throws java.io.IOException { String kind = kindElement.getAsString(); // Use the delegate Gson to deserialize to the concrete type return switch (kind) { - case "text" -> - delegateGson.fromJson(jsonElement, TextPart_v0_3.class); - case "file" -> - delegateGson.fromJson(jsonElement, FilePart_v0_3.class); - case "data" -> - delegateGson.fromJson(jsonElement, DataPart_v0_3.class); - default -> - throw new JsonSyntaxException("Unknown Part kind: " + kind); + case "text" -> delegateGson.fromJson(jsonElement, TextPart_v0_3.class); + case "file" -> delegateGson.fromJson(jsonElement, FilePart_v0_3.class); + case "data" -> delegateGson.fromJson(jsonElement, DataPart_v0_3.class); + default -> throw new JsonSyntaxException("Unknown Part kind: " + kind); }; } } @@ -666,9 +652,10 @@ Part_v0_3 read(JsonReader in) throws java.io.IOException { */ static class EventKindTypeAdapter extends TypeAdapter { - private final Gson delegateGson = createBaseGsonBuilder() - .registerTypeHierarchyAdapter(Part_v0_3.class, new PartTypeAdapter()) - .create(); + private final Gson delegateGson = + createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part_v0_3.class, new PartTypeAdapter()) + .create(); @Override public void write(JsonWriter out, EventKind_v0_3 value) throws java.io.IOException { @@ -681,7 +668,8 @@ public void write(JsonWriter out, EventKind_v0_3 value) throws java.io.IOExcepti } else if (value instanceof Message_v0_3 message) { delegateGson.toJson(message, Message_v0_3.class, out); } else { - throw new JsonSyntaxException("Unknown EventKind implementation: " + value.getClass().getName()); + throw new JsonSyntaxException( + "Unknown EventKind implementation: " + value.getClass().getName()); } } @@ -730,12 +718,14 @@ public void write(JsonWriter out, EventKind_v0_3 value) throws java.io.IOExcepti static class StreamingEventKindTypeAdapter extends TypeAdapter { // Create separate Gson instance without the StreamingEventKind adapter to avoid recursion - private final Gson delegateGson = createBaseGsonBuilder() - .registerTypeHierarchyAdapter(Part_v0_3.class, new PartTypeAdapter()) - .create(); + private final Gson delegateGson = + createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part_v0_3.class, new PartTypeAdapter()) + .create(); @Override - public void write(JsonWriter out, StreamingEventKind_v0_3 value) throws java.io.IOException { + public void write(JsonWriter out, StreamingEventKind_v0_3 value) + throws java.io.IOException { if (value == null) { out.nullValue(); return; @@ -750,13 +740,13 @@ public void write(JsonWriter out, StreamingEventKind_v0_3 value) throws java.io. } else if (value instanceof TaskArtifactUpdateEvent_v0_3 event) { delegateGson.toJson(event, TaskArtifactUpdateEvent_v0_3.class, out); } else { - throw new JsonSyntaxException("Unknown StreamingEventKind implementation: " + value.getClass().getName()); + throw new JsonSyntaxException( + "Unknown StreamingEventKind implementation: " + value.getClass().getName()); } } @Override - public @Nullable - StreamingEventKind_v0_3 read(JsonReader in) throws java.io.IOException { + public @Nullable StreamingEventKind_v0_3 read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -777,16 +767,14 @@ StreamingEventKind_v0_3 read(JsonReader in) throws java.io.IOException { String kind = kindElement.getAsString(); // Use the delegate Gson to deserialize to the concrete type return switch (kind) { - case "task" -> - delegateGson.fromJson(jsonElement, Task_v0_3.class); - case "message" -> - delegateGson.fromJson(jsonElement, Message_v0_3.class); + case "task" -> delegateGson.fromJson(jsonElement, Task_v0_3.class); + case "message" -> delegateGson.fromJson(jsonElement, Message_v0_3.class); case "status-update" -> - delegateGson.fromJson(jsonElement, TaskStatusUpdateEvent_v0_3.class); + delegateGson.fromJson(jsonElement, TaskStatusUpdateEvent_v0_3.class); case "artifact-update" -> - delegateGson.fromJson(jsonElement, TaskArtifactUpdateEvent_v0_3.class); + delegateGson.fromJson(jsonElement, TaskArtifactUpdateEvent_v0_3.class); default -> - throw new JsonSyntaxException("Unknown StreamingEventKind kind: " + kind); + throw new JsonSyntaxException("Unknown StreamingEventKind kind: " + kind); }; } } @@ -826,7 +814,8 @@ public void write(JsonWriter out, SecurityScheme_v0_3 value) throws java.io.IOEx } else if (value instanceof MutualTLSSecurityScheme_v0_3 v) { delegateGson.toJson(v, MutualTLSSecurityScheme_v0_3.class, out); } else { - throw new JsonSyntaxException("Unknown SecurityScheme implementation: " + value.getClass().getName()); + throw new JsonSyntaxException( + "Unknown SecurityScheme implementation: " + value.getClass().getName()); } } @@ -851,17 +840,16 @@ public void write(JsonWriter out, SecurityScheme_v0_3 value) throws java.io.IOEx String type = typeElement.getAsString(); return switch (type) { case APIKeySecurityScheme_v0_3.TYPE -> - delegateGson.fromJson(jsonElement, APIKeySecurityScheme_v0_3.class); + delegateGson.fromJson(jsonElement, APIKeySecurityScheme_v0_3.class); case HTTPAuthSecurityScheme_v0_3.TYPE -> - delegateGson.fromJson(jsonElement, HTTPAuthSecurityScheme_v0_3.class); + delegateGson.fromJson(jsonElement, HTTPAuthSecurityScheme_v0_3.class); case OAuth2SecurityScheme_v0_3.TYPE -> - delegateGson.fromJson(jsonElement, OAuth2SecurityScheme_v0_3.class); + delegateGson.fromJson(jsonElement, OAuth2SecurityScheme_v0_3.class); case OpenIdConnectSecurityScheme_v0_3.TYPE -> - delegateGson.fromJson(jsonElement, OpenIdConnectSecurityScheme_v0_3.class); + delegateGson.fromJson(jsonElement, OpenIdConnectSecurityScheme_v0_3.class); case MutualTLSSecurityScheme_v0_3.TYPE -> - delegateGson.fromJson(jsonElement, MutualTLSSecurityScheme_v0_3.class); - default -> - throw new JsonSyntaxException("Unknown SecurityScheme type: " + type); + delegateGson.fromJson(jsonElement, MutualTLSSecurityScheme_v0_3.class); + default -> throw new JsonSyntaxException("Unknown SecurityScheme type: " + type); }; } } @@ -900,13 +888,13 @@ public void write(JsonWriter out, FileContent_v0_3 value) throws java.io.IOExcep } else if (value instanceof FileWithUri_v0_3 fileWithUri) { delegateGson.toJson(fileWithUri, FileWithUri_v0_3.class, out); } else { - throw new JsonSyntaxException("Unknown FileContent implementation: " + value.getClass().getName()); + throw new JsonSyntaxException( + "Unknown FileContent implementation: " + value.getClass().getName()); } } @Override - public @Nullable - FileContent_v0_3 read(JsonReader in) throws java.io.IOException { + public @Nullable FileContent_v0_3 read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -920,13 +908,15 @@ FileContent_v0_3 read(JsonReader in) throws java.io.IOException { com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); - // Distinguish between FileWithBytes and FileWithUri by checking for "bytes" or "uri" field + // Distinguish between FileWithBytes and FileWithUri by checking for "bytes" or "uri" + // field if (jsonObject.has("bytes")) { return delegateGson.fromJson(jsonElement, FileWithBytes_v0_3.class); } else if (jsonObject.has("uri")) { return delegateGson.fromJson(jsonElement, FileWithUri_v0_3.class); } else { - throw new JsonSyntaxException("FileContent must have either 'bytes' or 'uri' field"); + throw new JsonSyntaxException( + "FileContent must have either 'bytes' or 'uri' field"); } } } @@ -963,7 +953,8 @@ public void write(JsonWriter out, AgentCapabilities_v0_3 value) throws java.io.I } @Override - public org.a2aproject.sdk.compat03.spec.@Nullable AgentCapabilities_v0_3 read(JsonReader in) throws java.io.IOException { + public org.a2aproject.sdk.compat03.spec.@Nullable AgentCapabilities_v0_3 read(JsonReader in) + throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -976,7 +967,6 @@ public void write(JsonWriter out, AgentCapabilities_v0_3 value) throws java.io.I static class VoidTypeAdapter extends TypeAdapter { - @Override @SuppressWarnings("resource") public void write(final JsonWriter out, final Void value) throws java.io.IOException { @@ -988,7 +978,6 @@ public void write(final JsonWriter out, final Void value) throws java.io.IOExcep in.skipValue(); return null; } - } /** @@ -1037,7 +1026,8 @@ public void write(JsonWriter out, T value) throws java.io.IOException { out.name("result"); Object result = response.getResult(); if (result == null) { - // JsonWriter.nullValue() skips both name+value when serializeNulls=false, + // JsonWriter.nullValue() skips both name+value when + // serializeNulls=false, // so we must temporarily enable it to write "result":null as required // by JSON-RPC 2.0. boolean prev = out.getSerializeNulls(); @@ -1060,5 +1050,4 @@ public T read(JsonReader in) throws java.io.IOException { }; } } - } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientError_v0_3.java index d5aeaa5e0..eb0500f3f 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientError_v0_3.java @@ -4,8 +4,7 @@ * Base exception for A2A Client errors. */ public class A2AClientError_v0_3 extends RuntimeException { - public A2AClientError_v0_3() { - } + public A2AClientError_v0_3() {} public A2AClientError_v0_3(String message) { super(message); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientInvalidArgsError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientInvalidArgsError_v0_3.java index 0900d6a04..ba192a21f 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientInvalidArgsError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientInvalidArgsError_v0_3.java @@ -2,8 +2,7 @@ public class A2AClientInvalidArgsError_v0_3 extends A2AClientError_v0_3 { - public A2AClientInvalidArgsError_v0_3() { - } + public A2AClientInvalidArgsError_v0_3() {} public A2AClientInvalidArgsError_v0_3(String message) { super("Invalid arguments error: " + message); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientInvalidStateError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientInvalidStateError_v0_3.java index 4b94d442b..21b21c2c5 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientInvalidStateError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientInvalidStateError_v0_3.java @@ -2,8 +2,7 @@ public class A2AClientInvalidStateError_v0_3 extends A2AClientError_v0_3 { - public A2AClientInvalidStateError_v0_3() { - } + public A2AClientInvalidStateError_v0_3() {} public A2AClientInvalidStateError_v0_3(String message) { super("Invalid state error: " + message); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientJSONError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientJSONError_v0_3.java index 2c094e4d5..39e0a72e5 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientJSONError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AClientJSONError_v0_3.java @@ -27,8 +27,7 @@ */ public class A2AClientJSONError_v0_3 extends A2AClientError_v0_3 { - public A2AClientJSONError_v0_3() { - } + public A2AClientJSONError_v0_3() {} public A2AClientJSONError_v0_3(String message) { super(message); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AError_v0_3.java index 05986fa39..d16e6a042 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AError_v0_3.java @@ -1,4 +1,3 @@ package org.a2aproject.sdk.compat03.spec; -public interface A2AError_v0_3 extends Event_v0_3 { -} +public interface A2AError_v0_3 extends Event_v0_3 {} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AException_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AException_v0_3.java index ae81343b2..2ea352522 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AException_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/A2AException_v0_3.java @@ -9,8 +9,7 @@ public class A2AException_v0_3 extends RuntimeException { * Constructs a new {@code A2AException} instance. The message is left blank ({@code null}), and no * cause is specified. */ - public A2AException_v0_3() { - } + public A2AException_v0_3() {} /** * Constructs a new {@code A2AException} instance with an initial message. No cause is specified. diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/APIKeySecurityScheme_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/APIKeySecurityScheme_v0_3.java index 7e4e818b8..d95b33ae6 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/APIKeySecurityScheme_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/APIKeySecurityScheme_v0_3.java @@ -6,7 +6,9 @@ /** * Defines a security scheme using an API key. */ -public record APIKeySecurityScheme_v0_3(String in, String name, @Nullable String description, String type) implements SecurityScheme_v0_3 { +public record APIKeySecurityScheme_v0_3( + String in, String name, @Nullable String description, String type) + implements SecurityScheme_v0_3 { public static final String TYPE = "apiKey"; @@ -39,12 +41,14 @@ public static Location fromString(String location) { case "query" -> { return QUERY; } - default -> throw new IllegalArgumentException("Invalid API key location: " + location); + default -> + throw new IllegalArgumentException("Invalid API key location: " + location); } } } - public APIKeySecurityScheme_v0_3(String in, String name, @Nullable String description, @Nullable String type) { + public APIKeySecurityScheme_v0_3( + String in, String name, @Nullable String description, @Nullable String type) { Assert.checkNotNullParam("in", in); Assert.checkNotNullParam("name", name); if (type != null && !type.equals(TYPE)) { diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCapabilities_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCapabilities_v0_3.java index c69eb62e7..0b012e660 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCapabilities_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCapabilities_v0_3.java @@ -5,8 +5,11 @@ /** * Defines optional capabilities supported by an agent. */ -public record AgentCapabilities_v0_3(boolean streaming, boolean pushNotifications, boolean stateTransitionHistory, - List extensions) { +public record AgentCapabilities_v0_3( + boolean streaming, + boolean pushNotifications, + boolean stateTransitionHistory, + List extensions) { public static class Builder { @@ -36,7 +39,8 @@ public Builder extensions(List extensions) { } public AgentCapabilities_v0_3 build() { - return new AgentCapabilities_v0_3(streaming, pushNotifications, stateTransitionHistory, extensions); + return new AgentCapabilities_v0_3( + streaming, pushNotifications, stateTransitionHistory, extensions); } } } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCardSignature_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCardSignature_v0_3.java index eeda617ac..6ba0fd4cc 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCardSignature_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCardSignature_v0_3.java @@ -2,15 +2,16 @@ import com.google.gson.annotations.SerializedName; import java.util.Map; - import org.a2aproject.sdk.util.Assert; /** * Represents a JWS signature of an AgentCard. * This follows the JSON format of an RFC 7515 JSON Web Signature (JWS). */ -public record AgentCardSignature_v0_3(Map header, @SerializedName("protected")String protectedHeader, - String signature) { +public record AgentCardSignature_v0_3( + Map header, + @SerializedName("protected") String protectedHeader, + String signature) { public AgentCardSignature_v0_3 { Assert.checkNotNullParam("protectedHeader", protectedHeader); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCard_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCard_v0_3.java index d5ab8010d..7239750f7 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCard_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentCard_v0_3.java @@ -3,7 +3,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.util.Assert; /** @@ -11,12 +10,25 @@ * metadata including the agent's identity, capabilities, skills, supported * communication methods, and security requirements. */ -public record AgentCard_v0_3(String name, String description, String url, AgentProvider_v0_3 provider, - String version, String documentationUrl, AgentCapabilities_v0_3 capabilities, - List defaultInputModes, List defaultOutputModes, List skills, - boolean supportsAuthenticatedExtendedCard, Map securitySchemes, - List>> security, String iconUrl, List additionalInterfaces, - String preferredTransport, String protocolVersion, List signatures) { +public record AgentCard_v0_3( + String name, + String description, + String url, + AgentProvider_v0_3 provider, + String version, + String documentationUrl, + AgentCapabilities_v0_3 capabilities, + List defaultInputModes, + List defaultOutputModes, + List skills, + boolean supportsAuthenticatedExtendedCard, + Map securitySchemes, + List>> security, + String iconUrl, + List additionalInterfaces, + String preferredTransport, + String protocolVersion, + List signatures) { private static final String DEFAULT_PROTOCOL_VERSION = "0.3.0"; private static final TransportProtocol_v0_3 DEFAULT_TRANSPORT = TransportProtocol_v0_3.JSONRPC; @@ -61,9 +73,7 @@ public static class Builder { /** * Creates a new Builder. */ - public Builder() { - - } + public Builder() {} /** * Creates a new Builder as a copy of an existing AgentCard. @@ -78,14 +88,22 @@ public Builder(AgentCard_v0_3 card) { this.version = card.version; this.documentationUrl = card.documentationUrl; this.capabilities = card.capabilities; - this.defaultInputModes = card.defaultInputModes != null ? new ArrayList<>(card.defaultInputModes) : null; - this.defaultOutputModes = card.defaultOutputModes != null ? new ArrayList<>(card.defaultOutputModes) : null; + this.defaultInputModes = + card.defaultInputModes != null ? new ArrayList<>(card.defaultInputModes) : null; + this.defaultOutputModes = + card.defaultOutputModes != null + ? new ArrayList<>(card.defaultOutputModes) + : null; this.skills = card.skills != null ? new ArrayList<>(card.skills) : null; this.supportsAuthenticatedExtendedCard = card.supportsAuthenticatedExtendedCard; - this.securitySchemes = card.securitySchemes != null ? Map.copyOf(card.securitySchemes) : null; + this.securitySchemes = + card.securitySchemes != null ? Map.copyOf(card.securitySchemes) : null; this.security = card.security != null ? new ArrayList<>(card.security) : null; this.iconUrl = card.iconUrl; - this.additionalInterfaces = card.additionalInterfaces != null ? new ArrayList<>(card.additionalInterfaces) : null; + this.additionalInterfaces = + card.additionalInterfaces != null + ? new ArrayList<>(card.additionalInterfaces) + : null; this.preferredTransport = card.preferredTransport; this.protocolVersion = card.protocolVersion; this.signatures = card.signatures != null ? new ArrayList<>(card.signatures) : null; @@ -141,7 +159,8 @@ public Builder skills(List skills) { return this; } - public Builder supportsAuthenticatedExtendedCard(boolean supportsAuthenticatedExtendedCard) { + public Builder supportsAuthenticatedExtendedCard( + boolean supportsAuthenticatedExtendedCard) { this.supportsAuthenticatedExtendedCard = supportsAuthenticatedExtendedCard; return this; } @@ -190,10 +209,25 @@ public AgentCard_v0_3 build() { additionalInterfaces = new ArrayList<>(); additionalInterfaces.add(new AgentInterface_v0_3(preferredTransport, url)); } - return new AgentCard_v0_3(name, description, url, provider, version, documentationUrl, - capabilities, defaultInputModes, defaultOutputModes, skills, - supportsAuthenticatedExtendedCard, securitySchemes, security, iconUrl, - additionalInterfaces, preferredTransport, protocolVersion, signatures); + return new AgentCard_v0_3( + name, + description, + url, + provider, + version, + documentationUrl, + capabilities, + defaultInputModes, + defaultOutputModes, + skills, + supportsAuthenticatedExtendedCard, + securitySchemes, + security, + iconUrl, + additionalInterfaces, + preferredTransport, + protocolVersion, + signatures); } } } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentExtension_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentExtension_v0_3.java index 7b06c26b7..db3613981 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentExtension_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentExtension_v0_3.java @@ -1,13 +1,13 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - import org.a2aproject.sdk.util.Assert; /** * A declaration of a protocol extension supported by an Agent. */ -public record AgentExtension_v0_3(String description, Map params, boolean required, String uri) { +public record AgentExtension_v0_3( + String description, Map params, boolean required, String uri) { public AgentExtension_v0_3 { Assert.checkNotNullParam("uri", uri); @@ -43,5 +43,4 @@ public AgentExtension_v0_3 build() { return new AgentExtension_v0_3(description, params, required, uri); } } - } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentInterface_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentInterface_v0_3.java index 839b7482d..297ba7c51 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentInterface_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentInterface_v0_3.java @@ -1,12 +1,10 @@ package org.a2aproject.sdk.compat03.spec; - import org.a2aproject.sdk.util.Assert; /** * Declares a combination of a target URL and a transport protocol for interacting with the agent. */ - public record AgentInterface_v0_3(String transport, String url) { public AgentInterface_v0_3 { Assert.checkNotNullParam("transport", transport); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentSkill_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentSkill_v0_3.java index cbb40a0fd..082c08b4b 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentSkill_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AgentSkill_v0_3.java @@ -2,15 +2,20 @@ import java.util.List; import java.util.Map; - import org.a2aproject.sdk.util.Assert; /** * The set of skills, or distinct capabilities, that the agent can perform. */ -public record AgentSkill_v0_3(String id, String name, String description, List tags, - List examples, List inputModes, List outputModes, - List>> security) { +public record AgentSkill_v0_3( + String id, + String name, + String description, + List tags, + List examples, + List inputModes, + List outputModes, + List>> security) { public AgentSkill_v0_3 { Assert.checkNotNullParam("description", description); @@ -71,7 +76,8 @@ public Builder security(List>> security) { } public AgentSkill_v0_3 build() { - return new AgentSkill_v0_3(id, name, description, tags, examples, inputModes, outputModes, security); + return new AgentSkill_v0_3( + id, name, description, tags, examples, inputModes, outputModes, security); } } } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Artifact_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Artifact_v0_3.java index e10c267c1..0e497d426 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Artifact_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Artifact_v0_3.java @@ -2,14 +2,18 @@ import java.util.List; import java.util.Map; - import org.a2aproject.sdk.util.Assert; /** * Represents a file, data structure, or other resource generated by an agent during a task. */ -public record Artifact_v0_3(String artifactId, String name, String description, List> parts, Map metadata, - List extensions) { +public record Artifact_v0_3( + String artifactId, + String name, + String description, + List> parts, + Map metadata, + List extensions) { public Artifact_v0_3 { Assert.checkNotNullParam("artifactId", artifactId); @@ -27,8 +31,7 @@ public static class Builder { private Map metadata; private List extensions; - public Builder(){ - } + public Builder() {} public Builder(Artifact_v0_3 existingArtifact) { artifactId = existingArtifact.artifactId(); @@ -44,7 +47,6 @@ public Builder artifactId(String artifactId) { return this; } - public Builder name(String name) { this.name = name; return this; diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthenticatedExtendedCardNotConfiguredError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthenticatedExtendedCardNotConfiguredError_v0_3.java index 2fb9b1928..619190205 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthenticatedExtendedCardNotConfiguredError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthenticatedExtendedCardNotConfiguredError_v0_3.java @@ -2,16 +2,16 @@ import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; - /** * An A2A-specific error indicating that the agent does not have an * Authenticated Extended Card configured */ public class AuthenticatedExtendedCardNotConfiguredError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = -32007; + public static final Integer DEFAULT_CODE = -32007; - public AuthenticatedExtendedCardNotConfiguredError_v0_3(Integer code, String message, Object data) { + public AuthenticatedExtendedCardNotConfiguredError_v0_3( + Integer code, String message, Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Authenticated Extended Card not configured"), diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthenticationInfo_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthenticationInfo_v0_3.java index 064cb3ec9..f2041ebc5 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthenticationInfo_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthenticationInfo_v0_3.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.compat03.spec; import java.util.List; - import org.a2aproject.sdk.util.Assert; /** diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthorizationCodeOAuthFlow_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthorizationCodeOAuthFlow_v0_3.java index 4dd875f0c..639028d38 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthorizationCodeOAuthFlow_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/AuthorizationCodeOAuthFlow_v0_3.java @@ -1,15 +1,13 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - - import org.a2aproject.sdk.util.Assert; /** * Defines configuration details for the OAuth 2.0 Authorization Code flow. */ -public record AuthorizationCodeOAuthFlow_v0_3(String authorizationUrl, String refreshUrl, Map scopes, - String tokenUrl) { +public record AuthorizationCodeOAuthFlow_v0_3( + String authorizationUrl, String refreshUrl, Map scopes, String tokenUrl) { public AuthorizationCodeOAuthFlow_v0_3 { Assert.checkNotNullParam("authorizationUrl", authorizationUrl); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/CancelTaskRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/CancelTaskRequest_v0_3.java index f23d2e63e..3a1305125 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/CancelTaskRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/CancelTaskRequest_v0_3.java @@ -3,27 +3,25 @@ import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; import java.util.UUID; - - import org.a2aproject.sdk.util.Assert; /** * A request that can be used to cancel a task. */ -public final class CancelTaskRequest_v0_3 extends NonStreamingJSONRPCRequest_v0_3 { - - private CancelTaskRequest_v0_3() { - } +public final class CancelTaskRequest_v0_3 + extends NonStreamingJSONRPCRequest_v0_3 { + private CancelTaskRequest_v0_3() {} public static final String METHOD = "tasks/cancel"; - public CancelTaskRequest_v0_3(String jsonrpc, Object id, String method, TaskIdParams_v0_3 params) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + public CancelTaskRequest_v0_3( + String jsonrpc, Object id, String method, TaskIdParams_v0_3 params) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } Assert.checkNotNullParam("method", method); - if (! method.equals(METHOD)) { + if (!method.equals(METHOD)) { throw new IllegalArgumentException("Invalid CancelTaskRequest method"); } Assert.checkNotNullParam("params", params); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/CancelTaskResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/CancelTaskResponse_v0_3.java index 7c3433f75..cf2b0d7e0 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/CancelTaskResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/CancelTaskResponse_v0_3.java @@ -3,13 +3,12 @@ /** * A response to a cancel task request. */ - public final class CancelTaskResponse_v0_3 extends JSONRPCResponse_v0_3 { - private CancelTaskResponse_v0_3() { - } + private CancelTaskResponse_v0_3() {} - public CancelTaskResponse_v0_3(String jsonrpc, Object id, Task_v0_3 result, JSONRPCError_v0_3 error) { + public CancelTaskResponse_v0_3( + String jsonrpc, Object id, Task_v0_3 result, JSONRPCError_v0_3 error) { super(jsonrpc, id, result, error, Task_v0_3.class); } @@ -17,7 +16,6 @@ public CancelTaskResponse_v0_3(Object id, JSONRPCError_v0_3 error) { this(null, id, null, error); } - public CancelTaskResponse_v0_3(Object id, Task_v0_3 result) { this(null, id, result, null); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ClientCredentialsOAuthFlow_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ClientCredentialsOAuthFlow_v0_3.java index a0f8b31f3..b3641878a 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ClientCredentialsOAuthFlow_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ClientCredentialsOAuthFlow_v0_3.java @@ -1,19 +1,16 @@ package org.a2aproject.sdk.compat03.spec; - import java.util.Map; - - import org.a2aproject.sdk.util.Assert; /** * Defines configuration details for the OAuth 2.0 Client Credentials flow. */ -public record ClientCredentialsOAuthFlow_v0_3(String refreshUrl, Map scopes, String tokenUrl) { +public record ClientCredentialsOAuthFlow_v0_3( + String refreshUrl, Map scopes, String tokenUrl) { public ClientCredentialsOAuthFlow_v0_3 { Assert.checkNotNullParam("scopes", scopes); Assert.checkNotNullParam("tokenUrl", tokenUrl); } - } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ContentTypeNotSupportedError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ContentTypeNotSupportedError_v0_3.java index 028a80efe..cfb70ae18 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ContentTypeNotSupportedError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ContentTypeNotSupportedError_v0_3.java @@ -3,17 +3,17 @@ import static org.a2aproject.sdk.compat03.spec.A2AErrorCodes_v0_3.CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; - /** * An A2A-specific error indicating an incompatibility between the requested * content types and the agent's capabilities. */ public class ContentTypeNotSupportedError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; + public static final Integer DEFAULT_CODE = CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE; public ContentTypeNotSupportedError_v0_3(Integer code, String message, Object data) { - super(defaultIfNull(code, CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE), + super( + defaultIfNull(code, CONTENT_TYPE_NOT_SUPPORTED_ERROR_CODE), defaultIfNull(message, "Incompatible content types"), data); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DataPart_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DataPart_v0_3.java index f18d784a3..a13683faf 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DataPart_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DataPart_v0_3.java @@ -1,18 +1,20 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** * Represents a structured data segment (e.g., JSON) within a message or artifact. */ -public record DataPart_v0_3(Map data, @Nullable Map metadata, Kind kind) implements Part_v0_3> { +public record DataPart_v0_3( + Map data, @Nullable Map metadata, Kind kind) + implements Part_v0_3> { public static final String DATA = "data"; - public DataPart_v0_3(Map data, @Nullable Map metadata, Kind kind) { + public DataPart_v0_3( + Map data, @Nullable Map metadata, Kind kind) { Assert.checkNotNullParam("data", data); if (kind != Kind.DATA) { throw new IllegalArgumentException("Invalid DataPart kind: " + kind); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigParams_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigParams_v0_3.java index 400ef8c25..a4740c8da 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigParams_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigParams_v0_3.java @@ -1,14 +1,13 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - - import org.a2aproject.sdk.util.Assert; /** * Parameters for removing pushNotificationConfiguration associated with a Task. */ -public record DeleteTaskPushNotificationConfigParams_v0_3(String id, String pushNotificationConfigId, Map metadata) { +public record DeleteTaskPushNotificationConfigParams_v0_3( + String id, String pushNotificationConfigId, Map metadata) { public DeleteTaskPushNotificationConfigParams_v0_3 { Assert.checkNotNullParam("id", id); @@ -40,7 +39,8 @@ public Builder metadata(Map metadata) { } public DeleteTaskPushNotificationConfigParams_v0_3 build() { - return new DeleteTaskPushNotificationConfigParams_v0_3(id, pushNotificationConfigId, metadata); + return new DeleteTaskPushNotificationConfigParams_v0_3( + id, pushNotificationConfigId, metadata); } } } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigRequest_v0_3.java index 18c3e5246..6beb825e5 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigRequest_v0_3.java @@ -1,28 +1,31 @@ package org.a2aproject.sdk.compat03.spec; import java.util.UUID; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.compat03.util.Utils_v0_3; +import org.a2aproject.sdk.util.Assert; /** * A delete task push notification config request. */ -public final class DeleteTaskPushNotificationConfigRequest_v0_3 extends NonStreamingJSONRPCRequest_v0_3 { - - private DeleteTaskPushNotificationConfigRequest_v0_3() { - } +public final class DeleteTaskPushNotificationConfigRequest_v0_3 + extends NonStreamingJSONRPCRequest_v0_3 { + private DeleteTaskPushNotificationConfigRequest_v0_3() {} public static final String METHOD = "tasks/pushNotificationConfig/delete"; - public DeleteTaskPushNotificationConfigRequest_v0_3(String jsonrpc, Object id, String method, DeleteTaskPushNotificationConfigParams_v0_3 params) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + public DeleteTaskPushNotificationConfigRequest_v0_3( + String jsonrpc, + Object id, + String method, + DeleteTaskPushNotificationConfigParams_v0_3 params) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } Assert.checkNotNullParam("method", method); - if (! method.equals(METHOD)) { - throw new IllegalArgumentException("Invalid DeleteTaskPushNotificationConfigRequest method"); + if (!method.equals(METHOD)) { + throw new IllegalArgumentException( + "Invalid DeleteTaskPushNotificationConfigRequest method"); } Assert.isValidJsonRpcId(id); this.jsonrpc = Utils_v0_3.defaultIfNull(jsonrpc, JSONRPC_VERSION); @@ -31,7 +34,8 @@ public DeleteTaskPushNotificationConfigRequest_v0_3(String jsonrpc, Object id, S this.params = params; } - public DeleteTaskPushNotificationConfigRequest_v0_3(String id, DeleteTaskPushNotificationConfigParams_v0_3 params) { + public DeleteTaskPushNotificationConfigRequest_v0_3( + String id, DeleteTaskPushNotificationConfigParams_v0_3 params) { this(null, id, METHOD, params); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigResponse_v0_3.java index f42d2e122..1439c6f07 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/DeleteTaskPushNotificationConfigResponse_v0_3.java @@ -3,12 +3,13 @@ /** * A response for a delete task push notification config request. */ -public final class DeleteTaskPushNotificationConfigResponse_v0_3 extends JSONRPCResponse_v0_3 { +public final class DeleteTaskPushNotificationConfigResponse_v0_3 + extends JSONRPCResponse_v0_3 { - private DeleteTaskPushNotificationConfigResponse_v0_3() { - } + private DeleteTaskPushNotificationConfigResponse_v0_3() {} - public DeleteTaskPushNotificationConfigResponse_v0_3(String jsonrpc, Object id, Void result, JSONRPCError_v0_3 error) { + public DeleteTaskPushNotificationConfigResponse_v0_3( + String jsonrpc, Object id, Void result, JSONRPCError_v0_3 error) { super(jsonrpc, id, result, error, Void.class); } @@ -19,5 +20,4 @@ public DeleteTaskPushNotificationConfigResponse_v0_3(Object id, JSONRPCError_v0_ public DeleteTaskPushNotificationConfigResponse_v0_3(Object id) { this(null, id, null, null); } - } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Event_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Event_v0_3.java index 8aa7e6369..0ca93ea97 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Event_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Event_v0_3.java @@ -1,4 +1,3 @@ package org.a2aproject.sdk.compat03.spec; -public interface Event_v0_3 { -} +public interface Event_v0_3 {} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FilePart_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FilePart_v0_3.java index d3c89abdf..8eed60a0e 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FilePart_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FilePart_v0_3.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; @@ -9,11 +8,12 @@ * Represents a file segment within a message or artifact. The file content can be * provided either directly as bytes or as a URI. */ -public record FilePart_v0_3(FileContent_v0_3 file, Map metadata, Kind kind) implements Part_v0_3 { +public record FilePart_v0_3(FileContent_v0_3 file, Map metadata, Kind kind) + implements Part_v0_3 { public static final String FILE = "file"; - public FilePart_v0_3 (FileContent_v0_3 file, @Nullable Map metadata, Kind kind){ + public FilePart_v0_3(FileContent_v0_3 file, @Nullable Map metadata, Kind kind) { Assert.checkNotNullParam("file", file); if (kind != Kind.FILE) { throw new IllegalArgumentException("Invalid FilePart kind: " + kind); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FileWithBytes_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FileWithBytes_v0_3.java index 5740908be..e7d86e4dc 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FileWithBytes_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FileWithBytes_v0_3.java @@ -3,5 +3,5 @@ /** * Represents a file with its content provided directly as a base64-encoded string. */ -public record FileWithBytes_v0_3(String mimeType, String name, String bytes) implements FileContent_v0_3 { -} +public record FileWithBytes_v0_3(String mimeType, String name, String bytes) + implements FileContent_v0_3 {} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FileWithUri_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FileWithUri_v0_3.java index 7f302f9e8..24eb3bc53 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FileWithUri_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/FileWithUri_v0_3.java @@ -3,6 +3,5 @@ /** * Represents a file with its content located at a specific URI. */ -public record FileWithUri_v0_3(String mimeType, String name, String uri) implements FileContent_v0_3 { -} - +public record FileWithUri_v0_3(String mimeType, String name, String uri) + implements FileContent_v0_3 {} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetAuthenticatedExtendedCardRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetAuthenticatedExtendedCardRequest_v0_3.java index 466ea4c87..059ddda34 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetAuthenticatedExtendedCardRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetAuthenticatedExtendedCardRequest_v0_3.java @@ -1,29 +1,28 @@ package org.a2aproject.sdk.compat03.spec; import java.util.UUID; - - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.compat03.util.Utils_v0_3; +import org.a2aproject.sdk.util.Assert; /** * Represents a JSON-RPC request for the `agent/getAuthenticatedExtendedCard` method. */ -public final class GetAuthenticatedExtendedCardRequest_v0_3 extends NonStreamingJSONRPCRequest_v0_3 { - - private GetAuthenticatedExtendedCardRequest_v0_3() { - } +public final class GetAuthenticatedExtendedCardRequest_v0_3 + extends NonStreamingJSONRPCRequest_v0_3 { + private GetAuthenticatedExtendedCardRequest_v0_3() {} public static final String METHOD = "agent/getAuthenticatedExtendedCard"; - public GetAuthenticatedExtendedCardRequest_v0_3(String jsonrpc, Object id, String method, Void params) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + public GetAuthenticatedExtendedCardRequest_v0_3( + String jsonrpc, Object id, String method, Void params) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } Assert.checkNotNullParam("method", method); - if (! method.equals(METHOD)) { - throw new IllegalArgumentException("Invalid GetAuthenticatedExtendedCardRequest method"); + if (!method.equals(METHOD)) { + throw new IllegalArgumentException( + "Invalid GetAuthenticatedExtendedCardRequest method"); } Assert.isValidJsonRpcId(id); this.jsonrpc = Utils_v0_3.defaultIfNull(jsonrpc, JSONRPC_VERSION); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetAuthenticatedExtendedCardResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetAuthenticatedExtendedCardResponse_v0_3.java index a6e4e5d04..81f6f3c56 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetAuthenticatedExtendedCardResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetAuthenticatedExtendedCardResponse_v0_3.java @@ -3,12 +3,13 @@ /** * A response for the `agent/getAuthenticatedExtendedCard` method. */ -public final class GetAuthenticatedExtendedCardResponse_v0_3 extends JSONRPCResponse_v0_3 { +public final class GetAuthenticatedExtendedCardResponse_v0_3 + extends JSONRPCResponse_v0_3 { - private GetAuthenticatedExtendedCardResponse_v0_3() { - } + private GetAuthenticatedExtendedCardResponse_v0_3() {} - public GetAuthenticatedExtendedCardResponse_v0_3(String jsonrpc, Object id, AgentCard_v0_3 result, JSONRPCError_v0_3 error) { + public GetAuthenticatedExtendedCardResponse_v0_3( + String jsonrpc, Object id, AgentCard_v0_3 result, JSONRPCError_v0_3 error) { super(jsonrpc, id, result, error, AgentCard_v0_3.class); } @@ -19,5 +20,4 @@ public GetAuthenticatedExtendedCardResponse_v0_3(Object id, JSONRPCError_v0_3 er public GetAuthenticatedExtendedCardResponse_v0_3(Object id, AgentCard_v0_3 result) { this(null, id, result, null); } - } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigParams_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigParams_v0_3.java index 6a5687565..3583f16e7 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigParams_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigParams_v0_3.java @@ -1,15 +1,16 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** * Parameters for fetching a pushNotificationConfiguration associated with a Task. */ -public record GetTaskPushNotificationConfigParams_v0_3(String id, @Nullable String pushNotificationConfigId, @Nullable Map metadata) { +public record GetTaskPushNotificationConfigParams_v0_3( + String id, + @Nullable String pushNotificationConfigId, + @Nullable Map metadata) { public GetTaskPushNotificationConfigParams_v0_3 { Assert.checkNotNullParam("id", id); @@ -44,7 +45,8 @@ public Builder metadata(Map metadata) { } public GetTaskPushNotificationConfigParams_v0_3 build() { - return new GetTaskPushNotificationConfigParams_v0_3(id, pushNotificationConfigId, metadata); + return new GetTaskPushNotificationConfigParams_v0_3( + id, pushNotificationConfigId, metadata); } } } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigRequest_v0_3.java index d86854612..876870a8c 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigRequest_v0_3.java @@ -1,27 +1,29 @@ package org.a2aproject.sdk.compat03.spec; -import org.a2aproject.sdk.util.Assert; -import org.a2aproject.sdk.compat03.util.Utils_v0_3; - import java.util.UUID; +import org.a2aproject.sdk.compat03.util.Utils_v0_3; +import org.a2aproject.sdk.util.Assert; /** * A get task push notification request. */ -public final class GetTaskPushNotificationConfigRequest_v0_3 extends NonStreamingJSONRPCRequest_v0_3 { - - private GetTaskPushNotificationConfigRequest_v0_3() { - } +public final class GetTaskPushNotificationConfigRequest_v0_3 + extends NonStreamingJSONRPCRequest_v0_3 { + private GetTaskPushNotificationConfigRequest_v0_3() {} public static final String METHOD = "tasks/pushNotificationConfig/get"; - public GetTaskPushNotificationConfigRequest_v0_3(String jsonrpc, Object id, String method, GetTaskPushNotificationConfigParams_v0_3 params) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + public GetTaskPushNotificationConfigRequest_v0_3( + String jsonrpc, + Object id, + String method, + GetTaskPushNotificationConfigParams_v0_3 params) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } Assert.checkNotNullParam("method", method); - if (! method.equals(METHOD)) { + if (!method.equals(METHOD)) { throw new IllegalArgumentException("Invalid GetTaskPushNotificationRequest method"); } Assert.isValidJsonRpcId(id); @@ -31,7 +33,8 @@ public GetTaskPushNotificationConfigRequest_v0_3(String jsonrpc, Object id, Stri this.params = params; } - public GetTaskPushNotificationConfigRequest_v0_3(String id, GetTaskPushNotificationConfigParams_v0_3 params) { + public GetTaskPushNotificationConfigRequest_v0_3( + String id, GetTaskPushNotificationConfigParams_v0_3 params) { this(null, id, METHOD, params); } @@ -56,7 +59,8 @@ public GetTaskPushNotificationConfigRequest_v0_3.Builder method(String method) { return this; } - public GetTaskPushNotificationConfigRequest_v0_3.Builder params(GetTaskPushNotificationConfigParams_v0_3 params) { + public GetTaskPushNotificationConfigRequest_v0_3.Builder params( + GetTaskPushNotificationConfigParams_v0_3 params) { this.params = params; return this; } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigResponse_v0_3.java index 82e0bf455..0d57fe976 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskPushNotificationConfigResponse_v0_3.java @@ -3,12 +3,16 @@ /** * A response for a get task push notification request. */ -public final class GetTaskPushNotificationConfigResponse_v0_3 extends JSONRPCResponse_v0_3 { +public final class GetTaskPushNotificationConfigResponse_v0_3 + extends JSONRPCResponse_v0_3 { - private GetTaskPushNotificationConfigResponse_v0_3() { - } + private GetTaskPushNotificationConfigResponse_v0_3() {} - public GetTaskPushNotificationConfigResponse_v0_3(String jsonrpc, Object id, TaskPushNotificationConfig_v0_3 result, JSONRPCError_v0_3 error) { + public GetTaskPushNotificationConfigResponse_v0_3( + String jsonrpc, + Object id, + TaskPushNotificationConfig_v0_3 result, + JSONRPCError_v0_3 error) { super(jsonrpc, id, result, error, TaskPushNotificationConfig_v0_3.class); } @@ -16,8 +20,8 @@ public GetTaskPushNotificationConfigResponse_v0_3(Object id, JSONRPCError_v0_3 e this(null, id, null, error); } - public GetTaskPushNotificationConfigResponse_v0_3(Object id, TaskPushNotificationConfig_v0_3 result) { + public GetTaskPushNotificationConfigResponse_v0_3( + Object id, TaskPushNotificationConfig_v0_3 result) { this(null, id, result, null); } - } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskRequest_v0_3.java index 6e51ec89e..38b917130 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskRequest_v0_3.java @@ -3,27 +3,25 @@ import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; import java.util.UUID; - - import org.a2aproject.sdk.util.Assert; /** * A get task request. */ -public final class GetTaskRequest_v0_3 extends NonStreamingJSONRPCRequest_v0_3 { - - private GetTaskRequest_v0_3() { - } +public final class GetTaskRequest_v0_3 + extends NonStreamingJSONRPCRequest_v0_3 { + private GetTaskRequest_v0_3() {} public static final String METHOD = "tasks/get"; - public GetTaskRequest_v0_3(String jsonrpc, Object id, String method, TaskQueryParams_v0_3 params) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + public GetTaskRequest_v0_3( + String jsonrpc, Object id, String method, TaskQueryParams_v0_3 params) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } Assert.checkNotNullParam("method", method); - if (! method.equals(METHOD)) { + if (!method.equals(METHOD)) { throw new IllegalArgumentException("Invalid GetTaskRequest method"); } Assert.checkNotNullParam("params", params); @@ -38,7 +36,6 @@ public GetTaskRequest_v0_3(Object id, TaskQueryParams_v0_3 params) { this(null, id, METHOD, params); } - public static class Builder { private String jsonrpc; private Object id; diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskResponse_v0_3.java index 63c6a35e2..045cc10fb 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/GetTaskResponse_v0_3.java @@ -5,10 +5,10 @@ */ public final class GetTaskResponse_v0_3 extends JSONRPCResponse_v0_3 { - private GetTaskResponse_v0_3() { - } + private GetTaskResponse_v0_3() {} - public GetTaskResponse_v0_3(String jsonrpc, Object id, Task_v0_3 result, JSONRPCError_v0_3 error) { + public GetTaskResponse_v0_3( + String jsonrpc, Object id, Task_v0_3 result, JSONRPCError_v0_3 error) { super(jsonrpc, id, result, error, Task_v0_3.class); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/HTTPAuthSecurityScheme_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/HTTPAuthSecurityScheme_v0_3.java index 0fa66d74a..e7a74fa07 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/HTTPAuthSecurityScheme_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/HTTPAuthSecurityScheme_v0_3.java @@ -7,15 +7,16 @@ * Defines a security scheme using HTTP authentication. */ public record HTTPAuthSecurityScheme_v0_3( - @Nullable String bearerFormat, - String scheme, - @Nullable String description, - String type -) implements SecurityScheme_v0_3 { + @Nullable String bearerFormat, String scheme, @Nullable String description, String type) + implements SecurityScheme_v0_3 { public static final String TYPE = "http"; - public HTTPAuthSecurityScheme_v0_3(@Nullable String bearerFormat, String scheme, @Nullable String description, @Nullable String type) { + public HTTPAuthSecurityScheme_v0_3( + @Nullable String bearerFormat, + String scheme, + @Nullable String description, + @Nullable String type) { Assert.checkNotNullParam("scheme", scheme); if (type != null && !TYPE.equals(type)) { throw new IllegalArgumentException("Invalid type for HTTPAuthSecurityScheme"); @@ -26,7 +27,8 @@ public HTTPAuthSecurityScheme_v0_3(@Nullable String bearerFormat, String scheme, this.type = TYPE; } - public HTTPAuthSecurityScheme_v0_3(@Nullable String bearerFormat, String scheme, @Nullable String description) { + public HTTPAuthSecurityScheme_v0_3( + @Nullable String bearerFormat, String scheme, @Nullable String description) { this(bearerFormat, scheme, description, TYPE); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ImplicitOAuthFlow_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ImplicitOAuthFlow_v0_3.java index 200abacbe..94ecc6496 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ImplicitOAuthFlow_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ImplicitOAuthFlow_v0_3.java @@ -1,14 +1,13 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - - import org.a2aproject.sdk.util.Assert; /** * Defines configuration details for the OAuth 2.0 Implicit flow. */ -public record ImplicitOAuthFlow_v0_3(String authorizationUrl, String refreshUrl, Map scopes) { +public record ImplicitOAuthFlow_v0_3( + String authorizationUrl, String refreshUrl, Map scopes) { public ImplicitOAuthFlow_v0_3 { Assert.checkNotNullParam("authorizationUrl", authorizationUrl); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InternalError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InternalError_v0_3.java index ac01992b3..dc384703d 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InternalError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InternalError_v0_3.java @@ -2,13 +2,12 @@ import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; - /** * An error indicating an internal error on the server. */ public class InternalError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = A2AErrorCodes_v0_3.INTERNAL_ERROR_CODE; + public static final Integer DEFAULT_CODE = A2AErrorCodes_v0_3.INTERNAL_ERROR_CODE; public InternalError_v0_3(Integer code, String message, Object data) { super( diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidAgentResponseError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidAgentResponseError_v0_3.java index ee222fe71..735179206 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidAgentResponseError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidAgentResponseError_v0_3.java @@ -2,7 +2,6 @@ import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; - /** * A2A Protocol error indicating that an agent returned a response not conforming to protocol specifications. *

    @@ -35,11 +34,11 @@ * @see JSONRPCResponse_v0_3 for response structure * @see SendMessageResponse_v0_3 for message send response * @see A2A Protocol Specification - + * */ public class InvalidAgentResponseError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = A2AErrorCodes_v0_3.INVALID_AGENT_RESPONSE_ERROR_CODE; + public static final Integer DEFAULT_CODE = A2AErrorCodes_v0_3.INVALID_AGENT_RESPONSE_ERROR_CODE; public InvalidAgentResponseError_v0_3(Integer code, String message, Object data) { super( diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidParamsError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidParamsError_v0_3.java index a7864a9ac..038af04ba 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidParamsError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidParamsError_v0_3.java @@ -31,7 +31,7 @@ */ public class InvalidParamsError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = A2AErrorCodes_v0_3.INVALID_PARAMS_ERROR_CODE; + public static final Integer DEFAULT_CODE = A2AErrorCodes_v0_3.INVALID_PARAMS_ERROR_CODE; public InvalidParamsError_v0_3(Integer code, String message, Object data) { super( diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidRequestError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidRequestError_v0_3.java index f86c2de48..6e8a3204d 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidRequestError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/InvalidRequestError_v0_3.java @@ -2,7 +2,6 @@ import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; - /** * JSON-RPC error indicating that the request payload is not a valid JSON-RPC Request object. *

    @@ -32,7 +31,7 @@ */ public class InvalidRequestError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = A2AErrorCodes_v0_3.INVALID_REQUEST_ERROR_CODE; + public static final Integer DEFAULT_CODE = A2AErrorCodes_v0_3.INVALID_REQUEST_ERROR_CODE; public InvalidRequestError_v0_3() { this(null, null, null); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONErrorResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONErrorResponse_v0_3.java index acf16d9a8..5daacb447 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONErrorResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONErrorResponse_v0_3.java @@ -15,5 +15,4 @@ * @see JSONRPCErrorResponse_v0_3 * @see JSONRPCError_v0_3 */ -public record JSONErrorResponse_v0_3(String error) { -} +public record JSONErrorResponse_v0_3(String error) {} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONParseError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONParseError_v0_3.java index 78c1d398b..2043fd897 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONParseError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONParseError_v0_3.java @@ -1,6 +1,5 @@ package org.a2aproject.sdk.compat03.spec; - import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; /** @@ -28,7 +27,7 @@ */ public class JSONParseError_v0_3 extends JSONRPCError_v0_3 implements A2AError_v0_3 { - public final static Integer DEFAULT_CODE = -32700; + public static final Integer DEFAULT_CODE = -32700; public JSONParseError_v0_3() { this(null, null, null); @@ -38,10 +37,7 @@ public JSONParseError_v0_3(String message) { this(null, message, null); } - public JSONParseError_v0_3( - Integer code, - String message, - Object data) { + public JSONParseError_v0_3(Integer code, String message, Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Invalid JSON payload"), diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCErrorResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCErrorResponse_v0_3.java index c5b20540f..f41f23764 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCErrorResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCErrorResponse_v0_3.java @@ -7,8 +7,7 @@ */ public final class JSONRPCErrorResponse_v0_3 extends JSONRPCResponse_v0_3 { - private JSONRPCErrorResponse_v0_3() { - } + private JSONRPCErrorResponse_v0_3() {} /** * Constructs a JSON-RPC error response with all fields. @@ -21,7 +20,8 @@ private JSONRPCErrorResponse_v0_3() { * @param error the error object describing what went wrong (required) * @throws IllegalArgumentException if error is null */ - public JSONRPCErrorResponse_v0_3(String jsonrpc, Object id, Void result, JSONRPCError_v0_3 error) { + public JSONRPCErrorResponse_v0_3( + String jsonrpc, Object id, Void result, JSONRPCError_v0_3 error) { super(jsonrpc, id, result, error, Void.class); Assert.checkNotNullParam("error", error); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCError_v0_3.java index 3dcde042a..42ff7e654 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCError_v0_3.java @@ -1,6 +1,5 @@ package org.a2aproject.sdk.compat03.spec; - import org.a2aproject.sdk.util.Assert; /** diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCMessage_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCMessage_v0_3.java index 10e4da716..b80def4a3 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCMessage_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCMessage_v0_3.java @@ -8,6 +8,6 @@ public sealed interface JSONRPCMessage_v0_3 permits JSONRPCRequest_v0_3, JSONRPC String JSONRPC_VERSION = "2.0"; String getJsonrpc(); - Object getId(); + Object getId(); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCRequest_v0_3.java index 9a337a6ca..1cab35c63 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCRequest_v0_3.java @@ -7,15 +7,15 @@ /** * Represents a JSONRPC request. */ -public abstract sealed class JSONRPCRequest_v0_3 implements JSONRPCMessage_v0_3 permits NonStreamingJSONRPCRequest_v0_3, StreamingJSONRPCRequest_v0_3 { +public abstract sealed class JSONRPCRequest_v0_3 implements JSONRPCMessage_v0_3 + permits NonStreamingJSONRPCRequest_v0_3, StreamingJSONRPCRequest_v0_3 { protected String jsonrpc; protected Object id; protected String method; protected T params; - public JSONRPCRequest_v0_3() { - } + public JSONRPCRequest_v0_3() {} public JSONRPCRequest_v0_3(String jsonrpc, Object id, String method, T params) { Assert.checkNotNullParam("jsonrpc", jsonrpc); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCResponse_v0_3.java index 009db94d4..87e3ecb22 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JSONRPCResponse_v0_3.java @@ -7,27 +7,34 @@ /** * Represents a JSONRPC response. */ -public abstract sealed class JSONRPCResponse_v0_3 implements JSONRPCMessage_v0_3 permits SendStreamingMessageResponse_v0_3, - GetTaskResponse_v0_3, CancelTaskResponse_v0_3, SetTaskPushNotificationConfigResponse_v0_3, GetTaskPushNotificationConfigResponse_v0_3, - SendMessageResponse_v0_3, DeleteTaskPushNotificationConfigResponse_v0_3, ListTaskPushNotificationConfigResponse_v0_3, JSONRPCErrorResponse_v0_3, - GetAuthenticatedExtendedCardResponse_v0_3 { +public abstract sealed class JSONRPCResponse_v0_3 implements JSONRPCMessage_v0_3 + permits SendStreamingMessageResponse_v0_3, + GetTaskResponse_v0_3, + CancelTaskResponse_v0_3, + SetTaskPushNotificationConfigResponse_v0_3, + GetTaskPushNotificationConfigResponse_v0_3, + SendMessageResponse_v0_3, + DeleteTaskPushNotificationConfigResponse_v0_3, + ListTaskPushNotificationConfigResponse_v0_3, + JSONRPCErrorResponse_v0_3, + GetAuthenticatedExtendedCardResponse_v0_3 { protected String jsonrpc; protected Object id; protected T result; protected JSONRPCError_v0_3 error; - public JSONRPCResponse_v0_3() { - } + public JSONRPCResponse_v0_3() {} - public JSONRPCResponse_v0_3(String jsonrpc, Object id, T result, JSONRPCError_v0_3 error, Class resultType) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + public JSONRPCResponse_v0_3( + String jsonrpc, Object id, T result, JSONRPCError_v0_3 error, Class resultType) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } if (error != null && result != null) { throw new IllegalArgumentException("Invalid JSON-RPC error response"); } - if (error == null && result == null && ! Void.class.equals(resultType)) { + if (error == null && result == null && !Void.class.equals(resultType)) { throw new IllegalArgumentException("Invalid JSON-RPC success response"); } Assert.isValidJsonRpcId(id); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JsonrpcId_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JsonrpcId_v0_3.java index 1817c46f2..99b289ac2 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JsonrpcId_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/JsonrpcId_v0_3.java @@ -1,4 +1,3 @@ package org.a2aproject.sdk.compat03.spec; -public interface JsonrpcId_v0_3 { -} +public interface JsonrpcId_v0_3 {} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigParams_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigParams_v0_3.java index b68456343..ef86a7f9c 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigParams_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigParams_v0_3.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - import org.a2aproject.sdk.util.Assert; /** diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigRequest_v0_3.java index 513ca555d..5c80b69b3 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigRequest_v0_3.java @@ -1,28 +1,31 @@ package org.a2aproject.sdk.compat03.spec; import java.util.UUID; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.compat03.util.Utils_v0_3; +import org.a2aproject.sdk.util.Assert; /** * A list task push notification config request. */ -public final class ListTaskPushNotificationConfigRequest_v0_3 extends NonStreamingJSONRPCRequest_v0_3 { - - private ListTaskPushNotificationConfigRequest_v0_3() { - } +public final class ListTaskPushNotificationConfigRequest_v0_3 + extends NonStreamingJSONRPCRequest_v0_3 { + private ListTaskPushNotificationConfigRequest_v0_3() {} public static final String METHOD = "tasks/pushNotificationConfig/list"; - public ListTaskPushNotificationConfigRequest_v0_3(String jsonrpc, Object id, String method, ListTaskPushNotificationConfigParams_v0_3 params) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + public ListTaskPushNotificationConfigRequest_v0_3( + String jsonrpc, + Object id, + String method, + ListTaskPushNotificationConfigParams_v0_3 params) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } Assert.checkNotNullParam("method", method); - if (! method.equals(METHOD)) { - throw new IllegalArgumentException("Invalid ListTaskPushNotificationConfigRequest method"); + if (!method.equals(METHOD)) { + throw new IllegalArgumentException( + "Invalid ListTaskPushNotificationConfigRequest method"); } Assert.isValidJsonRpcId(id); this.jsonrpc = Utils_v0_3.defaultIfNull(jsonrpc, JSONRPC_VERSION); @@ -31,7 +34,8 @@ public ListTaskPushNotificationConfigRequest_v0_3(String jsonrpc, Object id, Str this.params = params; } - public ListTaskPushNotificationConfigRequest_v0_3(String id, ListTaskPushNotificationConfigParams_v0_3 params) { + public ListTaskPushNotificationConfigRequest_v0_3( + String id, ListTaskPushNotificationConfigParams_v0_3 params) { this(null, id, METHOD, params); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigResponse_v0_3.java index 5a174c86a..42ac73748 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/ListTaskPushNotificationConfigResponse_v0_3.java @@ -5,21 +5,30 @@ /** * A response for a list task push notification config request. */ -public final class ListTaskPushNotificationConfigResponse_v0_3 extends JSONRPCResponse_v0_3> { +public final class ListTaskPushNotificationConfigResponse_v0_3 + extends JSONRPCResponse_v0_3> { - private ListTaskPushNotificationConfigResponse_v0_3() { - } + private ListTaskPushNotificationConfigResponse_v0_3() {} - public ListTaskPushNotificationConfigResponse_v0_3(String jsonrpc, Object id, List result, JSONRPCError_v0_3 error) { - super(jsonrpc, id, result, error, (Class>) (Class) List.class); + public ListTaskPushNotificationConfigResponse_v0_3( + String jsonrpc, + Object id, + List result, + JSONRPCError_v0_3 error) { + super( + jsonrpc, + id, + result, + error, + (Class>) (Class) List.class); } public ListTaskPushNotificationConfigResponse_v0_3(Object id, JSONRPCError_v0_3 error) { this(null, id, null, error); } - public ListTaskPushNotificationConfigResponse_v0_3(Object id, List result) { - this(null, id, result, null); + public ListTaskPushNotificationConfigResponse_v0_3( + Object id, List result) { + this(null, id, result, null); } - } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MessageSendConfiguration_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MessageSendConfiguration_v0_3.java index 6ca4cef69..857c6f24a 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MessageSendConfiguration_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MessageSendConfiguration_v0_3.java @@ -1,15 +1,17 @@ package org.a2aproject.sdk.compat03.spec; import java.util.List; - import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; /** * Defines configuration options for a `message/send` or `message/stream` request. */ -public record MessageSendConfiguration_v0_3(List acceptedOutputModes, Integer historyLength, - PushNotificationConfig_v0_3 pushNotificationConfig, Boolean blocking) { +public record MessageSendConfiguration_v0_3( + List acceptedOutputModes, + Integer historyLength, + PushNotificationConfig_v0_3 pushNotificationConfig, + Boolean blocking) { public MessageSendConfiguration_v0_3 { if (historyLength != null && historyLength < 0) { @@ -29,7 +31,8 @@ public Builder acceptedOutputModes(List acceptedOutputModes) { return this; } - public Builder pushNotificationConfig(@Nullable PushNotificationConfig_v0_3 pushNotificationConfig) { + public Builder pushNotificationConfig( + @Nullable PushNotificationConfig_v0_3 pushNotificationConfig) { this.pushNotificationConfig = pushNotificationConfig; return this; } @@ -48,7 +51,8 @@ public Builder blocking(@NonNull Boolean blocking) { } public MessageSendConfiguration_v0_3 build() { - return new MessageSendConfiguration_v0_3(acceptedOutputModes, historyLength, pushNotificationConfig, blocking); + return new MessageSendConfiguration_v0_3( + acceptedOutputModes, historyLength, pushNotificationConfig, blocking); } } } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MessageSendParams_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MessageSendParams_v0_3.java index fff0204b0..6fd78bfc7 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MessageSendParams_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MessageSendParams_v0_3.java @@ -1,15 +1,16 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - import org.a2aproject.sdk.util.Assert; /** * Defines the parameters for a request to send a message to an agent. This can be used * to create a new task, continue an existing one, or restart a task. */ -public record MessageSendParams_v0_3(Message_v0_3 message, MessageSendConfiguration_v0_3 configuration, - Map metadata) { +public record MessageSendParams_v0_3( + Message_v0_3 message, + MessageSendConfiguration_v0_3 configuration, + Map metadata) { public MessageSendParams_v0_3 { Assert.checkNotNullParam("message", message); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Message_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Message_v0_3.java index 474d47aff..2d159a2dd 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Message_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Message_v0_3.java @@ -3,7 +3,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; @@ -19,14 +18,21 @@ public record Message_v0_3( @Nullable List referenceTaskIds, @Nullable Map metadata, @Nullable List extensions, - String kind -) implements EventKind_v0_3, StreamingEventKind_v0_3 { + String kind) + implements EventKind_v0_3, StreamingEventKind_v0_3 { public static final String KIND = "message"; - public Message_v0_3(Role role, List> parts, @Nullable String messageId, @Nullable String contextId, - @Nullable String taskId, @Nullable List referenceTaskIds, - @Nullable Map metadata, @Nullable List extensions, String kind) { + public Message_v0_3( + Role role, + List> parts, + @Nullable String messageId, + @Nullable String contextId, + @Nullable String taskId, + @Nullable List referenceTaskIds, + @Nullable Map metadata, + @Nullable List extensions, + String kind) { Assert.checkNotNullParam("role", role); Assert.checkNotNullParam("parts", parts); this.role = role; @@ -46,10 +52,25 @@ public Message_v0_3(Role role, List> parts, @Nullable String messag this.extensions = extensions != null ? List.copyOf(extensions) : null; } - public Message_v0_3(Role role, List> parts, @Nullable String messageId, @Nullable String contextId, - @Nullable String taskId, @Nullable List referenceTaskIds, - @Nullable Map metadata, @Nullable List extensions) { - this(role, parts, messageId, contextId, taskId, referenceTaskIds, metadata, extensions, KIND); + public Message_v0_3( + Role role, + List> parts, + @Nullable String messageId, + @Nullable String contextId, + @Nullable String taskId, + @Nullable List referenceTaskIds, + @Nullable Map metadata, + @Nullable List extensions) { + this( + role, + parts, + messageId, + contextId, + taskId, + referenceTaskIds, + metadata, + extensions, + KIND); } public enum Role { @@ -83,8 +104,7 @@ public static class Builder { private Map metadata; private List extensions; - public Builder() { - } + public Builder() {} public Builder(Message_v0_3 message) { role = message.role; @@ -143,8 +163,15 @@ public Builder extensions(List extensions) { } public Message_v0_3 build() { - return new Message_v0_3(role, parts, messageId == null ? UUID.randomUUID().toString() : messageId, - contextId, taskId, referenceTaskIds, metadata, extensions); + return new Message_v0_3( + role, + parts, + messageId == null ? UUID.randomUUID().toString() : messageId, + contextId, + taskId, + referenceTaskIds, + metadata, + extensions); } } } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MethodNotFoundError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MethodNotFoundError_v0_3.java index 1ceba9637..795f684db 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MethodNotFoundError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MethodNotFoundError_v0_3.java @@ -8,10 +8,13 @@ */ public class MethodNotFoundError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = METHOD_NOT_FOUND_ERROR_CODE; + public static final Integer DEFAULT_CODE = METHOD_NOT_FOUND_ERROR_CODE; public MethodNotFoundError_v0_3(Integer code, String message, Object data) { - super(defaultIfNull(code, METHOD_NOT_FOUND_ERROR_CODE), defaultIfNull(message, "Method not found"), data); + super( + defaultIfNull(code, METHOD_NOT_FOUND_ERROR_CODE), + defaultIfNull(message, "Method not found"), + data); } public MethodNotFoundError_v0_3() { diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MutualTLSSecurityScheme_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MutualTLSSecurityScheme_v0_3.java index 65212a730..d2f16d15f 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MutualTLSSecurityScheme_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/MutualTLSSecurityScheme_v0_3.java @@ -5,7 +5,8 @@ /** * Defines a security scheme using mTLS authentication. */ -public record MutualTLSSecurityScheme_v0_3(@Nullable String description, String type) implements SecurityScheme_v0_3 { +public record MutualTLSSecurityScheme_v0_3(@Nullable String description, String type) + implements SecurityScheme_v0_3 { public static final String TYPE = "mutualTLS"; diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/NonStreamingJSONRPCRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/NonStreamingJSONRPCRequest_v0_3.java index 972a1fb39..59823f675 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/NonStreamingJSONRPCRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/NonStreamingJSONRPCRequest_v0_3.java @@ -3,8 +3,12 @@ /** * Represents a non-streaming JSON-RPC request. */ -public abstract sealed class NonStreamingJSONRPCRequest_v0_3 extends JSONRPCRequest_v0_3 permits GetTaskRequest_v0_3, - CancelTaskRequest_v0_3, SetTaskPushNotificationConfigRequest_v0_3, GetTaskPushNotificationConfigRequest_v0_3, - SendMessageRequest_v0_3, DeleteTaskPushNotificationConfigRequest_v0_3, ListTaskPushNotificationConfigRequest_v0_3, - GetAuthenticatedExtendedCardRequest_v0_3 { -} +public abstract sealed class NonStreamingJSONRPCRequest_v0_3 extends JSONRPCRequest_v0_3 + permits GetTaskRequest_v0_3, + CancelTaskRequest_v0_3, + SetTaskPushNotificationConfigRequest_v0_3, + GetTaskPushNotificationConfigRequest_v0_3, + SendMessageRequest_v0_3, + DeleteTaskPushNotificationConfigRequest_v0_3, + ListTaskPushNotificationConfigRequest_v0_3, + GetAuthenticatedExtendedCardRequest_v0_3 {} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OAuth2SecurityScheme_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OAuth2SecurityScheme_v0_3.java index 37e90378a..ed01c9c61 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OAuth2SecurityScheme_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OAuth2SecurityScheme_v0_3.java @@ -10,12 +10,16 @@ public record OAuth2SecurityScheme_v0_3( OAuthFlows_v0_3 flows, @Nullable String description, @Nullable String oauth2MetadataUrl, - String type -) implements SecurityScheme_v0_3 { + String type) + implements SecurityScheme_v0_3 { public static final String TYPE = "oauth2"; - public OAuth2SecurityScheme_v0_3(OAuthFlows_v0_3 flows, @Nullable String description, @Nullable String oauth2MetadataUrl, @Nullable String type) { + public OAuth2SecurityScheme_v0_3( + OAuthFlows_v0_3 flows, + @Nullable String description, + @Nullable String oauth2MetadataUrl, + @Nullable String type) { Assert.checkNotNullParam("flows", flows); if (type != null && !type.equals(TYPE)) { throw new IllegalArgumentException("Invalid type for OAuth2SecurityScheme"); @@ -26,7 +30,10 @@ public OAuth2SecurityScheme_v0_3(OAuthFlows_v0_3 flows, @Nullable String descrip this.type = TYPE; } - public OAuth2SecurityScheme_v0_3(OAuthFlows_v0_3 flows, @Nullable String description, @Nullable String oauth2MetadataUrl) { + public OAuth2SecurityScheme_v0_3( + OAuthFlows_v0_3 flows, + @Nullable String description, + @Nullable String oauth2MetadataUrl) { this(flows, description, oauth2MetadataUrl, TYPE); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OAuthFlows_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OAuthFlows_v0_3.java index 1acf64c57..4a9a9f6ca 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OAuthFlows_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OAuthFlows_v0_3.java @@ -3,8 +3,11 @@ /** * Defines the configuration for the supported OAuth 2.0 flows. */ -public record OAuthFlows_v0_3(AuthorizationCodeOAuthFlow_v0_3 authorizationCode, ClientCredentialsOAuthFlow_v0_3 clientCredentials, - ImplicitOAuthFlow_v0_3 implicit, PasswordOAuthFlow_v0_3 password) { +public record OAuthFlows_v0_3( + AuthorizationCodeOAuthFlow_v0_3 authorizationCode, + ClientCredentialsOAuthFlow_v0_3 clientCredentials, + ImplicitOAuthFlow_v0_3 implicit, + PasswordOAuthFlow_v0_3 password) { public static class Builder { private AuthorizationCodeOAuthFlow_v0_3 authorizationCode; diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OpenIdConnectSecurityScheme_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OpenIdConnectSecurityScheme_v0_3.java index 5e7b06ad4..04323cf8e 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OpenIdConnectSecurityScheme_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/OpenIdConnectSecurityScheme_v0_3.java @@ -7,10 +7,8 @@ * Defines a security scheme using OpenID Connect. */ public record OpenIdConnectSecurityScheme_v0_3( - String openIdConnectUrl, - @Nullable String description, - String type -) implements SecurityScheme_v0_3 { + String openIdConnectUrl, @Nullable String description, String type) + implements SecurityScheme_v0_3 { public static final String TYPE = "openIdConnect"; diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PasswordOAuthFlow_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PasswordOAuthFlow_v0_3.java index eb1dec150..18209c022 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PasswordOAuthFlow_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PasswordOAuthFlow_v0_3.java @@ -1,13 +1,13 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - import org.a2aproject.sdk.util.Assert; /** * Defines configuration details for the OAuth 2.0 Resource Owner Password flow. */ -public record PasswordOAuthFlow_v0_3(String refreshUrl, Map scopes, String tokenUrl) { +public record PasswordOAuthFlow_v0_3( + String refreshUrl, Map scopes, String tokenUrl) { public PasswordOAuthFlow_v0_3 { Assert.checkNotNullParam("scopes", scopes); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PushNotificationConfig_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PushNotificationConfig_v0_3.java index de8c7a68e..6705b513e 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PushNotificationConfig_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PushNotificationConfig_v0_3.java @@ -5,7 +5,11 @@ /** * Defines the configuration for setting up push notifications for task updates. */ -public record PushNotificationConfig_v0_3(String url, String token, PushNotificationAuthenticationInfo_v0_3 authentication, String id) { +public record PushNotificationConfig_v0_3( + String url, + String token, + PushNotificationAuthenticationInfo_v0_3 authentication, + String id) { public PushNotificationConfig_v0_3 { Assert.checkNotNullParam("url", url); @@ -17,8 +21,7 @@ public static class Builder { private PushNotificationAuthenticationInfo_v0_3 authentication; private String id; - public Builder() { - } + public Builder() {} public Builder(PushNotificationConfig_v0_3 notificationConfig) { this.url = notificationConfig.url; @@ -37,7 +40,8 @@ public Builder token(String token) { return this; } - public Builder authenticationInfo(PushNotificationAuthenticationInfo_v0_3 authenticationInfo) { + public Builder authenticationInfo( + PushNotificationAuthenticationInfo_v0_3 authenticationInfo) { this.authentication = authenticationInfo; return this; } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PushNotificationNotSupportedError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PushNotificationNotSupportedError_v0_3.java index 1b660e287..216f1c9d6 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PushNotificationNotSupportedError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/PushNotificationNotSupportedError_v0_3.java @@ -7,16 +7,13 @@ */ public class PushNotificationNotSupportedError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = -32003; + public static final Integer DEFAULT_CODE = -32003; public PushNotificationNotSupportedError_v0_3() { this(null, null, null); } - public PushNotificationNotSupportedError_v0_3( - Integer code, - String message, - Object data) { + public PushNotificationNotSupportedError_v0_3(Integer code, String message, Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Push Notification is not supported"), diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SecurityScheme_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SecurityScheme_v0_3.java index 33885638c..50315d15c 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SecurityScheme_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SecurityScheme_v0_3.java @@ -4,8 +4,12 @@ * Defines a security scheme that can be used to secure an agent's endpoints. * This is a discriminated union type based on the OpenAPI 3.0 Security Scheme Object. */ -public sealed interface SecurityScheme_v0_3 permits APIKeySecurityScheme_v0_3, HTTPAuthSecurityScheme_v0_3, OAuth2SecurityScheme_v0_3, - OpenIdConnectSecurityScheme_v0_3, MutualTLSSecurityScheme_v0_3 { +public sealed interface SecurityScheme_v0_3 + permits APIKeySecurityScheme_v0_3, + HTTPAuthSecurityScheme_v0_3, + OAuth2SecurityScheme_v0_3, + OpenIdConnectSecurityScheme_v0_3, + MutualTLSSecurityScheme_v0_3 { @org.jspecify.annotations.Nullable String description(); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendMessageRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendMessageRequest_v0_3.java index c316bd09f..eba19c74b 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendMessageRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendMessageRequest_v0_3.java @@ -3,17 +3,15 @@ import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; import java.util.UUID; - import org.a2aproject.sdk.util.Assert; /** * Used to send a message request. */ -public final class SendMessageRequest_v0_3 extends NonStreamingJSONRPCRequest_v0_3 { - - private SendMessageRequest_v0_3() { - } +public final class SendMessageRequest_v0_3 + extends NonStreamingJSONRPCRequest_v0_3 { + private SendMessageRequest_v0_3() {} public static final String METHOD = "message/send"; @@ -29,15 +27,16 @@ private SendMessageRequest_v0_3() { * @param params the message send parameters (required) * @throws IllegalArgumentException if validation fails */ - public SendMessageRequest_v0_3(String jsonrpc, Object id, String method, MessageSendParams_v0_3 params) { + public SendMessageRequest_v0_3( + String jsonrpc, Object id, String method, MessageSendParams_v0_3 params) { if (jsonrpc == null || jsonrpc.isEmpty()) { throw new IllegalArgumentException("JSON-RPC protocol version cannot be null or empty"); } - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } Assert.checkNotNullParam("method", method); - if (! method.equals(METHOD)) { + if (!method.equals(METHOD)) { throw new IllegalArgumentException("Invalid SendMessageRequest method"); } Assert.checkNotNullParam("params", params); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendMessageResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendMessageResponse_v0_3.java index 7bd1e84ba..fab9ff58a 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendMessageResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendMessageResponse_v0_3.java @@ -5,10 +5,10 @@ */ public final class SendMessageResponse_v0_3 extends JSONRPCResponse_v0_3 { - private SendMessageResponse_v0_3() { - } + private SendMessageResponse_v0_3() {} - public SendMessageResponse_v0_3(String jsonrpc, Object id, EventKind_v0_3 result, JSONRPCError_v0_3 error) { + public SendMessageResponse_v0_3( + String jsonrpc, Object id, EventKind_v0_3 result, JSONRPCError_v0_3 error) { super(jsonrpc, id, result, error, EventKind_v0_3.class); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendStreamingMessageRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendStreamingMessageRequest_v0_3.java index 42f0ae266..0a92617af 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendStreamingMessageRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendStreamingMessageRequest_v0_3.java @@ -2,27 +2,26 @@ import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; -import org.a2aproject.sdk.util.Assert; - import java.util.UUID; +import org.a2aproject.sdk.util.Assert; /** * Used to initiate a task with streaming. */ -public final class SendStreamingMessageRequest_v0_3 extends StreamingJSONRPCRequest_v0_3 { - - private SendStreamingMessageRequest_v0_3() { - } +public final class SendStreamingMessageRequest_v0_3 + extends StreamingJSONRPCRequest_v0_3 { + private SendStreamingMessageRequest_v0_3() {} public static final String METHOD = "message/stream"; - public SendStreamingMessageRequest_v0_3(String jsonrpc, Object id, String method, MessageSendParams_v0_3 params) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + public SendStreamingMessageRequest_v0_3( + String jsonrpc, Object id, String method, MessageSendParams_v0_3 params) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } Assert.checkNotNullParam("method", method); - if (! method.equals(METHOD)) { + if (!method.equals(METHOD)) { throw new IllegalArgumentException("Invalid SendStreamingMessageRequest method"); } Assert.checkNotNullParam("params", params); @@ -51,36 +50,36 @@ public SendStreamingMessageRequest_v0_3(Object id, MessageSendParams_v0_3 params } public static class Builder { - private String jsonrpc; - private Object id; - private String method = METHOD; - private MessageSendParams_v0_3 params; - - public Builder jsonrpc(String jsonrpc) { - this.jsonrpc = jsonrpc; - return this; - } + private String jsonrpc; + private Object id; + private String method = METHOD; + private MessageSendParams_v0_3 params; + + public Builder jsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; + return this; + } - public Builder id(Object id) { - this.id = id; - return this; - } + public Builder id(Object id) { + this.id = id; + return this; + } - public Builder method(String method) { - this.method = method; - return this; - } + public Builder method(String method) { + this.method = method; + return this; + } - public Builder params(MessageSendParams_v0_3 params) { - this.params = params; - return this; - } + public Builder params(MessageSendParams_v0_3 params) { + this.params = params; + return this; + } - public SendStreamingMessageRequest_v0_3 build() { - if (id == null) { - id = UUID.randomUUID().toString(); - } - return new SendStreamingMessageRequest_v0_3(jsonrpc, id, method, params); + public SendStreamingMessageRequest_v0_3 build() { + if (id == null) { + id = UUID.randomUUID().toString(); } + return new SendStreamingMessageRequest_v0_3(jsonrpc, id, method, params); } } +} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendStreamingMessageResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendStreamingMessageResponse_v0_3.java index 14df2a197..3afe204e9 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendStreamingMessageResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SendStreamingMessageResponse_v0_3.java @@ -3,12 +3,13 @@ /** * The response after receiving a request to initiate a task with streaming. */ -public final class SendStreamingMessageResponse_v0_3 extends JSONRPCResponse_v0_3 { +public final class SendStreamingMessageResponse_v0_3 + extends JSONRPCResponse_v0_3 { - private SendStreamingMessageResponse_v0_3() { - } + private SendStreamingMessageResponse_v0_3() {} - public SendStreamingMessageResponse_v0_3(String jsonrpc, Object id, StreamingEventKind_v0_3 result, JSONRPCError_v0_3 error) { + public SendStreamingMessageResponse_v0_3( + String jsonrpc, Object id, StreamingEventKind_v0_3 result, JSONRPCError_v0_3 error) { super(jsonrpc, id, result, error, StreamingEventKind_v0_3.class); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SetTaskPushNotificationConfigRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SetTaskPushNotificationConfigRequest_v0_3.java index 41a23c6be..4b86cefd6 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SetTaskPushNotificationConfigRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SetTaskPushNotificationConfigRequest_v0_3.java @@ -3,26 +3,25 @@ import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; import java.util.UUID; - import org.a2aproject.sdk.util.Assert; /** * Used to set a task push notification request. */ -public final class SetTaskPushNotificationConfigRequest_v0_3 extends NonStreamingJSONRPCRequest_v0_3 { - - private SetTaskPushNotificationConfigRequest_v0_3() { - } +public final class SetTaskPushNotificationConfigRequest_v0_3 + extends NonStreamingJSONRPCRequest_v0_3 { + private SetTaskPushNotificationConfigRequest_v0_3() {} public static final String METHOD = "tasks/pushNotificationConfig/set"; - public SetTaskPushNotificationConfigRequest_v0_3(String jsonrpc, Object id, String method, TaskPushNotificationConfig_v0_3 params) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + public SetTaskPushNotificationConfigRequest_v0_3( + String jsonrpc, Object id, String method, TaskPushNotificationConfig_v0_3 params) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } Assert.checkNotNullParam("method", method); - if (! method.equals(METHOD)) { + if (!method.equals(METHOD)) { throw new IllegalArgumentException("Invalid SetTaskPushNotificationRequest method"); } Assert.checkNotNullParam("params", params); @@ -33,7 +32,8 @@ public SetTaskPushNotificationConfigRequest_v0_3(String jsonrpc, Object id, Stri this.params = params; } - public SetTaskPushNotificationConfigRequest_v0_3(String id, TaskPushNotificationConfig_v0_3 taskPushConfig) { + public SetTaskPushNotificationConfigRequest_v0_3( + String id, TaskPushNotificationConfig_v0_3 taskPushConfig) { this(null, id, METHOD, taskPushConfig); } @@ -58,7 +58,8 @@ public SetTaskPushNotificationConfigRequest_v0_3.Builder method(String method) { return this; } - public SetTaskPushNotificationConfigRequest_v0_3.Builder params(TaskPushNotificationConfig_v0_3 params) { + public SetTaskPushNotificationConfigRequest_v0_3.Builder params( + TaskPushNotificationConfig_v0_3 params) { this.params = params; return this; } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SetTaskPushNotificationConfigResponse_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SetTaskPushNotificationConfigResponse_v0_3.java index 6d1a14cbb..f409fd35d 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SetTaskPushNotificationConfigResponse_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/SetTaskPushNotificationConfigResponse_v0_3.java @@ -3,12 +3,16 @@ /** * The response after receiving a set task push notification request. */ -public final class SetTaskPushNotificationConfigResponse_v0_3 extends JSONRPCResponse_v0_3 { +public final class SetTaskPushNotificationConfigResponse_v0_3 + extends JSONRPCResponse_v0_3 { - private SetTaskPushNotificationConfigResponse_v0_3() { - } + private SetTaskPushNotificationConfigResponse_v0_3() {} - public SetTaskPushNotificationConfigResponse_v0_3(String jsonrpc, Object id, TaskPushNotificationConfig_v0_3 result, JSONRPCError_v0_3 error) { + public SetTaskPushNotificationConfigResponse_v0_3( + String jsonrpc, + Object id, + TaskPushNotificationConfig_v0_3 result, + JSONRPCError_v0_3 error) { super(jsonrpc, id, result, error, TaskPushNotificationConfig_v0_3.class); } @@ -16,7 +20,8 @@ public SetTaskPushNotificationConfigResponse_v0_3(Object id, JSONRPCError_v0_3 e this(null, id, null, error); } - public SetTaskPushNotificationConfigResponse_v0_3(Object id, TaskPushNotificationConfig_v0_3 result) { + public SetTaskPushNotificationConfigResponse_v0_3( + Object id, TaskPushNotificationConfig_v0_3 result) { this(null, id, result, null); } } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/StreamingEventKind_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/StreamingEventKind_v0_3.java index 0ace23176..0078af18c 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/StreamingEventKind_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/StreamingEventKind_v0_3.java @@ -29,7 +29,8 @@ * @see EventKind_v0_3 * @see UpdateEvent_v0_3 */ -public sealed interface StreamingEventKind_v0_3 extends Event_v0_3 permits Task_v0_3, Message_v0_3, TaskStatusUpdateEvent_v0_3, TaskArtifactUpdateEvent_v0_3 { +public sealed interface StreamingEventKind_v0_3 extends Event_v0_3 + permits Task_v0_3, Message_v0_3, TaskStatusUpdateEvent_v0_3, TaskArtifactUpdateEvent_v0_3 { String kind(); } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/StreamingJSONRPCRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/StreamingJSONRPCRequest_v0_3.java index 956fd8aab..b1772b4db 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/StreamingJSONRPCRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/StreamingJSONRPCRequest_v0_3.java @@ -3,8 +3,5 @@ /** * Represents a streaming JSON-RPC request. */ - -public abstract sealed class StreamingJSONRPCRequest_v0_3 extends JSONRPCRequest_v0_3 permits TaskResubscriptionRequest_v0_3, - SendStreamingMessageRequest_v0_3 { - -} +public abstract sealed class StreamingJSONRPCRequest_v0_3 extends JSONRPCRequest_v0_3 + permits TaskResubscriptionRequest_v0_3, SendStreamingMessageRequest_v0_3 {} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskArtifactUpdateEvent_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskArtifactUpdateEvent_v0_3.java index 64c8dc0c5..734aaee94 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskArtifactUpdateEvent_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskArtifactUpdateEvent_v0_3.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; @@ -16,13 +15,19 @@ public record TaskArtifactUpdateEvent_v0_3( Artifact_v0_3 artifact, String contextId, Map metadata, - String kind -) implements EventKind_v0_3, StreamingEventKind_v0_3, UpdateEvent_v0_3 { + String kind) + implements EventKind_v0_3, StreamingEventKind_v0_3, UpdateEvent_v0_3 { public static final String KIND = "artifact-update"; - public TaskArtifactUpdateEvent_v0_3 (String taskId, @Nullable Boolean append, @Nullable Boolean lastChunk, - Artifact_v0_3 artifact, String contextId,@Nullable Map metadata, String kind){ + public TaskArtifactUpdateEvent_v0_3( + String taskId, + @Nullable Boolean append, + @Nullable Boolean lastChunk, + Artifact_v0_3 artifact, + String contextId, + @Nullable Map metadata, + String kind) { Assert.checkNotNullParam("taskId", taskId); Assert.checkNotNullParam("artifact", artifact); Assert.checkNotNullParam("contextId", contextId); @@ -38,9 +43,13 @@ public TaskArtifactUpdateEvent_v0_3 (String taskId, @Nullable Boolean append, @N this.metadata = metadata != null ? Map.copyOf(metadata) : null; } - public TaskArtifactUpdateEvent_v0_3(String taskId, Artifact_v0_3 artifact, String contextId, - @Nullable Boolean append, @Nullable Boolean lastChunk, - @Nullable Map metadata) { + public TaskArtifactUpdateEvent_v0_3( + String taskId, + Artifact_v0_3 artifact, + String contextId, + @Nullable Boolean append, + @Nullable Boolean lastChunk, + @Nullable Map metadata) { this(taskId, append, lastChunk, artifact, contextId, metadata, KIND); } @@ -53,8 +62,7 @@ public static class Builder { private Boolean lastChunk; private Map metadata; - public Builder() { - } + public Builder() {} public Builder(TaskArtifactUpdateEvent_v0_3 existing) { this.taskId = existing.taskId; @@ -96,7 +104,8 @@ public Builder metadata(Map metadata) { } public TaskArtifactUpdateEvent_v0_3 build() { - return new TaskArtifactUpdateEvent_v0_3(taskId, artifact, contextId, append, lastChunk, metadata); + return new TaskArtifactUpdateEvent_v0_3( + taskId, artifact, contextId, append, lastChunk, metadata); } } } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskIdParams_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskIdParams_v0_3.java index 5c44557da..b8761c9bc 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskIdParams_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskIdParams_v0_3.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - import org.a2aproject.sdk.util.Assert; /** diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskNotCancelableError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskNotCancelableError_v0_3.java index 859f3c004..43bde0948 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskNotCancelableError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskNotCancelableError_v0_3.java @@ -7,16 +7,13 @@ */ public class TaskNotCancelableError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = -32002; + public static final Integer DEFAULT_CODE = -32002; public TaskNotCancelableError_v0_3() { this(null, null, null); } - public TaskNotCancelableError_v0_3( - Integer code, - String message, - Object data) { + public TaskNotCancelableError_v0_3(Integer code, String message, Object data) { super( defaultIfNull(code, DEFAULT_CODE), defaultIfNull(message, "Task cannot be canceled"), @@ -26,5 +23,4 @@ public TaskNotCancelableError_v0_3( public TaskNotCancelableError_v0_3(String message) { this(null, message, null); } - } diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskNotFoundError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskNotFoundError_v0_3.java index b0786e906..42f2ea7a5 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskNotFoundError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskNotFoundError_v0_3.java @@ -7,7 +7,7 @@ */ public class TaskNotFoundError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = A2AErrorCodes_v0_3.TASK_NOT_FOUND_ERROR_CODE; + public static final Integer DEFAULT_CODE = A2AErrorCodes_v0_3.TASK_NOT_FOUND_ERROR_CODE; public TaskNotFoundError_v0_3() { this(null, null, null); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskPushNotificationConfig_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskPushNotificationConfig_v0_3.java index 2efbe9a6f..5dbeee486 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskPushNotificationConfig_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskPushNotificationConfig_v0_3.java @@ -5,7 +5,8 @@ /** * A container associating a push notification configuration with a specific task. */ -public record TaskPushNotificationConfig_v0_3(String taskId, PushNotificationConfig_v0_3 pushNotificationConfig) { +public record TaskPushNotificationConfig_v0_3( + String taskId, PushNotificationConfig_v0_3 pushNotificationConfig) { public TaskPushNotificationConfig_v0_3 { Assert.checkNotNullParam("taskId", taskId); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskQueryParams_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskQueryParams_v0_3.java index b96bfa8c2..47bec9d61 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskQueryParams_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskQueryParams_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.spec; -import org.a2aproject.sdk.util.Assert; import java.util.Map; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -11,7 +11,8 @@ * @param historyLength the maximum number of items of history for the task to include in the response * @param metadata additional properties */ -public record TaskQueryParams_v0_3(String id, int historyLength, @Nullable Map metadata) { +public record TaskQueryParams_v0_3( + String id, int historyLength, @Nullable Map metadata) { public TaskQueryParams_v0_3 { Assert.checkNotNullParam("id", id); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskResubscriptionRequest_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskResubscriptionRequest_v0_3.java index 70a25c9ce..ffe78068a 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskResubscriptionRequest_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskResubscriptionRequest_v0_3.java @@ -2,27 +2,26 @@ import static org.a2aproject.sdk.compat03.util.Utils_v0_3.defaultIfNull; -import org.a2aproject.sdk.util.Assert; - import java.util.UUID; +import org.a2aproject.sdk.util.Assert; /** * Used to resubscribe to a task. */ -public final class TaskResubscriptionRequest_v0_3 extends StreamingJSONRPCRequest_v0_3 { - - private TaskResubscriptionRequest_v0_3() { - } +public final class TaskResubscriptionRequest_v0_3 + extends StreamingJSONRPCRequest_v0_3 { + private TaskResubscriptionRequest_v0_3() {} public static final String METHOD = "tasks/resubscribe"; - public TaskResubscriptionRequest_v0_3(String jsonrpc, Object id, String method, TaskIdParams_v0_3 params) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + public TaskResubscriptionRequest_v0_3( + String jsonrpc, Object id, String method, TaskIdParams_v0_3 params) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } Assert.checkNotNullParam("method", method); - if (! method.equals(METHOD)) { + if (!method.equals(METHOD)) { throw new IllegalArgumentException("Invalid TaskResubscriptionRequest method"); } Assert.checkNotNullParam("params", params); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskState_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskState_v0_3.java index 0bcbaa54d..3a96111eb 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskState_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskState_v0_3.java @@ -38,7 +38,7 @@ public String asString() { return state; } - public boolean isFinal(){ + public boolean isFinal() { return isFinal; } @@ -66,4 +66,4 @@ public static TaskState_v0_3 fromString(String state) { default -> throw new IllegalArgumentException("Invalid TaskState: " + state); }; } -} \ No newline at end of file +} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskStatusUpdateEvent_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskStatusUpdateEvent_v0_3.java index d527815b3..03c4d3c6f 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskStatusUpdateEvent_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskStatusUpdateEvent_v0_3.java @@ -2,7 +2,6 @@ import com.google.gson.annotations.SerializedName; import java.util.Map; - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; @@ -16,13 +15,18 @@ public record TaskStatusUpdateEvent_v0_3( String contextId, @SerializedName("final") boolean isFinal, Map metadata, - String kind -) implements EventKind_v0_3, StreamingEventKind_v0_3, UpdateEvent_v0_3 { + String kind) + implements EventKind_v0_3, StreamingEventKind_v0_3, UpdateEvent_v0_3 { public static final String KIND = "status-update"; - public TaskStatusUpdateEvent_v0_3 (String taskId, TaskStatus_v0_3 status, String contextId, boolean isFinal, - @Nullable Map metadata, String kind) { + public TaskStatusUpdateEvent_v0_3( + String taskId, + TaskStatus_v0_3 status, + String contextId, + boolean isFinal, + @Nullable Map metadata, + String kind) { Assert.checkNotNullParam("taskId", taskId); Assert.checkNotNullParam("status", status); Assert.checkNotNullParam("contextId", contextId); @@ -37,8 +41,12 @@ public TaskStatusUpdateEvent_v0_3 (String taskId, TaskStatus_v0_3 status, String this.metadata = metadata != null ? Map.copyOf(metadata) : null; } - public TaskStatusUpdateEvent_v0_3(String taskId, TaskStatus_v0_3 status, String contextId, boolean isFinal, - @Nullable Map metadata) { + public TaskStatusUpdateEvent_v0_3( + String taskId, + TaskStatus_v0_3 status, + String contextId, + boolean isFinal, + @Nullable Map metadata) { this(taskId, status, contextId, isFinal, metadata, KIND); } @@ -49,8 +57,7 @@ public static class Builder { private boolean isFinal; private Map metadata; - public Builder() { - } + public Builder() {} public Builder(TaskStatusUpdateEvent_v0_3 existing) { this.taskId = existing.taskId; diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskStatus_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskStatus_v0_3.java index 6ffa77ac3..a25661846 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskStatus_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TaskStatus_v0_3.java @@ -2,14 +2,13 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; - import org.a2aproject.sdk.util.Assert; /** * Represents the status of a task at a specific point in time. */ -public record TaskStatus_v0_3(TaskState_v0_3 state, Message_v0_3 message, - OffsetDateTime timestamp) { +public record TaskStatus_v0_3( + TaskState_v0_3 state, Message_v0_3 message, OffsetDateTime timestamp) { public TaskStatus_v0_3 { Assert.checkNotNullParam("state", state); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Task_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Task_v0_3.java index 3b0a28cdc..2896130c9 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Task_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/Task_v0_3.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Map; - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; @@ -16,13 +15,19 @@ public record Task_v0_3( List artifacts, List history, @Nullable Map metadata, - String kind -) implements EventKind_v0_3, StreamingEventKind_v0_3 { + String kind) + implements EventKind_v0_3, StreamingEventKind_v0_3 { public static final String KIND = "task"; - public Task_v0_3 (String id, String contextId, TaskStatus_v0_3 status, List artifacts, - List history, @Nullable Map metadata, String kind){ + public Task_v0_3( + String id, + String contextId, + TaskStatus_v0_3 status, + List artifacts, + List history, + @Nullable Map metadata, + String kind) { Assert.checkNotNullParam("id", id); Assert.checkNotNullParam("contextId", contextId); Assert.checkNotNullParam("status", status); @@ -38,8 +43,13 @@ public Task_v0_3 (String id, String contextId, TaskStatus_v0_3 status, List artifacts, - List history, @Nullable Map metadata) { + public Task_v0_3( + String id, + String contextId, + TaskStatus_v0_3 status, + List artifacts, + List history, + @Nullable Map metadata) { this(id, contextId, status, artifacts, history, metadata, KIND); } @@ -51,8 +61,7 @@ public static class Builder { private List history; private Map metadata; - public Builder() { - } + public Builder() {} public Builder(Task_v0_3 task) { id = task.id; diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TextPart_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TextPart_v0_3.java index 70f8563c7..d583e434f 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TextPart_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/TextPart_v0_3.java @@ -1,18 +1,18 @@ package org.a2aproject.sdk.compat03.spec; import java.util.Map; - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** * Represents a text segment within a message or artifact. */ -public record TextPart_v0_3(String text, Map metadata, Kind kind) implements Part_v0_3 { +public record TextPart_v0_3(String text, Map metadata, Kind kind) + implements Part_v0_3 { public static final String TEXT = "text"; - public TextPart_v0_3 (String text, @Nullable Map metadata, Kind kind){ + public TextPart_v0_3(String text, @Nullable Map metadata, Kind kind) { Assert.checkNotNullParam("text", text); if (kind != Kind.TEXT) { throw new IllegalArgumentException("Invalid TextPart kind: " + kind); diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/UnsupportedOperationError_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/UnsupportedOperationError_v0_3.java index 434aab21c..7ee8d454a 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/UnsupportedOperationError_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/UnsupportedOperationError_v0_3.java @@ -7,7 +7,7 @@ */ public class UnsupportedOperationError_v0_3 extends JSONRPCError_v0_3 { - public final static Integer DEFAULT_CODE = A2AErrorCodes_v0_3.UNSUPPORTED_OPERATION_ERROR_CODE; + public static final Integer DEFAULT_CODE = A2AErrorCodes_v0_3.UNSUPPORTED_OPERATION_ERROR_CODE; public UnsupportedOperationError_v0_3(Integer code, String message, Object data) { super( diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/UpdateEvent_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/UpdateEvent_v0_3.java index d2f7b8e7f..3b45bcd9f 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/UpdateEvent_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/spec/UpdateEvent_v0_3.java @@ -1,4 +1,4 @@ package org.a2aproject.sdk.compat03.spec; -public sealed interface UpdateEvent_v0_3 permits TaskStatusUpdateEvent_v0_3, TaskArtifactUpdateEvent_v0_3 { -} +public sealed interface UpdateEvent_v0_3 + permits TaskStatusUpdateEvent_v0_3, TaskArtifactUpdateEvent_v0_3 {} diff --git a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/util/Utils_v0_3.java b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/util/Utils_v0_3.java index f12830974..8f8470ce9 100644 --- a/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/util/Utils_v0_3.java +++ b/compat-0.3/spec/src/main/java/org/a2aproject/sdk/compat03/util/Utils_v0_3.java @@ -2,17 +2,13 @@ import java.util.ArrayList; import java.util.List; - +import java.util.logging.Logger; import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; - import org.a2aproject.sdk.compat03.spec.Artifact_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; import org.a2aproject.sdk.compat03.spec.Part_v0_3; -import java.util.logging.Logger; - - +import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; /** * Utility class providing common helper methods for A2A Protocol operations. @@ -32,7 +28,6 @@ */ public class Utils_v0_3 { - private static final Logger LOGGER = Logger.getLogger(Utils_v0_3.class.getName()); public static String toJsonString(Object data) { @@ -73,7 +68,8 @@ public static T defaultIfNull(T value, T defaultValue) { * @see TaskArtifactUpdateEvent_v0_3 for streaming artifact updates * @see Artifact_v0_3 for artifact structure */ - public static Task_v0_3 appendArtifactToTask(Task_v0_3 task, TaskArtifactUpdateEvent_v0_3 event, String taskId) { + public static Task_v0_3 appendArtifactToTask( + Task_v0_3 task, TaskArtifactUpdateEvent_v0_3 event, String taskId) { // Append artifacts List artifacts = new ArrayList<>(task.artifacts()); @@ -97,36 +93,39 @@ public static Task_v0_3 appendArtifactToTask(Task_v0_3 task, TaskArtifactUpdateE // This represents the first chunk for this artifact index if (existingArtifactIndex >= 0) { // Replace the existing artifact entirely with the new artifact - LOGGER.fine(String.format("Replacing artifact at id %s for task %s", artifactId, taskId)); + LOGGER.fine( + String.format( + "Replacing artifact at id %s for task %s", artifactId, taskId)); artifacts.set(existingArtifactIndex, newArtifact); } else { // Append the new artifact since no artifact with this id/index exists yet - LOGGER.fine(String.format("Adding artifact at id %s for task %s", artifactId, taskId)); + LOGGER.fine( + String.format("Adding artifact at id %s for task %s", artifactId, taskId)); artifacts.add(newArtifact); } } else if (existingArtifact != null) { // Append new parts to the existing artifact's parts list // Do this to a copy - LOGGER.fine(String.format("Appending parts to artifact id %s for task %s", artifactId, taskId)); + LOGGER.fine( + String.format( + "Appending parts to artifact id %s for task %s", artifactId, taskId)); List> parts = new ArrayList<>(existingArtifact.parts()); parts.addAll(newArtifact.parts()); - Artifact_v0_3 updated = new Artifact_v0_3.Builder(existingArtifact) - .parts(parts) - .build(); + Artifact_v0_3 updated = + new Artifact_v0_3.Builder(existingArtifact).parts(parts).build(); artifacts.set(existingArtifactIndex, updated); } else { // We received a chunk to append, but we don't have an existing artifact. // We will ignore this chunk LOGGER.warning( - String.format("Received append=true for nonexistent artifact index for artifact %s in task %s. Ignoring chunk.", - artifactId, taskId)); + String.format( + "Received append=true for nonexistent artifact index for artifact %s in" + + " task %s. Ignoring chunk.", + artifactId, taskId)); } - return new Task_v0_3.Builder(task) - .artifacts(artifacts) - .build(); - + return new Task_v0_3.Builder(task).artifacts(artifacts).build(); } /** @@ -136,11 +135,12 @@ public static Task_v0_3 appendArtifactToTask(Task_v0_3 task, TaskArtifactUpdateE * @return the first defined URL in the supported interaces of the agent card. * @throws A2AClientException */ -// public static String getFavoriteInterface(AgentCard agentCard) throws A2AClientException { -// if (agentCard.supportedInterfaces() == null || agentCard.supportedInterfaces().isEmpty()) { -// throw new A2AClientException("No server interface available in the AgentCard"); -// } -// return agentCard.supportedInterfaces().get(0).url(); -// } + // public static String getFavoriteInterface(AgentCard agentCard) throws A2AClientException { + // if (agentCard.supportedInterfaces() == null || + // agentCard.supportedInterfaces().isEmpty()) { + // throw new A2AClientException("No server interface available in the AgentCard"); + // } + // return agentCard.supportedInterfaces().get(0).url(); + // } } diff --git a/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/JSONRPCErrorSerialization_v0_3_Test.java b/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/JSONRPCErrorSerialization_v0_3_Test.java index 5060770b8..2c161bd5e 100644 --- a/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/JSONRPCErrorSerialization_v0_3_Test.java +++ b/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/JSONRPCErrorSerialization_v0_3_Test.java @@ -1,42 +1,62 @@ package org.a2aproject.sdk.compat03.spec; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; +import java.util.Map; import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; - +import org.junit.jupiter.api.Test; public class JSONRPCErrorSerialization_v0_3_Test { @Test - public void shouldDeserializeToCorrectJSONRPCErrorSubclass() throws JsonProcessingException_v0_3 { - String jsonTemplate = """ + public void shouldDeserializeToCorrectJSONRPCErrorSubclass() + throws JsonProcessingException_v0_3 { + String jsonTemplate = + """ {"code": %s, "message": "error", "data": "anything"} """; record ErrorCase(int code, Class clazz) {} - List cases = List.of( - new ErrorCase(JSONParseError_v0_3.DEFAULT_CODE, JSONParseError_v0_3.class), - new ErrorCase(InvalidRequestError_v0_3.DEFAULT_CODE, InvalidRequestError_v0_3.class), - new ErrorCase(MethodNotFoundError_v0_3.DEFAULT_CODE, MethodNotFoundError_v0_3.class), - new ErrorCase(InvalidParamsError_v0_3.DEFAULT_CODE, InvalidParamsError_v0_3.class), - new ErrorCase(InternalError_v0_3.DEFAULT_CODE, InternalError_v0_3.class), - new ErrorCase(PushNotificationNotSupportedError_v0_3.DEFAULT_CODE, PushNotificationNotSupportedError_v0_3.class), - new ErrorCase(UnsupportedOperationError_v0_3.DEFAULT_CODE, UnsupportedOperationError_v0_3.class), - new ErrorCase(ContentTypeNotSupportedError_v0_3.DEFAULT_CODE, ContentTypeNotSupportedError_v0_3.class), - new ErrorCase(InvalidAgentResponseError_v0_3.DEFAULT_CODE, InvalidAgentResponseError_v0_3.class), - new ErrorCase(TaskNotCancelableError_v0_3.DEFAULT_CODE, TaskNotCancelableError_v0_3.class), - new ErrorCase(TaskNotFoundError_v0_3.DEFAULT_CODE, TaskNotFoundError_v0_3.class), - new ErrorCase(Integer.MAX_VALUE, JSONRPCError_v0_3.class) // Any unknown code will be treated as JSONRPCError - ); + List cases = + List.of( + new ErrorCase(JSONParseError_v0_3.DEFAULT_CODE, JSONParseError_v0_3.class), + new ErrorCase( + InvalidRequestError_v0_3.DEFAULT_CODE, + InvalidRequestError_v0_3.class), + new ErrorCase( + MethodNotFoundError_v0_3.DEFAULT_CODE, + MethodNotFoundError_v0_3.class), + new ErrorCase( + InvalidParamsError_v0_3.DEFAULT_CODE, + InvalidParamsError_v0_3.class), + new ErrorCase(InternalError_v0_3.DEFAULT_CODE, InternalError_v0_3.class), + new ErrorCase( + PushNotificationNotSupportedError_v0_3.DEFAULT_CODE, + PushNotificationNotSupportedError_v0_3.class), + new ErrorCase( + UnsupportedOperationError_v0_3.DEFAULT_CODE, + UnsupportedOperationError_v0_3.class), + new ErrorCase( + ContentTypeNotSupportedError_v0_3.DEFAULT_CODE, + ContentTypeNotSupportedError_v0_3.class), + new ErrorCase( + InvalidAgentResponseError_v0_3.DEFAULT_CODE, + InvalidAgentResponseError_v0_3.class), + new ErrorCase( + TaskNotCancelableError_v0_3.DEFAULT_CODE, + TaskNotCancelableError_v0_3.class), + new ErrorCase( + TaskNotFoundError_v0_3.DEFAULT_CODE, TaskNotFoundError_v0_3.class), + new ErrorCase( + Integer.MAX_VALUE, + JSONRPCError_v0_3 + .class) // Any unknown code will be treated as JSONRPCError + ); for (ErrorCase errorCase : cases) { String json = jsonTemplate.formatted(errorCase.code()); @@ -49,7 +69,8 @@ record ErrorCase(int code, Class clazz) {} @Test @SuppressWarnings("unchecked") - void deleteTaskPushNotificationConfigSuccessResponseSerializesResultAsNull() throws JsonProcessingException_v0_3 { + void deleteTaskPushNotificationConfigSuccessResponseSerializesResultAsNull() + throws JsonProcessingException_v0_3 { DeleteTaskPushNotificationConfigResponse_v0_3 response = new DeleteTaskPushNotificationConfigResponse_v0_3("req-123"); @@ -60,14 +81,17 @@ void deleteTaskPushNotificationConfigSuccessResponseSerializesResultAsNull() thr assertEquals("req-123", map.get("id")); assertTrue(map.containsKey("result"), "result field must be present in success response"); assertEquals(null, map.get("result"), "result must be null for delete response"); - assertFalse(map.containsKey("error"), "error field must not be present in success response"); + assertFalse( + map.containsKey("error"), "error field must not be present in success response"); } @Test @SuppressWarnings("unchecked") - void deleteTaskPushNotificationConfigErrorResponseSerializesErrorWithoutResult() throws JsonProcessingException_v0_3 { + void deleteTaskPushNotificationConfigErrorResponseSerializesErrorWithoutResult() + throws JsonProcessingException_v0_3 { DeleteTaskPushNotificationConfigResponse_v0_3 response = - new DeleteTaskPushNotificationConfigResponse_v0_3("req-456", new TaskNotFoundError_v0_3()); + new DeleteTaskPushNotificationConfigResponse_v0_3( + "req-456", new TaskNotFoundError_v0_3()); String json = JsonUtil_v0_3.toJson(response); Map map = JsonUtil_v0_3.fromJson(json, Map.class); @@ -75,8 +99,7 @@ void deleteTaskPushNotificationConfigErrorResponseSerializesErrorWithoutResult() assertEquals("2.0", map.get("jsonrpc")); assertEquals("req-456", map.get("id")); assertTrue(map.containsKey("error"), "error field must be present in error response"); - assertFalse(map.containsKey("result"), "result field must not be present in error response"); + assertFalse( + map.containsKey("result"), "result field must not be present in error response"); } - - } diff --git a/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/SubTypeSerialization_v0_3_Test.java b/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/SubTypeSerialization_v0_3_Test.java index a55c54d67..187145a55 100644 --- a/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/SubTypeSerialization_v0_3_Test.java +++ b/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/SubTypeSerialization_v0_3_Test.java @@ -1,27 +1,29 @@ package org.a2aproject.sdk.compat03.spec; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import java.util.stream.Stream; import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.Map; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.Assertions.assertEquals; - public class SubTypeSerialization_v0_3_Test { - private static final Task_v0_3 MINIMAL_TASK = new Task_v0_3.Builder() - .id("task-123") - .contextId("session-xyz") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .build(); + private static final Task_v0_3 MINIMAL_TASK = + new Task_v0_3.Builder() + .id("task-123") + .contextId("session-xyz") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .build(); @ParameterizedTest @MethodSource("serializationTestCases") - void testSubtypeSerialization(Object objectToSerialize, String typePropertyName, String expectedTypeValue) throws JsonProcessingException_v0_3 { + void testSubtypeSerialization( + Object objectToSerialize, String typePropertyName, String expectedTypeValue) + throws JsonProcessingException_v0_3 { String json = JsonUtil_v0_3.toJson(objectToSerialize); @SuppressWarnings("unchecked") Map map = JsonUtil_v0_3.fromJson(json, Map.class); @@ -36,67 +38,73 @@ private static Stream serializationTestCases() { .contextId(MINIMAL_TASK.contextId()) .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) .isFinal(true) - .build(), "kind", TaskStatusUpdateEvent_v0_3.KIND - ), + .build(), + "kind", + TaskStatusUpdateEvent_v0_3.KIND), Arguments.of( new TaskArtifactUpdateEvent_v0_3.Builder() .taskId(MINIMAL_TASK.id()) .contextId(MINIMAL_TASK.contextId()) - .artifact(new Artifact_v0_3.Builder() - .artifactId("11") - .parts(new TextPart_v0_3("text")) - .build()) - .build(), "kind", TaskArtifactUpdateEvent_v0_3.KIND - ), - Arguments.of( - MINIMAL_TASK, "kind", Task_v0_3.KIND - ), + .artifact( + new Artifact_v0_3.Builder() + .artifactId("11") + .parts(new TextPart_v0_3("text")) + .build()) + .build(), + "kind", + TaskArtifactUpdateEvent_v0_3.KIND), + Arguments.of(MINIMAL_TASK, "kind", Task_v0_3.KIND), Arguments.of( new Message_v0_3.Builder() .role(Message_v0_3.Role.USER) .parts(new TextPart_v0_3("tell me some jokes")) .contextId("context-1234") .messageId("message-1234") - .build(), "kind", Message_v0_3.KIND - ), - Arguments.of( - new TextPart_v0_3("text"), "kind", TextPart_v0_3.TEXT - ), + .build(), + "kind", + Message_v0_3.KIND), + Arguments.of(new TextPart_v0_3("text"), "kind", TextPart_v0_3.TEXT), Arguments.of( - new FilePart_v0_3(new FileWithUri_v0_3( - "image/jpeg", null, "file:///path/to/image.jpg")), - "kind", FilePart_v0_3.FILE - ), + new FilePart_v0_3( + new FileWithUri_v0_3( + "image/jpeg", null, "file:///path/to/image.jpg")), + "kind", + FilePart_v0_3.FILE), Arguments.of( - new DataPart_v0_3(Map.of("chartType", "bar")), "kind", DataPart_v0_3.DATA - ), + new DataPart_v0_3(Map.of("chartType", "bar")), "kind", DataPart_v0_3.DATA), Arguments.of( new APIKeySecurityScheme_v0_3.Builder() - .in("test").name("name").description("description").build(), - "type", APIKeySecurityScheme_v0_3.TYPE - ), + .in("test") + .name("name") + .description("description") + .build(), + "type", + APIKeySecurityScheme_v0_3.TYPE), Arguments.of( new HTTPAuthSecurityScheme_v0_3.Builder() - .scheme("basic").description("Basic Auth").build(), - "type", HTTPAuthSecurityScheme_v0_3.TYPE - ), + .scheme("basic") + .description("Basic Auth") + .build(), + "type", + HTTPAuthSecurityScheme_v0_3.TYPE), Arguments.of( new OAuth2SecurityScheme_v0_3.Builder() .flows(new OAuthFlows_v0_3.Builder().build()) - .description("oAuth2SecurityScheme").build(), - "type", OAuth2SecurityScheme_v0_3.TYPE - ), + .description("oAuth2SecurityScheme") + .build(), + "type", + OAuth2SecurityScheme_v0_3.TYPE), Arguments.of( new OpenIdConnectSecurityScheme_v0_3.Builder() - .openIdConnectUrl("https://accounts.google.com/.well-known/openid-configuration") - .description("OpenId").build(), - "type", OpenIdConnectSecurityScheme_v0_3.TYPE - ), + .openIdConnectUrl( + "https://accounts.google.com/.well-known/openid-configuration") + .description("OpenId") + .build(), + "type", + OpenIdConnectSecurityScheme_v0_3.TYPE), Arguments.of( new MutualTLSSecurityScheme_v0_3("mutual tls test"), - "type", MutualTLSSecurityScheme_v0_3.TYPE - ) - ); + "type", + MutualTLSSecurityScheme_v0_3.TYPE)); } - } diff --git a/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/TaskSerialization_v0_3_Test.java b/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/TaskSerialization_v0_3_Test.java index 927854a7a..04fa66ce4 100644 --- a/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/TaskSerialization_v0_3_Test.java +++ b/compat-0.3/spec/src/test/java/org/a2aproject/sdk/compat03/spec/TaskSerialization_v0_3_Test.java @@ -8,11 +8,9 @@ import java.time.OffsetDateTime; import java.util.List; import java.util.Map; - -import org.junit.jupiter.api.Test; - import org.a2aproject.sdk.compat03.json.JsonProcessingException_v0_3; import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; +import org.junit.jupiter.api.Test; /** * Tests for Task serialization and deserialization using Gson. @@ -22,11 +20,12 @@ class TaskSerialization_v0_3_Test { @Test void testBasicTaskSerialization() throws JsonProcessingException_v0_3 { // Create a basic task - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - .build(); + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + .build(); // Serialize to JSON String json = JsonUtil_v0_3.toJson(task); @@ -48,11 +47,12 @@ void testBasicTaskSerialization() throws JsonProcessingException_v0_3 { void testTaskWithTimestamp() throws JsonProcessingException_v0_3 { OffsetDateTime timestamp = OffsetDateTime.now(); - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING, null, timestamp)) - .build(); + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING, null, timestamp)) + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); @@ -67,22 +67,21 @@ void testTaskWithTimestamp() throws JsonProcessingException_v0_3 { @Test void testTaskWithArtifacts() throws JsonProcessingException_v0_3 { - Artifact_v0_3 artifact = new Artifact_v0_3.Builder() - .artifactId("artifact-1") - .name("Test Artifact") - .description("Description of artifact") - .parts(List.of( - new TextPart_v0_3("Hello"), - new TextPart_v0_3("World") - )) - .build(); - - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) - .artifacts(List.of(artifact)) - .build(); + Artifact_v0_3 artifact = + new Artifact_v0_3.Builder() + .artifactId("artifact-1") + .name("Test Artifact") + .description("Description of artifact") + .parts(List.of(new TextPart_v0_3("Hello"), new TextPart_v0_3("World"))) + .build(); + + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); @@ -104,17 +103,19 @@ void testTaskWithArtifacts() throws JsonProcessingException_v0_3 { @Test void testTaskWithHistory() throws JsonProcessingException_v0_3 { - Message_v0_3 message = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(List.of(new TextPart_v0_3("Test message"))) - .build(); - - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING)) - .history(List.of(message)) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(List.of(new TextPart_v0_3("Test message"))) + .build(); + + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING)) + .history(List.of(message)) + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); @@ -137,28 +138,32 @@ void testTaskWithHistory() throws JsonProcessingException_v0_3 { void testTaskWithAllFields() throws JsonProcessingException_v0_3 { OffsetDateTime timestamp = OffsetDateTime.now(); - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-789") - .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING, null, timestamp)) - .history(List.of( - new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(List.of(new TextPart_v0_3("User message"))) - .build(), - new Message_v0_3.Builder() - .role(Message_v0_3.Role.AGENT) - .parts(List.of(new TextPart_v0_3("Agent response"))) - .build() - )) - .artifacts(List.of( - new Artifact_v0_3.Builder() - .artifactId("artifact-1") - .parts(List.of(new TextPart_v0_3("Artifact content"))) - .build() - )) - .metadata(Map.of("key1", "value1", "key2", 42)) - .build(); + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-789") + .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING, null, timestamp)) + .history( + List.of( + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts(List.of(new TextPart_v0_3("User message"))) + .build(), + new Message_v0_3.Builder() + .role(Message_v0_3.Role.AGENT) + .parts(List.of(new TextPart_v0_3("Agent response"))) + .build())) + .artifacts( + List.of( + new Artifact_v0_3.Builder() + .artifactId("artifact-1") + .parts( + List.of( + new TextPart_v0_3( + "Artifact content"))) + .build())) + .metadata(Map.of("key1", "value1", "key2", 42)) + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); @@ -180,11 +185,12 @@ void testTaskWithAllFields() throws JsonProcessingException_v0_3 { @Test void testTaskWithDifferentStates() throws JsonProcessingException_v0_3 { for (TaskState_v0_3 state : TaskState_v0_3.values()) { - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-" + state.asString()) - .contextId("context-123") - .status(new TaskStatus_v0_3(state)) - .build(); + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-" + state.asString()) + .contextId("context-123") + .status(new TaskStatus_v0_3(state)) + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); @@ -202,12 +208,13 @@ void testTaskWithDifferentStates() throws JsonProcessingException_v0_3 { @Test void testTaskWithNullOptionalFields() throws JsonProcessingException_v0_3 { - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) - // artifacts, history, metadata not set - .build(); + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus_v0_3(TaskState_v0_3.SUBMITTED)) + // artifacts, history, metadata not set + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); @@ -229,19 +236,23 @@ void testTaskWithNullOptionalFields() throws JsonProcessingException_v0_3 { @Test void testTaskWithFilePartBytes() throws JsonProcessingException_v0_3 { - FilePart_v0_3 filePart = new FilePart_v0_3(new FileWithBytes_v0_3("application/pdf", "document.pdf", "base64data")); - - Artifact_v0_3 artifact = new Artifact_v0_3.Builder() - .artifactId("file-artifact") - .parts(List.of(filePart)) - .build(); - - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) - .artifacts(List.of(artifact)) - .build(); + FilePart_v0_3 filePart = + new FilePart_v0_3( + new FileWithBytes_v0_3("application/pdf", "document.pdf", "base64data")); + + Artifact_v0_3 artifact = + new Artifact_v0_3.Builder() + .artifactId("file-artifact") + .parts(List.of(filePart)) + .build(); + + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); @@ -266,19 +277,24 @@ void testTaskWithFilePartBytes() throws JsonProcessingException_v0_3 { @Test void testTaskWithFilePartUri() throws JsonProcessingException_v0_3 { - FilePart_v0_3 filePart = new FilePart_v0_3(new FileWithUri_v0_3("image/png", "photo.png", "https://example.com/photo.png")); - - Artifact_v0_3 artifact = new Artifact_v0_3.Builder() - .artifactId("uri-artifact") - .parts(List.of(filePart)) - .build(); - - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) - .artifacts(List.of(artifact)) - .build(); + FilePart_v0_3 filePart = + new FilePart_v0_3( + new FileWithUri_v0_3( + "image/png", "photo.png", "https://example.com/photo.png")); + + Artifact_v0_3 artifact = + new Artifact_v0_3.Builder() + .artifactId("uri-artifact") + .parts(List.of(filePart)) + .build(); + + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); @@ -302,17 +318,19 @@ void testTaskWithFilePartUri() throws JsonProcessingException_v0_3 { void testTaskWithDataPart() throws JsonProcessingException_v0_3 { DataPart_v0_3 dataPart = new DataPart_v0_3(Map.of("temperature", 22.5, "humidity", 65)); - Artifact_v0_3 artifact = new Artifact_v0_3.Builder() - .artifactId("data-artifact") - .parts(List.of(dataPart)) - .build(); + Artifact_v0_3 artifact = + new Artifact_v0_3.Builder() + .artifactId("data-artifact") + .parts(List.of(dataPart)) + .build(); - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) - .artifacts(List.of(artifact)) - .build(); + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); @@ -336,28 +354,34 @@ void testTaskRoundTrip() throws JsonProcessingException_v0_3 { // Create a comprehensive task with all part types OffsetDateTime timestamp = OffsetDateTime.now(); - Task_v0_3 original = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-789") - .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING, null, timestamp)) - .history(List.of( - new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(List.of( - new TextPart_v0_3("Text"), - new FilePart_v0_3(new FileWithBytes_v0_3("text/plain", "file.txt", "data")), - new DataPart_v0_3(Map.of("key", "value")) - )) - .build() - )) - .artifacts(List.of( - new Artifact_v0_3.Builder() - .artifactId("artifact-1") - .parts(List.of(new TextPart_v0_3("Content"))) - .build() - )) - .metadata(Map.of("meta1", "value1")) - .build(); + Task_v0_3 original = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-789") + .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING, null, timestamp)) + .history( + List.of( + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts( + List.of( + new TextPart_v0_3("Text"), + new FilePart_v0_3( + new FileWithBytes_v0_3( + "text/plain", + "file.txt", + "data")), + new DataPart_v0_3( + Map.of("key", "value")))) + .build())) + .artifacts( + List.of( + new Artifact_v0_3.Builder() + .artifactId("artifact-1") + .parts(List.of(new TextPart_v0_3("Content"))) + .build())) + .metadata(Map.of("meta1", "value1")) + .build(); // Serialize to JSON String json = JsonUtil_v0_3.toJson(original); @@ -381,16 +405,18 @@ void testTaskRoundTrip() throws JsonProcessingException_v0_3 { @Test void testTaskStatusWithMessage() throws JsonProcessingException_v0_3 { - Message_v0_3 statusMessage = new Message_v0_3.Builder() - .role(Message_v0_3.Role.AGENT) - .parts(List.of(new TextPart_v0_3("Processing complete"))) - .build(); - - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED, statusMessage, null)) - .build(); + Message_v0_3 statusMessage = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.AGENT) + .parts(List.of(new TextPart_v0_3("Processing complete"))) + .build(); + + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED, statusMessage, null)) + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); @@ -411,16 +437,17 @@ void testTaskStatusWithMessage() throws JsonProcessingException_v0_3 { @Test void testDeserializeTaskFromJson() throws JsonProcessingException_v0_3 { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "submitted" - }, - "kind": "task" - } - """; + String json = + """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "submitted" + }, + "kind": "task" + } + """; Task_v0_3 task = JsonUtil_v0_3.fromJson(json, Task_v0_3.class); @@ -434,28 +461,29 @@ void testDeserializeTaskFromJson() throws JsonProcessingException_v0_3 { @Test void testDeserializeTaskWithArtifactsFromJson() throws JsonProcessingException_v0_3 { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "artifacts": [ + String json = + """ { - "artifactId": "artifact-1", - "name": "Result", - "parts": [ + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ { - "kind": "text", - "text": "Hello World" + "artifactId": "artifact-1", + "name": "Result", + "parts": [ + { + "kind": "text", + "text": "Hello World" + } + ] } - ] + ], + "kind": "task" } - ], - "kind": "task" - } - """; + """; Task_v0_3 task = JsonUtil_v0_3.fromJson(json, Task_v0_3.class); @@ -466,36 +494,38 @@ void testDeserializeTaskWithArtifactsFromJson() throws JsonProcessingException_v assertEquals("Result", task.artifacts().get(0).name()); assertEquals(1, task.artifacts().get(0).parts().size()); assertTrue(task.artifacts().get(0).parts().get(0) instanceof TextPart_v0_3); - assertEquals("Hello World", ((TextPart_v0_3) task.artifacts().get(0).parts().get(0)).text()); + assertEquals( + "Hello World", ((TextPart_v0_3) task.artifacts().get(0).parts().get(0)).text()); } @Test void testDeserializeTaskWithFilePartBytesFromJson() throws JsonProcessingException_v0_3 { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "artifacts": [ + String json = + """ { - "artifactId": "file-artifact", - "parts": [ + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ { - "kind": "file", - "file": { - "mimeType": "application/pdf", - "name": "document.pdf", - "bytes": "base64encodeddata" - } + "artifactId": "file-artifact", + "parts": [ + { + "kind": "file", + "file": { + "mimeType": "application/pdf", + "name": "document.pdf", + "bytes": "base64encodeddata" + } + } + ] } - ] + ], + "kind": "task" } - ], - "kind": "task" - } - """; + """; Task_v0_3 task = JsonUtil_v0_3.fromJson(json, Task_v0_3.class); @@ -513,31 +543,32 @@ void testDeserializeTaskWithFilePartBytesFromJson() throws JsonProcessingExcepti @Test void testDeserializeTaskWithFilePartUriFromJson() throws JsonProcessingException_v0_3 { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "artifacts": [ + String json = + """ { - "artifactId": "uri-artifact", - "parts": [ + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ { - "kind": "file", - "file": { - "mimeType": "image/png", - "name": "photo.png", - "uri": "https://example.com/photo.png" - } + "artifactId": "uri-artifact", + "parts": [ + { + "kind": "file", + "file": { + "mimeType": "image/png", + "name": "photo.png", + "uri": "https://example.com/photo.png" + } + } + ] } - ] + ], + "kind": "task" } - ], - "kind": "task" - } - """; + """; Task_v0_3 task = JsonUtil_v0_3.fromJson(json, Task_v0_3.class); @@ -554,30 +585,31 @@ void testDeserializeTaskWithFilePartUriFromJson() throws JsonProcessingException @Test void testDeserializeTaskWithDataPartFromJson() throws JsonProcessingException_v0_3 { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "artifacts": [ + String json = + """ { - "artifactId": "data-artifact", - "parts": [ + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "artifacts": [ { - "kind": "data", - "data": { - "temperature": 22.5, - "humidity": 65 - } + "artifactId": "data-artifact", + "parts": [ + { + "kind": "data", + "data": { + "temperature": 22.5, + "humidity": 65 + } + } + ] } - ] + ], + "kind": "task" } - ], - "kind": "task" - } - """; + """; Task_v0_3 task = JsonUtil_v0_3.fromJson(json, Task_v0_3.class); @@ -590,38 +622,39 @@ void testDeserializeTaskWithDataPartFromJson() throws JsonProcessingException_v0 @Test void testDeserializeTaskWithHistoryFromJson() throws JsonProcessingException_v0_3 { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "working" - }, - "history": [ + String json = + """ { - "messageId": "msg-001", - "role": "user", - "parts": [ + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "working" + }, + "history": [ { - "kind": "text", - "text": "User message" - } - ] - }, - { - "messageId": "msg-002", - "role": "agent", - "parts": [ + "messageId": "msg-001", + "role": "user", + "parts": [ + { + "kind": "text", + "text": "User message" + } + ] + }, { - "kind": "text", - "text": "Agent response" + "messageId": "msg-002", + "role": "agent", + "parts": [ + { + "kind": "text", + "text": "Agent response" + } + ] } - ] + ], + "kind": "task" } - ], - "kind": "task" - } - """; + """; Task_v0_3 task = JsonUtil_v0_3.fromJson(json, Task_v0_3.class); @@ -635,17 +668,18 @@ void testDeserializeTaskWithHistoryFromJson() throws JsonProcessingException_v0_ @Test void testDeserializeTaskWithTimestampFromJson() throws JsonProcessingException_v0_3 { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "working", - "timestamp": "2023-10-01T12:00:00.234-05:00" - }, - "kind": "task" - } - """; + String json = + """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "working", + "timestamp": "2023-10-01T12:00:00.234-05:00" + }, + "kind": "task" + } + """; Task_v0_3 task = JsonUtil_v0_3.fromJson(json, Task_v0_3.class); @@ -657,20 +691,21 @@ void testDeserializeTaskWithTimestampFromJson() throws JsonProcessingException_v @Test void testDeserializeTaskWithMetadataFromJson() throws JsonProcessingException_v0_3 { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "completed" - }, - "metadata": { - "key1": "value1", - "key2": 42 - }, - "kind": "task" - } - """; + String json = + """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "completed" + }, + "metadata": { + "key1": "value1", + "key2": 42 + }, + "kind": "task" + } + """; Task_v0_3 task = JsonUtil_v0_3.fromJson(json, Task_v0_3.class); @@ -681,22 +716,30 @@ void testDeserializeTaskWithMetadataFromJson() throws JsonProcessingException_v0 @Test void testTaskWithMixedPartTypes() throws JsonProcessingException_v0_3 { - Artifact_v0_3 artifact = new Artifact_v0_3.Builder() - .artifactId("mixed-artifact") - .parts(List.of( - new TextPart_v0_3("Text content"), - new FilePart_v0_3(new FileWithBytes_v0_3("application/json", "data.json", "{}")), - new DataPart_v0_3(Map.of("result", 42)), - new FilePart_v0_3(new FileWithUri_v0_3("image/png", "image.png", "https://example.com/img.png")) - )) - .build(); - - Task_v0_3 task = new Task_v0_3.Builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) - .artifacts(List.of(artifact)) - .build(); + Artifact_v0_3 artifact = + new Artifact_v0_3.Builder() + .artifactId("mixed-artifact") + .parts( + List.of( + new TextPart_v0_3("Text content"), + new FilePart_v0_3( + new FileWithBytes_v0_3( + "application/json", "data.json", "{}")), + new DataPart_v0_3(Map.of("result", 42)), + new FilePart_v0_3( + new FileWithUri_v0_3( + "image/png", + "image.png", + "https://example.com/img.png")))) + .build(); + + Task_v0_3 task = + new Task_v0_3.Builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) + .artifacts(List.of(artifact)) + .build(); // Serialize String json = JsonUtil_v0_3.toJson(task); diff --git a/compat-0.3/tck/pom.xml b/compat-0.3/tck/pom.xml index 5804cac76..15cb8b1eb 100644 --- a/compat-0.3/tck/pom.xml +++ b/compat-0.3/tck/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -49,6 +47,25 @@ + + + + io.quarkus + quarkus-maven-plugin + true + + + + build + generate-code + generate-code-tests + + + + + + + multi-version @@ -82,10 +99,10 @@ add-multi-version-source - generate-sources add-source + generate-sources src/multi-version/java @@ -98,23 +115,4 @@ - - - - - io.quarkus - quarkus-maven-plugin - true - - - - build - generate-code - generate-code-tests - - - - - - diff --git a/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/AgentCardProducer_v0_3.java b/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/AgentCardProducer_v0_3.java index 14b1f1528..4ff8373d5 100644 --- a/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/AgentCardProducer_v0_3.java +++ b/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/AgentCardProducer_v0_3.java @@ -1,17 +1,15 @@ package org.a2aproject.sdk.compat03.tck.server; -import java.util.Collections; -import java.util.List; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - -import org.a2aproject.sdk.server.PublicAgentCard; +import java.util.Collections; +import java.util.List; import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.AgentInterface_v0_3; import org.a2aproject.sdk.compat03.spec.AgentSkill_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; +import org.a2aproject.sdk.server.PublicAgentCard; @ApplicationScoped public class AgentCardProducer_v0_3 { @@ -31,25 +29,32 @@ public AgentCard_v0_3 agentCard() { .url(sutJsonRpcUrl) .version("1.0.0") .documentationUrl("http://example.com/docs") - .capabilities(new AgentCapabilities_v0_3.Builder() - .streaming(true) - .pushNotifications(true) - .stateTransitionHistory(true) - .build()) + .capabilities( + new AgentCapabilities_v0_3.Builder() + .streaming(true) + .pushNotifications(true) + .stateTransitionHistory(true) + .build()) .defaultInputModes(Collections.singletonList("text")) .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(new AgentSkill_v0_3.Builder() - .id("hello_world") - .name("Returns hello world") - .description("just returns hello world") - .tags(Collections.singletonList("hello world")) - .examples(List.of("hi", "hello world")) - .build())) + .skills( + Collections.singletonList( + new AgentSkill_v0_3.Builder() + .id("hello_world") + .name("Returns hello world") + .description("just returns hello world") + .tags(Collections.singletonList("hello world")) + .examples(List.of("hi", "hello world")) + .build())) .protocolVersion("0.3.0") - .additionalInterfaces(List.of( - new AgentInterface_v0_3(TransportProtocol_v0_3.JSONRPC.asString(), sutJsonRpcUrl), - new AgentInterface_v0_3(TransportProtocol_v0_3.GRPC.asString(), sutGrpcUrl), - new AgentInterface_v0_3(TransportProtocol_v0_3.HTTP_JSON.asString(), sutRestcUrl))) + .additionalInterfaces( + List.of( + new AgentInterface_v0_3( + TransportProtocol_v0_3.JSONRPC.asString(), sutJsonRpcUrl), + new AgentInterface_v0_3( + TransportProtocol_v0_3.GRPC.asString(), sutGrpcUrl), + new AgentInterface_v0_3( + TransportProtocol_v0_3.HTTP_JSON.asString(), sutRestcUrl))) .build(); } @@ -58,4 +63,3 @@ private static String getEnvOrDefault(String envVar, String defaultValue) { return value == null || value.isBlank() ? defaultValue : value; } } - diff --git a/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/AgentExecutorProducer_v0_3.java b/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/AgentExecutorProducer_v0_3.java index 9f89162c7..d67d0515a 100644 --- a/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/AgentExecutorProducer_v0_3.java +++ b/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/AgentExecutorProducer_v0_3.java @@ -3,7 +3,6 @@ import jakarta.annotation.PreDestroy; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; @@ -35,9 +34,16 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro // Sleep to allow task state persistence before TCK resubscribe test Message message = context.getMessage(); - if (message != null && message.messageId() != null && message.messageId().startsWith("test-resubscribe-message-id")) { - int timeoutMs = Integer.parseInt(System.getenv().getOrDefault("RESUBSCRIBE_TIMEOUT_MS", "3000")); - System.out.println("====> task id starts with test-resubscribe-message-id, sleeping for " + timeoutMs + " ms"); + if (message != null + && message.messageId() != null + && message.messageId().startsWith("test-resubscribe-message-id")) { + int timeoutMs = + Integer.parseInt( + System.getenv().getOrDefault("RESUBSCRIBE_TIMEOUT_MS", "3000")); + System.out.println( + "====> task id starts with test-resubscribe-message-id, sleeping for " + + timeoutMs + + " ms"); try { Thread.sleep(timeoutMs); } catch (InterruptedException e) { @@ -50,7 +56,8 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro System.out.println("====> task set to WORKING, starting background execution"); // Method returns immediately - task continues in background - System.out.println("====> execute() method returning immediately, task running in background"); + System.out.println( + "====> execute() method returning immediately, task running in background"); } @Override @@ -79,6 +86,6 @@ public void cancel(RequestContext context, AgentEmitter emitter) throws A2AError @PreDestroy public void cleanup() { System.out.println("====> shutting down task executor"); - } + } } -} \ No newline at end of file +} diff --git a/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/package-info.java b/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/package-info.java index 4539f9136..ea7e9d22b 100644 --- a/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/package-info.java +++ b/compat-0.3/tck/src/main/java/org/a2aproject/sdk/compat03/tck/server/package-info.java @@ -3,8 +3,7 @@ import org.jspecify.annotations.NullMarked; -//The following had @Nullable annotation applied from JSpecify -//AgentCardProducer.java getEnvOrDefault method, -//AgentExecutorProducer.java execute method +// The following had @Nullable annotation applied from JSpecify +// AgentCardProducer.java getEnvOrDefault method, +// AgentExecutorProducer.java execute method // - diff --git a/compat-0.3/tests/server-common/pom.xml b/compat-0.3/tests/server-common/pom.xml index a7a1d21cf..26d8f60ff 100644 --- a/compat-0.3/tests/server-common/pom.xml +++ b/compat-0.3/tests/server-common/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/compat-0.3/tests/server-common/src/main/java/org/a2aproject/sdk/compat03/conversion/test/AgentExecutorProducer_v0_3.java b/compat-0.3/tests/server-common/src/main/java/org/a2aproject/sdk/compat03/conversion/test/AgentExecutorProducer_v0_3.java index fae5412cb..6ece689ed 100644 --- a/compat-0.3/tests/server-common/src/main/java/org/a2aproject/sdk/compat03/conversion/test/AgentExecutorProducer_v0_3.java +++ b/compat-0.3/tests/server-common/src/main/java/org/a2aproject/sdk/compat03/conversion/test/AgentExecutorProducer_v0_3.java @@ -1,25 +1,22 @@ package org.a2aproject.sdk.compat03.conversion.test; -import java.util.List; - +import io.quarkus.arc.profile.IfBuildProfile; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; - +import java.util.List; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; import org.a2aproject.sdk.spec.A2AError; import org.a2aproject.sdk.spec.TextPart; import org.a2aproject.sdk.spec.UnsupportedOperationError; -import io.quarkus.arc.profile.IfBuildProfile; @ApplicationScoped @IfBuildProfile("test") public class AgentExecutorProducer_v0_3 { - @Inject - RequestScopedBean_v0_3 requestScopedBean; + @Inject RequestScopedBean_v0_3 requestScopedBean; @Produces public AgentExecutor agentExecutor() { @@ -27,7 +24,10 @@ public AgentExecutor agentExecutor() { @Override public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2AError { String taskId = context.getTaskId(); - String input = context.getMessage() != null ? extractTextFromMessage(context.getMessage()) : ""; + String input = + context.getMessage() != null + ? extractTextFromMessage(context.getMessage()) + : ""; // Request-scoped bean test: verify CDI request context propagation if (input.startsWith("request-scoped:")) { @@ -55,8 +55,10 @@ public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2 } if (input.startsWith("multi-event:second")) { agentEmitter.addArtifact( - List.of(new TextPart("Second message artifact")), - "artifact-2", "Second Artifact", null); + List.of(new TextPart("Second message artifact")), + "artifact-2", + "Second Artifact", + null); agentEmitter.complete(); return; } @@ -70,23 +72,28 @@ public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2 return; } // First call: emit INPUT_REQUIRED - agentEmitter.requiresInput(agentEmitter.newAgentMessage( - List.of(new TextPart("Please provide additional information")), - context.getMessage().metadata())); + agentEmitter.requiresInput( + agentEmitter.newAgentMessage( + List.of(new TextPart("Please provide additional information")), + context.getMessage().metadata())); return; } // Special handling for auth-required test (routed by message content) if (input.startsWith("auth-required:")) { - agentEmitter.requiresAuth(agentEmitter.newAgentMessage( - List.of(new TextPart("Please authenticate with OAuth provider")), - context.getMessage().metadata())); + agentEmitter.requiresAuth( + agentEmitter.newAgentMessage( + List.of( + new TextPart( + "Please authenticate with OAuth provider")), + context.getMessage().metadata())); try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new org.a2aproject.sdk.spec.InternalError("Auth simulation interrupted: " + e.getMessage()); + throw new org.a2aproject.sdk.spec.InternalError( + "Auth simulation interrupted: " + e.getMessage()); } agentEmitter.complete(); diff --git a/compat-0.3/tests/server-common/src/main/java/org/a2aproject/sdk/compat03/conversion/test/TestTaskAuthorizationProvider_v0_3.java b/compat-0.3/tests/server-common/src/main/java/org/a2aproject/sdk/compat03/conversion/test/TestTaskAuthorizationProvider_v0_3.java index 81af8da66..71c164d8a 100644 --- a/compat-0.3/tests/server-common/src/main/java/org/a2aproject/sdk/compat03/conversion/test/TestTaskAuthorizationProvider_v0_3.java +++ b/compat-0.3/tests/server-common/src/main/java/org/a2aproject/sdk/compat03/conversion/test/TestTaskAuthorizationProvider_v0_3.java @@ -1,18 +1,19 @@ package org.a2aproject.sdk.compat03.conversion.test; -import java.util.concurrent.ConcurrentHashMap; - -import jakarta.enterprise.context.ApplicationScoped; - import io.quarkus.arc.Unremovable; import io.quarkus.arc.properties.IfBuildProperty; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.concurrent.ConcurrentHashMap; import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.auth.TaskAuthorizationProvider; import org.a2aproject.sdk.server.auth.TaskOperation; @ApplicationScoped @Unremovable -@IfBuildProperty(name = "test.task-authorization.enabled", stringValue = "true", enableIfMissing = false) +@IfBuildProperty( + name = "test.task-authorization.enabled", + stringValue = "true", + enableIfMissing = false) public class TestTaskAuthorizationProvider_v0_3 implements TaskAuthorizationProvider { private final ConcurrentHashMap taskOwners = new ConcurrentHashMap<>(); @@ -20,14 +21,16 @@ public class TestTaskAuthorizationProvider_v0_3 implements TaskAuthorizationProv @Override public boolean checkRead(ServerCallContext context, String taskId, TaskOperation operation) { String owner = taskOwners.get(taskId); - // Intentionally fail-open for testing; production implementations should fail-closed (deny unknown tasks) + // Intentionally fail-open for testing; production implementations should fail-closed (deny + // unknown tasks) return owner == null || owner.equals(context.getUser().getUsername()); } @Override public boolean checkWrite(ServerCallContext context, String taskId, TaskOperation operation) { String owner = taskOwners.get(taskId); - // Intentionally fail-open for testing; production implementations should fail-closed (deny unknown tasks) + // Intentionally fail-open for testing; production implementations should fail-closed (deny + // unknown tasks) return owner == null || owner.equals(context.getUser().getUsername()); } diff --git a/compat-0.3/transport/grpc/pom.xml b/compat-0.3/transport/grpc/pom.xml index 84f83ae51..65c688e4e 100644 --- a/compat-0.3/transport/grpc/pom.xml +++ b/compat-0.3/transport/grpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -89,5 +87,5 @@ - + diff --git a/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/context/GrpcContextKeys_v0_3.java b/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/context/GrpcContextKeys_v0_3.java index 178725bc3..b91abd681 100644 --- a/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/context/GrpcContextKeys_v0_3.java +++ b/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/context/GrpcContextKeys_v0_3.java @@ -15,29 +15,25 @@ public final class GrpcContextKeys_v0_3 { * Context key for storing the X-A2A-Extensions header value. * Set by server interceptors and accessed by service handlers. */ - public static final Context.Key EXTENSIONS_HEADER_KEY = - Context.key("x-a2a-extensions"); + public static final Context.Key EXTENSIONS_HEADER_KEY = Context.key("x-a2a-extensions"); /** * Context key for storing the complete gRPC Metadata object. * Provides access to all request headers and metadata. */ - public static final Context.Key METADATA_KEY = - Context.key("grpc-metadata"); + public static final Context.Key METADATA_KEY = Context.key("grpc-metadata"); /** * Context key for storing the method name being called. * Equivalent to Python's context.method() functionality. */ - public static final Context.Key METHOD_NAME_KEY = - Context.key("grpc-method-name"); + public static final Context.Key METHOD_NAME_KEY = Context.key("grpc-method-name"); /** * Context key for storing the peer information. * Provides access to client connection details. */ - public static final Context.Key PEER_INFO_KEY = - Context.key("grpc-peer-info"); + public static final Context.Key PEER_INFO_KEY = Context.key("grpc-peer-info"); private GrpcContextKeys_v0_3() { // Utility class diff --git a/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/handler/CallContextFactory_v0_3.java b/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/handler/CallContextFactory_v0_3.java index 829c92d96..15712e103 100644 --- a/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/handler/CallContextFactory_v0_3.java +++ b/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/handler/CallContextFactory_v0_3.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.compat03.transport.grpc.handler; -import org.a2aproject.sdk.server.ServerCallContext; import io.grpc.stub.StreamObserver; +import org.a2aproject.sdk.server.ServerCallContext; /** * Factory interface for creating ServerCallContext from gRPC StreamObserver. diff --git a/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcHandler_v0_3.java b/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcHandler_v0_3.java index 72ce9faf0..81119bcce 100644 --- a/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcHandler_v0_3.java +++ b/compat-0.3/transport/grpc/src/main/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcHandler_v0_3.java @@ -3,15 +3,21 @@ import static org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3.FromProto; import static org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3.ToProto; +import com.google.protobuf.Empty; +import io.grpc.Context; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; import jakarta.enterprise.inject.Vetoed; - import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Flow; - -import com.google.protobuf.Empty; +import java.util.function.UnaryOperator; +import org.a2aproject.sdk.common.A2AErrorMessages; import org.a2aproject.sdk.compat03.conversion.A2AProtocol_v0_3; import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; import org.a2aproject.sdk.compat03.conversion.ErrorConverter_v0_3; @@ -31,32 +37,24 @@ import org.a2aproject.sdk.compat03.spec.MethodNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3; import org.a2aproject.sdk.server.ServerCallContext; -import org.a2aproject.sdk.common.A2AErrorMessages; import org.a2aproject.sdk.server.auth.UnauthenticatedUser; import org.a2aproject.sdk.server.auth.User; import org.a2aproject.sdk.spec.A2AError; -import io.grpc.Context; -import io.grpc.Status; -import io.grpc.stub.StreamObserver; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.UnaryOperator; /** * Abstract gRPC handler for v0.3 protocol with translation layer to v1.0. */ @Vetoed -public abstract class GrpcHandler_v0_3 extends org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc.A2AServiceImplBase { +public abstract class GrpcHandler_v0_3 + extends org.a2aproject.sdk.compat03.grpc.A2AServiceGrpc.A2AServiceImplBase { private Convert_v0_3_To10RequestHandler requestHandler; @@ -79,13 +77,15 @@ protected void setRequestHandler(Convert_v0_3_To10RequestHandler requestHandler) } @Override - public void sendMessage(org.a2aproject.sdk.compat03.grpc.SendMessageRequest request, - StreamObserver responseObserver) { + public void sendMessage( + org.a2aproject.sdk.compat03.grpc.SendMessageRequest request, + StreamObserver responseObserver) { try { ServerCallContext context = createCallContext(responseObserver); MessageSendParams_v0_3 params = FromProto.messageSendParams(request); EventKind_v0_3 taskOrMessage = requestHandler.onMessageSend(params, context); - org.a2aproject.sdk.compat03.grpc.SendMessageResponse response = ToProto.taskOrMessage(taskOrMessage); + org.a2aproject.sdk.compat03.grpc.SendMessageResponse response = + ToProto.taskOrMessage(taskOrMessage); responseObserver.onNext(response); responseObserver.onCompleted(); } catch (A2AError e) { @@ -100,8 +100,9 @@ public void sendMessage(org.a2aproject.sdk.compat03.grpc.SendMessageRequest requ } @Override - public void getTask(org.a2aproject.sdk.compat03.grpc.GetTaskRequest request, - StreamObserver responseObserver) { + public void getTask( + org.a2aproject.sdk.compat03.grpc.GetTaskRequest request, + StreamObserver responseObserver) { try { ServerCallContext context = createCallContext(responseObserver); TaskQueryParams_v0_3 params = FromProto.taskQueryParams(request); @@ -124,8 +125,9 @@ public void getTask(org.a2aproject.sdk.compat03.grpc.GetTaskRequest request, } @Override - public void cancelTask(org.a2aproject.sdk.compat03.grpc.CancelTaskRequest request, - StreamObserver responseObserver) { + public void cancelTask( + org.a2aproject.sdk.compat03.grpc.CancelTaskRequest request, + StreamObserver responseObserver) { try { ServerCallContext context = createCallContext(responseObserver); TaskIdParams_v0_3 params = FromProto.taskIdParams(request); @@ -148,8 +150,10 @@ public void cancelTask(org.a2aproject.sdk.compat03.grpc.CancelTaskRequest reques } @Override - public void createTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequest request, - StreamObserver responseObserver) { + public void createTaskPushNotificationConfig( + org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequest request, + StreamObserver + responseObserver) { if (!getAgentCard().capabilities().pushNotifications()) { handleError(responseObserver, new PushNotificationNotSupportedError_v0_3()); return; @@ -158,7 +162,8 @@ public void createTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.Cr try { ServerCallContext context = createCallContext(responseObserver); TaskPushNotificationConfig_v0_3 config = FromProto.taskPushNotificationConfig(request); - TaskPushNotificationConfig_v0_3 responseConfig = requestHandler.onSetTaskPushNotificationConfig(config, context); + TaskPushNotificationConfig_v0_3 responseConfig = + requestHandler.onSetTaskPushNotificationConfig(config, context); responseObserver.onNext(ToProto.taskPushNotificationConfig(responseConfig)); responseObserver.onCompleted(); } catch (A2AError e) { @@ -173,8 +178,10 @@ public void createTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.Cr } @Override - public void getTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.GetTaskPushNotificationConfigRequest request, - StreamObserver responseObserver) { + public void getTaskPushNotificationConfig( + org.a2aproject.sdk.compat03.grpc.GetTaskPushNotificationConfigRequest request, + StreamObserver + responseObserver) { if (!getAgentCard().capabilities().pushNotifications()) { handleError(responseObserver, new PushNotificationNotSupportedError_v0_3()); return; @@ -182,8 +189,10 @@ public void getTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.GetTa try { ServerCallContext context = createCallContext(responseObserver); - GetTaskPushNotificationConfigParams_v0_3 params = FromProto.getTaskPushNotificationConfigParams(request); - TaskPushNotificationConfig_v0_3 config = requestHandler.onGetTaskPushNotificationConfig(params, context); + GetTaskPushNotificationConfigParams_v0_3 params = + FromProto.getTaskPushNotificationConfigParams(request); + TaskPushNotificationConfig_v0_3 config = + requestHandler.onGetTaskPushNotificationConfig(params, context); responseObserver.onNext(ToProto.taskPushNotificationConfig(config)); responseObserver.onCompleted(); } catch (A2AError e) { @@ -198,8 +207,10 @@ public void getTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.GetTa } @Override - public void listTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigRequest request, - StreamObserver responseObserver) { + public void listTaskPushNotificationConfig( + org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigRequest request, + StreamObserver + responseObserver) { if (!getAgentCard().capabilities().pushNotifications()) { handleError(responseObserver, new PushNotificationNotSupportedError_v0_3()); return; @@ -207,10 +218,14 @@ public void listTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.List try { ServerCallContext context = createCallContext(responseObserver); - ListTaskPushNotificationConfigParams_v0_3 params = FromProto.listTaskPushNotificationConfigParams(request); - List configList = requestHandler.onListTaskPushNotificationConfig(params, context); - org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse.Builder responseBuilder = - org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse.newBuilder(); + ListTaskPushNotificationConfigParams_v0_3 params = + FromProto.listTaskPushNotificationConfigParams(request); + List configList = + requestHandler.onListTaskPushNotificationConfig(params, context); + org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse.Builder + responseBuilder = + org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse + .newBuilder(); for (TaskPushNotificationConfig_v0_3 config : configList) { responseBuilder.addConfigs(ToProto.taskPushNotificationConfig(config)); } @@ -228,10 +243,13 @@ public void listTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.List } @Override - public void sendStreamingMessage(org.a2aproject.sdk.compat03.grpc.SendMessageRequest request, - StreamObserver responseObserver) { + public void sendStreamingMessage( + org.a2aproject.sdk.compat03.grpc.SendMessageRequest request, + StreamObserver responseObserver) { if (!getAgentCard().capabilities().streaming()) { - handleError(responseObserver, new InvalidRequestError_v0_3("Streaming is not supported by the agent")); + handleError( + responseObserver, + new InvalidRequestError_v0_3("Streaming is not supported by the agent")); return; } @@ -239,7 +257,8 @@ public void sendStreamingMessage(org.a2aproject.sdk.compat03.grpc.SendMessageReq ServerCallContext context = createCallContext(responseObserver); installForkedContextWrapper(context); MessageSendParams_v0_3 params = FromProto.messageSendParams(request); - Flow.Publisher publisher = requestHandler.onMessageSendStream(params, context); + Flow.Publisher publisher = + requestHandler.onMessageSendStream(params, context); convertToStreamResponse(publisher, responseObserver); } catch (A2AError e) { handleError(responseObserver, ErrorConverter_v0_3.convertA2AError(e)); @@ -253,10 +272,13 @@ public void sendStreamingMessage(org.a2aproject.sdk.compat03.grpc.SendMessageReq } @Override - public void taskSubscription(org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequest request, - StreamObserver responseObserver) { + public void taskSubscription( + org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequest request, + StreamObserver responseObserver) { if (!getAgentCard().capabilities().streaming()) { - handleError(responseObserver, new InvalidRequestError_v0_3("Streaming is not supported by the agent")); + handleError( + responseObserver, + new InvalidRequestError_v0_3("Streaming is not supported by the agent")); return; } @@ -264,7 +286,8 @@ public void taskSubscription(org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRe ServerCallContext context = createCallContext(responseObserver); installForkedContextWrapper(context); TaskIdParams_v0_3 params = FromProto.taskIdParams(request); - Flow.Publisher publisher = requestHandler.onResubscribeToTask(params, context); + Flow.Publisher publisher = + requestHandler.onResubscribeToTask(params, context); convertToStreamResponse(publisher, responseObserver); } catch (A2AError e) { handleError(responseObserver, ErrorConverter_v0_3.convertA2AError(e)); @@ -279,66 +302,80 @@ public void taskSubscription(org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRe private void installForkedContextWrapper(ServerCallContext context) { Context forked = Context.current().fork(); - context.getState().put(ServerCallContext.EXECUTION_WRAPPER_KEY, - (UnaryOperator) (runnable -> () -> { - Context prev = forked.attach(); - try { - runnable.run(); - } finally { - forked.detach(prev); - } - })); + context.getState() + .put( + ServerCallContext.EXECUTION_WRAPPER_KEY, + (UnaryOperator) + (runnable -> + () -> { + Context prev = forked.attach(); + try { + runnable.run(); + } finally { + forked.detach(prev); + } + })); } - private void convertToStreamResponse(Flow.Publisher publisher, - StreamObserver responseObserver) { - CompletableFuture.runAsync(() -> { - publisher.subscribe(new Flow.Subscriber() { - private Flow.Subscription subscription; - - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - if (streamingSubscribedRunnable != null) { - streamingSubscribedRunnable.run(); - } - subscription.request(1); - } - - @Override - public void onNext(StreamingEventKind_v0_3 event) { - org.a2aproject.sdk.compat03.grpc.StreamResponse response = ToProto.streamResponse(event); - responseObserver.onNext(response); - if (response.hasStatusUpdate() && response.getStatusUpdate().getFinal()) { - responseObserver.onCompleted(); - } else { - subscription.request(1); - } - } - - @Override - public void onError(Throwable throwable) { - if (throwable instanceof A2AError a2aError) { - handleError(responseObserver, ErrorConverter_v0_3.convertA2AError(a2aError)); - } else if (throwable instanceof JSONRPCError_v0_3 jsonrpcError) { - handleError(responseObserver, jsonrpcError); - } else { - handleInternalError(responseObserver, throwable); - } - responseObserver.onCompleted(); - } - - @Override - public void onComplete() { - responseObserver.onCompleted(); - } - }); - }, getExecutor()); + private void convertToStreamResponse( + Flow.Publisher publisher, + StreamObserver responseObserver) { + CompletableFuture.runAsync( + () -> { + publisher.subscribe( + new Flow.Subscriber() { + private Flow.Subscription subscription; + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + if (streamingSubscribedRunnable != null) { + streamingSubscribedRunnable.run(); + } + subscription.request(1); + } + + @Override + public void onNext(StreamingEventKind_v0_3 event) { + org.a2aproject.sdk.compat03.grpc.StreamResponse response = + ToProto.streamResponse(event); + responseObserver.onNext(response); + if (response.hasStatusUpdate() + && response.getStatusUpdate().getFinal()) { + responseObserver.onCompleted(); + } else { + subscription.request(1); + } + } + + @Override + public void onError(Throwable throwable) { + if (throwable instanceof A2AError a2aError) { + handleError( + responseObserver, + ErrorConverter_v0_3.convertA2AError(a2aError)); + } else if (throwable + instanceof JSONRPCError_v0_3 jsonrpcError) { + handleError(responseObserver, jsonrpcError); + } else { + handleInternalError(responseObserver, throwable); + } + responseObserver.onCompleted(); + } + + @Override + public void onComplete() { + responseObserver.onCompleted(); + } + }); + }, + getExecutor()); } @Override - public void getAgentCard(org.a2aproject.sdk.compat03.grpc.GetAgentCardRequest request, - StreamObserver responseObserver) { + public void getAgentCard( + org.a2aproject.sdk.compat03.grpc.GetAgentCardRequest request, + StreamObserver responseObserver) { try { responseObserver.onNext(ToProto.agentCard(getAgentCard())); responseObserver.onCompleted(); @@ -348,8 +385,9 @@ public void getAgentCard(org.a2aproject.sdk.compat03.grpc.GetAgentCardRequest re } @Override - public void deleteTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequest request, - StreamObserver responseObserver) { + public void deleteTaskPushNotificationConfig( + org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequest request, + StreamObserver responseObserver) { if (!getAgentCard().capabilities().pushNotifications()) { handleError(responseObserver, new PushNotificationNotSupportedError_v0_3()); return; @@ -357,7 +395,8 @@ public void deleteTaskPushNotificationConfig(org.a2aproject.sdk.compat03.grpc.De try { ServerCallContext context = createCallContext(responseObserver); - DeleteTaskPushNotificationConfigParams_v0_3 params = FromProto.deleteTaskPushNotificationConfigParams(request); + DeleteTaskPushNotificationConfigParams_v0_3 params = + FromProto.deleteTaskPushNotificationConfigParams(request); requestHandler.onDeleteTaskPushNotificationConfig(params, context); // void response responseObserver.onNext(Empty.getDefaultInstance()); @@ -381,7 +420,8 @@ private ServerCallContext createCallContext(StreamObserver responseObserv Map state = new HashMap<>(); state.put("grpc_response_observer", responseObserver); Set requestedExtensions = new HashSet<>(); - return new ServerCallContext(user, state, requestedExtensions, A2AProtocol_v0_3.PROTOCOL_VERSION); + return new ServerCallContext( + user, state, requestedExtensions, A2AProtocol_v0_3.PROTOCOL_VERSION); } else { return factory.create(responseObserver); } @@ -430,25 +470,28 @@ private void handleError(StreamObserver responseObserver, JSONRPCError_v0 responseObserver.onError(status.withDescription(description).asRuntimeException()); } - private void handleSecurityException(StreamObserver responseObserver, SecurityException e) { + private void handleSecurityException( + StreamObserver responseObserver, SecurityException e) { Status status; String description; String exceptionClassName = e.getClass().getName(); - if (exceptionClassName.contains("Unauthorized") || - exceptionClassName.contains("Unauthenticated") || - exceptionClassName.contains("Authentication")) { + if (exceptionClassName.contains("Unauthorized") + || exceptionClassName.contains("Unauthenticated") + || exceptionClassName.contains("Authentication")) { status = Status.UNAUTHENTICATED; description = A2AErrorMessages.AUTHENTICATION_FAILED; - } else if (exceptionClassName.contains("Forbidden") || - exceptionClassName.contains("AccessDenied") || - exceptionClassName.contains("Authorization")) { + } else if (exceptionClassName.contains("Forbidden") + || exceptionClassName.contains("AccessDenied") + || exceptionClassName.contains("Authorization")) { status = Status.PERMISSION_DENIED; description = A2AErrorMessages.AUTHORIZATION_FAILED; } else { status = Status.PERMISSION_DENIED; - description = "Authorization failed: " + (e.getMessage() != null ? e.getMessage() : "Access denied"); + description = + "Authorization failed: " + + (e.getMessage() != null ? e.getMessage() : "Access denied"); } responseObserver.onError(status.withDescription(description).asRuntimeException()); diff --git a/compat-0.3/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcHandler_v0_3_Test.java b/compat-0.3/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcHandler_v0_3_Test.java index 588c82a23..7198035bd 100644 --- a/compat-0.3/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcHandler_v0_3_Test.java +++ b/compat-0.3/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcHandler_v0_3_Test.java @@ -6,31 +6,17 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.testing.StreamRecorder; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; - import org.a2aproject.sdk.compat03.conversion.A2AProtocol_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2ARequestHandlerTest_v0_3; import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskMapper_v0_3; -import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; -import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; -import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; -import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; -import org.a2aproject.sdk.server.ServerCallContext; -import org.a2aproject.sdk.server.auth.UnauthenticatedUser; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -// gRPC test imports -import io.grpc.Status; -import io.grpc.StatusRuntimeException; -import io.grpc.testing.StreamRecorder; - -// v0.3 gRPC proto imports import org.a2aproject.sdk.compat03.grpc.CancelTaskRequest; import org.a2aproject.sdk.compat03.grpc.GetTaskRequest; import org.a2aproject.sdk.compat03.grpc.Message; @@ -42,6 +28,15 @@ import org.a2aproject.sdk.compat03.grpc.Task; import org.a2aproject.sdk.compat03.grpc.TaskState; import org.a2aproject.sdk.compat03.grpc.TaskSubscriptionRequest; +import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; +import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; +import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; +import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; +import org.a2aproject.sdk.server.ServerCallContext; +import org.a2aproject.sdk.server.auth.UnauthenticatedUser; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; /** * Test suite for v0.3 GrpcHandler with v1.0 backend. @@ -57,16 +52,24 @@ public class GrpcHandler_v0_3_Test extends AbstractA2ARequestHandlerTest_v0_3 { // gRPC Message fixture (protobuf format) - private static final Message GRPC_MESSAGE = Message.newBuilder() - .setTaskId(MINIMAL_TASK.id()) - .setContextId(MINIMAL_TASK.contextId()) - .setMessageId(MESSAGE.messageId()) - .setRole(Role.ROLE_AGENT) - .addContent(Part.newBuilder().setText(((TextPart_v0_3) MESSAGE.parts().get(0)).text()).build()) - .build(); - - private final ServerCallContext callContext = new ServerCallContext( - UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), A2AProtocol_v0_3.PROTOCOL_VERSION); + private static final Message GRPC_MESSAGE = + Message.newBuilder() + .setTaskId(MINIMAL_TASK.id()) + .setContextId(MINIMAL_TASK.contextId()) + .setMessageId(MESSAGE.messageId()) + .setRole(Role.ROLE_AGENT) + .addContent( + Part.newBuilder() + .setText(((TextPart_v0_3) MESSAGE.parts().get(0)).text()) + .build()) + .build(); + + private final ServerCallContext callContext = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + new HashSet<>(), + A2AProtocol_v0_3.PROTOCOL_VERSION); // ======================================== // GetTask Tests @@ -79,9 +82,8 @@ public void testOnGetTaskSuccess() throws Exception { // Save v0.3 task by converting to v1.0 taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); - GetTaskRequest request = GetTaskRequest.newBuilder() - .setName("tasks/" + MINIMAL_TASK.id()) - .build(); + GetTaskRequest request = + GetTaskRequest.newBuilder().setName("tasks/" + MINIMAL_TASK.id()).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.getTask(request, streamRecorder); @@ -99,9 +101,8 @@ public void testOnGetTaskSuccess() throws Exception { public void testOnGetTaskNotFound() throws Exception { TestGrpcHandler handler = new TestGrpcHandler(CARD, convert03To10Handler, internalExecutor); - GetTaskRequest request = GetTaskRequest.newBuilder() - .setName("tasks/" + MINIMAL_TASK.id()) - .build(); + GetTaskRequest request = + GetTaskRequest.newBuilder().setName("tasks/" + MINIMAL_TASK.id()).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.getTask(request, streamRecorder); @@ -122,13 +123,13 @@ public void testOnCancelTaskSuccess() throws Exception { taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to cancel the task - agentExecutorCancel = (context, emitter) -> { - emitter.cancel(); - }; + agentExecutorCancel = + (context, emitter) -> { + emitter.cancel(); + }; - CancelTaskRequest request = CancelTaskRequest.newBuilder() - .setName("tasks/" + MINIMAL_TASK.id()) - .build(); + CancelTaskRequest request = + CancelTaskRequest.newBuilder().setName("tasks/" + MINIMAL_TASK.id()).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.cancelTask(request, streamRecorder); @@ -151,13 +152,13 @@ public void testOnCancelTaskNotSupported() throws Exception { taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to throw UnsupportedOperationError - agentExecutorCancel = (context, emitter) -> { - throw new org.a2aproject.sdk.spec.UnsupportedOperationError(); - }; + agentExecutorCancel = + (context, emitter) -> { + throw new org.a2aproject.sdk.spec.UnsupportedOperationError(); + }; - CancelTaskRequest request = CancelTaskRequest.newBuilder() - .setName("tasks/" + MINIMAL_TASK.id()) - .build(); + CancelTaskRequest request = + CancelTaskRequest.newBuilder().setName("tasks/" + MINIMAL_TASK.id()).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.cancelTask(request, streamRecorder); @@ -170,9 +171,8 @@ public void testOnCancelTaskNotSupported() throws Exception { public void testOnCancelTaskNotFound() throws Exception { TestGrpcHandler handler = new TestGrpcHandler(CARD, convert03To10Handler, internalExecutor); - CancelTaskRequest request = CancelTaskRequest.newBuilder() - .setName("tasks/" + MINIMAL_TASK.id()) - .build(); + CancelTaskRequest request = + CancelTaskRequest.newBuilder().setName("tasks/" + MINIMAL_TASK.id()).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.cancelTask(request, streamRecorder); @@ -193,9 +193,10 @@ public void testOnMessageNewMessageSuccess() throws Exception { taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to echo the message back - agentExecutorExecute = (context, emitter) -> { - emitter.emitEvent(context.getMessage()); - }; + agentExecutorExecute = + (context, emitter) -> { + emitter.emitEvent(context.getMessage()); + }; StreamRecorder streamRecorder = sendMessageRequest(handler); @@ -216,9 +217,10 @@ public void testOnMessageNewMessageWithExistingTaskSuccess() throws Exception { taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to emit message - agentExecutorExecute = (context, emitter) -> { - emitter.emitEvent(context.getMessage()); - }; + agentExecutorExecute = + (context, emitter) -> { + emitter.emitEvent(context.getMessage()); + }; StreamRecorder streamRecorder = sendMessageRequest(handler); @@ -239,9 +241,10 @@ public void testOnMessageError() throws Exception { taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to throw error - agentExecutorExecute = (context, emitter) -> { - emitter.emitEvent(new org.a2aproject.sdk.spec.UnsupportedOperationError()); - }; + agentExecutorExecute = + (context, emitter) -> { + emitter.emitEvent(new org.a2aproject.sdk.spec.UnsupportedOperationError()); + }; StreamRecorder streamRecorder = sendMessageRequest(handler); @@ -260,9 +263,10 @@ public void testOnMessageStreamNewMessageSuccess() throws Exception { taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to emit the message back (v1.0 context contains v1.0 Message) - agentExecutorExecute = (context, emitter) -> { - emitter.emitEvent(context.getMessage()); - }; + agentExecutorExecute = + (context, emitter) -> { + emitter.emitEvent(context.getMessage()); + }; StreamRecorder streamRecorder = sendStreamingMessageRequest(handler); @@ -285,7 +289,8 @@ public void testStreamingNotSupportedError() throws Exception { .capabilities(new AgentCapabilities_v0_3(false, true, false, null)) .build(); - TestGrpcHandler handler = new TestGrpcHandler(nonStreamingCard, convert03To10Handler, internalExecutor); + TestGrpcHandler handler = + new TestGrpcHandler(nonStreamingCard, convert03To10Handler, internalExecutor); StreamRecorder streamRecorder = sendStreamingMessageRequest(handler); @@ -301,11 +306,11 @@ public void testStreamingNotSupportedErrorOnResubscribeToTask() throws Exception .capabilities(new AgentCapabilities_v0_3(false, true, false, null)) .build(); - TestGrpcHandler handler = new TestGrpcHandler(nonStreamingCard, convert03To10Handler, internalExecutor); + TestGrpcHandler handler = + new TestGrpcHandler(nonStreamingCard, convert03To10Handler, internalExecutor); - TaskSubscriptionRequest request = TaskSubscriptionRequest.newBuilder() - .setName("tasks/" + MINIMAL_TASK.id()) - .build(); + TaskSubscriptionRequest request = + TaskSubscriptionRequest.newBuilder().setName("tasks/" + MINIMAL_TASK.id()).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.taskSubscription(request, streamRecorder); @@ -318,7 +323,8 @@ public void testStreamingNotSupportedErrorOnResubscribeToTask() throws Exception @Test public void testOnMessageStreamInternalError() throws Exception { // Mock the Convert03To10RequestHandler to throw InternalError - Convert_v0_3_To10RequestHandler mockedHandler = Mockito.mock(Convert_v0_3_To10RequestHandler.class); + Convert_v0_3_To10RequestHandler mockedHandler = + Mockito.mock(Convert_v0_3_To10RequestHandler.class); Mockito.doThrow(new org.a2aproject.sdk.spec.InternalError("Internal Error")) .when(mockedHandler) .onMessageSendStream( @@ -346,12 +352,14 @@ public void testSetPushNotificationConfigSuccess() throws Exception { createTaskPushNotificationConfigRequest(handler, NAME); assertNull(streamRecorder.getError()); - List result = streamRecorder.getValues(); + List result = + streamRecorder.getValues(); assertNotNull(result); assertEquals(1, result.size()); org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig response = result.get(0); assertEquals(NAME, response.getName()); - org.a2aproject.sdk.compat03.grpc.PushNotificationConfig responseConfig = response.getPushNotificationConfig(); + org.a2aproject.sdk.compat03.grpc.PushNotificationConfig responseConfig = + response.getPushNotificationConfig(); assertEquals("config456", responseConfig.getId()); assertEquals("http://example.com", responseConfig.getUrl()); } @@ -360,9 +368,11 @@ public void testSetPushNotificationConfigSuccess() throws Exception { public void testGetPushNotificationConfigSuccess() throws Exception { TestGrpcHandler handler = new TestGrpcHandler(CARD, convert03To10Handler, internalExecutor); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; String NAME = "tasks/" + MINIMAL_TASK.id() + "/pushNotificationConfigs/config456"; @@ -374,12 +384,14 @@ public void testGetPushNotificationConfigSuccess() throws Exception { // Then get the task push notification config streamRecorder = getTaskPushNotificationConfigRequest(handler, NAME); assertNull(streamRecorder.getError()); - List result = streamRecorder.getValues(); + List result = + streamRecorder.getValues(); assertNotNull(result); assertEquals(1, result.size()); org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig response = result.get(0); assertEquals(NAME, response.getName()); - org.a2aproject.sdk.compat03.grpc.PushNotificationConfig responseConfig = response.getPushNotificationConfig(); + org.a2aproject.sdk.compat03.grpc.PushNotificationConfig responseConfig = + response.getPushNotificationConfig(); assertEquals("config456", responseConfig.getId()); assertEquals("http://example.com", responseConfig.getUrl()); } @@ -389,7 +401,8 @@ public void testPushNotificationsNotSupportedError() throws Exception { AgentCard_v0_3 card = createAgentCard(true, false, false); TestGrpcHandler handler = new TestGrpcHandler(card, convert03To10Handler, internalExecutor); - String NAME = "tasks/" + MINIMAL_TASK.id() + "/pushNotificationConfigs/" + MINIMAL_TASK.id(); + String NAME = + "tasks/" + MINIMAL_TASK.id() + "/pushNotificationConfigs/" + MINIMAL_TASK.id(); StreamRecorder streamRecorder = createTaskPushNotificationConfigRequest(handler, NAME); @@ -406,9 +419,11 @@ public void testDeletePushNotificationConfig() throws Exception { // Save task to v1.0 backend taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); - String NAME = "tasks/" + MINIMAL_TASK.id() + "/pushNotificationConfigs/" + MINIMAL_TASK.id(); + String NAME = + "tasks/" + MINIMAL_TASK.id() + "/pushNotificationConfigs/" + MINIMAL_TASK.id(); org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequest request = - org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder() + org.a2aproject.sdk.compat03.grpc.DeleteTaskPushNotificationConfigRequest + .newBuilder() .setName(NAME) .build(); @@ -434,12 +449,14 @@ public void testListPushNotificationConfig() throws Exception { .setParent(PARENT) .build(); - StreamRecorder streamRecorder = StreamRecorder.create(); + StreamRecorder + streamRecorder = StreamRecorder.create(); handler.listTaskPushNotificationConfig(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); assertNull(streamRecorder.getError()); - List result = streamRecorder.getValues(); + List result = + streamRecorder.getValues(); assertEquals(1, result.size()); } @@ -447,36 +464,39 @@ public void testListPushNotificationConfig() throws Exception { // Helper Methods // ======================================== - private StreamRecorder sendMessageRequest(TestGrpcHandler handler) throws Exception { - SendMessageRequest request = SendMessageRequest.newBuilder() - .setRequest(GRPC_MESSAGE) - .build(); + private StreamRecorder sendMessageRequest(TestGrpcHandler handler) + throws Exception { + SendMessageRequest request = + SendMessageRequest.newBuilder().setRequest(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); return streamRecorder; } - private StreamRecorder sendStreamingMessageRequest(TestGrpcHandler handler) throws Exception { - SendMessageRequest request = SendMessageRequest.newBuilder() - .setRequest(GRPC_MESSAGE) - .build(); + private StreamRecorder sendStreamingMessageRequest(TestGrpcHandler handler) + throws Exception { + SendMessageRequest request = + SendMessageRequest.newBuilder().setRequest(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendStreamingMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); return streamRecorder; } - private void assertGrpcError(StreamRecorder streamRecorder, Status.Code expectedStatusCode) { + private void assertGrpcError( + StreamRecorder streamRecorder, Status.Code expectedStatusCode) { Assertions.assertNotNull(streamRecorder.getError()); Assertions.assertInstanceOf(StatusRuntimeException.class, streamRecorder.getError()); - Assertions.assertEquals(expectedStatusCode, ((StatusRuntimeException) streamRecorder.getError()).getStatus().getCode()); + Assertions.assertEquals( + expectedStatusCode, + ((StatusRuntimeException) streamRecorder.getError()).getStatus().getCode()); Assertions.assertTrue(streamRecorder.getValues().isEmpty()); } - - private StreamRecorder createTaskPushNotificationConfigRequest( - TestGrpcHandler handler, String name) throws Exception { + private StreamRecorder + createTaskPushNotificationConfigRequest(TestGrpcHandler handler, String name) + throws Exception { // Save task to v1.0 backend taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); @@ -492,28 +512,33 @@ private StreamRecorder streamRecorder = StreamRecorder.create(); + StreamRecorder streamRecorder = + StreamRecorder.create(); handler.createTaskPushNotificationConfig(setRequest, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); return streamRecorder; } - private StreamRecorder getTaskPushNotificationConfigRequest( - TestGrpcHandler handler, String name) throws Exception { + private StreamRecorder + getTaskPushNotificationConfigRequest(TestGrpcHandler handler, String name) + throws Exception { org.a2aproject.sdk.compat03.grpc.GetTaskPushNotificationConfigRequest request = org.a2aproject.sdk.compat03.grpc.GetTaskPushNotificationConfigRequest.newBuilder() .setName(name) .build(); - StreamRecorder streamRecorder = StreamRecorder.create(); + StreamRecorder streamRecorder = + StreamRecorder.create(); handler.getTaskPushNotificationConfig(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); return streamRecorder; } + // ======================================== // Test Handler Implementation // ======================================== @@ -523,9 +548,10 @@ private static class TestGrpcHandler extends GrpcHandler_v0_3 { private final Convert_v0_3_To10RequestHandler handler; private final java.util.concurrent.Executor executor; - TestGrpcHandler(AgentCard_v0_3 card, - Convert_v0_3_To10RequestHandler handler, - java.util.concurrent.Executor executor) { + TestGrpcHandler( + AgentCard_v0_3 card, + Convert_v0_3_To10RequestHandler handler, + java.util.concurrent.Executor executor) { this.card = card; this.handler = handler; this.executor = executor; @@ -552,5 +578,4 @@ protected java.util.concurrent.Executor getExecutor() { return executor; } } - } diff --git a/compat-0.3/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcTestTransportMetadata_v0_3.java b/compat-0.3/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcTestTransportMetadata_v0_3.java index 55ebd84cd..e40ca6804 100644 --- a/compat-0.3/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcTestTransportMetadata_v0_3.java +++ b/compat-0.3/transport/grpc/src/test/java/org/a2aproject/sdk/compat03/transport/grpc/handler/GrpcTestTransportMetadata_v0_3.java @@ -5,5 +5,4 @@ /** * Placeholder stub - awaiting server-common port. */ -public class GrpcTestTransportMetadata_v0_3 { -} +public class GrpcTestTransportMetadata_v0_3 {} diff --git a/compat-0.3/transport/jsonrpc/pom.xml b/compat-0.3/transport/jsonrpc/pom.xml index 92d13a584..346aa78d8 100644 --- a/compat-0.3/transport/jsonrpc/pom.xml +++ b/compat-0.3/transport/jsonrpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -65,5 +63,5 @@ - + diff --git a/compat-0.3/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCHandler_v0_3.java b/compat-0.3/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCHandler_v0_3.java index a5b1bbe4f..09a5661bf 100644 --- a/compat-0.3/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCHandler_v0_3.java +++ b/compat-0.3/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCHandler_v0_3.java @@ -5,15 +5,13 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; - import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Flow; - -import org.a2aproject.sdk.server.ExtendedAgentCard; -import org.a2aproject.sdk.server.PublicAgentCard; -import org.a2aproject.sdk.server.ServerCallContext; +import mutiny.zero.ZeroPublisher; +import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; +import org.a2aproject.sdk.compat03.conversion.ErrorConverter_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.AuthenticatedExtendedCardNotConfiguredError_v0_3; import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3; @@ -41,14 +39,14 @@ import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3; import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigResponse_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; +import org.a2aproject.sdk.server.ExtendedAgentCard; +import org.a2aproject.sdk.server.PublicAgentCard; +import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.util.async.Internal; -import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; -import org.a2aproject.sdk.compat03.conversion.ErrorConverter_v0_3; import org.a2aproject.sdk.spec.A2AError; -import mutiny.zero.ZeroPublisher; @ApplicationScoped public class JSONRPCHandler_v0_3 { @@ -63,8 +61,11 @@ protected JSONRPCHandler_v0_3() { } @Inject - public JSONRPCHandler_v0_3(@PublicAgentCard AgentCard_v0_3 agentCard, @ExtendedAgentCard Instance extendedAgentCard, - @Internal Executor executor, Convert_v0_3_To10RequestHandler requestHandler) { + public JSONRPCHandler_v0_3( + @PublicAgentCard AgentCard_v0_3 agentCard, + @ExtendedAgentCard Instance extendedAgentCard, + @Internal Executor executor, + Convert_v0_3_To10RequestHandler requestHandler) { this.agentCard = agentCard; this.extendedAgentCard = extendedAgentCard; this.requestHandler = requestHandler; @@ -74,21 +75,28 @@ public JSONRPCHandler_v0_3(@PublicAgentCard AgentCard_v0_3 agentCard, @ExtendedA // AgentCardValidator.validateTransportConfiguration(agentCard); } - public JSONRPCHandler_v0_3(@PublicAgentCard AgentCard_v0_3 agentCard, Executor executor, Convert_v0_3_To10RequestHandler requestHandler) { + public JSONRPCHandler_v0_3( + @PublicAgentCard AgentCard_v0_3 agentCard, + Executor executor, + Convert_v0_3_To10RequestHandler requestHandler) { this(agentCard, null, executor, requestHandler); } - public SendMessageResponse_v0_3 onMessageSend(SendMessageRequest_v0_3 request, ServerCallContext context) { + public SendMessageResponse_v0_3 onMessageSend( + SendMessageRequest_v0_3 request, ServerCallContext context) { try { request.check(); EventKind_v0_3 result = requestHandler.onMessageSend(request.getParams(), context); return new SendMessageResponse_v0_3(request.getId(), result); } catch (A2AError e) { - return new SendMessageResponse_v0_3(request.getId(), ErrorConverter_v0_3.convertA2AError(e)); + return new SendMessageResponse_v0_3( + request.getId(), ErrorConverter_v0_3.convertA2AError(e)); } catch (IllegalArgumentException t) { - return new SendMessageResponse_v0_3(request.getId(), new InvalidParamsError_v0_3(t.getMessage())); + return new SendMessageResponse_v0_3( + request.getId(), new InvalidParamsError_v0_3(t.getMessage())); } catch (Throwable t) { - return new SendMessageResponse_v0_3(request.getId(), new InternalError_v0_3(t.getMessage())); + return new SendMessageResponse_v0_3( + request.getId(), new InternalError_v0_3(t.getMessage())); } } @@ -98,27 +106,36 @@ public Flow.Publisher onMessageSendStream( return ZeroPublisher.fromItems( new SendStreamingMessageResponse_v0_3( request.getId(), - new InvalidRequestError_v0_3("Streaming is not supported by the agent"))); + new InvalidRequestError_v0_3( + "Streaming is not supported by the agent"))); } try { request.check(); - Flow.Publisher publisher = requestHandler.onMessageSendStream(request.getParams(), context); + Flow.Publisher publisher = + requestHandler.onMessageSendStream(request.getParams(), context); return convertToSendStreamingMessageResponse(request.getId(), publisher); } catch (A2AError e) { - return ZeroPublisher.fromItems(new SendStreamingMessageResponse_v0_3(request.getId(), ErrorConverter_v0_3.convertA2AError(e))); + return ZeroPublisher.fromItems( + new SendStreamingMessageResponse_v0_3( + request.getId(), ErrorConverter_v0_3.convertA2AError(e))); } catch (Throwable t) { - return ZeroPublisher.fromItems(new SendStreamingMessageResponse_v0_3(request.getId(), new InternalError_v0_3(t.getMessage()))); + return ZeroPublisher.fromItems( + new SendStreamingMessageResponse_v0_3( + request.getId(), new InternalError_v0_3(t.getMessage()))); } } - public CancelTaskResponse_v0_3 onCancelTask(CancelTaskRequest_v0_3 request, ServerCallContext context) { + public CancelTaskResponse_v0_3 onCancelTask( + CancelTaskRequest_v0_3 request, ServerCallContext context) { try { Task_v0_3 result = requestHandler.onCancelTask(request.getParams(), context); return new CancelTaskResponse_v0_3(request.getId(), result); } catch (A2AError e) { - return new CancelTaskResponse_v0_3(request.getId(), ErrorConverter_v0_3.convertA2AError(e)); + return new CancelTaskResponse_v0_3( + request.getId(), ErrorConverter_v0_3.convertA2AError(e)); } catch (Throwable t) { - return new CancelTaskResponse_v0_3(request.getId(), new InternalError_v0_3(t.getMessage())); + return new CancelTaskResponse_v0_3( + request.getId(), new InternalError_v0_3(t.getMessage())); } } @@ -128,48 +145,60 @@ public Flow.Publisher onResubscribeToTask( return ZeroPublisher.fromItems( new SendStreamingMessageResponse_v0_3( request.getId(), - new InvalidRequestError_v0_3("Streaming is not supported by the agent"))); + new InvalidRequestError_v0_3( + "Streaming is not supported by the agent"))); } try { - Flow.Publisher publisher = requestHandler.onResubscribeToTask(request.getParams(), context); + Flow.Publisher publisher = + requestHandler.onResubscribeToTask(request.getParams(), context); return convertToSendStreamingMessageResponse(request.getId(), publisher); } catch (A2AError e) { - return ZeroPublisher.fromItems(new SendStreamingMessageResponse_v0_3(request.getId(), ErrorConverter_v0_3.convertA2AError(e))); + return ZeroPublisher.fromItems( + new SendStreamingMessageResponse_v0_3( + request.getId(), ErrorConverter_v0_3.convertA2AError(e))); } catch (Throwable t) { - return ZeroPublisher.fromItems(new SendStreamingMessageResponse_v0_3(request.getId(), new InternalError_v0_3(t.getMessage()))); + return ZeroPublisher.fromItems( + new SendStreamingMessageResponse_v0_3( + request.getId(), new InternalError_v0_3(t.getMessage()))); } } public GetTaskPushNotificationConfigResponse_v0_3 getPushNotificationConfig( GetTaskPushNotificationConfigRequest_v0_3 request, ServerCallContext context) { if (!agentCard.capabilities().pushNotifications()) { - return new GetTaskPushNotificationConfigResponse_v0_3(request.getId(), - new PushNotificationNotSupportedError_v0_3()); + return new GetTaskPushNotificationConfigResponse_v0_3( + request.getId(), new PushNotificationNotSupportedError_v0_3()); } try { - TaskPushNotificationConfig_v0_3 result = requestHandler.onGetTaskPushNotificationConfig(request.getParams(), context); + TaskPushNotificationConfig_v0_3 result = + requestHandler.onGetTaskPushNotificationConfig(request.getParams(), context); return new GetTaskPushNotificationConfigResponse_v0_3(request.getId(), result); } catch (A2AError e) { - return new GetTaskPushNotificationConfigResponse_v0_3(request.getId(), ErrorConverter_v0_3.convertA2AError(e)); + return new GetTaskPushNotificationConfigResponse_v0_3( + request.getId(), ErrorConverter_v0_3.convertA2AError(e)); } catch (Throwable t) { - return new GetTaskPushNotificationConfigResponse_v0_3(request.getId(), new InternalError_v0_3(t.getMessage())); + return new GetTaskPushNotificationConfigResponse_v0_3( + request.getId(), new InternalError_v0_3(t.getMessage())); } } public SetTaskPushNotificationConfigResponse_v0_3 setPushNotificationConfig( SetTaskPushNotificationConfigRequest_v0_3 request, ServerCallContext context) { if (!agentCard.capabilities().pushNotifications()) { - return new SetTaskPushNotificationConfigResponse_v0_3(request.getId(), - new PushNotificationNotSupportedError_v0_3()); + return new SetTaskPushNotificationConfigResponse_v0_3( + request.getId(), new PushNotificationNotSupportedError_v0_3()); } try { - TaskPushNotificationConfig_v0_3 result = requestHandler.onSetTaskPushNotificationConfig(request.getParams(), context); + TaskPushNotificationConfig_v0_3 result = + requestHandler.onSetTaskPushNotificationConfig(request.getParams(), context); return new SetTaskPushNotificationConfigResponse_v0_3(request.getId(), result); } catch (A2AError e) { - return new SetTaskPushNotificationConfigResponse_v0_3(request.getId(), ErrorConverter_v0_3.convertA2AError(e)); + return new SetTaskPushNotificationConfigResponse_v0_3( + request.getId(), ErrorConverter_v0_3.convertA2AError(e)); } catch (Throwable t) { - return new SetTaskPushNotificationConfigResponse_v0_3(request.getId(), new InternalError_v0_3(t.getMessage())); + return new SetTaskPushNotificationConfigResponse_v0_3( + request.getId(), new InternalError_v0_3(t.getMessage())); } } @@ -178,42 +207,49 @@ public GetTaskResponse_v0_3 onGetTask(GetTaskRequest_v0_3 request, ServerCallCon Task_v0_3 result = requestHandler.onGetTask(request.getParams(), context); return new GetTaskResponse_v0_3(request.getId(), result); } catch (A2AError e) { - return new GetTaskResponse_v0_3(request.getId(), ErrorConverter_v0_3.convertA2AError(e)); + return new GetTaskResponse_v0_3( + request.getId(), ErrorConverter_v0_3.convertA2AError(e)); } catch (Throwable t) { - return new GetTaskResponse_v0_3(request.getId(), new InternalError_v0_3(t.getMessage())); + return new GetTaskResponse_v0_3( + request.getId(), new InternalError_v0_3(t.getMessage())); } } public ListTaskPushNotificationConfigResponse_v0_3 listPushNotificationConfig( ListTaskPushNotificationConfigRequest_v0_3 request, ServerCallContext context) { if (!agentCard.capabilities().pushNotifications()) { - return new ListTaskPushNotificationConfigResponse_v0_3(request.getId(), - new PushNotificationNotSupportedError_v0_3()); + return new ListTaskPushNotificationConfigResponse_v0_3( + request.getId(), new PushNotificationNotSupportedError_v0_3()); } try { List pushNotificationConfigList = requestHandler.onListTaskPushNotificationConfig(request.getParams(), context); - return new ListTaskPushNotificationConfigResponse_v0_3(request.getId(), pushNotificationConfigList); + return new ListTaskPushNotificationConfigResponse_v0_3( + request.getId(), pushNotificationConfigList); } catch (A2AError e) { - return new ListTaskPushNotificationConfigResponse_v0_3(request.getId(), ErrorConverter_v0_3.convertA2AError(e)); + return new ListTaskPushNotificationConfigResponse_v0_3( + request.getId(), ErrorConverter_v0_3.convertA2AError(e)); } catch (Throwable t) { - return new ListTaskPushNotificationConfigResponse_v0_3(request.getId(), new InternalError_v0_3(t.getMessage())); + return new ListTaskPushNotificationConfigResponse_v0_3( + request.getId(), new InternalError_v0_3(t.getMessage())); } } public DeleteTaskPushNotificationConfigResponse_v0_3 deletePushNotificationConfig( DeleteTaskPushNotificationConfigRequest_v0_3 request, ServerCallContext context) { if (!agentCard.capabilities().pushNotifications()) { - return new DeleteTaskPushNotificationConfigResponse_v0_3(request.getId(), - new PushNotificationNotSupportedError_v0_3()); + return new DeleteTaskPushNotificationConfigResponse_v0_3( + request.getId(), new PushNotificationNotSupportedError_v0_3()); } try { requestHandler.onDeleteTaskPushNotificationConfig(request.getParams(), context); return new DeleteTaskPushNotificationConfigResponse_v0_3(request.getId()); } catch (A2AError e) { - return new DeleteTaskPushNotificationConfigResponse_v0_3(request.getId(), ErrorConverter_v0_3.convertA2AError(e)); + return new DeleteTaskPushNotificationConfigResponse_v0_3( + request.getId(), ErrorConverter_v0_3.convertA2AError(e)); } catch (Throwable t) { - return new DeleteTaskPushNotificationConfigResponse_v0_3(request.getId(), new InternalError_v0_3(t.getMessage())); + return new DeleteTaskPushNotificationConfigResponse_v0_3( + request.getId(), new InternalError_v0_3(t.getMessage())); } } @@ -221,15 +257,19 @@ public DeleteTaskPushNotificationConfigResponse_v0_3 deletePushNotificationConfi public GetAuthenticatedExtendedCardResponse_v0_3 onGetAuthenticatedExtendedCardRequest( GetAuthenticatedExtendedCardRequest_v0_3 request, ServerCallContext context) { if (!agentCard.supportsAuthenticatedExtendedCard() || !extendedAgentCard.isResolvable()) { - return new GetAuthenticatedExtendedCardResponse_v0_3(request.getId(), - new AuthenticatedExtendedCardNotConfiguredError_v0_3(null, "Authenticated Extended Card not configured", null)); + return new GetAuthenticatedExtendedCardResponse_v0_3( + request.getId(), + new AuthenticatedExtendedCardNotConfiguredError_v0_3( + null, "Authenticated Extended Card not configured", null)); } try { - return new GetAuthenticatedExtendedCardResponse_v0_3(request.getId(), extendedAgentCard.get()); + return new GetAuthenticatedExtendedCardResponse_v0_3( + request.getId(), extendedAgentCard.get()); } catch (JSONRPCError_v0_3 e) { return new GetAuthenticatedExtendedCardResponse_v0_3(request.getId(), e); } catch (Throwable t) { - return new GetAuthenticatedExtendedCardResponse_v0_3(request.getId(), new InternalError_v0_3(t.getMessage())); + return new GetAuthenticatedExtendedCardResponse_v0_3( + request.getId(), new InternalError_v0_3(t.getMessage())); } } @@ -238,46 +278,59 @@ public AgentCard_v0_3 getAgentCard() { } private Flow.Publisher convertToSendStreamingMessageResponse( - Object requestId, - Flow.Publisher publisher) { - // We can't use the normal convertingProcessor since that propagates any errors as an error handled + Object requestId, Flow.Publisher publisher) { + // We can't use the normal convertingProcessor since that propagates any errors as an error + // handled // via Subscriber.onError() rather than as part of the SendStreamingResponse payload - return ZeroPublisher.create(createTubeConfig(), tube -> { - CompletableFuture.runAsync(() -> { - publisher.subscribe(new Flow.Subscriber() { - Flow.Subscription subscription; + return ZeroPublisher.create( + createTubeConfig(), + tube -> { + CompletableFuture.runAsync( + () -> { + publisher.subscribe( + new Flow.Subscriber() { + Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe( + Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(StreamingEventKind_v0_3 item) { - tube.send(new SendStreamingMessageResponse_v0_3(requestId, item)); - subscription.request(1); - } + @Override + public void onNext(StreamingEventKind_v0_3 item) { + tube.send( + new SendStreamingMessageResponse_v0_3( + requestId, item)); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - if (throwable instanceof JSONRPCError_v0_3 jsonrpcError) { - tube.send(new SendStreamingMessageResponse_v0_3(requestId, jsonrpcError)); - } else { - tube.send( - new SendStreamingMessageResponse_v0_3( - requestId, new - InternalError_v0_3(throwable.getMessage()))); - } - onComplete(); - } + @Override + public void onError(Throwable throwable) { + if (throwable + instanceof JSONRPCError_v0_3 jsonrpcError) { + tube.send( + new SendStreamingMessageResponse_v0_3( + requestId, jsonrpcError)); + } else { + tube.send( + new SendStreamingMessageResponse_v0_3( + requestId, + new InternalError_v0_3( + throwable + .getMessage()))); + } + onComplete(); + } - @Override - public void onComplete() { - tube.complete(); - } + @Override + public void onComplete() { + tube.complete(); + } + }); + }, + executor); }); - }, executor); - }); } } diff --git a/compat-0.3/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCRequestDecoder_v0_3.java b/compat-0.3/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCRequestDecoder_v0_3.java index 7c8c8b646..4059538a9 100644 --- a/compat-0.3/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCRequestDecoder_v0_3.java +++ b/compat-0.3/transport/jsonrpc/src/main/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCRequestDecoder_v0_3.java @@ -29,15 +29,14 @@ /** Decodes compat JSON-RPC envelopes and typed requests without a server runtime. */ public final class JSONRPCRequestDecoder_v0_3 { - private JSONRPCRequestDecoder_v0_3() { - } + private JSONRPCRequestDecoder_v0_3() {} - public sealed interface DecodeResult permits DecodedRequest, DecodeFailure { - } + public sealed interface DecodeResult permits DecodedRequest, DecodeFailure {} /** The recovered ID is kept separately from the deserialized wrapper's ID. */ - public record DecodedRequest(@Nullable Object requestId, String methodName, - JSONRPCRequest_v0_3 request) implements DecodeResult { + public record DecodedRequest( + @Nullable Object requestId, String methodName, JSONRPCRequest_v0_3 request) + implements DecodeResult { public DecodedRequest { Assert.checkNotNullParam("methodName", methodName); Assert.checkNotNullParam("request", request); @@ -45,8 +44,9 @@ public record DecodedRequest(@Nullable Object requestId, String methodName, } /** The method is available only after envelope validation has recovered it. */ - public record DecodeFailure(@Nullable Object requestId, @Nullable String methodName, - JSONRPCError_v0_3 error) implements DecodeResult { + public record DecodeFailure( + @Nullable Object requestId, @Nullable String methodName, JSONRPCError_v0_3 error) + implements DecodeResult { public DecodeFailure { Assert.checkNotNullParam("error", error); } @@ -69,39 +69,50 @@ public static DecodeResult decode(@Nullable String body) { // Extract id field early so error responses can include it com.google.gson.JsonElement idElement = node.get("id"); if (idElement != null && !idElement.isJsonNull() && !idElement.isJsonPrimitive()) { - throw new InvalidRequestError_v0_3("Invalid JSON-RPC request: 'id' must be a string, number, or null"); + throw new InvalidRequestError_v0_3( + "Invalid JSON-RPC request: 'id' must be a string, number, or null"); } if (idElement != null && !idElement.isJsonNull() && idElement.isJsonPrimitive()) { com.google.gson.JsonPrimitive idPrimitive = idElement.getAsJsonPrimitive(); - requestId = idPrimitive.isNumber() ? idPrimitive.getAsLong() : idPrimitive.getAsString(); + requestId = + idPrimitive.isNumber() + ? idPrimitive.getAsLong() + : idPrimitive.getAsString(); } // Validate jsonrpc field com.google.gson.JsonElement jsonrpcElement = node.get("jsonrpc"); - if (jsonrpcElement == null || !jsonrpcElement.isJsonPrimitive() + if (jsonrpcElement == null + || !jsonrpcElement.isJsonPrimitive() || !JSONRPCMessage_v0_3.JSONRPC_VERSION.equals(jsonrpcElement.getAsString())) { - throw new InvalidRequestError_v0_3("Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); + throw new InvalidRequestError_v0_3( + "Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); } // Validate method field com.google.gson.JsonElement methodElement = node.get("method"); - if (methodElement == null || !methodElement.isJsonPrimitive() + if (methodElement == null + || !methodElement.isJsonPrimitive() || !methodElement.getAsJsonPrimitive().isString()) { - throw new InvalidRequestError_v0_3("Invalid JSON-RPC request: missing or invalid 'method' field"); + throw new InvalidRequestError_v0_3( + "Invalid JSON-RPC request: missing or invalid 'method' field"); } methodName = methodElement.getAsString(); - boolean streaming = SendStreamingMessageRequest_v0_3.METHOD.equals(methodName) - || TaskResubscriptionRequest_v0_3.METHOD.equals(methodName); - JSONRPCRequest_v0_3 request = streaming - ? deserializeStreamingRequest(requestBody, methodName) - : deserializeNonStreamingRequest(requestBody, methodName); + boolean streaming = + SendStreamingMessageRequest_v0_3.METHOD.equals(methodName) + || TaskResubscriptionRequest_v0_3.METHOD.equals(methodName); + JSONRPCRequest_v0_3 request = + streaming + ? deserializeStreamingRequest(requestBody, methodName) + : deserializeNonStreamingRequest(requestBody, methodName); return new DecodedRequest(requestId, methodName, request); } catch (JSONRPCError_v0_3 e) { return new DecodeFailure(requestId, methodName, e); } catch (JsonSyntaxException e) { - return new DecodeFailure(requestId, methodName, new JSONParseError_v0_3(e.getMessage())); + return new DecodeFailure( + requestId, methodName, new JSONParseError_v0_3(e.getMessage())); } catch (Throwable t) { return new DecodeFailure(requestId, methodName, new InternalError_v0_3(t.getMessage())); } @@ -114,17 +125,31 @@ private static com.google.gson.JsonObject parseBody(@Nullable String body) { return JsonParser.parseString(body).getAsJsonObject(); } - private static NonStreamingJSONRPCRequest_v0_3 deserializeNonStreamingRequest(String body, String methodName) { + private static NonStreamingJSONRPCRequest_v0_3 deserializeNonStreamingRequest( + String body, String methodName) { try { return switch (methodName) { - case GetTaskRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, GetTaskRequest_v0_3.class); - case CancelTaskRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, CancelTaskRequest_v0_3.class); - case SendMessageRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, SendMessageRequest_v0_3.class); - case SetTaskPushNotificationConfigRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, SetTaskPushNotificationConfigRequest_v0_3.class); - case GetTaskPushNotificationConfigRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, GetTaskPushNotificationConfigRequest_v0_3.class); - case ListTaskPushNotificationConfigRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, ListTaskPushNotificationConfigRequest_v0_3.class); - case DeleteTaskPushNotificationConfigRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, DeleteTaskPushNotificationConfigRequest_v0_3.class); - case GetAuthenticatedExtendedCardRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, GetAuthenticatedExtendedCardRequest_v0_3.class); + case GetTaskRequest_v0_3.METHOD -> + JsonUtil_v0_3.fromJson(body, GetTaskRequest_v0_3.class); + case CancelTaskRequest_v0_3.METHOD -> + JsonUtil_v0_3.fromJson(body, CancelTaskRequest_v0_3.class); + case SendMessageRequest_v0_3.METHOD -> + JsonUtil_v0_3.fromJson(body, SendMessageRequest_v0_3.class); + case SetTaskPushNotificationConfigRequest_v0_3.METHOD -> + JsonUtil_v0_3.fromJson( + body, SetTaskPushNotificationConfigRequest_v0_3.class); + case GetTaskPushNotificationConfigRequest_v0_3.METHOD -> + JsonUtil_v0_3.fromJson( + body, GetTaskPushNotificationConfigRequest_v0_3.class); + case ListTaskPushNotificationConfigRequest_v0_3.METHOD -> + JsonUtil_v0_3.fromJson( + body, ListTaskPushNotificationConfigRequest_v0_3.class); + case DeleteTaskPushNotificationConfigRequest_v0_3.METHOD -> + JsonUtil_v0_3.fromJson( + body, DeleteTaskPushNotificationConfigRequest_v0_3.class); + case GetAuthenticatedExtendedCardRequest_v0_3.METHOD -> + JsonUtil_v0_3.fromJson( + body, GetAuthenticatedExtendedCardRequest_v0_3.class); default -> throw new MethodNotFoundError_v0_3(); }; } catch (JSONRPCError_v0_3 e) { @@ -134,11 +159,14 @@ private static NonStreamingJSONRPCRequest_v0_3 deserializeNonStreamingRequest } } - private static StreamingJSONRPCRequest_v0_3 deserializeStreamingRequest(String body, String methodName) { + private static StreamingJSONRPCRequest_v0_3 deserializeStreamingRequest( + String body, String methodName) { try { return switch (methodName) { - case SendStreamingMessageRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, SendStreamingMessageRequest_v0_3.class); - case TaskResubscriptionRequest_v0_3.METHOD -> JsonUtil_v0_3.fromJson(body, TaskResubscriptionRequest_v0_3.class); + case SendStreamingMessageRequest_v0_3.METHOD -> + JsonUtil_v0_3.fromJson(body, SendStreamingMessageRequest_v0_3.class); + case TaskResubscriptionRequest_v0_3.METHOD -> + JsonUtil_v0_3.fromJson(body, TaskResubscriptionRequest_v0_3.class); default -> throw new MethodNotFoundError_v0_3(); }; } catch (JSONRPCError_v0_3 e) { @@ -147,5 +175,4 @@ private static StreamingJSONRPCRequest_v0_3 deserializeStreamingRequest(Strin throw new InvalidParamsError_v0_3(e.getMessage()); } } - } diff --git a/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCHandler_v0_3_Test.java b/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCHandler_v0_3_Test.java index 4c80ad535..5030adb7a 100644 --- a/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCHandler_v0_3_Test.java +++ b/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCHandler_v0_3_Test.java @@ -5,7 +5,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; import java.util.ArrayList; import java.util.Collections; @@ -18,21 +17,12 @@ import java.util.concurrent.Flow; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; - import org.a2aproject.sdk.compat03.conversion.A2AProtocol_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2ARequestHandlerTest_v0_3; import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskArtifactUpdateEventMapper_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskMapper_v0_3; import org.a2aproject.sdk.compat03.conversion.mappers.domain.TaskStatusUpdateEventMapper_v0_3; -import org.a2aproject.sdk.compat03.spec.EventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Event_v0_3; -import org.a2aproject.sdk.server.ServerCallContext; -import org.a2aproject.sdk.server.auth.UnauthenticatedUser; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -// V0.3 spec imports (client perspective) import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.Artifact_v0_3; @@ -41,6 +31,8 @@ import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigParams_v0_3; import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3; import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigResponse_v0_3; +import org.a2aproject.sdk.compat03.spec.EventKind_v0_3; +import org.a2aproject.sdk.compat03.spec.Event_v0_3; import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigParams_v0_3; import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigRequest_v0_3; import org.a2aproject.sdk.compat03.spec.GetTaskPushNotificationConfigResponse_v0_3; @@ -48,18 +40,17 @@ import org.a2aproject.sdk.compat03.spec.GetTaskResponse_v0_3; import org.a2aproject.sdk.compat03.spec.InternalError_v0_3; import org.a2aproject.sdk.compat03.spec.InvalidRequestError_v0_3; -import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.MessageSendParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Message_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError_v0_3; import org.a2aproject.sdk.compat03.spec.SendMessageRequest_v0_3; import org.a2aproject.sdk.compat03.spec.SendMessageResponse_v0_3; -import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3; -import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigResponse_v0_3; import org.a2aproject.sdk.compat03.spec.SendStreamingMessageRequest_v0_3; import org.a2aproject.sdk.compat03.spec.SendStreamingMessageResponse_v0_3; +import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigRequest_v0_3; +import org.a2aproject.sdk.compat03.spec.SetTaskPushNotificationConfigResponse_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskArtifactUpdateEvent_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3; @@ -67,10 +58,15 @@ import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskResubscriptionRequest_v0_3; import org.a2aproject.sdk.compat03.spec.TaskState_v0_3; -import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; import org.a2aproject.sdk.compat03.spec.TaskStatusUpdateEvent_v0_3; +import org.a2aproject.sdk.compat03.spec.TaskStatus_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TextPart_v0_3; import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3; +import org.a2aproject.sdk.server.ServerCallContext; +import org.a2aproject.sdk.server.auth.UnauthenticatedUser; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; /** * Test suite for v0.3 JSONRPCHandler with v1.0 backend. @@ -85,8 +81,12 @@ */ public class JSONRPCHandler_v0_3_Test extends AbstractA2ARequestHandlerTest_v0_3 { - private final ServerCallContext callContext = new ServerCallContext( - UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), A2AProtocol_v0_3.PROTOCOL_VERSION); + private final ServerCallContext callContext = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + new HashSet<>(), + A2AProtocol_v0_3.PROTOCOL_VERSION); // ======================================== // GetTask Tests @@ -94,12 +94,14 @@ public class JSONRPCHandler_v0_3_Test extends AbstractA2ARequestHandlerTest_v0_3 @Test public void testOnGetTaskSuccess() throws Exception { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save v0.3 task by converting to v1.0 for taskStore taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); - GetTaskRequest_v0_3 request = new GetTaskRequest_v0_3("1", new TaskQueryParams_v0_3(MINIMAL_TASK.id())); + GetTaskRequest_v0_3 request = + new GetTaskRequest_v0_3("1", new TaskQueryParams_v0_3(MINIMAL_TASK.id())); GetTaskResponse_v0_3 response = handler.onGetTask(request, callContext); assertEquals(request.getId(), response.getId()); @@ -113,9 +115,11 @@ public void testOnGetTaskSuccess() throws Exception { @Test public void testOnGetTaskNotFound() throws Exception { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); - GetTaskRequest_v0_3 request = new GetTaskRequest_v0_3("1", new TaskQueryParams_v0_3(MINIMAL_TASK.id())); + GetTaskRequest_v0_3 request = + new GetTaskRequest_v0_3("1", new TaskQueryParams_v0_3(MINIMAL_TASK.id())); GetTaskResponse_v0_3 response = handler.onGetTask(request, callContext); assertEquals(request.getId(), response.getId()); @@ -129,18 +133,21 @@ public void testOnGetTaskNotFound() throws Exception { @Test public void testOnCancelTaskSuccess() throws Exception { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save v0.3 task by converting to v1.0 taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to cancel the task // In v1.0, we use AgentEmitter.cancel() instead of TaskUpdater - agentExecutorCancel = (context, emitter) -> { - emitter.cancel(); - }; + agentExecutorCancel = + (context, emitter) -> { + emitter.cancel(); + }; - CancelTaskRequest_v0_3 request = new CancelTaskRequest_v0_3("111", new TaskIdParams_v0_3(MINIMAL_TASK.id())); + CancelTaskRequest_v0_3 request = + new CancelTaskRequest_v0_3("111", new TaskIdParams_v0_3(MINIMAL_TASK.id())); CancelTaskResponse_v0_3 response = handler.onCancelTask(request, callContext); assertNull(response.getError()); @@ -155,17 +162,20 @@ public void testOnCancelTaskSuccess() throws Exception { @Test public void testOnCancelTaskNotSupported() { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save v0.3 task by converting to v1.0 taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to throw UnsupportedOperationError - agentExecutorCancel = (context, emitter) -> { - throw new org.a2aproject.sdk.spec.UnsupportedOperationError(); - }; + agentExecutorCancel = + (context, emitter) -> { + throw new org.a2aproject.sdk.spec.UnsupportedOperationError(); + }; - CancelTaskRequest_v0_3 request = new CancelTaskRequest_v0_3("1", new TaskIdParams_v0_3(MINIMAL_TASK.id())); + CancelTaskRequest_v0_3 request = + new CancelTaskRequest_v0_3("1", new TaskIdParams_v0_3(MINIMAL_TASK.id())); CancelTaskResponse_v0_3 response = handler.onCancelTask(request, callContext); assertEquals(request.getId(), response.getId()); @@ -175,9 +185,11 @@ public void testOnCancelTaskNotSupported() { @Test public void testOnCancelTaskNotFound() { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); - CancelTaskRequest_v0_3 request = new CancelTaskRequest_v0_3("1", new TaskIdParams_v0_3(MINIMAL_TASK.id())); + CancelTaskRequest_v0_3 request = + new CancelTaskRequest_v0_3("1", new TaskIdParams_v0_3(MINIMAL_TASK.id())); CancelTaskResponse_v0_3 response = handler.onCancelTask(request, callContext); assertEquals(request.getId(), response.getId()); @@ -191,24 +203,29 @@ public void testOnCancelTaskNotFound() { @Test public void testOnMessageSendSuccess() { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save existing task taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to echo the message back - agentExecutorExecute = (context, emitter) -> { - // Note: context.getMessage() contains v1.0 Message (already converted by Convert03To10RequestHandler) - // Emit the v1.0 message, it will be converted back to v0.3 in the response - emitter.emitEvent(context.getMessage()); - }; - - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - - SendMessageRequest_v0_3 request = new SendMessageRequest_v0_3("1", new MessageSendParams_v0_3(message, null, null)); + agentExecutorExecute = + (context, emitter) -> { + // Note: context.getMessage() contains v1.0 Message (already converted by + // Convert03To10RequestHandler) + // Emit the v1.0 message, it will be converted back to v0.3 in the response + emitter.emitEvent(context.getMessage()); + }; + + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + + SendMessageRequest_v0_3 request = + new SendMessageRequest_v0_3("1", new MessageSendParams_v0_3(message, null, null)); SendMessageResponse_v0_3 response = handler.onMessageSend(request, callContext); assertNull(response.getError()); @@ -221,23 +238,27 @@ public void testOnMessageSendSuccess() { @Test public void testOnMessageSendWithExistingTaskSuccess() { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save existing task taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to emit message - agentExecutorExecute = (context, emitter) -> { - // Emit v1.0 message from context - emitter.emitEvent(context.getMessage()); - }; - - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); + agentExecutorExecute = + (context, emitter) -> { + // Emit v1.0 message from context + emitter.emitEvent(context.getMessage()); + }; + + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); - SendMessageRequest_v0_3 request = new SendMessageRequest_v0_3("1", new MessageSendParams_v0_3(message, null, null)); + SendMessageRequest_v0_3 request = + new SendMessageRequest_v0_3("1", new MessageSendParams_v0_3(message, null, null)); SendMessageResponse_v0_3 response = handler.onMessageSend(request, callContext); assertNull(response.getError()); @@ -249,22 +270,26 @@ public void testOnMessageSendWithExistingTaskSuccess() { @Test public void testOnMessageSendError() { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save existing task taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to throw error - agentExecutorExecute = (context, emitter) -> { - emitter.emitEvent(new org.a2aproject.sdk.spec.UnsupportedOperationError()); - }; + agentExecutorExecute = + (context, emitter) -> { + emitter.emitEvent(new org.a2aproject.sdk.spec.UnsupportedOperationError()); + }; - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); - SendMessageRequest_v0_3 request = new SendMessageRequest_v0_3("1", new MessageSendParams_v0_3(message, null, null)); + SendMessageRequest_v0_3 request = + new SendMessageRequest_v0_3("1", new MessageSendParams_v0_3(message, null, null)); SendMessageResponse_v0_3 response = handler.onMessageSend(request, callContext); assertInstanceOf(UnsupportedOperationError_v0_3.class, response.getError()); @@ -277,61 +302,67 @@ public void testOnMessageSendError() { @Test public void testOnMessageSendStreamSuccess() throws InterruptedException { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save existing task taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to emit the message back (v1.0 context contains v1.0 Message) - agentExecutorExecute = (context, emitter) -> { - // Emit v1.0 message - will be converted to v0.3 StreamingEventKind - emitter.emitEvent(context.getMessage()); - }; - - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); + agentExecutorExecute = + (context, emitter) -> { + // Emit v1.0 message - will be converted to v0.3 StreamingEventKind + emitter.emitEvent(context.getMessage()); + }; + + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); - SendStreamingMessageRequest_v0_3 request = new SendStreamingMessageRequest_v0_3( - "1", new MessageSendParams_v0_3(message, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest_v0_3 request = + new SendStreamingMessageRequest_v0_3( + "1", new MessageSendParams_v0_3(message, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); List results = new ArrayList<>(); CountDownLatch latch = new CountDownLatch(1); AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse_v0_3 item) { - results.add(item.getResult()); - subscription.request(1); - latch.countDown(); - } + @Override + public void onNext(SendStreamingMessageResponse_v0_3 item) { + results.add(item.getResult()); + subscription.request(1); + latch.countDown(); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - // Release latch to prevent timeout - while (latch.getCount() > 0) { - latch.countDown(); - } - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + // Release latch to prevent timeout + while (latch.getCount() > 0) { + latch.countDown(); + } + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + } + }); // Wait for event to be received assertTrue(latch.await(2, TimeUnit.SECONDS), "Expected to receive 1 event within timeout"); @@ -343,91 +374,105 @@ public void onComplete() { assertEquals(1, results.size(), "Should have received exactly 1 event"); // Verify the event is the message (converted back from v1.0) - Message_v0_3 receivedMessage = assertInstanceOf(Message_v0_3.class, results.get(0), "Event should be a Message"); + Message_v0_3 receivedMessage = + assertInstanceOf(Message_v0_3.class, results.get(0), "Event should be a Message"); assertEquals(message.messageId(), receivedMessage.messageId()); } @Test public void testOnMessageSendStreamMultipleEventsSuccess() throws InterruptedException { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save existing task taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Create v0.3 events for reference (we'll emit v1.0 equivalents) - Task_v0_3 v03TaskEvent = new Task_v0_3.Builder(MINIMAL_TASK) - .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING)) - .build(); - - TaskArtifactUpdateEvent_v0_3 v03ArtifactEvent = new TaskArtifactUpdateEvent_v0_3.Builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(new Artifact_v0_3.Builder() - .artifactId("artifact-1") - .parts(new TextPart_v0_3("Generated artifact content")) - .build()) - .build(); - - TaskStatusUpdateEvent_v0_3 v03StatusEvent = new TaskStatusUpdateEvent_v0_3.Builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) - .isFinal(true) // Must be true for COMPLETED state in v1.0 - .build(); + Task_v0_3 v03TaskEvent = + new Task_v0_3.Builder(MINIMAL_TASK) + .status(new TaskStatus_v0_3(TaskState_v0_3.WORKING)) + .build(); + + TaskArtifactUpdateEvent_v0_3 v03ArtifactEvent = + new TaskArtifactUpdateEvent_v0_3.Builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + new Artifact_v0_3.Builder() + .artifactId("artifact-1") + .parts(new TextPart_v0_3("Generated artifact content")) + .build()) + .build(); + + TaskStatusUpdateEvent_v0_3 v03StatusEvent = + new TaskStatusUpdateEvent_v0_3.Builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) + .isFinal(true) // Must be true for COMPLETED state in v1.0 + .build(); // Configure the agent executor to emit multiple v1.0 events - agentExecutorExecute = (context, emitter) -> { - // Convert v0.3 events to v1.0 and emit them - // The emitter will convert them back to v0.3 StreamingEventKind for the response - emitter.emitEvent(TaskMapper_v0_3.INSTANCE.toV10(v03TaskEvent)); - emitter.emitEvent(TaskArtifactUpdateEventMapper_v0_3.INSTANCE.toV10(v03ArtifactEvent)); - emitter.emitEvent(TaskStatusUpdateEventMapper_v0_3.INSTANCE.toV10(v03StatusEvent)); - }; - - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - - SendStreamingMessageRequest_v0_3 request = new SendStreamingMessageRequest_v0_3( - "1", new MessageSendParams_v0_3(message, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + agentExecutorExecute = + (context, emitter) -> { + // Convert v0.3 events to v1.0 and emit them + // The emitter will convert them back to v0.3 StreamingEventKind for the + // response + emitter.emitEvent(TaskMapper_v0_3.INSTANCE.toV10(v03TaskEvent)); + emitter.emitEvent( + TaskArtifactUpdateEventMapper_v0_3.INSTANCE.toV10(v03ArtifactEvent)); + emitter.emitEvent( + TaskStatusUpdateEventMapper_v0_3.INSTANCE.toV10(v03StatusEvent)); + }; + + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + + SendStreamingMessageRequest_v0_3 request = + new SendStreamingMessageRequest_v0_3( + "1", new MessageSendParams_v0_3(message, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); List results = new ArrayList<>(); CountDownLatch latch = new CountDownLatch(3); // Expect 3 events AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse_v0_3 item) { - results.add(item.getResult()); - subscription.request(1); - latch.countDown(); - } + @Override + public void onNext(SendStreamingMessageResponse_v0_3 item) { + results.add(item.getResult()); + subscription.request(1); + latch.countDown(); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - // Release latch to prevent timeout - while (latch.getCount() > 0) { - latch.countDown(); - } - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + // Release latch to prevent timeout + while (latch.getCount() > 0) { + latch.countDown(); + } + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + } + }); // Wait for all events to be received assertTrue(latch.await(2, TimeUnit.SECONDS), "Expected to receive 3 events within timeout"); @@ -439,48 +484,58 @@ public void onComplete() { assertEquals(3, results.size(), "Should have received exactly 3 events"); // Verify the first event is the task - Task_v0_3 receivedTask = assertInstanceOf(Task_v0_3.class, results.get(0), "First event should be a Task"); + Task_v0_3 receivedTask = + assertInstanceOf(Task_v0_3.class, results.get(0), "First event should be a Task"); assertEquals(MINIMAL_TASK.id(), receivedTask.id()); assertEquals(MINIMAL_TASK.contextId(), receivedTask.contextId()); assertEquals(TaskState_v0_3.WORKING, receivedTask.status().state()); // Verify the second event is the artifact update - TaskArtifactUpdateEvent_v0_3 receivedArtifact = assertInstanceOf(TaskArtifactUpdateEvent_v0_3.class, results.get(1), - "Second event should be a TaskArtifactUpdateEvent"); + TaskArtifactUpdateEvent_v0_3 receivedArtifact = + assertInstanceOf( + TaskArtifactUpdateEvent_v0_3.class, + results.get(1), + "Second event should be a TaskArtifactUpdateEvent"); assertEquals(MINIMAL_TASK.id(), receivedArtifact.taskId()); assertEquals("artifact-1", receivedArtifact.artifact().artifactId()); // Verify the third event is the status update - TaskStatusUpdateEvent_v0_3 receivedStatus = assertInstanceOf(TaskStatusUpdateEvent_v0_3.class, results.get(2), - "Third event should be a TaskStatusUpdateEvent"); + TaskStatusUpdateEvent_v0_3 receivedStatus = + assertInstanceOf( + TaskStatusUpdateEvent_v0_3.class, + results.get(2), + "Third event should be a TaskStatusUpdateEvent"); assertEquals(MINIMAL_TASK.id(), receivedStatus.taskId()); assertEquals(TaskState_v0_3.COMPLETED, receivedStatus.status().state()); } @Test public void testOnMessageSendStreamExistingTaskSuccess() throws InterruptedException { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Configure agent to emit the task (v1.0 context contains v1.0 Task) - agentExecutorExecute = (context, emitter) -> { - // Emit v1.0 task - will be converted to v0.3 StreamingEventKind - emitter.emitEvent(context.getTask()); - }; + agentExecutorExecute = + (context, emitter) -> { + // Emit v1.0 task - will be converted to v0.3 StreamingEventKind + emitter.emitEvent(context.getTask()); + }; // Save existing v0.3 task (convert to v1.0 for storage) - Task_v0_3 v03Task = new Task_v0_3.Builder(MINIMAL_TASK) - .history(new ArrayList<>()) - .build(); + Task_v0_3 v03Task = new Task_v0_3.Builder(MINIMAL_TASK).history(new ArrayList<>()).build(); taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(v03Task), false); - Message_v0_3 message = new Message_v0_3.Builder(MESSAGE) - .taskId(v03Task.id()) - .contextId(v03Task.contextId()) - .build(); + Message_v0_3 message = + new Message_v0_3.Builder(MESSAGE) + .taskId(v03Task.id()) + .contextId(v03Task.contextId()) + .build(); - SendStreamingMessageRequest_v0_3 request = new SendStreamingMessageRequest_v0_3( - "1", new MessageSendParams_v0_3(message, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest_v0_3 request = + new SendStreamingMessageRequest_v0_3( + "1", new MessageSendParams_v0_3(message, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); // For non-final tasks, the publisher doesn't complete, so we subscribe in a new thread // and manually cancel after receiving the first event @@ -489,37 +544,40 @@ public void testOnMessageSendStreamExistingTaskSuccess() throws InterruptedExcep final CountDownLatch latch = new CountDownLatch(1); final AtomicReference error = new AtomicReference<>(); - Executors.newSingleThreadExecutor().execute(() -> { - response.subscribe(new Flow.Subscriber<>() { - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscriptionRef.set(subscription); - subscription.request(1); - } - - @Override - public void onNext(SendStreamingMessageResponse_v0_3 item) { - results.add(item.getResult()); - subscriptionRef.get().request(1); - latch.countDown(); - } - - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscriptionRef.get().cancel(); - // Release latch to prevent timeout - while (latch.getCount() > 0) { - latch.countDown(); - } - } - - @Override - public void onComplete() { - subscriptionRef.get().cancel(); - } - }); - }); + Executors.newSingleThreadExecutor() + .execute( + () -> { + response.subscribe( + new Flow.Subscriber<>() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscriptionRef.set(subscription); + subscription.request(1); + } + + @Override + public void onNext(SendStreamingMessageResponse_v0_3 item) { + results.add(item.getResult()); + subscriptionRef.get().request(1); + latch.countDown(); + } + + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscriptionRef.get().cancel(); + // Release latch to prevent timeout + while (latch.getCount() > 0) { + latch.countDown(); + } + } + + @Override + public void onComplete() { + subscriptionRef.get().cancel(); + } + }); + }); // Wait for the first event assertTrue(latch.await(2, TimeUnit.SECONDS), "Expected to receive 1 event within timeout"); @@ -530,7 +588,8 @@ public void onComplete() { // Verify the task was received assertEquals(1, results.size(), "Should have received exactly 1 event"); - Task_v0_3 receivedTask = assertInstanceOf(Task_v0_3.class, results.get(0), "Event should be a Task"); + Task_v0_3 receivedTask = + assertInstanceOf(Task_v0_3.class, results.get(0), "Event should be a Task"); assertEquals(v03Task.id(), receivedTask.id()); assertEquals(v03Task.contextId(), receivedTask.contextId()); // Note: v1.0 backend manages task history differently than v0.3 @@ -544,105 +603,119 @@ public void onComplete() { @Test public void testStreamingNotSupportedError() { // Create agent card with streaming disabled - AgentCard_v0_3 nonStreamingCard = new AgentCard_v0_3.Builder(CARD) - .capabilities(new AgentCapabilities_v0_3(false, true, false, null)) - .build(); + AgentCard_v0_3 nonStreamingCard = + new AgentCard_v0_3.Builder(CARD) + .capabilities(new AgentCapabilities_v0_3(false, true, false, null)) + .build(); - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(nonStreamingCard, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(nonStreamingCard, internalExecutor, convert03To10Handler); - SendStreamingMessageRequest_v0_3 request = new SendStreamingMessageRequest_v0_3( - "1", new MessageSendParams_v0_3(MESSAGE, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest_v0_3 request = + new SendStreamingMessageRequest_v0_3( + "1", new MessageSendParams_v0_3(MESSAGE, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); List results = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse_v0_3 item) { - results.add(item); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse_v0_3 item) { + results.add(item); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + } + }); // Verify that an error response was returned assertEquals(1, results.size(), "Should receive exactly one error response"); SendStreamingMessageResponse_v0_3 errorResponse = results.get(0); assertNotNull(errorResponse.getError(), "Response should contain an error"); - assertInstanceOf(InvalidRequestError_v0_3.class, errorResponse.getError(), "Error should be InvalidRequestError"); - assertEquals("Streaming is not supported by the agent", + assertInstanceOf( + InvalidRequestError_v0_3.class, + errorResponse.getError(), + "Error should be InvalidRequestError"); + assertEquals( + "Streaming is not supported by the agent", ((InvalidRequestError_v0_3) errorResponse.getError()).getMessage()); } @Test public void testOnMessageStreamTaskIdMismatch() { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save existing task taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to emit a task with DIFFERENT task ID than the message - agentExecutorExecute = (context, emitter) -> { - // Emit MINIMAL_TASK (which has different ID from MESSAGE) - emitter.emitEvent(context.getTask()); - }; + agentExecutorExecute = + (context, emitter) -> { + // Emit MINIMAL_TASK (which has different ID from MESSAGE) + emitter.emitEvent(context.getTask()); + }; // Send MESSAGE (which has a different task ID) - SendStreamingMessageRequest_v0_3 request = new SendStreamingMessageRequest_v0_3( - "1", new MessageSendParams_v0_3(MESSAGE, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest_v0_3 request = + new SendStreamingMessageRequest_v0_3( + "1", new MessageSendParams_v0_3(MESSAGE, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); CompletableFuture future = new CompletableFuture<>(); List results = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse_v0_3 item) { - results.add(item); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse_v0_3 item) { + results.add(item); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - future.completeExceptionally(throwable); - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + future.completeExceptionally(throwable); + } - @Override - public void onComplete() { - subscription.cancel(); - future.complete(null); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + future.complete(null); + } + }); future.join(); @@ -652,108 +725,128 @@ public void onComplete() { // Should receive an error response for the task ID mismatch assertEquals(1, results.size(), "Should receive exactly one error response"); SendStreamingMessageResponse_v0_3 errorResponse = results.get(0); - assertInstanceOf(InternalError_v0_3.class, errorResponse.getError(), + assertInstanceOf( + InternalError_v0_3.class, + errorResponse.getError(), "Task ID mismatch should result in InternalError"); } @Test public void testOnMessageStreamInternalError() { // Mock the Convert03To10RequestHandler to throw InternalError - Convert_v0_3_To10RequestHandler mockedHandler = Mockito.mock(Convert_v0_3_To10RequestHandler.class); + Convert_v0_3_To10RequestHandler mockedHandler = + Mockito.mock(Convert_v0_3_To10RequestHandler.class); Mockito.doThrow(new org.a2aproject.sdk.spec.InternalError("Internal Error")) .when(mockedHandler) .onMessageSendStream( Mockito.any(MessageSendParams_v0_3.class), Mockito.any(ServerCallContext.class)); - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, mockedHandler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, mockedHandler); - SendStreamingMessageRequest_v0_3 request = new SendStreamingMessageRequest_v0_3("1", new MessageSendParams_v0_3(MESSAGE, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest_v0_3 request = + new SendStreamingMessageRequest_v0_3( + "1", new MessageSendParams_v0_3(MESSAGE, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); List results = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse_v0_3 item) { - results.add(item); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse_v0_3 item) { + results.add(item); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + } + }); // Verify that an InternalError response was returned assertEquals(1, results.size(), "Should receive exactly one error response"); - assertInstanceOf(InternalError_v0_3.class, results.get(0).getError(), "Error should be InternalError"); + assertInstanceOf( + InternalError_v0_3.class, + results.get(0).getError(), + "Error should be InternalError"); } @Test public void testStreamingNotSupportedErrorOnResubscribeToTask() { // Create agent card with streaming disabled - AgentCard_v0_3 nonStreamingCard = new AgentCard_v0_3.Builder(CARD) - .capabilities(new AgentCapabilities_v0_3(false, true, false, null)) - .build(); + AgentCard_v0_3 nonStreamingCard = + new AgentCard_v0_3.Builder(CARD) + .capabilities(new AgentCapabilities_v0_3(false, true, false, null)) + .build(); - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(nonStreamingCard, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(nonStreamingCard, internalExecutor, convert03To10Handler); - TaskResubscriptionRequest_v0_3 request = new TaskResubscriptionRequest_v0_3("1", new TaskIdParams_v0_3(MINIMAL_TASK.id())); - Flow.Publisher response = handler.onResubscribeToTask(request, callContext); + TaskResubscriptionRequest_v0_3 request = + new TaskResubscriptionRequest_v0_3("1", new TaskIdParams_v0_3(MINIMAL_TASK.id())); + Flow.Publisher response = + handler.onResubscribeToTask(request, callContext); List results = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse_v0_3 item) { - results.add(item); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse_v0_3 item) { + results.add(item); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + } + }); // Verify that an error response was returned assertEquals(1, results.size(), "Should receive exactly one error response"); SendStreamingMessageResponse_v0_3 errorResponse = results.get(0); assertNotNull(errorResponse.getError(), "Response should contain an error"); - assertInstanceOf(InvalidRequestError_v0_3.class, errorResponse.getError(), "Error should be InvalidRequestError"); - assertEquals("Streaming is not supported by the agent", + assertInstanceOf( + InvalidRequestError_v0_3.class, + errorResponse.getError(), + "Error should be InvalidRequestError"); + assertEquals( + "Streaming is not supported by the agent", ((InvalidRequestError_v0_3) errorResponse.getError()).getMessage()); } @@ -763,7 +856,8 @@ public void onComplete() { @Test public void testSetPushNotificationConfigSuccess() { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save task to v1.0 backend (conversion happens internally) org.a2aproject.sdk.spec.Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK); @@ -775,8 +869,10 @@ public void testSetPushNotificationConfigSuccess() { new PushNotificationConfig_v0_3.Builder() .url("http://example.com") .build()); - SetTaskPushNotificationConfigRequest_v0_3 request = new SetTaskPushNotificationConfigRequest_v0_3("1", taskPushConfig); - SetTaskPushNotificationConfigResponse_v0_3 response = handler.setPushNotificationConfig(request, callContext); + SetTaskPushNotificationConfigRequest_v0_3 request = + new SetTaskPushNotificationConfigRequest_v0_3("1", taskPushConfig); + SetTaskPushNotificationConfigResponse_v0_3 response = + handler.setPushNotificationConfig(request, callContext); assertNull(response.getError(), "Error: " + response.getError()); assertNotNull(response.getResult()); @@ -793,15 +889,18 @@ public void testSetPushNotificationConfigSuccess() { @Test public void testGetPushNotificationConfigSuccess() { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save task to v1.0 backend org.a2aproject.sdk.spec.Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK); taskStore.save(v10Task, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; TaskPushNotificationConfig_v0_3 taskPushConfig = new TaskPushNotificationConfig_v0_3( @@ -810,29 +909,40 @@ public void testGetPushNotificationConfigSuccess() { .url("http://example.com") .build()); - SetTaskPushNotificationConfigRequest_v0_3 request = new SetTaskPushNotificationConfigRequest_v0_3("1", taskPushConfig); + SetTaskPushNotificationConfigRequest_v0_3 request = + new SetTaskPushNotificationConfigRequest_v0_3("1", taskPushConfig); handler.setPushNotificationConfig(request, callContext); GetTaskPushNotificationConfigRequest_v0_3 getRequest = - new GetTaskPushNotificationConfigRequest_v0_3("111", new GetTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id())); - GetTaskPushNotificationConfigResponse_v0_3 getResponse = handler.getPushNotificationConfig(getRequest, callContext); + new GetTaskPushNotificationConfigRequest_v0_3( + "111", new GetTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id())); + GetTaskPushNotificationConfigResponse_v0_3 getResponse = + handler.getPushNotificationConfig(getRequest, callContext); - TaskPushNotificationConfig_v0_3 expectedConfig = new TaskPushNotificationConfig_v0_3(MINIMAL_TASK.id(), - new PushNotificationConfig_v0_3.Builder().id(MINIMAL_TASK.id()).url("http://example.com").build()); + TaskPushNotificationConfig_v0_3 expectedConfig = + new TaskPushNotificationConfig_v0_3( + MINIMAL_TASK.id(), + new PushNotificationConfig_v0_3.Builder() + .id(MINIMAL_TASK.id()) + .url("http://example.com") + .build()); assertEquals(expectedConfig, getResponse.getResult()); } @Test public void testDeletePushNotificationConfig() { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save task to v1.0 backend org.a2aproject.sdk.spec.Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK); taskStore.save(v10Task, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; TaskPushNotificationConfig_v0_3 taskPushConfig = new TaskPushNotificationConfig_v0_3( @@ -841,11 +951,15 @@ public void testDeletePushNotificationConfig() { .url("http://example.com") .id(MINIMAL_TASK.id()) .build()); - SetTaskPushNotificationConfigRequest_v0_3 request = new SetTaskPushNotificationConfigRequest_v0_3("1", taskPushConfig); + SetTaskPushNotificationConfigRequest_v0_3 request = + new SetTaskPushNotificationConfigRequest_v0_3("1", taskPushConfig); handler.setPushNotificationConfig(request, callContext); DeleteTaskPushNotificationConfigRequest_v0_3 deleteRequest = - new DeleteTaskPushNotificationConfigRequest_v0_3("111", new DeleteTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id(), MINIMAL_TASK.id())); + new DeleteTaskPushNotificationConfigRequest_v0_3( + "111", + new DeleteTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id(), MINIMAL_TASK.id())); DeleteTaskPushNotificationConfigResponse_v0_3 deleteResponse = handler.deletePushNotificationConfig(deleteRequest, callContext); @@ -869,18 +983,22 @@ public void testOnGetPushNotificationNoPushNotifierConfig() { .build(); // Wrap in v0.3 conversion handler - Convert_v0_3_To10RequestHandler handlerWithoutPushConfig = new Convert_v0_3_To10RequestHandler(v10Handler); + Convert_v0_3_To10RequestHandler handlerWithoutPushConfig = + new Convert_v0_3_To10RequestHandler(v10Handler); AgentCard_v0_3 card = createAgentCard(false, true, false); - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(card, internalExecutor, handlerWithoutPushConfig); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(card, internalExecutor, handlerWithoutPushConfig); // Save task to v1.0 backend org.a2aproject.sdk.spec.Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK); taskStore.save(v10Task, false); GetTaskPushNotificationConfigRequest_v0_3 request = - new GetTaskPushNotificationConfigRequest_v0_3("id", new GetTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id())); - GetTaskPushNotificationConfigResponse_v0_3 response = handler.getPushNotificationConfig(request, callContext); + new GetTaskPushNotificationConfigRequest_v0_3( + "id", new GetTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id())); + GetTaskPushNotificationConfigResponse_v0_3 response = + handler.getPushNotificationConfig(request, callContext); assertNotNull(response.getError()); assertInstanceOf(UnsupportedOperationError_v0_3.class, response.getError()); @@ -902,10 +1020,12 @@ public void testOnSetPushNotificationNoPushNotifierConfig() { .build(); // Wrap in v0.3 conversion handler - Convert_v0_3_To10RequestHandler handlerWithoutPushConfig = new Convert_v0_3_To10RequestHandler(v10Handler); + Convert_v0_3_To10RequestHandler handlerWithoutPushConfig = + new Convert_v0_3_To10RequestHandler(v10Handler); AgentCard_v0_3 card = createAgentCard(false, true, false); - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(card, internalExecutor, handlerWithoutPushConfig); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(card, internalExecutor, handlerWithoutPushConfig); // Save task to v1.0 backend org.a2aproject.sdk.spec.Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK); @@ -918,10 +1038,10 @@ public void testOnSetPushNotificationNoPushNotifierConfig() { .url("http://example.com") .build()); - SetTaskPushNotificationConfigRequest_v0_3 request = new SetTaskPushNotificationConfigRequest_v0_3.Builder() - .params(config) - .build(); - SetTaskPushNotificationConfigResponse_v0_3 response = handler.setPushNotificationConfig(request, callContext); + SetTaskPushNotificationConfigRequest_v0_3 request = + new SetTaskPushNotificationConfigRequest_v0_3.Builder().params(config).build(); + SetTaskPushNotificationConfigResponse_v0_3 response = + handler.setPushNotificationConfig(request, callContext); assertInstanceOf(UnsupportedOperationError_v0_3.class, response.getError()); assertEquals("This operation is not supported", response.getError().getMessage()); @@ -930,15 +1050,18 @@ public void testOnSetPushNotificationNoPushNotifierConfig() { @Test public void testDeletePushNotificationConfigNotSupported() { AgentCard_v0_3 card = createAgentCard(true, false, false); - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(card, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(card, internalExecutor, convert03To10Handler); // Save task to v1.0 backend org.a2aproject.sdk.spec.Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK); taskStore.save(v10Task, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; TaskPushNotificationConfig_v0_3 taskPushConfig = new TaskPushNotificationConfig_v0_3( @@ -947,11 +1070,15 @@ public void testDeletePushNotificationConfigNotSupported() { .url("http://example.com") .id(MINIMAL_TASK.id()) .build()); - SetTaskPushNotificationConfigRequest_v0_3 request = new SetTaskPushNotificationConfigRequest_v0_3("1", taskPushConfig); + SetTaskPushNotificationConfigRequest_v0_3 request = + new SetTaskPushNotificationConfigRequest_v0_3("1", taskPushConfig); handler.setPushNotificationConfig(request, callContext); DeleteTaskPushNotificationConfigRequest_v0_3 deleteRequest = - new DeleteTaskPushNotificationConfigRequest_v0_3("111", new DeleteTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id(), MINIMAL_TASK.id())); + new DeleteTaskPushNotificationConfigRequest_v0_3( + "111", + new DeleteTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id(), MINIMAL_TASK.id())); DeleteTaskPushNotificationConfigResponse_v0_3 deleteResponse = handler.deletePushNotificationConfig(deleteRequest, callContext); @@ -975,17 +1102,21 @@ public void testDeletePushNotificationConfigNoPushConfigStore() { .build(); // Wrap in v0.3 conversion handler - Convert_v0_3_To10RequestHandler handlerWithoutPushConfig = new Convert_v0_3_To10RequestHandler(v10Handler); + Convert_v0_3_To10RequestHandler handlerWithoutPushConfig = + new Convert_v0_3_To10RequestHandler(v10Handler); - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, handlerWithoutPushConfig); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, handlerWithoutPushConfig); // Save task to v1.0 backend org.a2aproject.sdk.spec.Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK); taskStore.save(v10Task, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; TaskPushNotificationConfig_v0_3 taskPushConfig = new TaskPushNotificationConfig_v0_3( @@ -994,11 +1125,15 @@ public void testDeletePushNotificationConfigNoPushConfigStore() { .url("http://example.com") .id(MINIMAL_TASK.id()) .build()); - SetTaskPushNotificationConfigRequest_v0_3 request = new SetTaskPushNotificationConfigRequest_v0_3("1", taskPushConfig); + SetTaskPushNotificationConfigRequest_v0_3 request = + new SetTaskPushNotificationConfigRequest_v0_3("1", taskPushConfig); handler.setPushNotificationConfig(request, callContext); DeleteTaskPushNotificationConfigRequest_v0_3 deleteRequest = - new DeleteTaskPushNotificationConfigRequest_v0_3("111", new DeleteTaskPushNotificationConfigParams_v0_3(MINIMAL_TASK.id(), MINIMAL_TASK.id())); + new DeleteTaskPushNotificationConfigRequest_v0_3( + "111", + new DeleteTaskPushNotificationConfigParams_v0_3( + MINIMAL_TASK.id(), MINIMAL_TASK.id())); DeleteTaskPushNotificationConfigResponse_v0_3 deleteResponse = handler.deletePushNotificationConfig(deleteRequest, callContext); @@ -1014,7 +1149,8 @@ public void testOnMessageStreamNewMessageSendPushNotificationSuccess() throws Ex mainEventBusProcessor.setPushNotificationExecutor(Runnable::run); try { - JSONRPCHandler_v0_3 handler = new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + JSONRPCHandler_v0_3 handler = + new JSONRPCHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save task to v1.0 backend org.a2aproject.sdk.spec.Task v10Task = TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK); @@ -1023,139 +1159,179 @@ public void testOnMessageStreamNewMessageSendPushNotificationSuccess() throws Ex // Clear any previous events from httpClient httpClient.events.clear(); - // Create v0.3 events that the agent executor will emit - List events = List.of( - MINIMAL_TASK, - new TaskArtifactUpdateEvent_v0_3.Builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(new Artifact_v0_3.Builder() - .artifactId("11") - .parts(new TextPart_v0_3("text")) - .build()) - .build(), - new TaskStatusUpdateEvent_v0_3.Builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) - .isFinal(true) - .build()); - - agentExecutorExecute = (context, agentEmitter) -> { - // Convert v0.3 events to v1.0 and emit - for (Event_v0_3 event : events) { - if (event instanceof Task_v0_3) { - agentEmitter.emitEvent(TaskMapper_v0_3.INSTANCE.toV10((Task_v0_3) event)); - } else if (event instanceof TaskArtifactUpdateEvent_v0_3) { - agentEmitter.emitEvent(TaskArtifactUpdateEventMapper_v0_3.INSTANCE.toV10((TaskArtifactUpdateEvent_v0_3) event)); - } else if (event instanceof TaskStatusUpdateEvent_v0_3) { - agentEmitter.emitEvent(TaskStatusUpdateEventMapper_v0_3.INSTANCE.toV10((TaskStatusUpdateEvent_v0_3) event)); - } - } - }; - - // Set push notification config - TaskPushNotificationConfig_v0_3 config = new TaskPushNotificationConfig_v0_3( - MINIMAL_TASK.id(), - new PushNotificationConfig_v0_3.Builder().url("http://example.com").build()); - SetTaskPushNotificationConfigRequest_v0_3 stpnRequest = new SetTaskPushNotificationConfigRequest_v0_3("1", config); - SetTaskPushNotificationConfigResponse_v0_3 stpnResponse = handler.setPushNotificationConfig(stpnRequest, callContext); - assertNull(stpnResponse.getError()); - - // Send streaming message - Message_v0_3 msg = new Message_v0_3.Builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .build(); - SendStreamingMessageRequest_v0_3 request = new SendStreamingMessageRequest_v0_3("1", new MessageSendParams_v0_3(msg, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); - - final List results = Collections.synchronizedList(new ArrayList<>()); - final AtomicReference subscriptionRef = new AtomicReference<>(); - final CountDownLatch latch = new CountDownLatch(6); // 3 streaming responses + 3 push notifications - httpClient.latch = latch; - - Executors.newSingleThreadExecutor().execute(() -> { - response.subscribe(new Flow.Subscriber<>() { - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscriptionRef.set(subscription); - subscription.request(1); - } - - @Override - public void onNext(SendStreamingMessageResponse_v0_3 item) { - results.add(item.getResult()); - subscriptionRef.get().request(1); - latch.countDown(); - } - - @Override - public void onError(Throwable throwable) { - subscriptionRef.get().cancel(); - } - - @Override - public void onComplete() { - subscriptionRef.get().cancel(); - } - }); - }); - - boolean timedOut = !latch.await(5, TimeUnit.SECONDS); - if (timedOut) { - System.out.println("Test timed out! Received " + results.size() + " streaming responses, " + - httpClient.events.size() + " push notifications. Latch count: " + latch.getCount()); - System.out.println("Push notifications received:"); - for (int i = 0; i < httpClient.events.size(); i++) { - org.a2aproject.sdk.spec.StreamingEventKind event = httpClient.events.get(i); - if (event instanceof org.a2aproject.sdk.spec.Task) { - System.out.println(" [" + i + "] Task"); - } else if (event instanceof org.a2aproject.sdk.spec.TaskArtifactUpdateEvent) { - System.out.println(" [" + i + "] TaskArtifactUpdateEvent"); - } else if (event instanceof org.a2aproject.sdk.spec.TaskStatusUpdateEvent) { - System.out.println(" [" + i + "] TaskStatusUpdateEvent"); - } else if (event instanceof org.a2aproject.sdk.spec.Message) { - System.out.println(" [" + i + "] Message"); + // Create v0.3 events that the agent executor will emit + List events = + List.of( + MINIMAL_TASK, + new TaskArtifactUpdateEvent_v0_3.Builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + new Artifact_v0_3.Builder() + .artifactId("11") + .parts(new TextPart_v0_3("text")) + .build()) + .build(), + new TaskStatusUpdateEvent_v0_3.Builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .status(new TaskStatus_v0_3(TaskState_v0_3.COMPLETED)) + .isFinal(true) + .build()); + + agentExecutorExecute = + (context, agentEmitter) -> { + // Convert v0.3 events to v1.0 and emit + for (Event_v0_3 event : events) { + if (event instanceof Task_v0_3) { + agentEmitter.emitEvent( + TaskMapper_v0_3.INSTANCE.toV10((Task_v0_3) event)); + } else if (event instanceof TaskArtifactUpdateEvent_v0_3) { + agentEmitter.emitEvent( + TaskArtifactUpdateEventMapper_v0_3.INSTANCE.toV10( + (TaskArtifactUpdateEvent_v0_3) event)); + } else if (event instanceof TaskStatusUpdateEvent_v0_3) { + agentEmitter.emitEvent( + TaskStatusUpdateEventMapper_v0_3.INSTANCE.toV10( + (TaskStatusUpdateEvent_v0_3) event)); + } + } + }; + + // Set push notification config + TaskPushNotificationConfig_v0_3 config = + new TaskPushNotificationConfig_v0_3( + MINIMAL_TASK.id(), + new PushNotificationConfig_v0_3.Builder() + .url("http://example.com") + .build()); + SetTaskPushNotificationConfigRequest_v0_3 stpnRequest = + new SetTaskPushNotificationConfigRequest_v0_3("1", config); + SetTaskPushNotificationConfigResponse_v0_3 stpnResponse = + handler.setPushNotificationConfig(stpnRequest, callContext); + assertNull(stpnResponse.getError()); + + // Send streaming message + Message_v0_3 msg = new Message_v0_3.Builder(MESSAGE).taskId(MINIMAL_TASK.id()).build(); + SendStreamingMessageRequest_v0_3 request = + new SendStreamingMessageRequest_v0_3( + "1", new MessageSendParams_v0_3(msg, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); + + final List results = + Collections.synchronizedList(new ArrayList<>()); + final AtomicReference subscriptionRef = new AtomicReference<>(); + final CountDownLatch latch = + new CountDownLatch(6); // 3 streaming responses + 3 push notifications + httpClient.latch = latch; + + Executors.newSingleThreadExecutor() + .execute( + () -> { + response.subscribe( + new Flow.Subscriber<>() { + @Override + public void onSubscribe( + Flow.Subscription subscription) { + subscriptionRef.set(subscription); + subscription.request(1); + } + + @Override + public void onNext( + SendStreamingMessageResponse_v0_3 item) { + results.add(item.getResult()); + subscriptionRef.get().request(1); + latch.countDown(); + } + + @Override + public void onError(Throwable throwable) { + subscriptionRef.get().cancel(); + } + + @Override + public void onComplete() { + subscriptionRef.get().cancel(); + } + }); + }); + + boolean timedOut = !latch.await(5, TimeUnit.SECONDS); + if (timedOut) { + System.out.println( + "Test timed out! Received " + + results.size() + + " streaming responses, " + + httpClient.events.size() + + " push notifications. Latch count: " + + latch.getCount()); + System.out.println("Push notifications received:"); + for (int i = 0; i < httpClient.events.size(); i++) { + org.a2aproject.sdk.spec.StreamingEventKind event = httpClient.events.get(i); + if (event instanceof org.a2aproject.sdk.spec.Task) { + System.out.println(" [" + i + "] Task"); + } else if (event instanceof org.a2aproject.sdk.spec.TaskArtifactUpdateEvent) { + System.out.println(" [" + i + "] TaskArtifactUpdateEvent"); + } else if (event instanceof org.a2aproject.sdk.spec.TaskStatusUpdateEvent) { + System.out.println(" [" + i + "] TaskStatusUpdateEvent"); + } else if (event instanceof org.a2aproject.sdk.spec.Message) { + System.out.println(" [" + i + "] Message"); + } } } - } - assertTrue(!timedOut, "Test timed out waiting for events. Received " + results.size() + " streaming responses, " + - httpClient.events.size() + " push notifications"); - subscriptionRef.get().cancel(); - - // Verify streaming responses (v0.3 format) - assertEquals(3, results.size()); - - // Verify push notifications were sent (v1.0 StreamingEventKind format) - assertEquals(3, httpClient.events.size()); - - // First event: task - org.a2aproject.sdk.spec.StreamingEventKind pushEvent0 = httpClient.events.get(0); - assertTrue(pushEvent0 instanceof org.a2aproject.sdk.spec.Task); - org.a2aproject.sdk.spec.Task v10PushedTask0 = (org.a2aproject.sdk.spec.Task) pushEvent0; - assertEquals(MINIMAL_TASK.id(), v10PushedTask0.id()); - assertEquals(MINIMAL_TASK.contextId(), v10PushedTask0.contextId()); - // v0.3 SUBMITTED maps to v1.0 TASK_STATE_SUBMITTED - assertEquals(org.a2aproject.sdk.spec.TaskState.TASK_STATE_SUBMITTED, v10PushedTask0.status().state()); - assertTrue(v10PushedTask0.artifacts() == null || v10PushedTask0.artifacts().isEmpty()); - - // Second event: artifact update - org.a2aproject.sdk.spec.StreamingEventKind pushEvent1 = httpClient.events.get(1); - assertTrue(pushEvent1 instanceof org.a2aproject.sdk.spec.TaskArtifactUpdateEvent); - org.a2aproject.sdk.spec.TaskArtifactUpdateEvent v10ArtifactUpdate = (org.a2aproject.sdk.spec.TaskArtifactUpdateEvent) pushEvent1; - assertEquals(MINIMAL_TASK.id(), v10ArtifactUpdate.taskId()); - assertEquals(MINIMAL_TASK.contextId(), v10ArtifactUpdate.contextId()); - assertNotNull(v10ArtifactUpdate.artifact()); - assertEquals(1, v10ArtifactUpdate.artifact().parts().size()); - assertEquals("text", ((org.a2aproject.sdk.spec.TextPart) v10ArtifactUpdate.artifact().parts().get(0)).text()); - - // Third event: status update - org.a2aproject.sdk.spec.StreamingEventKind pushEvent2 = httpClient.events.get(2); - assertTrue(pushEvent2 instanceof org.a2aproject.sdk.spec.TaskStatusUpdateEvent); - org.a2aproject.sdk.spec.TaskStatusUpdateEvent v10StatusUpdate = (org.a2aproject.sdk.spec.TaskStatusUpdateEvent) pushEvent2; - assertEquals(MINIMAL_TASK.id(), v10StatusUpdate.taskId()); - assertEquals(MINIMAL_TASK.contextId(), v10StatusUpdate.contextId()); - assertEquals(org.a2aproject.sdk.spec.TaskState.TASK_STATE_COMPLETED, v10StatusUpdate.status().state()); + assertTrue( + !timedOut, + "Test timed out waiting for events. Received " + + results.size() + + " streaming responses, " + + httpClient.events.size() + + " push notifications"); + subscriptionRef.get().cancel(); + + // Verify streaming responses (v0.3 format) + assertEquals(3, results.size()); + + // Verify push notifications were sent (v1.0 StreamingEventKind format) + assertEquals(3, httpClient.events.size()); + + // First event: task + org.a2aproject.sdk.spec.StreamingEventKind pushEvent0 = httpClient.events.get(0); + assertTrue(pushEvent0 instanceof org.a2aproject.sdk.spec.Task); + org.a2aproject.sdk.spec.Task v10PushedTask0 = (org.a2aproject.sdk.spec.Task) pushEvent0; + assertEquals(MINIMAL_TASK.id(), v10PushedTask0.id()); + assertEquals(MINIMAL_TASK.contextId(), v10PushedTask0.contextId()); + // v0.3 SUBMITTED maps to v1.0 TASK_STATE_SUBMITTED + assertEquals( + org.a2aproject.sdk.spec.TaskState.TASK_STATE_SUBMITTED, + v10PushedTask0.status().state()); + assertTrue(v10PushedTask0.artifacts() == null || v10PushedTask0.artifacts().isEmpty()); + + // Second event: artifact update + org.a2aproject.sdk.spec.StreamingEventKind pushEvent1 = httpClient.events.get(1); + assertTrue(pushEvent1 instanceof org.a2aproject.sdk.spec.TaskArtifactUpdateEvent); + org.a2aproject.sdk.spec.TaskArtifactUpdateEvent v10ArtifactUpdate = + (org.a2aproject.sdk.spec.TaskArtifactUpdateEvent) pushEvent1; + assertEquals(MINIMAL_TASK.id(), v10ArtifactUpdate.taskId()); + assertEquals(MINIMAL_TASK.contextId(), v10ArtifactUpdate.contextId()); + assertNotNull(v10ArtifactUpdate.artifact()); + assertEquals(1, v10ArtifactUpdate.artifact().parts().size()); + assertEquals( + "text", + ((org.a2aproject.sdk.spec.TextPart) v10ArtifactUpdate.artifact().parts().get(0)) + .text()); + + // Third event: status update + org.a2aproject.sdk.spec.StreamingEventKind pushEvent2 = httpClient.events.get(2); + assertTrue(pushEvent2 instanceof org.a2aproject.sdk.spec.TaskStatusUpdateEvent); + org.a2aproject.sdk.spec.TaskStatusUpdateEvent v10StatusUpdate = + (org.a2aproject.sdk.spec.TaskStatusUpdateEvent) pushEvent2; + assertEquals(MINIMAL_TASK.id(), v10StatusUpdate.taskId()); + assertEquals(MINIMAL_TASK.contextId(), v10StatusUpdate.contextId()); + assertEquals( + org.a2aproject.sdk.spec.TaskState.TASK_STATE_COMPLETED, + v10StatusUpdate.status().state()); } finally { // Reset push notification executor to async mainEventBusProcessor.setPushNotificationExecutor(null); diff --git a/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCRequestDecoder_v0_3_Test.java b/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCRequestDecoder_v0_3_Test.java index 6cf9d2c4e..0a491c82d 100644 --- a/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCRequestDecoder_v0_3_Test.java +++ b/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCRequestDecoder_v0_3_Test.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.stream.Stream; - import org.a2aproject.sdk.compat03.spec.CancelTaskRequest_v0_3; import org.a2aproject.sdk.compat03.spec.DeleteTaskPushNotificationConfigRequest_v0_3; import org.a2aproject.sdk.compat03.spec.GetAuthenticatedExtendedCardRequest_v0_3; @@ -37,18 +36,19 @@ /** Decoder parity with the reference route's captured wire responses. */ public class JSONRPCRequestDecoder_v0_3_Test { - private static final String MESSAGE_PARAMS = "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\"," - + "\"role\":\"user\",\"parts\":[{\"kind\":\"text\",\"text\":\"hello\"}]}}"; + private static final String MESSAGE_PARAMS = + "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\"," + + "\"role\":\"user\",\"parts\":[{\"kind\":\"text\",\"text\":\"hello\"}]}}"; @Test public void testInvalidJsonBeforeEnvelopeValidation() { String[][] cases = { - {"{", "java.io.EOFException: End of input at line 1 column 2 path $."}, - {"[]", "Not a JSON Object: []"}, - {"null", "Not a JSON Object: null"}, - {"42", "Not a JSON Object: 42"}, - {"\"text\"", "Not a JSON Object: \"text\""}, - {"", "Not a JSON Object: null"} + {"{", "java.io.EOFException: End of input at line 1 column 2 path $."}, + {"[]", "Not a JSON Object: []"}, + {"null", "Not a JSON Object: null"}, + {"42", "Not a JSON Object: 42"}, + {"\"text\"", "Not a JSON Object: \"text\""}, + {"", "Not a JSON Object: null"} }; for (String[] entry : cases) { assertFailure(entry[0], null, null, JSONParseError_v0_3.class, -32700, entry[1]); @@ -57,7 +57,8 @@ public void testInvalidJsonBeforeEnvelopeValidation() { @Test public void testNullBodyIsJsonParseError() { - DecodeFailure failure = assertInstanceOf(DecodeFailure.class, JSONRPCRequestDecoder_v0_3.decode(null)); + DecodeFailure failure = + assertInstanceOf(DecodeFailure.class, JSONRPCRequestDecoder_v0_3.decode(null)); assertInstanceOf(JSONParseError_v0_3.class, failure.error()); assertEquals(-32700, failure.error().getCode()); assertEquals("Request body is required", failure.error().getMessage()); @@ -66,8 +67,12 @@ public void testNullBodyIsJsonParseError() { @Test public void testInvalidIdBeforeVersionAndMethod() { for (String id : List.of("{}", "[]")) { - assertFailure("{\"id\":" + id + ",\"jsonrpc\":false,\"method\":null}", null, null, - InvalidRequestError_v0_3.class, -32600, + assertFailure( + "{\"id\":" + id + ",\"jsonrpc\":false,\"method\":null}", + null, + null, + InvalidRequestError_v0_3.class, + -32600, "Invalid JSON-RPC request: 'id' must be a string, number, or null"); } } @@ -77,9 +82,12 @@ public void testVersionFieldErrorRetainsId() { for (String id : List.of("\"original\"", "17")) { for (String version : new String[] {null, "null", "{}", "[]", "true", "1", "\"1.0\""}) { String field = version == null ? "" : ",\"jsonrpc\":" + version; - assertFailure("{\"id\":" + id + ",\"method\":{}" + field + "}", - id.equals("17") ? Long.valueOf(17) : "original", null, - InvalidRequestError_v0_3.class, -32600, + assertFailure( + "{\"id\":" + id + ",\"method\":{}" + field + "}", + id.equals("17") ? Long.valueOf(17) : "original", + null, + InvalidRequestError_v0_3.class, + -32600, "Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); } } @@ -89,8 +97,12 @@ public void testVersionFieldErrorRetainsId() { public void testMethodFieldErrorRetainsId() { for (String method : new String[] {null, "null", "{}", "[]", "true", "17"}) { String field = method == null ? "" : ",\"method\":" + method; - assertFailure("{\"jsonrpc\":\"2.0\",\"id\":17" + field + "}", 17L, null, - InvalidRequestError_v0_3.class, -32600, + assertFailure( + "{\"jsonrpc\":\"2.0\",\"id\":17" + field + "}", + 17L, + null, + InvalidRequestError_v0_3.class, + -32600, "Invalid JSON-RPC request: missing or invalid 'method' field"); } } @@ -99,52 +111,86 @@ public void testMethodFieldErrorRetainsId() { public void testUnknownMethodBeforeParams() { String[][] cases = {{"\"unknown\"", "unknown"}, {"\"\"", ""}}; for (String[] entry : cases) { - assertFailure("{\"jsonrpc\":\"2.0\",\"id\":\"unknown\",\"method\":" + entry[0] - + ",\"params\":[]}", "unknown", entry[1], MethodNotFoundError_v0_3.class, -32601, "Method not found"); + assertFailure( + "{\"jsonrpc\":\"2.0\",\"id\":\"unknown\",\"method\":" + + entry[0] + + ",\"params\":[]}", + "unknown", + entry[1], + MethodNotFoundError_v0_3.class, + -32601, + "Method not found"); } } @Test public void testParamsErrorRetainsId() { - for (String params : List.of("[]", "\"bad\"", "{\"historyLength\":-1}", - "{\"id\":\"task\",\"historyLength\":\"bad\"}")) { - assertFailure(request("tasks/get", params), "original", "tasks/get", InvalidParamsError_v0_3.class, -32602, - params.equals("{\"historyLength\":-1}") - ? null - : "Failed to parse JSON"); + for (String params : + List.of( + "[]", + "\"bad\"", + "{\"historyLength\":-1}", + "{\"id\":\"task\",\"historyLength\":\"bad\"}")) { + assertFailure( + request("tasks/get", params), + "original", + "tasks/get", + InvalidParamsError_v0_3.class, + -32602, + params.equals("{\"historyLength\":-1}") ? null : "Failed to parse JSON"); } for (String params : new String[] {null, "null"}) { - for (String method : List.of("tasks/get", "message/send", "message/stream", "tasks/resubscribe")) { - DecodedRequest decoded = assertInstanceOf(DecodedRequest.class, - JSONRPCRequestDecoder_v0_3.decode(request(method, params))); + for (String method : + List.of("tasks/get", "message/send", "message/stream", "tasks/resubscribe")) { + DecodedRequest decoded = + assertInstanceOf( + DecodedRequest.class, + JSONRPCRequestDecoder_v0_3.decode(request(method, params))); assertEquals("original", decoded.requestId()); assertEquals(method, decoded.methodName()); assertEquals(method, decoded.request().getMethod()); assertNull(decoded.request().getParams()); } } - for (String params : List.of("{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"invalid\",\"parts\":[{\"kind\":\"text\",\"text\":\"hello\"}]}}", - "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{}]}}", - "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{\"kind\":\"file\",\"file\":{}}]}}")) { - assertFailure(request("message/send", params), "original", "message/send", InvalidParamsError_v0_3.class, - -32602, "Failed to parse JSON"); + for (String params : + List.of( + "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"invalid\",\"parts\":[{\"kind\":\"text\",\"text\":\"hello\"}]}}", + "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{}]}}", + "{\"message\":{\"kind\":\"message\",\"messageId\":\"m\",\"role\":\"user\",\"parts\":[{\"kind\":\"file\",\"file\":{}}]}}")) { + assertFailure( + request("message/send", params), + "original", + "message/send", + InvalidParamsError_v0_3.class, + -32602, + "Failed to parse JSON"); } } @Test public void testStreamingDecodeFailureIsHttpJson() { for (String method : List.of("message/stream", "tasks/resubscribe")) { - assertFailure(request(method, "[]"), "original", method, InvalidParamsError_v0_3.class, - -32602, "Failed to parse JSON"); + assertFailure( + request(method, "[]"), + "original", + method, + InvalidParamsError_v0_3.class, + -32602, + "Failed to parse JSON"); } } @ParameterizedTest(name = "{0}") @MethodSource("supportedMethods") - public void testSupportedMethodsDecode(String method, @Nullable String params, - Class> requestClass, boolean streaming) { - DecodedRequest decoded = assertInstanceOf(DecodedRequest.class, - JSONRPCRequestDecoder_v0_3.decode(request(method, params))); + public void testSupportedMethodsDecode( + String method, + @Nullable String params, + Class> requestClass, + boolean streaming) { + DecodedRequest decoded = + assertInstanceOf( + DecodedRequest.class, + JSONRPCRequestDecoder_v0_3.decode(request(method, params))); assertEquals("original", decoded.requestId()); assertEquals(method, decoded.methodName()); assertEquals(requestClass, decoded.request().getClass()); @@ -166,41 +212,94 @@ public void testSupportedMethodsDecode(String method, @Nullable String params, private static Stream supportedMethods() { return Stream.of( Arguments.of("tasks/get", "{\"id\":\"task\"}", GetTaskRequest_v0_3.class, false), - Arguments.of("tasks/cancel", "{\"id\":\"task\"}", CancelTaskRequest_v0_3.class, false), + Arguments.of( + "tasks/cancel", "{\"id\":\"task\"}", CancelTaskRequest_v0_3.class, false), Arguments.of("message/send", MESSAGE_PARAMS, SendMessageRequest_v0_3.class, false), - Arguments.of("tasks/pushNotificationConfig/set", "{\"taskId\":\"task\",\"pushNotificationConfig\":{\"url\":\"http://localhost/callback\"}}", SetTaskPushNotificationConfigRequest_v0_3.class, false), - Arguments.of("tasks/pushNotificationConfig/get", "{\"id\":\"task\"}", GetTaskPushNotificationConfigRequest_v0_3.class, false), - Arguments.of("tasks/pushNotificationConfig/list", "{\"id\":\"task\"}", ListTaskPushNotificationConfigRequest_v0_3.class, false), - Arguments.of("tasks/pushNotificationConfig/delete", "{\"id\":\"task\",\"pushNotificationConfigId\":\"config\"}", DeleteTaskPushNotificationConfigRequest_v0_3.class, false), - Arguments.of("agent/getAuthenticatedExtendedCard", null, GetAuthenticatedExtendedCardRequest_v0_3.class, false), - Arguments.of("message/stream", MESSAGE_PARAMS, SendStreamingMessageRequest_v0_3.class, true), - Arguments.of("tasks/resubscribe", "{\"id\":\"task\"}", TaskResubscriptionRequest_v0_3.class, true)); + Arguments.of( + "tasks/pushNotificationConfig/set", + "{\"taskId\":\"task\",\"pushNotificationConfig\":{\"url\":\"http://localhost/callback\"}}", + SetTaskPushNotificationConfigRequest_v0_3.class, + false), + Arguments.of( + "tasks/pushNotificationConfig/get", + "{\"id\":\"task\"}", + GetTaskPushNotificationConfigRequest_v0_3.class, + false), + Arguments.of( + "tasks/pushNotificationConfig/list", + "{\"id\":\"task\"}", + ListTaskPushNotificationConfigRequest_v0_3.class, + false), + Arguments.of( + "tasks/pushNotificationConfig/delete", + "{\"id\":\"task\",\"pushNotificationConfigId\":\"config\"}", + DeleteTaskPushNotificationConfigRequest_v0_3.class, + false), + Arguments.of( + "agent/getAuthenticatedExtendedCard", + null, + GetAuthenticatedExtendedCardRequest_v0_3.class, + false), + Arguments.of( + "message/stream", + MESSAGE_PARAMS, + SendStreamingMessageRequest_v0_3.class, + true), + Arguments.of( + "tasks/resubscribe", + "{\"id\":\"task\"}", + TaskResubscriptionRequest_v0_3.class, + true)); } @Test public void testExistingIdCoercion() { - Object[][] cases = {{"\"request\"", "request", "request"}, {"7", 7L, 7L}, {"7.9", 7L, 7.9}, - {"true", "true", true}, {"null", null, null}, {null, null, null}}; + Object[][] cases = { + {"\"request\"", "request", "request"}, + {"7", 7L, 7L}, + {"7.9", 7L, 7.9}, + {"true", "true", true}, + {"null", null, null}, + {null, null, null} + }; for (Object[] entry : cases) { String id = entry[0] == null ? "" : "\"id\":" + entry[0] + ","; - assertFailure("{" + id + "\"jsonrpc\":\"wrong\"}", entry[1], null, - InvalidRequestError_v0_3.class, -32600, + assertFailure( + "{" + id + "\"jsonrpc\":\"wrong\"}", + entry[1], + null, + InvalidRequestError_v0_3.class, + -32600, "Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); - DecodedRequest decoded = assertInstanceOf(DecodedRequest.class, JSONRPCRequestDecoder_v0_3.decode( - "{" + id + "\"jsonrpc\":\"2.0\",\"method\":\"tasks/get\",\"params\":{\"id\":\"task\"}}")); + DecodedRequest decoded = + assertInstanceOf( + DecodedRequest.class, + JSONRPCRequestDecoder_v0_3.decode( + "{" + + id + + "\"jsonrpc\":\"2.0\",\"method\":\"tasks/get\",\"params\":{\"id\":\"task\"}}")); assertEquals(entry[1], decoded.requestId()); assertEquals(entry[2], decoded.request().getId()); } } private static String request(String method, @Nullable String params) { - return "{\"jsonrpc\":\"2.0\",\"id\":\"original\",\"method\":\"" + method + "\"" - + (params == null ? "" : ",\"params\":" + params) + "}"; + return "{\"jsonrpc\":\"2.0\",\"id\":\"original\",\"method\":\"" + + method + + "\"" + + (params == null ? "" : ",\"params\":" + params) + + "}"; } - private static void assertFailure(String body, @Nullable Object id, @Nullable String method, - Class errorClass, int code, @Nullable String message) { - DecodeFailure failure = assertInstanceOf(DecodeFailure.class, JSONRPCRequestDecoder_v0_3.decode(body)); + private static void assertFailure( + String body, + @Nullable Object id, + @Nullable String method, + Class errorClass, + int code, + @Nullable String message) { + DecodeFailure failure = + assertInstanceOf(DecodeFailure.class, JSONRPCRequestDecoder_v0_3.decode(body)); assertEquals(id, failure.requestId()); assertEquals(method, failure.methodName()); assertEquals(errorClass, failure.error().getClass()); diff --git a/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCTestTransportMetadata_v0_3.java b/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCTestTransportMetadata_v0_3.java index ab9e169ee..95445b984 100644 --- a/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCTestTransportMetadata_v0_3.java +++ b/compat-0.3/transport/jsonrpc/src/test/java/org/a2aproject/sdk/compat03/transport/jsonrpc/handler/JSONRPCTestTransportMetadata_v0_3.java @@ -15,5 +15,4 @@ /** * Placeholder stub - awaiting server-common port. */ -public class JSONRPCTestTransportMetadata_v0_3 { -} +public class JSONRPCTestTransportMetadata_v0_3 {} diff --git a/compat-0.3/transport/rest/pom.xml b/compat-0.3/transport/rest/pom.xml index c513578b6..6df21ca63 100644 --- a/compat-0.3/transport/rest/pom.xml +++ b/compat-0.3/transport/rest/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -83,4 +81,4 @@ - \ No newline at end of file + diff --git a/compat-0.3/transport/rest/src/main/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestHandler_v0_3.java b/compat-0.3/transport/rest/src/main/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestHandler_v0_3.java index 57d948ae0..8535dcfaa 100644 --- a/compat-0.3/transport/rest/src/main/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestHandler_v0_3.java +++ b/compat-0.3/transport/rest/src/main/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestHandler_v0_3.java @@ -6,18 +6,22 @@ import com.google.gson.JsonSyntaxException; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; -import org.a2aproject.sdk.compat03.grpc.utils.ProtoJsonUtils_v0_3; -import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; - import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.concurrent.Flow; - -import org.a2aproject.sdk.server.ExtendedAgentCard; -import org.a2aproject.sdk.server.PublicAgentCard; -import org.a2aproject.sdk.server.ServerCallContext; +import java.util.logging.Level; +import java.util.logging.Logger; +import mutiny.zero.ZeroPublisher; +import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; +import org.a2aproject.sdk.compat03.conversion.ErrorConverter_v0_3; +import org.a2aproject.sdk.compat03.grpc.utils.ProtoJsonUtils_v0_3; +import org.a2aproject.sdk.compat03.grpc.utils.ProtoUtils_v0_3; +import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.compat03.spec.AuthenticatedExtendedCardNotConfiguredError_v0_3; import org.a2aproject.sdk.compat03.spec.ContentTypeNotSupportedError_v0_3; @@ -34,25 +38,19 @@ import org.a2aproject.sdk.compat03.spec.MethodNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.PushNotificationNotSupportedError_v0_3; import org.a2aproject.sdk.compat03.spec.StreamingEventKind_v0_3; -import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.TaskIdParams_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotCancelableError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3; import org.a2aproject.sdk.compat03.spec.TaskPushNotificationConfig_v0_3; import org.a2aproject.sdk.compat03.spec.TaskQueryParams_v0_3; +import org.a2aproject.sdk.compat03.spec.Task_v0_3; import org.a2aproject.sdk.compat03.spec.UnsupportedOperationError_v0_3; -import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; -import org.a2aproject.sdk.compat03.conversion.ErrorConverter_v0_3; -import org.a2aproject.sdk.compat03.json.JsonUtil_v0_3; +import org.a2aproject.sdk.server.ExtendedAgentCard; +import org.a2aproject.sdk.server.PublicAgentCard; +import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.util.async.Internal; -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.A2AError; -import jakarta.enterprise.inject.Instance; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.logging.Level; -import java.util.logging.Logger; -import mutiny.zero.ZeroPublisher; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; @ApplicationScoped @@ -60,8 +58,7 @@ public class RestHandler_v0_3 { private static final Logger LOGGER = Logger.getLogger(RestHandler_v0_3.class.getName()); private AgentCard_v0_3 agentCard; - private @Nullable - Instance extendedAgentCard; + private @Nullable Instance extendedAgentCard; private Convert_v0_3_To10RequestHandler requestHandler; private final Executor executor; @@ -72,8 +69,11 @@ protected RestHandler_v0_3() { } @Inject - public RestHandler_v0_3(@PublicAgentCard AgentCard_v0_3 agentCard, @ExtendedAgentCard Instance extendedAgentCard, - @Internal Executor executor, Convert_v0_3_To10RequestHandler requestHandler) { + public RestHandler_v0_3( + @PublicAgentCard AgentCard_v0_3 agentCard, + @ExtendedAgentCard Instance extendedAgentCard, + @Internal Executor executor, + Convert_v0_3_To10RequestHandler requestHandler) { this.agentCard = agentCard; this.extendedAgentCard = extendedAgentCard; this.requestHandler = requestHandler; @@ -83,7 +83,10 @@ public RestHandler_v0_3(@PublicAgentCard AgentCard_v0_3 agentCard, @ExtendedAgen // AgentCardValidator.validateTransportConfiguration(agentCard); } - public RestHandler_v0_3(AgentCard_v0_3 agentCard, Executor executor, Convert_v0_3_To10RequestHandler requestHandler) { + public RestHandler_v0_3( + AgentCard_v0_3 agentCard, + Executor executor, + Convert_v0_3_To10RequestHandler requestHandler) { this.agentCard = agentCard; this.executor = executor; this.requestHandler = requestHandler; @@ -91,10 +94,16 @@ public RestHandler_v0_3(AgentCard_v0_3 agentCard, Executor executor, Convert_v0_ public HTTPRestResponse sendMessage(String body, ServerCallContext context) { try { - org.a2aproject.sdk.compat03.grpc.SendMessageRequest.Builder request = org.a2aproject.sdk.compat03.grpc.SendMessageRequest.newBuilder(); + org.a2aproject.sdk.compat03.grpc.SendMessageRequest.Builder request = + org.a2aproject.sdk.compat03.grpc.SendMessageRequest.newBuilder(); parseRequestBody(body, request); - EventKind_v0_3 result = requestHandler.onMessageSend(ProtoUtils_v0_3.FromProto.messageSendParams(request), context); - return createSuccessResponse(200, org.a2aproject.sdk.compat03.grpc.SendMessageResponse.newBuilder(ProtoUtils_v0_3.ToProto.taskOrMessage(result))); + EventKind_v0_3 result = + requestHandler.onMessageSend( + ProtoUtils_v0_3.FromProto.messageSendParams(request), context); + return createSuccessResponse( + 200, + org.a2aproject.sdk.compat03.grpc.SendMessageResponse.newBuilder( + ProtoUtils_v0_3.ToProto.taskOrMessage(result))); } catch (A2AError e) { return createErrorResponse(ErrorConverter_v0_3.convertA2AError(e)); } catch (JSONRPCError_v0_3 e) { @@ -107,18 +116,30 @@ public HTTPRestResponse sendMessage(String body, ServerCallContext context) { public HTTPRestResponse sendStreamingMessage(String body, ServerCallContext context) { try { if (!agentCard.capabilities().streaming()) { - return createErrorResponse(new InvalidRequestError_v0_3("Streaming is not supported by the agent")); + return createErrorResponse( + new InvalidRequestError_v0_3("Streaming is not supported by the agent")); } - org.a2aproject.sdk.compat03.grpc.SendMessageRequest.Builder request = org.a2aproject.sdk.compat03.grpc.SendMessageRequest.newBuilder(); + org.a2aproject.sdk.compat03.grpc.SendMessageRequest.Builder request = + org.a2aproject.sdk.compat03.grpc.SendMessageRequest.newBuilder(); parseRequestBody(body, request); - Flow.Publisher publisher = requestHandler.onMessageSendStream(ProtoUtils_v0_3.FromProto.messageSendParams(request), context); + Flow.Publisher publisher = + requestHandler.onMessageSendStream( + ProtoUtils_v0_3.FromProto.messageSendParams(request), context); return createStreamingResponse(publisher); } catch (A2AError e) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(ErrorConverter_v0_3.convertA2AError(e)).toJson())); + return new HTTPRestStreamingResponse( + ZeroPublisher.fromItems( + new HTTPRestErrorResponse(ErrorConverter_v0_3.convertA2AError(e)) + .toJson())); } catch (JSONRPCError_v0_3 e) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(e).toJson())); + return new HTTPRestStreamingResponse( + ZeroPublisher.fromItems(new HTTPRestErrorResponse(e).toJson())); } catch (Throwable throwable) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(new InternalError_v0_3(throwable.getMessage())).toJson())); + return new HTTPRestStreamingResponse( + ZeroPublisher.fromItems( + new HTTPRestErrorResponse( + new InternalError_v0_3(throwable.getMessage())) + .toJson())); } } @@ -128,7 +149,10 @@ public HTTPRestResponse cancelTask(@Nullable String taskId, ServerCallContext co TaskIdParams_v0_3 params = new TaskIdParams_v0_3(requiredTaskId); Task_v0_3 task = requestHandler.onCancelTask(params, context); if (task != null) { - return createSuccessResponse(200, org.a2aproject.sdk.compat03.grpc.Task.newBuilder(ProtoUtils_v0_3.ToProto.task(task))); + return createSuccessResponse( + 200, + org.a2aproject.sdk.compat03.grpc.Task.newBuilder( + ProtoUtils_v0_3.ToProto.task(task))); } throw new UnsupportedOperationError_v0_3(); } catch (A2AError e) { @@ -140,13 +164,17 @@ public HTTPRestResponse cancelTask(@Nullable String taskId, ServerCallContext co } } - public HTTPRestResponse setTaskPushNotificationConfiguration(@Nullable String taskId, String body, ServerCallContext context) { + public HTTPRestResponse setTaskPushNotificationConfiguration( + @Nullable String taskId, String body, ServerCallContext context) { try { String requiredTaskId = requireNonEmpty("task id", taskId); if (!agentCard.capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError_v0_3(); } - org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequest.Builder builder = org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequest.newBuilder(); + org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequest.Builder + builder = + org.a2aproject.sdk.compat03.grpc.CreateTaskPushNotificationConfigRequest + .newBuilder(); parseRequestBody(body, builder); String parent = "tasks/" + requiredTaskId; if (!builder.getParent().isBlank() && !parent.equals(builder.getParent())) { @@ -168,17 +196,26 @@ public HTTPRestResponse setTaskPushNotificationConfiguration(@Nullable String ta } } String requestConfigId = builder.getConfigId().isBlank() ? "" : builder.getConfigId(); - if (requestConfigId.contains("/") || (!nameConfigId.isEmpty() && !requestConfigId.isEmpty() - && !nameConfigId.equals(requestConfigId))) { + if (requestConfigId.contains("/") + || (!nameConfigId.isEmpty() + && !requestConfigId.isEmpty() + && !nameConfigId.equals(requestConfigId))) { throw new InvalidParamsError_v0_3("bad config id"); } - String configId = !requestConfigId.isEmpty() ? requestConfigId - : !nameConfigId.isEmpty() ? nameConfigId : requiredTaskId; + String configId = + !requestConfigId.isEmpty() + ? requestConfigId + : !nameConfigId.isEmpty() ? nameConfigId : requiredTaskId; builder.setParent(parent); builder.setConfigId(configId); builder.getConfigBuilder().setName(configNamePrefix + "/" + configId); - TaskPushNotificationConfig_v0_3 result = requestHandler.onSetTaskPushNotificationConfig(ProtoUtils_v0_3.FromProto.taskPushNotificationConfig(builder), context); - return createSuccessResponse(201, org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder(ProtoUtils_v0_3.ToProto.taskPushNotificationConfig(result))); + TaskPushNotificationConfig_v0_3 result = + requestHandler.onSetTaskPushNotificationConfig( + ProtoUtils_v0_3.FromProto.taskPushNotificationConfig(builder), context); + return createSuccessResponse( + 201, + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder( + ProtoUtils_v0_3.ToProto.taskPushNotificationConfig(result))); } catch (A2AError e) { return createErrorResponse(ErrorConverter_v0_3.convertA2AError(e)); } catch (JSONRPCError_v0_3 e) { @@ -197,22 +234,35 @@ public HTTPRestResponse resubscribeTask(@Nullable String taskId, ServerCallConte } try { if (!agentCard.capabilities().streaming()) { - return createErrorResponse(new InvalidRequestError_v0_3("Streaming is not supported by the agent")); + return createErrorResponse( + new InvalidRequestError_v0_3("Streaming is not supported by the agent")); } TaskIdParams_v0_3 params = new TaskIdParams_v0_3(requiredTaskId); - Flow.Publisher publisher = requestHandler.onResubscribeToTask(params, context); + Flow.Publisher publisher = + requestHandler.onResubscribeToTask(params, context); return createStreamingResponse(publisher); } catch (A2AError e) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(ErrorConverter_v0_3.convertA2AError(e)).toJson())); + return new HTTPRestStreamingResponse( + ZeroPublisher.fromItems( + new HTTPRestErrorResponse(ErrorConverter_v0_3.convertA2AError(e)) + .toJson())); } catch (JSONRPCError_v0_3 e) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(e).toJson())); + return new HTTPRestStreamingResponse( + ZeroPublisher.fromItems(new HTTPRestErrorResponse(e).toJson())); } catch (Throwable throwable) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(new InternalError_v0_3(throwable.getMessage())).toJson())); + return new HTTPRestStreamingResponse( + ZeroPublisher.fromItems( + new HTTPRestErrorResponse( + new InternalError_v0_3(throwable.getMessage())) + .toJson())); } } - public HTTPRestResponse getTask(@Nullable String taskId, @Nullable String historyLengthSnakeCase, - @Nullable String historyLengthCamelCase, ServerCallContext context) { + public HTTPRestResponse getTask( + @Nullable String taskId, + @Nullable String historyLengthSnakeCase, + @Nullable String historyLengthCamelCase, + ServerCallContext context) { String requiredTaskId; try { requiredTaskId = requireNonEmpty("task id", taskId); @@ -220,8 +270,9 @@ public HTTPRestResponse getTask(@Nullable String taskId, @Nullable String histor return createErrorResponse(e); } if (historyLengthSnakeCase != null && historyLengthCamelCase != null) { - return createErrorResponse(new InvalidParamsError_v0_3( - "Only one of 'history_length' or 'historyLength' may be specified")); + return createErrorResponse( + new InvalidParamsError_v0_3( + "Only one of 'history_length' or 'historyLength' may be specified")); } int historyLength = 0; try { @@ -231,16 +282,21 @@ public HTTPRestResponse getTask(@Nullable String taskId, @Nullable String histor historyLength = Integer.parseInt(historyLengthCamelCase); } } catch (NumberFormatException e) { - return createErrorResponse(new InvalidParamsError_v0_3("bad history_length or historyLength")); + return createErrorResponse( + new InvalidParamsError_v0_3("bad history_length or historyLength")); } if (historyLength < 0) { - return createErrorResponse(new InvalidParamsError_v0_3("bad history_length or historyLength")); + return createErrorResponse( + new InvalidParamsError_v0_3("bad history_length or historyLength")); } try { TaskQueryParams_v0_3 params = new TaskQueryParams_v0_3(requiredTaskId, historyLength); Task_v0_3 task = requestHandler.onGetTask(params, context); if (task != null) { - return createSuccessResponse(200, org.a2aproject.sdk.compat03.grpc.Task.newBuilder(ProtoUtils_v0_3.ToProto.task(task))); + return createSuccessResponse( + 200, + org.a2aproject.sdk.compat03.grpc.Task.newBuilder( + ProtoUtils_v0_3.ToProto.task(task))); } throw new TaskNotFoundError_v0_3(); } catch (A2AError e) { @@ -255,15 +311,21 @@ public HTTPRestResponse getTask(@Nullable String taskId, @Nullable String histor /** * A missing or empty config ID selects the task's default push notification configuration in v0.3. */ - public HTTPRestResponse getTaskPushNotificationConfiguration(@Nullable String taskId, @Nullable String configId, ServerCallContext context) { + public HTTPRestResponse getTaskPushNotificationConfiguration( + @Nullable String taskId, @Nullable String configId, ServerCallContext context) { try { String requiredTaskId = requireNonEmpty("task id", taskId); if (!agentCard.capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError_v0_3(); } - GetTaskPushNotificationConfigParams_v0_3 params = new GetTaskPushNotificationConfigParams_v0_3(requiredTaskId, configId); - TaskPushNotificationConfig_v0_3 config = requestHandler.onGetTaskPushNotificationConfig(params, context); - return createSuccessResponse(200, org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder(ProtoUtils_v0_3.ToProto.taskPushNotificationConfig(config))); + GetTaskPushNotificationConfigParams_v0_3 params = + new GetTaskPushNotificationConfigParams_v0_3(requiredTaskId, configId); + TaskPushNotificationConfig_v0_3 config = + requestHandler.onGetTaskPushNotificationConfig(params, context); + return createSuccessResponse( + 200, + org.a2aproject.sdk.compat03.grpc.TaskPushNotificationConfig.newBuilder( + ProtoUtils_v0_3.ToProto.taskPushNotificationConfig(config))); } catch (A2AError e) { return createErrorResponse(ErrorConverter_v0_3.convertA2AError(e)); } catch (JSONRPCError_v0_3 e) { @@ -273,15 +335,23 @@ public HTTPRestResponse getTaskPushNotificationConfiguration(@Nullable String ta } } - public HTTPRestResponse listTaskPushNotificationConfigurations(@Nullable String taskId, ServerCallContext context) { + public HTTPRestResponse listTaskPushNotificationConfigurations( + @Nullable String taskId, ServerCallContext context) { try { String requiredTaskId = requireNonEmpty("task id", taskId); if (!agentCard.capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError_v0_3(); } - ListTaskPushNotificationConfigParams_v0_3 params = new ListTaskPushNotificationConfigParams_v0_3(requiredTaskId); - List configs = requestHandler.onListTaskPushNotificationConfig(params, context); - return createSuccessResponse(200, org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse.newBuilder(ProtoUtils_v0_3.ToProto.listTaskPushNotificationConfigResponse(configs))); + ListTaskPushNotificationConfigParams_v0_3 params = + new ListTaskPushNotificationConfigParams_v0_3(requiredTaskId); + List configs = + requestHandler.onListTaskPushNotificationConfig(params, context); + return createSuccessResponse( + 200, + org.a2aproject.sdk.compat03.grpc.ListTaskPushNotificationConfigResponse + .newBuilder( + ProtoUtils_v0_3.ToProto.listTaskPushNotificationConfigResponse( + configs))); } catch (A2AError e) { return createErrorResponse(ErrorConverter_v0_3.convertA2AError(e)); } catch (JSONRPCError_v0_3 e) { @@ -291,14 +361,17 @@ public HTTPRestResponse listTaskPushNotificationConfigurations(@Nullable String } } - public HTTPRestResponse deleteTaskPushNotificationConfiguration(@Nullable String taskId, @Nullable String configId, ServerCallContext context) { + public HTTPRestResponse deleteTaskPushNotificationConfiguration( + @Nullable String taskId, @Nullable String configId, ServerCallContext context) { try { String requiredTaskId = requireNonEmpty("task id", taskId); String requiredConfigId = requireNonEmpty("config id", configId); if (!agentCard.capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError_v0_3(); } - DeleteTaskPushNotificationConfigParams_v0_3 params = new DeleteTaskPushNotificationConfigParams_v0_3(requiredTaskId, requiredConfigId); + DeleteTaskPushNotificationConfigParams_v0_3 params = + new DeleteTaskPushNotificationConfigParams_v0_3( + requiredTaskId, requiredConfigId); requestHandler.onDeleteTaskPushNotificationConfig(params, context); return new HTTPRestResponse(204, "application/json", ""); } catch (A2AError e) { @@ -310,7 +383,8 @@ public HTTPRestResponse deleteTaskPushNotificationConfiguration(@Nullable String } } - private void parseRequestBody(String body, com.google.protobuf.Message.Builder builder) throws JSONRPCError_v0_3 { + private void parseRequestBody(String body, com.google.protobuf.Message.Builder builder) + throws JSONRPCError_v0_3 { try { if (body == null || body.trim().isEmpty()) { throw new InvalidRequestError_v0_3("Request body is required"); @@ -318,7 +392,9 @@ private void parseRequestBody(String body, com.google.protobuf.Message.Builder b validate(body); JsonFormat.parser().merge(body, builder); } catch (InvalidProtocolBufferException e) { - LOGGER.log(Level.SEVERE, "Error parsing JSON request body (length={0})", + LOGGER.log( + Level.SEVERE, + "Error parsing JSON request body (length={0})", body != null ? body.length() : 0); LOGGER.log(Level.SEVERE, "Parse error details", e); throw new InvalidParamsError_v0_3("Failed to parse request body: " + e.getMessage()); @@ -336,16 +412,19 @@ private void validate(String json) { try { JsonParser.parseString(json); } catch (JsonSyntaxException e) { - throw new JSONParseError_v0_3(JSONParseError_v0_3.DEFAULT_CODE, "Failed to parse json", e.getMessage()); + throw new JSONParseError_v0_3( + JSONParseError_v0_3.DEFAULT_CODE, "Failed to parse json", e.getMessage()); } } - private HTTPRestResponse createSuccessResponse(int statusCode, com.google.protobuf.Message.Builder builder) { + private HTTPRestResponse createSuccessResponse( + int statusCode, com.google.protobuf.Message.Builder builder) { try { String jsonBody = ProtoJsonUtils_v0_3.toJson(JsonFormat.printer(), builder); return new HTTPRestResponse(statusCode, "application/json", jsonBody); } catch (InvalidProtocolBufferException e) { - return createErrorResponse(new InternalError_v0_3("Failed to serialize response: " + e.getMessage())); + return createErrorResponse( + new InternalError_v0_3("Failed to serialize response: " + e.getMessage())); } } @@ -359,57 +438,78 @@ private HTTPRestResponse createErrorResponse(int statusCode, JSONRPCError_v0_3 e return new HTTPRestResponse(statusCode, "application/json", jsonBody); } - private HTTPRestStreamingResponse createStreamingResponse(Flow.Publisher publisher) { + private HTTPRestStreamingResponse createStreamingResponse( + Flow.Publisher publisher) { return new HTTPRestStreamingResponse(convertToSendStreamingMessageResponse(publisher)); } @SuppressWarnings("FutureReturnValueIgnored") private Flow.Publisher convertToSendStreamingMessageResponse( Flow.Publisher publisher) { - // We can't use the normal convertingProcessor since that propagates any errors as an error handled + // We can't use the normal convertingProcessor since that propagates any errors as an error + // handled // via Subscriber.onError() rather than as part of the SendStreamingResponse payload - return ZeroPublisher.create(createTubeConfig(), tube -> { - CompletableFuture.runAsync(() -> { - publisher.subscribe(new Flow.Subscriber() { - Flow.@Nullable Subscription subscription; - - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } - - @Override - public void onNext(StreamingEventKind_v0_3 item) { - try { - String payload = ProtoJsonUtils_v0_3.toJson( - JsonFormat.printer().omittingInsignificantWhitespace(), ProtoUtils_v0_3.ToProto.taskOrMessageStream(item)); - tube.send(payload); - if (subscription != null) { - subscription.request(1); - } - } catch (InvalidProtocolBufferException ex) { - onError(ex); - } - } - - @Override - public void onError(Throwable throwable) { - if (throwable instanceof JSONRPCError_v0_3 jsonrpcError) { - tube.send(new HTTPRestErrorResponse(jsonrpcError).toJson()); - } else { - tube.send(new HTTPRestErrorResponse(new InternalError_v0_3(throwable.getMessage())).toJson()); - } - onComplete(); - } - - @Override - public void onComplete() { - tube.complete(); - } + return ZeroPublisher.create( + createTubeConfig(), + tube -> { + CompletableFuture.runAsync( + () -> { + publisher.subscribe( + new Flow.Subscriber() { + Flow.@Nullable Subscription subscription; + + @Override + public void onSubscribe( + Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } + + @Override + public void onNext(StreamingEventKind_v0_3 item) { + try { + String payload = + ProtoJsonUtils_v0_3.toJson( + JsonFormat.printer() + .omittingInsignificantWhitespace(), + ProtoUtils_v0_3.ToProto + .taskOrMessageStream( + item)); + tube.send(payload); + if (subscription != null) { + subscription.request(1); + } + } catch (InvalidProtocolBufferException ex) { + onError(ex); + } + } + + @Override + public void onError(Throwable throwable) { + if (throwable + instanceof JSONRPCError_v0_3 jsonrpcError) { + tube.send( + new HTTPRestErrorResponse(jsonrpcError) + .toJson()); + } else { + tube.send( + new HTTPRestErrorResponse( + new InternalError_v0_3( + throwable + .getMessage())) + .toJson()); + } + onComplete(); + } + + @Override + public void onComplete() { + tube.complete(); + } + }); + }, + executor); }); - }, executor); - }); } private int mapErrorToHttpStatus(JSONRPCError_v0_3 error) { @@ -419,13 +519,16 @@ private int mapErrorToHttpStatus(JSONRPCError_v0_3 error) { if (error instanceof InvalidParamsError_v0_3) { return 422; } - if (error instanceof MethodNotFoundError_v0_3 || error instanceof TaskNotFoundError_v0_3 || error instanceof AuthenticatedExtendedCardNotConfiguredError_v0_3) { + if (error instanceof MethodNotFoundError_v0_3 + || error instanceof TaskNotFoundError_v0_3 + || error instanceof AuthenticatedExtendedCardNotConfiguredError_v0_3) { return 404; } if (error instanceof TaskNotCancelableError_v0_3) { return 409; } - if (error instanceof PushNotificationNotSupportedError_v0_3 || error instanceof UnsupportedOperationError_v0_3) { + if (error instanceof PushNotificationNotSupportedError_v0_3 + || error instanceof UnsupportedOperationError_v0_3) { return 501; } if (error instanceof ContentTypeNotSupportedError_v0_3) { @@ -442,10 +545,14 @@ private int mapErrorToHttpStatus(JSONRPCError_v0_3 error) { public HTTPRestResponse getAuthenticatedExtendedCard() { try { - if (!agentCard.supportsAuthenticatedExtendedCard() || extendedAgentCard == null || !extendedAgentCard.isResolvable()) { - throw new AuthenticatedExtendedCardNotConfiguredError_v0_3(null, "Authenticated Extended Card not configured", null); + if (!agentCard.supportsAuthenticatedExtendedCard() + || extendedAgentCard == null + || !extendedAgentCard.isResolvable()) { + throw new AuthenticatedExtendedCardNotConfiguredError_v0_3( + null, "Authenticated Extended Card not configured", null); } - return new HTTPRestResponse(200, "application/json", JsonUtil_v0_3.toJson(extendedAgentCard.get())); + return new HTTPRestResponse( + 200, "application/json", JsonUtil_v0_3.toJson(extendedAgentCard.get())); } catch (JSONRPCError_v0_3 e) { return createErrorResponse(e); } catch (Throwable t) { @@ -472,7 +579,8 @@ public HTTPRestResponse(int statusCode, String contentType, String body) { this(statusCode, contentType, body, Map.of()); } - public HTTPRestResponse(int statusCode, String contentType, String body, Map headers) { + public HTTPRestResponse( + int statusCode, String contentType, String body, Map headers) { this.statusCode = statusCode; this.contentType = contentType; this.body = body; @@ -497,7 +605,16 @@ public Map getHeaders() { @Override public String toString() { - return "HTTPRestResponse{" + "statusCode=" + statusCode + ", contentType=" + contentType + ", body=" + body + ", headers=" + headers + '}'; + return "HTTPRestResponse{" + + "statusCode=" + + statusCode + + ", contentType=" + + contentType + + ", body=" + + body + + ", headers=" + + headers + + '}'; } } @@ -518,8 +635,7 @@ public Flow.Publisher getPublisher() { private static class HTTPRestErrorResponse { private final String error; - private final @Nullable - String message; + private final @Nullable String message; private HTTPRestErrorResponse(JSONRPCError_v0_3 jsonRpcError) { this.error = jsonRpcError.getClass().getName(); diff --git a/compat-0.3/transport/rest/src/test/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestHandler_v0_3_Test.java b/compat-0.3/transport/rest/src/test/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestHandler_v0_3_Test.java index e7aae196e..261f67e30 100644 --- a/compat-0.3/transport/rest/src/test/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestHandler_v0_3_Test.java +++ b/compat-0.3/transport/rest/src/test/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestHandler_v0_3_Test.java @@ -12,7 +12,6 @@ import java.util.HashSet; import java.util.Map; - import org.a2aproject.sdk.compat03.conversion.A2AProtocol_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2ARequestHandlerTest_v0_3; import org.a2aproject.sdk.compat03.conversion.Convert_v0_3_To10RequestHandler; @@ -25,8 +24,8 @@ import org.a2aproject.sdk.server.requesthandlers.RequestHandler; import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams; import org.a2aproject.sdk.spec.InternalError; -import org.a2aproject.sdk.spec.TaskQueryParams; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; +import org.a2aproject.sdk.spec.TaskQueryParams; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -46,8 +45,12 @@ */ public class RestHandler_v0_3_Test extends AbstractA2ARequestHandlerTest_v0_3 { - private final ServerCallContext callContext = new ServerCallContext( - UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), A2AProtocol_v0_3.PROTOCOL_VERSION); + private final ServerCallContext callContext = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + new HashSet<>(), + A2AProtocol_v0_3.PROTOCOL_VERSION); // ======================================== // GetTask Tests @@ -55,12 +58,14 @@ public class RestHandler_v0_3_Test extends AbstractA2ARequestHandlerTest_v0_3 { @Test public void testGetTaskSuccess() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save v0.3 task by converting to v1.0 taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); - RestHandler_v0_3.HTTPRestResponse response = handler.getTask(MINIMAL_TASK.id(), null, null, callContext); + RestHandler_v0_3.HTTPRestResponse response = + handler.getTask(MINIMAL_TASK.id(), null, null, callContext); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals("application/json", response.getContentType()); @@ -76,9 +81,11 @@ public void testGetTaskSuccess() { @Test public void testGetTaskNotFound() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); - RestHandler_v0_3.HTTPRestResponse response = handler.getTask("nonexistent", null, null, callContext); + RestHandler_v0_3.HTTPRestResponse response = + handler.getTask("nonexistent", null, null, callContext); Assertions.assertEquals(404, response.getStatusCode()); Assertions.assertEquals("application/json", response.getContentType()); @@ -91,31 +98,35 @@ public void testGetTaskNotFound() { @Test public void testSendMessage() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Configure agent to echo the message back - agentExecutorExecute = (context, emitter) -> { - emitter.emitEvent(context.getMessage()); - }; + agentExecutorExecute = + (context, emitter) -> { + emitter.emitEvent(context.getMessage()); + }; - String requestBody = """ - { - "message": + String requestBody = + """ { - "messageId": "message-1234", - "contextId": "context-1234", - "role": "ROLE_USER", - "content": [{ - "text": "tell me a joke" - }], - "metadata": { + "message": + { + "messageId": "message-1234", + "contextId": "context-1234", + "role": "ROLE_USER", + "content": [{ + "text": "tell me a joke" + }], + "metadata": { + } + }, + "configuration": + { + "blocking": true } - }, - "configuration": - { - "blocking": true - } - }"""; + }\ + """; RestHandler_v0_3.HTTPRestResponse response = handler.sendMessage(requestBody, callContext); @@ -126,7 +137,8 @@ public void testSendMessage() { @Test public void testSendMessageInvalidBody() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); String invalidBody = "invalid json"; RestHandler_v0_3.HTTPRestResponse response = handler.sendMessage(invalidBody, callContext); @@ -138,23 +150,26 @@ public void testSendMessageInvalidBody() { @Test public void testSendMessageWrongValueBody() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Invalid role value "user" instead of "ROLE_USER" - String requestBody = """ + String requestBody = + """ + { + "message": { - "message": - { - "messageId": "message-1234", - "contextId": "context-1234", - "role": "user", - "content": [{ - "text": "tell me a joke" - }], - "metadata": { - } + "messageId": "message-1234", + "contextId": "context-1234", + "role": "user", + "content": [{ + "text": "tell me a joke" + }], + "metadata": { } - }"""; + } + }\ + """; RestHandler_v0_3.HTTPRestResponse response = handler.sendMessage(requestBody, callContext); @@ -165,7 +180,8 @@ public void testSendMessageWrongValueBody() { @Test public void testSendMessageEmptyBody() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); RestHandler_v0_3.HTTPRestResponse response = handler.sendMessage("", callContext); @@ -180,17 +196,20 @@ public void testSendMessageEmptyBody() { @Test public void testCancelTaskSuccess() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save v0.3 task by converting to v1.0 taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // Configure agent to cancel the task - agentExecutorCancel = (context, emitter) -> { - emitter.cancel(); - }; + agentExecutorCancel = + (context, emitter) -> { + emitter.cancel(); + }; - RestHandler_v0_3.HTTPRestResponse response = handler.cancelTask(MINIMAL_TASK.id(), callContext); + RestHandler_v0_3.HTTPRestResponse response = + handler.cancelTask(MINIMAL_TASK.id(), callContext); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals("application/json", response.getContentType()); @@ -199,7 +218,8 @@ public void testCancelTaskSuccess() { @Test public void testCancelTaskNotFound() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); RestHandler_v0_3.HTTPRestResponse response = handler.cancelTask("nonexistent", callContext); @@ -214,71 +234,88 @@ public void testCancelTaskNotFound() { @Test public void testSendStreamingMessageSuccess() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Configure agent to emit the message back - agentExecutorExecute = (context, emitter) -> { - emitter.emitEvent(context.getMessage()); - }; - - String requestBody = """ - { - "message": { - "role": "ROLE_USER", - "content": [ - { - "text": "tell me some jokes" + agentExecutorExecute = + (context, emitter) -> { + emitter.emitEvent(context.getMessage()); + }; + + String requestBody = + """ + { + "message": { + "role": "ROLE_USER", + "content": [ + { + "text": "tell me some jokes" + } + ], + "messageId": "message-1234", + "contextId": "context-1234" + }, + "configuration": { + "acceptedOutputModes": ["text"] } - ], - "messageId": "message-1234", - "contextId": "context-1234" - }, - "configuration": { - "acceptedOutputModes": ["text"] - } - }"""; + }\ + """; - RestHandler_v0_3.HTTPRestResponse response = handler.sendStreamingMessage(requestBody, callContext); + RestHandler_v0_3.HTTPRestResponse response = + handler.sendStreamingMessage(requestBody, callContext); // Verify streaming response assertEquals(200, response.getStatusCode(), response.toString()); - assertInstanceOf(RestHandler_v0_3.HTTPRestStreamingResponse.class, response, + assertInstanceOf( + RestHandler_v0_3.HTTPRestStreamingResponse.class, + response, "Response should be HTTPRestStreamingResponse"); RestHandler_v0_3.HTTPRestStreamingResponse streamingResponse = (RestHandler_v0_3.HTTPRestStreamingResponse) response; assertNotNull(streamingResponse.getPublisher(), "Publisher should not be null"); - assertEquals("text/event-stream", streamingResponse.getContentType(), + assertEquals( + "text/event-stream", + streamingResponse.getContentType(), "Content type should be text/event-stream for SSE"); } @Test public void testSendStreamingMessageNotSupported() { // Create agent card with streaming disabled - AgentCard_v0_3 nonStreamingCard = new AgentCard_v0_3.Builder(CARD) - .capabilities(new AgentCapabilities_v0_3(false, true, false, null)) - .build(); + AgentCard_v0_3 nonStreamingCard = + new AgentCard_v0_3.Builder(CARD) + .capabilities(new AgentCapabilities_v0_3(false, true, false, null)) + .build(); - RestHandler_v0_3 handler = new RestHandler_v0_3(nonStreamingCard, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(nonStreamingCard, internalExecutor, convert03To10Handler); - String requestBody = """ - { - "message": { - "contextId": "ctx123", - "role": "ROLE_USER", - "content": [{ - "text": "Hello" - }] - } - }"""; + String requestBody = + """ + { + "message": { + "contextId": "ctx123", + "role": "ROLE_USER", + "content": [{ + "text": "Hello" + }] + } + }\ + """; - RestHandler_v0_3.HTTPRestResponse response = handler.sendStreamingMessage(requestBody, callContext); + RestHandler_v0_3.HTTPRestResponse response = + handler.sendStreamingMessage(requestBody, callContext); // Verify error response - assertEquals(400, response.getStatusCode(), "Should return 400 for streaming not supported"); - assertTrue(response.getBody().contains("InvalidRequestError"), + assertEquals( + 400, response.getStatusCode(), "Should return 400 for streaming not supported"); + assertTrue( + response.getBody().contains("InvalidRequestError"), "Error should be InvalidRequestError"); - assertTrue(response.getBody().contains("Streaming is not supported by the agent"), + assertTrue( + response.getBody().contains("Streaming is not supported by the agent"), "Error message should indicate streaming not supported"); } @@ -288,26 +325,32 @@ public void testSendStreamingMessageNotSupported() { @Test public void testPushNotificationConfigSuccess() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save task to v1.0 backend taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); - String requestBody = """ - { - "parent": "tasks/%s", - "config": { - "name": "tasks/%s/pushNotificationConfigs/", - "pushNotificationConfig": { - "url": "https://example.com/callback", - "authentication": { - "schemes": ["jwt"] + String requestBody = + """ + { + "parent": "tasks/%s", + "config": { + "name": "tasks/%s/pushNotificationConfigs/", + "pushNotificationConfig": { + "url": "https://example.com/callback", + "authentication": { + "schemes": ["jwt"] + } + } } - } - } - }""".formatted(MINIMAL_TASK.id(), MINIMAL_TASK.id()); + }\ + """ + .formatted(MINIMAL_TASK.id(), MINIMAL_TASK.id()); - RestHandler_v0_3.HTTPRestResponse response = handler.setTaskPushNotificationConfiguration(MINIMAL_TASK.id(), requestBody, callContext); + RestHandler_v0_3.HTTPRestResponse response = + handler.setTaskPushNotificationConfiguration( + MINIMAL_TASK.id(), requestBody, callContext); assertEquals(201, response.getStatusCode(), response.toString()); assertEquals("application/json", response.getContentType()); @@ -317,18 +360,23 @@ public void testPushNotificationConfigSuccess() { @Test public void testPushNotificationConfigNotSupported() { AgentCard_v0_3 card = createAgentCard(true, false, false); - RestHandler_v0_3 handler = new RestHandler_v0_3(card, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(card, internalExecutor, convert03To10Handler); - String requestBody = """ - { - "taskId": "%s", - "pushNotificationConfig": { - "url": "http://example.com" + String requestBody = + """ + { + "taskId": "%s", + "pushNotificationConfig": { + "url": "http://example.com" + } } - } - """.formatted(MINIMAL_TASK.id()); + """ + .formatted(MINIMAL_TASK.id()); - RestHandler_v0_3.HTTPRestResponse response = handler.setTaskPushNotificationConfiguration(MINIMAL_TASK.id(), requestBody, callContext); + RestHandler_v0_3.HTTPRestResponse response = + handler.setTaskPushNotificationConfiguration( + MINIMAL_TASK.id(), requestBody, callContext); assertEquals(501, response.getStatusCode()); assertTrue(response.getBody().contains("PushNotificationNotSupportedError")); @@ -336,16 +384,25 @@ public void testPushNotificationConfigNotSupported() { @Test public void testSetPushConfigUsesUrlTaskIdAndExplicitConfigId() { - for (String body : new String[] { - pushConfigBody("\"parent\":\"tasks/task\",\"configId\":\"chosen\",", - "tasks/task/pushNotificationConfigs/chosen"), - pushConfigBody("\"configId\":\"chosen\",", ""), - pushConfigBody("", "tasks/task/pushNotificationConfigs/chosen")}) { + for (String body : + new String[] { + pushConfigBody( + "\"parent\":\"tasks/task\",\"configId\":\"chosen\",", + "tasks/task/pushNotificationConfigs/chosen"), + pushConfigBody("\"configId\":\"chosen\",", ""), + pushConfigBody("", "tasks/task/pushNotificationConfigs/chosen") + }) { RequestHandler backend = mock(RequestHandler.class); - when(backend.onCreateTaskPushNotificationConfig(any(), any())).thenThrow(new InternalError("delegated")); - assertWireError(validationHandler(true, backend).setTaskPushNotificationConfiguration("task", body, callContext), - 500, "InternalError", "delegated"); - ArgumentCaptor config = ArgumentCaptor.forClass(TaskPushNotificationConfig.class); + when(backend.onCreateTaskPushNotificationConfig(any(), any())) + .thenThrow(new InternalError("delegated")); + assertWireError( + validationHandler(true, backend) + .setTaskPushNotificationConfiguration("task", body, callContext), + 500, + "InternalError", + "delegated"); + ArgumentCaptor config = + ArgumentCaptor.forClass(TaskPushNotificationConfig.class); verify(backend).onCreateTaskPushNotificationConfig(config.capture(), any()); assertEquals("task", config.getValue().taskId()); assertEquals("chosen", config.getValue().id()); @@ -355,13 +412,25 @@ public void testSetPushConfigUsesUrlTaskIdAndExplicitConfigId() { @Test public void testSetPushConfigDefaultsAbsentConfigIdToUrlTaskId() { for (String configId : new String[] {null, "", " "}) { - for (String name : new String[] {"", "tasks/task/pushNotificationConfigs", "tasks/task/pushNotificationConfigs/"}) { + for (String name : + new String[] { + "", + "tasks/task/pushNotificationConfigs", + "tasks/task/pushNotificationConfigs/" + }) { String fields = configId == null ? "" : "\"configId\":\"" + configId + "\","; RequestHandler backend = mock(RequestHandler.class); - when(backend.onCreateTaskPushNotificationConfig(any(), any())).thenThrow(new InternalError("delegated")); - assertWireError(validationHandler(true, backend).setTaskPushNotificationConfiguration("task", - pushConfigBody(fields, name), callContext), 500, "InternalError", "delegated"); - ArgumentCaptor config = ArgumentCaptor.forClass(TaskPushNotificationConfig.class); + when(backend.onCreateTaskPushNotificationConfig(any(), any())) + .thenThrow(new InternalError("delegated")); + assertWireError( + validationHandler(true, backend) + .setTaskPushNotificationConfiguration( + "task", pushConfigBody(fields, name), callContext), + 500, + "InternalError", + "delegated"); + ArgumentCaptor config = + ArgumentCaptor.forClass(TaskPushNotificationConfig.class); verify(backend).onCreateTaskPushNotificationConfig(config.capture(), any()); assertEquals("task", config.getValue().taskId()); assertEquals("task", config.getValue().id()); @@ -371,16 +440,23 @@ public void testSetPushConfigDefaultsAbsentConfigIdToUrlTaskId() { @Test public void testSetPushConfigRejectsConflictingAndMalformedResourceNames() { - for (String body : new String[] { - pushConfigBody("\"parent\":\"tasks/other\",", "tasks/task/pushNotificationConfigs/"), - pushConfigBody("\"parent\":\"tasks/task/extra\",", "tasks/task/pushNotificationConfigs/"), - pushConfigBody("", "tasks/other/pushNotificationConfigs/chosen"), - pushConfigBody("", "tasks/task/pushNotificationConfigs/chosen/extra"), - pushConfigBody("", "tasks/task/notPushNotificationConfigs/chosen"), - pushConfigBody("\"configId\":\"other\",", "tasks/task/pushNotificationConfigs/chosen")}) { + for (String body : + new String[] { + pushConfigBody( + "\"parent\":\"tasks/other\",", "tasks/task/pushNotificationConfigs/"), + pushConfigBody( + "\"parent\":\"tasks/task/extra\",", + "tasks/task/pushNotificationConfigs/"), + pushConfigBody("", "tasks/other/pushNotificationConfigs/chosen"), + pushConfigBody("", "tasks/task/pushNotificationConfigs/chosen/extra"), + pushConfigBody("", "tasks/task/notPushNotificationConfigs/chosen"), + pushConfigBody( + "\"configId\":\"other\",", "tasks/task/pushNotificationConfigs/chosen") + }) { RequestHandler backend = mock(RequestHandler.class); - RestHandler_v0_3.HTTPRestResponse response = validationHandler(true, backend) - .setTaskPushNotificationConfiguration("task", body, callContext); + RestHandler_v0_3.HTTPRestResponse response = + validationHandler(true, backend) + .setTaskPushNotificationConfiguration("task", body, callContext); assertEquals(422, response.getStatusCode(), body); assertTrue(response.getBody().contains("InvalidParamsError"), response.getBody()); verifyNoInteractions(backend); @@ -389,71 +465,93 @@ public void testSetPushConfigRejectsConflictingAndMalformedResourceNames() { @Test public void testSetPushConfigCapabilityPreflightPrecedesBodyValidation() { - for (String body : new String[] {"{broken", pushConfigBody("\"parent\":\"tasks/other\",", "bad/name")}) { + for (String body : + new String[] { + "{broken", pushConfigBody("\"parent\":\"tasks/other\",", "bad/name") + }) { RequestHandler backend = mock(RequestHandler.class); - RestHandler_v0_3.HTTPRestResponse response = validationHandler(false, backend) - .setTaskPushNotificationConfiguration("task", body, callContext); + RestHandler_v0_3.HTTPRestResponse response = + validationHandler(false, backend) + .setTaskPushNotificationConfiguration("task", body, callContext); assertEquals(501, response.getStatusCode()); - assertTrue(response.getBody().contains("PushNotificationNotSupportedError"), response.getBody()); + assertTrue( + response.getBody().contains("PushNotificationNotSupportedError"), + response.getBody()); verifyNoInteractions(backend); } } private String pushConfigBody(String fields, String name) { - return "{" + fields + "\"config\":{\"name\":\"" + name + return "{" + + fields + + "\"config\":{\"name\":\"" + + name + "\",\"pushNotificationConfig\":{\"url\":\"https://example.com/callback\"}}}"; } @Test public void testGetPushNotificationConfig() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save task to v1.0 backend taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); // First, create a push notification config - String createRequestBody = """ - { - "parent": "tasks/%s", - "config": { - "name": "tasks/%s/pushNotificationConfigs/", - "pushNotificationConfig": { - "url": "https://example.com/callback", - "authentication": { - "schemes": ["jwt"] + String createRequestBody = + """ + { + "parent": "tasks/%s", + "config": { + "name": "tasks/%s/pushNotificationConfigs/", + "pushNotificationConfig": { + "url": "https://example.com/callback", + "authentication": { + "schemes": ["jwt"] + } + } } - } - } - }""".formatted(MINIMAL_TASK.id(), MINIMAL_TASK.id()); - RestHandler_v0_3.HTTPRestResponse response = handler.setTaskPushNotificationConfiguration(MINIMAL_TASK.id(), createRequestBody, callContext); + }\ + """ + .formatted(MINIMAL_TASK.id(), MINIMAL_TASK.id()); + RestHandler_v0_3.HTTPRestResponse response = + handler.setTaskPushNotificationConfiguration( + MINIMAL_TASK.id(), createRequestBody, callContext); assertEquals(201, response.getStatusCode(), response.toString()); assertEquals("application/json", response.getContentType()); // Now get it (using taskId as configId since that's the default) - response = handler.getTaskPushNotificationConfiguration(MINIMAL_TASK.id(), MINIMAL_TASK.id(), callContext); + response = + handler.getTaskPushNotificationConfiguration( + MINIMAL_TASK.id(), MINIMAL_TASK.id(), callContext); assertEquals(200, response.getStatusCode(), response.toString()); assertEquals("application/json", response.getContentType()); } @Test public void testDeletePushNotificationConfig() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save task to v1.0 backend taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); - RestHandler_v0_3.HTTPRestResponse response = handler.deleteTaskPushNotificationConfiguration(MINIMAL_TASK.id(), MINIMAL_TASK.id(), callContext); + RestHandler_v0_3.HTTPRestResponse response = + handler.deleteTaskPushNotificationConfiguration( + MINIMAL_TASK.id(), MINIMAL_TASK.id(), callContext); assertEquals(204, response.getStatusCode()); } @Test public void testListPushNotificationConfigs() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); // Save task to v1.0 backend taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); - RestHandler_v0_3.HTTPRestResponse response = handler.listTaskPushNotificationConfigurations(MINIMAL_TASK.id(), callContext); + RestHandler_v0_3.HTTPRestResponse response = + handler.listTaskPushNotificationConfigurations(MINIMAL_TASK.id(), callContext); assertEquals(200, response.getStatusCode()); assertEquals("application/json", response.getContentType()); @@ -461,61 +559,87 @@ public void testListPushNotificationConfigs() { @Test public void testPushNotificationConfigParseError_DoesNotLogSensitiveData() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); taskStore.save(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK), false); - String requestBody = """ - { - "name": "tasks/%s/pushNotificationConfigs/", - "pushNotificationConfig": { - "url": "https://webhook.example.com", - "token": "secret-token-12345", - "authentication": { - "scheme": "Bearer", - "credentials": "oauth-secret-67890" - }, - "unknownField": "trigger-parse-error" - } - }""".formatted(MINIMAL_TASK.id()); + String requestBody = + """ + { + "name": "tasks/%s/pushNotificationConfigs/", + "pushNotificationConfig": { + "url": "https://webhook.example.com", + "token": "secret-token-12345", + "authentication": { + "scheme": "Bearer", + "credentials": "oauth-secret-67890" + }, + "unknownField": "trigger-parse-error" + } + }\ + """ + .formatted(MINIMAL_TASK.id()); // Request body with sensitive data and an unknown field to trigger parse error - java.util.logging.Logger logger = java.util.logging.Logger.getLogger(RestHandler_v0_3.class.getName()); - LogCaptureAssertions.assertSensitiveDataNotLogged(logger, - () -> assertEquals(422, - handler.setTaskPushNotificationConfiguration(MINIMAL_TASK.id(), requestBody, callContext) - .getStatusCode()), - "secret-token-12345", "oauth-secret-67890"); + java.util.logging.Logger logger = + java.util.logging.Logger.getLogger(RestHandler_v0_3.class.getName()); + LogCaptureAssertions.assertSensitiveDataNotLogged( + logger, + () -> + assertEquals( + 422, + handler.setTaskPushNotificationConfiguration( + MINIMAL_TASK.id(), requestBody, callContext) + .getStatusCode()), + "secret-token-12345", + "oauth-secret-67890"); } @Test public void testSharedErrorConversionPreservesWireMessage() { - RestHandler_v0_3 handler = new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); - RestHandler_v0_3.HTTPRestResponse response = handler.getTask("missing-conversion-task", null, null, callContext); + RestHandler_v0_3 handler = + new RestHandler_v0_3(CARD, internalExecutor, convert03To10Handler); + RestHandler_v0_3.HTTPRestResponse response = + handler.getTask("missing-conversion-task", null, null, callContext); assertEquals(404, response.getStatusCode()); assertEquals("application/json", response.getContentType()); assertEquals(Map.of(), response.getHeaders()); - assertEquals("{\"error\": \"org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3\", " - + "\"message\": \"Task not found\"}", response.getBody()); + assertEquals( + "{\"error\": \"org.a2aproject.sdk.compat03.spec.TaskNotFoundError_v0_3\", " + + "\"message\": \"Task not found\"}", + response.getBody()); response = handler.getTask("task", "-1", null, callContext); assertEquals(422, response.getStatusCode()); assertEquals("application/json", response.getContentType()); - assertEquals("{\"error\": \"org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3\", " - + "\"message\": \"bad history_length or historyLength\"}", response.getBody()); + assertEquals( + "{\"error\": \"org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3\", " + + "\"message\": \"bad history_length or historyLength\"}", + response.getBody()); } private RestHandler_v0_3 validationHandler(boolean capabilities, RequestHandler backend) { - AgentCard_v0_3 card = new AgentCard_v0_3.Builder(CARD) - .capabilities(new AgentCapabilities_v0_3(capabilities, capabilities, false, null)).build(); - return new RestHandler_v0_3(card, internalExecutor, new Convert_v0_3_To10RequestHandler(backend)); + AgentCard_v0_3 card = + new AgentCard_v0_3.Builder(CARD) + .capabilities( + new AgentCapabilities_v0_3(capabilities, capabilities, false, null)) + .build(); + return new RestHandler_v0_3( + card, internalExecutor, new Convert_v0_3_To10RequestHandler(backend)); } - private void assertWireError(RestHandler_v0_3.HTTPRestResponse response, int status, String type, String message) { + private void assertWireError( + RestHandler_v0_3.HTTPRestResponse response, int status, String type, String message) { assertEquals(RestHandler_v0_3.HTTPRestResponse.class, response.getClass()); assertEquals(status, response.getStatusCode()); assertEquals("application/json", response.getContentType()); assertEquals(Map.of(), response.getHeaders()); - assertEquals("{\"error\": \"org.a2aproject.sdk.compat03.spec." + type - + "_v0_3\", \"message\": \"" + message + "\"}", response.getBody()); + assertEquals( + "{\"error\": \"org.a2aproject.sdk.compat03.spec." + + type + + "_v0_3\", \"message\": \"" + + message + + "\"}", + response.getBody()); } @ParameterizedTest @@ -523,9 +647,21 @@ private void assertWireError(RestHandler_v0_3.HTTPRestResponse response, int sta public void testGetTaskRawAliasValidationOrder(String taskId) { RequestHandler backend = mock(RequestHandler.class); RestHandler_v0_3 handler = validationHandler(true, backend); - assertWireError(handler.getTask(taskId, "bad", "bad", callContext), 422, "InvalidParamsError", "bad task id"); - assertWireError(handler.getTask(taskId, "bad", null, callContext), 422, "InvalidParamsError", "bad task id"); - assertWireError(handler.getTask(taskId, null, "bad", callContext), 422, "InvalidParamsError", "bad task id"); + assertWireError( + handler.getTask(taskId, "bad", "bad", callContext), + 422, + "InvalidParamsError", + "bad task id"); + assertWireError( + handler.getTask(taskId, "bad", null, callContext), + 422, + "InvalidParamsError", + "bad task id"); + assertWireError( + handler.getTask(taskId, null, "bad", callContext), + 422, + "InvalidParamsError", + "bad task id"); verifyNoInteractions(backend); } @@ -534,27 +670,40 @@ public void testGetTaskRawAliases() { RequestHandler backend = mock(RequestHandler.class); RestHandler_v0_3 handler = validationHandler(true, backend); for (String snake : new String[] {"", "bad", "2"}) { - assertWireError(handler.getTask("task", snake, "bad", callContext), 422, "InvalidParamsError", + assertWireError( + handler.getTask("task", snake, "bad", callContext), + 422, + "InvalidParamsError", "Only one of 'history_length' or 'historyLength' may be specified"); } for (String value : new String[] {"", "bad", "2147483648", "-1"}) { - assertWireError(handler.getTask("task", value, null, callContext), 422, "InvalidParamsError", + assertWireError( + handler.getTask("task", value, null, callContext), + 422, + "InvalidParamsError", "bad history_length or historyLength"); - assertWireError(handler.getTask("task", null, value, callContext), 422, "InvalidParamsError", + assertWireError( + handler.getTask("task", null, value, callContext), + 422, + "InvalidParamsError", "bad history_length or historyLength"); } verifyNoInteractions(backend); for (String[] aliases : new String[][] {{null, null}, {"2", null}, {null, "2"}}) { RequestHandler successfulBackend = mock(RequestHandler.class); RestHandler_v0_3 successfulHandler = validationHandler(true, successfulBackend); - when(successfulBackend.onGetTask(any(), any())).thenReturn(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK)); - RestHandler_v0_3.HTTPRestResponse response = successfulHandler.getTask("task", aliases[0], aliases[1], callContext); + when(successfulBackend.onGetTask(any(), any())) + .thenReturn(TaskMapper_v0_3.INSTANCE.toV10(MINIMAL_TASK)); + RestHandler_v0_3.HTTPRestResponse response = + successfulHandler.getTask("task", aliases[0], aliases[1], callContext); assertEquals(200, response.getStatusCode()); assertEquals("application/json", response.getContentType()); assertTrue(response.getBody().contains(MINIMAL_TASK.id())); ArgumentCaptor query = ArgumentCaptor.forClass(TaskQueryParams.class); verify(successfulBackend).onGetTask(query.capture(), any()); - assertEquals(aliases[0] == null && aliases[1] == null ? null : Integer.valueOf(2), query.getValue().historyLength()); + assertEquals( + aliases[0] == null && aliases[1] == null ? null : Integer.valueOf(2), + query.getValue().historyLength()); } } @@ -562,8 +711,11 @@ public void testGetTaskRawAliases() { @NullAndEmptySource public void testCancelTaskPathValidationOrder(String taskId) { RequestHandler backend = mock(RequestHandler.class); - assertWireError(validationHandler(true, backend).cancelTask(taskId, callContext), - 422, "InvalidParamsError", "bad task id"); + assertWireError( + validationHandler(true, backend).cancelTask(taskId, callContext), + 422, + "InvalidParamsError", + "bad task id"); verifyNoInteractions(backend); } @@ -572,8 +724,11 @@ public void testCancelTaskPathValidationOrder(String taskId) { public void testResubscribeTaskPathValidationOrder(String taskId) { RequestHandler backend = mock(RequestHandler.class); for (boolean capabilities : new boolean[] {true, false}) { - assertWireError(validationHandler(capabilities, backend).resubscribeTask(taskId, callContext), - 422, "InvalidParamsError", "bad task id"); + assertWireError( + validationHandler(capabilities, backend).resubscribeTask(taskId, callContext), + 422, + "InvalidParamsError", + "bad task id"); } verifyNoInteractions(backend); } @@ -583,8 +738,12 @@ public void testResubscribeTaskPathValidationOrder(String taskId) { public void testSetPushConfigPathValidationOrder(String taskId) { RequestHandler backend = mock(RequestHandler.class); for (boolean capabilities : new boolean[] {true, false}) { - assertWireError(validationHandler(capabilities, backend).setTaskPushNotificationConfiguration(taskId, "{broken", callContext), - 422, "InvalidParamsError", "bad task id"); + assertWireError( + validationHandler(capabilities, backend) + .setTaskPushNotificationConfiguration(taskId, "{broken", callContext), + 422, + "InvalidParamsError", + "bad task id"); } verifyNoInteractions(backend); } @@ -594,8 +753,12 @@ public void testSetPushConfigPathValidationOrder(String taskId) { public void testGetPushConfigPathValidationOrder(String taskId) { RequestHandler backend = mock(RequestHandler.class); for (boolean capabilities : new boolean[] {true, false}) { - assertWireError(validationHandler(capabilities, backend).getTaskPushNotificationConfiguration(taskId, null, callContext), - 422, "InvalidParamsError", "bad task id"); + assertWireError( + validationHandler(capabilities, backend) + .getTaskPushNotificationConfiguration(taskId, null, callContext), + 422, + "InvalidParamsError", + "bad task id"); } verifyNoInteractions(backend); } @@ -604,10 +767,16 @@ public void testGetPushConfigPathValidationOrder(String taskId) { @NullAndEmptySource public void testGetPushConfigPermitsMissingConfigId(String configId) { RequestHandler backend = mock(RequestHandler.class); - when(backend.onGetTaskPushNotificationConfig(any(), any())).thenThrow(new InternalError("delegated")); - assertWireError(validationHandler(true, backend).getTaskPushNotificationConfiguration("task", configId, callContext), - 500, "InternalError", "delegated"); - ArgumentCaptor query = ArgumentCaptor.forClass(GetTaskPushNotificationConfigParams.class); + when(backend.onGetTaskPushNotificationConfig(any(), any())) + .thenThrow(new InternalError("delegated")); + assertWireError( + validationHandler(true, backend) + .getTaskPushNotificationConfiguration("task", configId, callContext), + 500, + "InternalError", + "delegated"); + ArgumentCaptor query = + ArgumentCaptor.forClass(GetTaskPushNotificationConfigParams.class); verify(backend).onGetTaskPushNotificationConfig(query.capture(), any()); assertEquals(configId == null ? "task" : "", query.getValue().id()); } @@ -617,8 +786,12 @@ public void testGetPushConfigPermitsMissingConfigId(String configId) { public void testListPushConfigsPathValidationOrder(String taskId) { RequestHandler backend = mock(RequestHandler.class); for (boolean capabilities : new boolean[] {true, false}) { - assertWireError(validationHandler(capabilities, backend).listTaskPushNotificationConfigurations(taskId, callContext), - 422, "InvalidParamsError", "bad task id"); + assertWireError( + validationHandler(capabilities, backend) + .listTaskPushNotificationConfigurations(taskId, callContext), + 422, + "InvalidParamsError", + "bad task id"); } verifyNoInteractions(backend); } @@ -629,12 +802,17 @@ public void testDeletePushConfigPathValidationOrder(String id) { RequestHandler backend = mock(RequestHandler.class); for (boolean capabilities : new boolean[] {true, false}) { RestHandler_v0_3 handler = validationHandler(capabilities, backend); - assertWireError(handler.deleteTaskPushNotificationConfiguration(id, id, callContext), - 422, "InvalidParamsError", "bad task id"); - assertWireError(handler.deleteTaskPushNotificationConfiguration("task", id, callContext), - 422, "InvalidParamsError", "bad config id"); + assertWireError( + handler.deleteTaskPushNotificationConfiguration(id, id, callContext), + 422, + "InvalidParamsError", + "bad task id"); + assertWireError( + handler.deleteTaskPushNotificationConfiguration("task", id, callContext), + 422, + "InvalidParamsError", + "bad config id"); } verifyNoInteractions(backend); } - } diff --git a/compat-0.3/transport/rest/src/test/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestTestTransportMetadata_v0_3.java b/compat-0.3/transport/rest/src/test/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestTestTransportMetadata_v0_3.java index 65484b4ed..506b1b480 100644 --- a/compat-0.3/transport/rest/src/test/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestTestTransportMetadata_v0_3.java +++ b/compat-0.3/transport/rest/src/test/java/org/a2aproject/sdk/compat03/transport/rest/handler/RestTestTransportMetadata_v0_3.java @@ -5,5 +5,4 @@ /** * Placeholder stub - awaiting server-common port. */ -public class RestTestTransportMetadata_v0_3 { -} +public class RestTestTransportMetadata_v0_3 {} diff --git a/docs/pom.xml b/docs/pom.xml index 3970e39b2..b1075ef2c 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -13,12 +13,12 @@ quarkus + 21 + true UTF-8 - true 2.1.6 2.1.4 - 21 - true + true @@ -104,6 +104,12 @@ + + io.quarkus.platform + quarkus-maven-plugin + ${quarkus.platform.version} + true + maven-enforcer-plugin ${maven-enforcer-plugin.version} @@ -125,24 +131,16 @@ - io.quarkus.platform - quarkus-maven-plugin - ${quarkus.platform.version} - true - - - maven-surefire-plugin + maven-failsafe-plugin + ${maven-surefire-plugin.version} @{argLine} + ${project.build.directory}/${project.build.finalName}-runner org.jboss.logmanager.LogManager ${maven.home} - - - maven-failsafe-plugin - ${maven-surefire-plugin.version} @@ -151,10 +149,12 @@ + + + maven-surefire-plugin @{argLine} - ${project.build.directory}/${project.build.finalName}-runner org.jboss.logmanager.LogManager ${maven.home} @@ -172,9 +172,9 @@ + true false false - true diff --git a/examples/cloud-deployment/server/pom.xml b/examples/cloud-deployment/server/pom.xml index 96a48f864..9909a9f96 100644 --- a/examples/cloud-deployment/server/pom.xml +++ b/examples/cloud-deployment/server/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/examples/cloud-deployment/server/src/main/java/org/a2aproject/sdk/examples/cloud/CloudAgentCardProducer.java b/examples/cloud-deployment/server/src/main/java/org/a2aproject/sdk/examples/cloud/CloudAgentCardProducer.java index 234e112ca..7e5110f49 100644 --- a/examples/cloud-deployment/server/src/main/java/org/a2aproject/sdk/examples/cloud/CloudAgentCardProducer.java +++ b/examples/cloud-deployment/server/src/main/java/org/a2aproject/sdk/examples/cloud/CloudAgentCardProducer.java @@ -1,13 +1,10 @@ package org.a2aproject.sdk.examples.cloud; - -import java.util.Collections; -import java.util.List; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; - +import java.util.Collections; +import java.util.List; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; @@ -30,32 +27,33 @@ public class CloudAgentCardProducer { public AgentCard agentCard() { return AgentCard.builder() .name("Cloud Deployment Demo Agent") - .description("Demonstrates A2A multi-pod deployment with Kafka event replication, " + - "PostgreSQL persistence, and round-robin load balancing across Kubernetes pods") - .supportedInterfaces(Collections.singletonList( - new AgentInterface("JSONRPC", agentUrl))) + .description( + "Demonstrates A2A multi-pod deployment with Kafka event replication," + + " PostgreSQL persistence, and round-robin load balancing across" + + " Kubernetes pods") + .supportedInterfaces( + Collections.singletonList(new AgentInterface("JSONRPC", agentUrl))) .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) .defaultInputModes(Collections.singletonList("text")) .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList( - AgentSkill.builder() - .id("multi_pod_demo") - .name("Multi-Pod Replication Demo") - .description("Demonstrates cross-pod event replication. " + - "Send 'start' to initialize, 'process' to add artifacts, " + - "'complete' to finalize. Each artifact shows which pod processed it.") - .tags(List.of("demo", "cloud", "kubernetes", "replication")) - .examples(List.of( - "start", - "process", - "complete" - )) - .build() - )) + .skills( + Collections.singletonList( + AgentSkill.builder() + .id("multi_pod_demo") + .name("Multi-Pod Replication Demo") + .description( + "Demonstrates cross-pod event replication. Send" + + " 'start' to initialize, 'process' to add" + + " artifacts, 'complete' to finalize. Each" + + " artifact shows which pod processed it.") + .tags(List.of("demo", "cloud", "kubernetes", "replication")) + .examples(List.of("start", "process", "complete")) + .build())) .build(); } } diff --git a/examples/cloud-deployment/server/src/main/java/org/a2aproject/sdk/examples/cloud/CloudAgentExecutorProducer.java b/examples/cloud-deployment/server/src/main/java/org/a2aproject/sdk/examples/cloud/CloudAgentExecutorProducer.java index 7e882254f..d2b74587a 100644 --- a/examples/cloud-deployment/server/src/main/java/org/a2aproject/sdk/examples/cloud/CloudAgentExecutorProducer.java +++ b/examples/cloud-deployment/server/src/main/java/org/a2aproject/sdk/examples/cloud/CloudAgentExecutorProducer.java @@ -1,10 +1,8 @@ package org.a2aproject.sdk.examples.cloud; -import java.util.List; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - +import java.util.List; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; @@ -49,7 +47,8 @@ public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2 try { // Extract user message and normalize - String messageText = extractTextFromMessage(context.getMessage()).trim().toLowerCase(); + String messageText = + extractTextFromMessage(context.getMessage()).trim().toLowerCase(); LOGGER.info("Received message: '{}'", messageText); // Get pod name from environment (set by Kubernetes Downward API) diff --git a/examples/cloud-deployment/server/src/test/java/org/a2aproject/sdk/examples/cloud/A2ACloudExampleClient.java b/examples/cloud-deployment/server/src/test/java/org/a2aproject/sdk/examples/cloud/A2ACloudExampleClient.java index 80fae4df5..966e0461f 100644 --- a/examples/cloud-deployment/server/src/test/java/org/a2aproject/sdk/examples/cloud/A2ACloudExampleClient.java +++ b/examples/cloud-deployment/server/src/test/java/org/a2aproject/sdk/examples/cloud/A2ACloudExampleClient.java @@ -9,7 +9,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; - import org.a2aproject.sdk.A2A; import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientEvent; @@ -48,11 +47,21 @@ */ public class A2ACloudExampleClient { - private static final String AGENT_URL = System.getProperty("agent.url", "http://localhost:8080"); - private static final int PROCESS_MESSAGE_COUNT = Integer.parseInt(System.getProperty("process.message.count", "8")); // Number of "process" messages to send - private static final int MESSAGE_INTERVAL_MS = Integer.parseInt(System.getProperty("message.interval.ms", "1500")); - private static final boolean CI_MODE = Boolean.parseBoolean(System.getProperty("ci.mode", "false")); // Early exit when 2 pods observed - private static final int MAX_MESSAGES_UNTIL_TWO_PODS = Integer.parseInt(System.getProperty("max.messages.until.two.pods", "20")); // Max messages before giving up + private static final String AGENT_URL = + System.getProperty("agent.url", "http://localhost:8080"); + private static final int PROCESS_MESSAGE_COUNT = + Integer.parseInt( + System.getProperty( + "process.message.count", "8")); // Number of "process" messages to send + private static final int MESSAGE_INTERVAL_MS = + Integer.parseInt(System.getProperty("message.interval.ms", "1500")); + private static final boolean CI_MODE = + Boolean.parseBoolean( + System.getProperty("ci.mode", "false")); // Early exit when 2 pods observed + private static final int MAX_MESSAGES_UNTIL_TWO_PODS = + Integer.parseInt( + System.getProperty( + "max.messages.until.two.pods", "20")); // Max messages before giving up private static final int MIN_PODS_TO_OBSERVE = 2; // Test state @@ -109,12 +118,14 @@ private AgentCard fetchAndConfigureAgentCard() { System.out.println("✓ Description: " + fetchedCard.description()); // Override agent card URL to use the port-forwarded URL instead of internal K8s service URL - AgentCard agentCard = AgentCard.builder(fetchedCard) - .supportedInterfaces( - Collections.singletonList( - // Use localhost URL for port-forwarded connection - new AgentInterface(TransportProtocol.JSONRPC.asString(), AGENT_URL))) - .build(); + AgentCard agentCard = + AgentCard.builder(fetchedCard) + .supportedInterfaces( + Collections.singletonList( + // Use localhost URL for port-forwarded connection + new AgentInterface( + TransportProtocol.JSONRPC.asString(), AGENT_URL))) + .build(); System.out.println(); return agentCard; } @@ -128,24 +139,22 @@ private String generateClientTaskId() { private void createClients(AgentCard agentCard) { System.out.println("Creating streaming client for subscription..."); - ClientConfig streamingConfig = new ClientConfig.Builder() - .setStreaming(true) - .build(); + ClientConfig streamingConfig = new ClientConfig.Builder().setStreaming(true).build(); - streamingClient = Client.builder(agentCard) - .clientConfig(streamingConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); + streamingClient = + Client.builder(agentCard) + .clientConfig(streamingConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); System.out.println("Creating non-streaming client for sending messages..."); - nonStreamingConfig = new ClientConfig.Builder() - .setStreaming(false) - .build(); + nonStreamingConfig = new ClientConfig.Builder().setStreaming(false).build(); - nonStreamingClient = Client.builder(agentCard) - .clientConfig(nonStreamingConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); + nonStreamingClient = + Client.builder(agentCard) + .clientConfig(nonStreamingConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); System.out.println("✓ Clients created"); System.out.println(); @@ -156,18 +165,22 @@ private void sendStartMessage(String clientTaskId) { Message startMessage = A2A.toUserMessage("start", clientTaskId); try { - nonStreamingClient.sendMessage(startMessage, List.of((ClientEvent event, AgentCard card) -> { - if (event instanceof TaskEvent te) { - serverTaskId = te.getTask().id(); - System.out.println("✓ Task created: " + serverTaskId); - System.out.println(" State: " + te.getTask().status().state()); - taskCreationLatch.countDown(); - } - }), error -> { - System.err.println("✗ Failed to create task: " + error.getMessage()); - testFailed.set(true); - taskCreationLatch.countDown(); - }); + nonStreamingClient.sendMessage( + startMessage, + List.of( + (ClientEvent event, AgentCard card) -> { + if (event instanceof TaskEvent te) { + serverTaskId = te.getTask().id(); + System.out.println("✓ Task created: " + serverTaskId); + System.out.println(" State: " + te.getTask().status().state()); + taskCreationLatch.countDown(); + } + }), + error -> { + System.err.println("✗ Failed to create task: " + error.getMessage()); + testFailed.set(true); + taskCreationLatch.countDown(); + }); // Wait for task creation to complete (max 5 seconds) if (!taskCreationLatch.await(5, TimeUnit.SECONDS)) { @@ -196,23 +209,32 @@ private void subscribeToTaskUpdates() { try { if (attempt > 1) { System.out.println("Retry attempt " + attempt + "/" + maxRetries + "..."); - Thread.sleep(1000); // Wait for Kafka events to propagate + Thread.sleep(1000); // Wait for Kafka events to propagate } streamingClient.subscribeToTask( new TaskIdParams(serverTaskId), List.of(this::handleSubscriptionEvent), - this::handleSubscriptionError - ); + this::handleSubscriptionError); System.out.println("✓ Subscribed to task updates"); subscribed.set(true); break; } catch (Exception e) { if (attempt < maxRetries) { - System.out.println("⚠ Failed to subscribe (attempt " + attempt + "/" + maxRetries + "): " + e.getMessage()); + System.out.println( + "⚠ Failed to subscribe (attempt " + + attempt + + "/" + + maxRetries + + "): " + + e.getMessage()); } else { - System.err.println("✗ Failed to subscribe after " + maxRetries + " attempts: " + e.getMessage()); + System.err.println( + "✗ Failed to subscribe after " + + maxRetries + + " attempts: " + + e.getMessage()); System.exit(1); } } @@ -236,7 +258,12 @@ private void handleSubscriptionEvent(ClientEvent event, AgentCard card) { if (podName != null && !podName.equals("unknown-pod")) { int invokeCount = observedPods.getOrDefault(podName, 0); observedPods.put(podName, ++invokeCount); - System.out.println(" → Pod: " + podName + " (Total unique pods: " + observedPods.size() + ")"); + System.out.println( + " → Pod: " + + podName + + " (Total unique pods: " + + observedPods.size() + + ")"); } } } else if (event instanceof TaskEvent te) { @@ -261,51 +288,78 @@ private void handleSubscriptionError(Throwable error) { private void sendProcessMessages() throws InterruptedException { System.out.println(); if (CI_MODE) { - System.out.println("Step 3: Sending 'process' messages until 2 pods observed (CI mode, max: " + MAX_MESSAGES_UNTIL_TWO_PODS + ", interval: " + MESSAGE_INTERVAL_MS + "ms)..."); + System.out.println( + "Step 3: Sending 'process' messages until 2 pods observed (CI mode, max: " + + MAX_MESSAGES_UNTIL_TWO_PODS + + ", interval: " + + MESSAGE_INTERVAL_MS + + "ms)..."); } else { - System.out.println("Step 3: Sending " + PROCESS_MESSAGE_COUNT + " 'process' messages (interval: " + MESSAGE_INTERVAL_MS + "ms)..."); + System.out.println( + "Step 3: Sending " + + PROCESS_MESSAGE_COUNT + + " 'process' messages (interval: " + + MESSAGE_INTERVAL_MS + + "ms)..."); } System.out.println("--------------------------------------------"); int messageCount = 0; int maxMessages = CI_MODE ? MAX_MESSAGES_UNTIL_TWO_PODS : PROCESS_MESSAGE_COUNT; - + while (messageCount < maxMessages) { messageCount++; final int messageNum = messageCount; // Create a new client for each request to force new HTTP connection - Client freshClient = Client.builder(agentCard) - .clientConfig(nonStreamingConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); - - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(new TextPart("process")) - .taskId(serverTaskId) - .build(); + Client freshClient = + Client.builder(agentCard) + .clientConfig(nonStreamingConfig) + .withTransport( + JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); + + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(new TextPart("process")) + .taskId(serverTaskId) + .build(); try { - freshClient.sendMessage(message, List.of((ClientEvent event, AgentCard card) -> { - if (event instanceof MessageEvent || event instanceof TaskEvent) { - System.out.println("✓ Process message " + messageNum + " sent"); - } - }), error -> { - System.err.println("✗ Process message " + messageNum + " failed: " + error.getMessage()); - testFailed.set(true); - }); + freshClient.sendMessage( + message, + List.of( + (ClientEvent event, AgentCard card) -> { + if (event instanceof MessageEvent + || event instanceof TaskEvent) { + System.out.println( + "✓ Process message " + messageNum + " sent"); + } + }), + error -> { + System.err.println( + "✗ Process message " + + messageNum + + " failed: " + + error.getMessage()); + testFailed.set(true); + }); Thread.sleep(MESSAGE_INTERVAL_MS); - + // In CI mode, check if we've observed 2 pods and can exit early if (CI_MODE && observedPods.size() >= 2) { System.out.println(); - System.out.println("✓ CI mode: Successfully observed 2 pods after " + messageNum + " messages. Stopping early."); + System.out.println( + "✓ CI mode: Successfully observed 2 pods after " + + messageNum + + " messages. Stopping early."); break; } } catch (Exception e) { - System.err.println("✗ Failed to send process message " + messageNum + ": " + e.getMessage()); + System.err.println( + "✗ Failed to send process message " + messageNum + ": " + e.getMessage()); testFailed.set(true); } } @@ -320,26 +374,35 @@ private void sendCompleteMessage() { System.out.println(); System.out.println("Step 4: Sending 'complete' to finalize task..."); - Client completeClient = Client.builder(agentCard) - .clientConfig(nonStreamingConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); + Client completeClient = + Client.builder(agentCard) + .clientConfig(nonStreamingConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); - Message completeMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(new TextPart("complete")) - .taskId(serverTaskId) - .build(); + Message completeMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(new TextPart("complete")) + .taskId(serverTaskId) + .build(); try { - completeClient.sendMessage(completeMessage, List.of((ClientEvent event, AgentCard card) -> { - if (event instanceof TaskEvent te) { - System.out.println("✓ Complete message sent, task state: " + te.getTask().status().state()); - } - }), error -> { - System.err.println("✗ Failed to send complete message: " + error.getMessage()); - testFailed.set(true); - }); + completeClient.sendMessage( + completeMessage, + List.of( + (ClientEvent event, AgentCard card) -> { + if (event instanceof TaskEvent te) { + System.out.println( + "✓ Complete message sent, task state: " + + te.getTask().status().state()); + } + }), + error -> { + System.err.println( + "✗ Failed to send complete message: " + error.getMessage()); + testFailed.set(true); + }); } catch (Exception e) { System.err.println("✗ Failed to send complete message: " + e.getMessage()); testFailed.set(true); @@ -368,16 +431,21 @@ private void printResults() { System.out.println("✗ TEST FAILED - Errors occurred during execution"); System.exit(1); } else if (observedPods.size() < MIN_PODS_TO_OBSERVE) { - System.out.printf("✗ TEST FAILED - Expected at least %d different pods, but only saw: %d\n", MIN_PODS_TO_OBSERVE, observedPods.size()); + System.out.printf( + "✗ TEST FAILED - Expected at least %d different pods, but only saw: %d\n", + MIN_PODS_TO_OBSERVE, observedPods.size()); System.out.println(" This suggests load balancing is not working correctly."); System.exit(1); } else { System.out.println("✓ TEST PASSED - Successfully demonstrated multi-pod processing!"); - System.out.println(" Messages were handled by " + observedPods.size() + " different pods."); + System.out.println( + " Messages were handled by " + observedPods.size() + " different pods."); System.out.println(" This proves that:"); System.out.println(" - Load balancing is working (round-robin across pods)"); - System.out.println(" - Event replication is working (subscriber sees events from all pods)"); - System.out.println(" - Database persistence is working (task state shared across pods)"); + System.out.println( + " - Event replication is working (subscriber sees events from all pods)"); + System.out.println( + " - Database persistence is working (task state shared across pods)"); System.exit(0); } } @@ -395,7 +463,8 @@ private static String extractTextFromArtifact(TaskArtifactUpdateEvent event) { } private static String extractPodName(String artifactText) { - // Artifact text format: "Started by " or "Processed by " or "Completed by " + // Artifact text format: "Started by " or "Processed by " or "Completed + // by " if (artifactText != null) { if (artifactText.startsWith("Started by ")) { return artifactText.substring("Started by ".length()).trim(); @@ -423,10 +492,10 @@ private static boolean isStreamClosedError(Throwable error) { String message = error.getMessage(); if (message != null) { // Filter out normal stream closure/cancellation errors - if (message.contains("Stream closed") || - message.contains("Stream") && message.contains("cancelled") || - message.contains("EOF reached") || - message.contains("CANCEL")) { + if (message.contains("Stream closed") + || message.contains("Stream") && message.contains("cancelled") + || message.contains("EOF reached") + || message.contains("CANCEL")) { return true; } } diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml index 4912d8b4e..89a803118 100644 --- a/examples/helloworld/client/pom.xml +++ b/examples/helloworld/client/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -63,8 +61,7 @@ org.apache.maven.plugins maven-compiler-plugin - - + org.apache.maven.plugins @@ -76,10 +73,10 @@ validate-jbang-versions - validate execute + validate @@ -104,4 +101,4 @@ - \ No newline at end of file + diff --git a/examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/client/HelloWorldClient.java b/examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/client/HelloWorldClient.java index 46a80ecb2..e6f05d7e9 100644 --- a/examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/client/HelloWorldClient.java +++ b/examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/client/HelloWorldClient.java @@ -4,6 +4,18 @@ import static org.a2aproject.sdk.extras.opentelemetry.client.OpenTelemetryClientTransportWrapper.OTEL_TRACER_KEY; import static org.a2aproject.sdk.extras.opentelemetry.client.propagation.OpenTelemetryClientPropagatorTransportWrapper.OTEL_OPEN_TELEMETRY_KEY; +import io.grpc.Channel; +import io.grpc.ManagedChannelBuilder; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -11,9 +23,8 @@ import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; import java.util.function.Consumer; - +import java.util.function.Function; import org.a2aproject.sdk.A2A; - import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.ClientEvent; @@ -31,19 +42,6 @@ import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.Part; import org.a2aproject.sdk.spec.TextPart; -import io.grpc.Channel; -import io.grpc.ManagedChannelBuilder; -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; -import io.opentelemetry.context.propagation.ContextPropagators; -import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; -import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; -import io.opentelemetry.sdk.OpenTelemetrySdk; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; -import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; -import io.opentelemetry.sdk.resources.Resource; -import io.opentelemetry.sdk.trace.SdkTracerProvider; -import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; -import java.util.function.Function; /** * A simple example of using the A2A Java SDK to communicate with an A2A server. @@ -57,69 +55,85 @@ public class HelloWorldClient { public static void main(String[] args) { OpenTelemetrySdk openTelemetrySdk = null; try { - AgentCard publicAgentCard = A2ACardResolver.builder().baseUrl(SERVER_URL).build().getAgentCard(); + AgentCard publicAgentCard = + A2ACardResolver.builder().baseUrl(SERVER_URL).build().getAgentCard(); System.out.println("Successfully fetched public agent card:"); System.out.println(JsonUtil.toJson(publicAgentCard)); System.out.println("Using public agent card for client initialization (default)."); AgentCard finalAgentCard = publicAgentCard; if (publicAgentCard.capabilities().extendedAgentCard()) { - System.out.println("Public card supports authenticated extended card. Attempting to fetch from: " + SERVER_URL + "/ExtendedAgentCard"); + System.out.println( + "Public card supports authenticated extended card. Attempting to fetch" + + " from: " + + SERVER_URL + + "/ExtendedAgentCard"); Map authHeaders = new HashMap<>(); authHeaders.put("Authorization", "Bearer dummy-token-for-extended-card"); - AgentCard extendedAgentCard = A2A.getAgentCard(SERVER_URL, "/ExtendedAgentCard", authHeaders); + AgentCard extendedAgentCard = + A2A.getAgentCard(SERVER_URL, "/ExtendedAgentCard", authHeaders); System.out.println("Successfully fetched authenticated extended agent card:"); System.out.println(JsonUtil.toJson(extendedAgentCard)); - System.out.println("Using AUTHENTICATED EXTENDED agent card for client initialization."); + System.out.println( + "Using AUTHENTICATED EXTENDED agent card for client initialization."); finalAgentCard = extendedAgentCard; } else { - System.out.println("Public card does not indicate support for an extended card. Using public card."); + System.out.println( + "Public card does not indicate support for an extended card. Using public" + + " card."); } final CompletableFuture messageResponse = new CompletableFuture<>(); // Create consumers list for handling client events List> consumers = new ArrayList<>(); - consumers.add((event, agentCard) -> { - if (event instanceof MessageEvent messageEvent) { - Message responseMessage = messageEvent.getMessage(); - StringBuilder textBuilder = new StringBuilder(); - if (responseMessage.parts() != null) { - for (Part part : responseMessage.parts()) { - if (part instanceof TextPart textPart) { - textBuilder.append(textPart.text()); + consumers.add( + (event, agentCard) -> { + if (event instanceof MessageEvent messageEvent) { + Message responseMessage = messageEvent.getMessage(); + StringBuilder textBuilder = new StringBuilder(); + if (responseMessage.parts() != null) { + for (Part part : responseMessage.parts()) { + if (part instanceof TextPart textPart) { + textBuilder.append(textPart.text()); + } + } } + messageResponse.complete(textBuilder.toString()); + } else { + System.out.println( + "Received client event: " + event.getClass().getSimpleName()); } - } - messageResponse.complete(textBuilder.toString()); - } else { - System.out.println("Received client event: " + event.getClass().getSimpleName()); - } - }); + }); // Create error handler for streaming errors - Consumer streamingErrorHandler = (error) -> { - System.err.println("Streaming error occurred: " + error.getMessage()); - error.printStackTrace(); - messageResponse.completeExceptionally(error); - }; + Consumer streamingErrorHandler = + (error) -> { + System.err.println("Streaming error occurred: " + error.getMessage()); + error.printStackTrace(); + messageResponse.completeExceptionally(error); + }; if (Boolean.getBoolean("opentelemetry")) { openTelemetrySdk = initOpenTelemetry(); } - ClientBuilder clientBuilder = Client - .builder(finalAgentCard) - .addConsumers(consumers) - .streamingErrorHandler(streamingErrorHandler); + ClientBuilder clientBuilder = + Client.builder(finalAgentCard) + .addConsumers(consumers) + .streamingErrorHandler(streamingErrorHandler); configureTransport(clientBuilder, openTelemetrySdk); - Client client = clientBuilder.build(); + Client client = clientBuilder.build(); - Message message = A2A.toUserMessage(MESSAGE_TEXT); // the message ID will be automatically generated for you + Message message = + A2A.toUserMessage( + MESSAGE_TEXT); // the message ID will be automatically generated for you try { System.out.println("Sending message: " + MESSAGE_TEXT); client.sendMessage(message); - System.out.println("Message sent successfully. Responses will be handled by the configured consumers."); + System.out.println( + "Message sent successfully. Responses will be handled by the configured" + + " consumers."); String responseText = messageResponse.get(); System.out.println("Response: " + responseText); @@ -140,28 +154,33 @@ public static void main(String[] args) { } static OpenTelemetrySdk initOpenTelemetry() { - Resource resource = Resource.getDefault().toBuilder() - .put("service.version", "1.0") - .put("service.name", "helloworld-client") - .build(); - - SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() - .addSpanProcessor(BatchSpanProcessor.builder( - OtlpGrpcSpanExporter.builder() - .setEndpoint("http://localhost:5317") - .build() - ).build()) - .setResource(resource) - .build(); - - SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder() - .registerMetricReader(PeriodicMetricReader.builder( - OtlpGrpcMetricExporter.builder() - .setEndpoint("http://localhost:5317") - .build() - ).build()) - .setResource(resource) - .build(); + Resource resource = + Resource.getDefault().toBuilder() + .put("service.version", "1.0") + .put("service.name", "helloworld-client") + .build(); + + SdkTracerProvider sdkTracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor( + BatchSpanProcessor.builder( + OtlpGrpcSpanExporter.builder() + .setEndpoint("http://localhost:5317") + .build()) + .build()) + .setResource(resource) + .build(); + + SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder() + .registerMetricReader( + PeriodicMetricReader.builder( + OtlpGrpcMetricExporter.builder() + .setEndpoint("http://localhost:5317") + .build()) + .build()) + .setResource(resource) + .build(); return OpenTelemetrySdk.builder() .setTracerProvider(sdkTracerProvider) @@ -169,18 +188,22 @@ static OpenTelemetrySdk initOpenTelemetry() { .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) .build(); } - private static void configureTransport(ClientBuilder clientBuilder, OpenTelemetrySdk openTelemetrySdk) { + + private static void configureTransport( + ClientBuilder clientBuilder, OpenTelemetrySdk openTelemetrySdk) { ClientTransportConfig transportConfig; - switch(System.getProperty("quarkus.agentcard.protocol", "JSONRPC")) { + switch (System.getProperty("quarkus.agentcard.protocol", "JSONRPC")) { case "GRPC": - Function channelFactory = url -> { - // Extract "localhost:9999" from "http://localhost:9999" - String target = url.replaceAll("^https?://", ""); - return ManagedChannelBuilder.forTarget(target) - .usePlaintext() // No TLS - .build(); - }; - transportConfig = new GrpcTransportConfigBuilder().channelFactory(channelFactory).build(); + Function channelFactory = + url -> { + // Extract "localhost:9999" from "http://localhost:9999" + String target = url.replaceAll("^https?://", ""); + return ManagedChannelBuilder.forTarget(target) + .usePlaintext() // No TLS + .build(); + }; + transportConfig = + new GrpcTransportConfigBuilder().channelFactory(channelFactory).build(); updateTransportConfig(transportConfig, openTelemetrySdk); clientBuilder.withTransport(GrpcTransport.class, transportConfig); break; @@ -198,7 +221,8 @@ private static void configureTransport(ClientBuilder clientBuilder, OpenTelemetr } } - private static void updateTransportConfig(ClientTransportConfig transportConfig, OpenTelemetrySdk openTelemetrySdk) { + private static void updateTransportConfig( + ClientTransportConfig transportConfig, OpenTelemetrySdk openTelemetrySdk) { if (openTelemetrySdk != null) { Map parameters = new HashMap<>(transportConfig.getParameters()); parameters.put(OTEL_TRACER_KEY, openTelemetrySdk.getTracer("helloworld-client")); diff --git a/examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/client/HelloWorldRunner.java b/examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/client/HelloWorldRunner.java index aec8b661d..bee188563 100644 --- a/examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/client/HelloWorldRunner.java +++ b/examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/client/HelloWorldRunner.java @@ -1,29 +1,28 @@ +/// usr/bin/env jbang "$0" "$@" ; exit $? -///usr/bin/env jbang "$0" "$@" ; exit $? - -//DEPS org.a2aproject.sdk:a2a-java-sdk-client:1.4.1.Final-SNAPSHOT -//DEPS org.a2aproject.sdk:a2a-java-sdk-client-transport-jsonrpc:1.4.1.Final-SNAPSHOT -//DEPS org.a2aproject.sdk:a2a-java-sdk-client-transport-grpc:1.4.1.Final-SNAPSHOT -//DEPS org.a2aproject.sdk:a2a-java-sdk-client-transport-rest:1.4.1.Final-SNAPSHOT -//DEPS org.a2aproject.sdk:a2a-java-sdk-opentelemetry-client:1.4.1.Final-SNAPSHOT -//DEPS org.a2aproject.sdk:a2a-java-sdk-opentelemetry-client-propagation:1.4.1.Final-SNAPSHOT -//DEPS io.opentelemetry:opentelemetry-sdk:1.60.1 -//DEPS io.opentelemetry:opentelemetry-exporter-otlp:1.60.1 -//DEPS io.opentelemetry:opentelemetry-exporter-logging:1.60.1 -//DEPS io.grpc:grpc-netty:1.81.0 -//SOURCES HelloWorldClient.java +// DEPS org.a2aproject.sdk:a2a-java-sdk-client:1.4.1.Final-SNAPSHOT +// DEPS org.a2aproject.sdk:a2a-java-sdk-client-transport-jsonrpc:1.4.1.Final-SNAPSHOT +// DEPS org.a2aproject.sdk:a2a-java-sdk-client-transport-grpc:1.4.1.Final-SNAPSHOT +// DEPS org.a2aproject.sdk:a2a-java-sdk-client-transport-rest:1.4.1.Final-SNAPSHOT +// DEPS org.a2aproject.sdk:a2a-java-sdk-opentelemetry-client:1.4.1.Final-SNAPSHOT +// DEPS org.a2aproject.sdk:a2a-java-sdk-opentelemetry-client-propagation:1.4.1.Final-SNAPSHOT +// DEPS io.opentelemetry:opentelemetry-sdk:1.60.1 +// DEPS io.opentelemetry:opentelemetry-exporter-otlp:1.60.1 +// DEPS io.opentelemetry:opentelemetry-exporter-logging:1.60.1 +// DEPS io.grpc:grpc-netty:1.81.0 +// SOURCES HelloWorldClient.java /** * JBang script to run the A2A HelloWorldClient example. * This script automatically handles the dependencies and runs the client. - * + * * Prerequisites: * - JBang installed (see https://www.jbang.dev/documentation/guide/latest/installation.html) * - A running A2A server (see README.md for instructions on setting up the Python server) - * - * Usage: + * + * Usage: * $ jbang HelloWorldRunner.java - * + * * The script will communicate with the A2A server at http://localhost:9999 */ public class HelloWorldRunner { diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 8a80f320a..d7fa834d2 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -15,11 +13,16 @@ pom Java SDK A2A Examples: Hello World + Examples for the Java SDK for the Agent2Agent Protocol (A2A) + + + client + server + true - Examples for the Java SDK for the Agent2Agent Protocol (A2A) @@ -36,9 +39,4 @@ - - - client - server - - \ No newline at end of file + diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index 053b91777..edd8050c5 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -54,6 +52,9 @@ io.quarkus quarkus-maven-plugin true + + --add-opens=java.base/java.lang=ALL-UNNAMED + @@ -63,9 +64,6 @@ - - --add-opens=java.base/java.lang=ALL-UNNAMED - @@ -90,4 +88,4 @@ - \ No newline at end of file + diff --git a/examples/helloworld/server/src/main/java/org/a2aproject/sdk/examples/helloworld/server/AgentCardProducer.java b/examples/helloworld/server/src/main/java/org/a2aproject/sdk/examples/helloworld/server/AgentCardProducer.java index 40e0b0b54..4af149479 100644 --- a/examples/helloworld/server/src/main/java/org/a2aproject/sdk/examples/helloworld/server/AgentCardProducer.java +++ b/examples/helloworld/server/src/main/java/org/a2aproject/sdk/examples/helloworld/server/AgentCardProducer.java @@ -1,18 +1,17 @@ package org.a2aproject.sdk.examples.helloworld.server; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import java.util.Collections; +import java.util.List; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.AgentSkill; import org.a2aproject.sdk.spec.TransportProtocol; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Produces; import org.eclipse.microprofile.config.inject.ConfigProperty; -import java.util.Collections; -import java.util.List; - @ApplicationScoped public class AgentCardProducer { @@ -31,28 +30,29 @@ public AgentCard agentCard() { .supportedInterfaces(Collections.singletonList(getAgentInterface())) .version("1.0.0") .documentationUrl("http://example.com/docs") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .build()) + .capabilities( + AgentCapabilities.builder().streaming(true).pushNotifications(true).build()) .defaultInputModes(Collections.singletonList("text")) .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(AgentSkill.builder() - .id("hello_world") - .name("Returns hello world") - .description("just returns hello world") - .tags(Collections.singletonList("hello world")) - .examples(List.of("hi", "hello world")) - .build())) + .skills( + Collections.singletonList( + AgentSkill.builder() + .id("hello_world") + .name("Returns hello world") + .description("just returns hello world") + .tags(Collections.singletonList("hello world")) + .examples(List.of("hi", "hello world")) + .build())) .build(); } private AgentInterface getAgentInterface() { TransportProtocol protocol = TransportProtocol.fromString(protocolStr); - String url = switch (protocol) { - case GRPC -> "localhost:9000"; - case JSONRPC, HTTP_JSON -> "http://localhost:9999"; - }; + String url = + switch (protocol) { + case GRPC -> "localhost:9000"; + case JSONRPC, HTTP_JSON -> "http://localhost:9999"; + }; return new AgentInterface(protocol.asString(), url); } } diff --git a/examples/helloworld/server/src/main/java/org/a2aproject/sdk/examples/helloworld/server/AgentExecutorProducer.java b/examples/helloworld/server/src/main/java/org/a2aproject/sdk/examples/helloworld/server/AgentExecutorProducer.java index a54bb8795..3224d97cb 100644 --- a/examples/helloworld/server/src/main/java/org/a2aproject/sdk/examples/helloworld/server/AgentExecutorProducer.java +++ b/examples/helloworld/server/src/main/java/org/a2aproject/sdk/examples/helloworld/server/AgentExecutorProducer.java @@ -2,7 +2,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; diff --git a/examples/stream-lifecycle/client/pom.xml b/examples/stream-lifecycle/client/pom.xml index 9b5f67eee..a067690eb 100644 --- a/examples/stream-lifecycle/client/pom.xml +++ b/examples/stream-lifecycle/client/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/examples/stream-lifecycle/client/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/client/StreamLifecycleClient.java b/examples/stream-lifecycle/client/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/client/StreamLifecycleClient.java index 74d5e941c..3a4705e50 100644 --- a/examples/stream-lifecycle/client/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/client/StreamLifecycleClient.java +++ b/examples/stream-lifecycle/client/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/client/StreamLifecycleClient.java @@ -3,6 +3,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.grpc.Channel; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -10,7 +13,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Function; - import org.a2aproject.sdk.A2A; import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientBuilder; @@ -35,10 +37,6 @@ import org.a2aproject.sdk.spec.TaskIdParams; import org.a2aproject.sdk.spec.TextPart; -import io.grpc.Channel; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; - /** * Demonstrates the TaskStreamLifecycleHook by connecting 3 subscribers to a task. *

    @@ -64,7 +62,8 @@ public class StreamLifecycleClient { public static void main(String[] args) throws Exception { try { - AgentCard agentCard = A2ACardResolver.builder().baseUrl(SERVER_URL).build().getAgentCard(); + AgentCard agentCard = + A2ACardResolver.builder().baseUrl(SERVER_URL).build().getAgentCard(); System.out.println("Resolved agent card: " + agentCard.name()); String protocol = System.getProperty("quarkus.agentcard.protocol", "JSONRPC"); runAndVerify(agentCard, protocol); @@ -89,28 +88,37 @@ public static void runAndVerify(AgentCard agentCard, String protocol) throws Exc CopyOnWriteArrayList sub2Events = new CopyOnWriteArrayList<>(); // --- Subscriber 1: Send a message (creates the task) --- - Thread sub1Thread = new Thread(() -> { - try { - Client client = createStreamingClient(agentCard, "Sub-1", (event, card) -> { - sub1Events.add(event); - logEvent("Sub-1", event); - if (taskIdRef.get() == null) { - String id = extractTaskId(event); - if (id != null) { - taskIdRef.set(id); - taskIdReady.countDown(); - } - } - }, sub1StreamDone, protocol); + Thread sub1Thread = + new Thread( + () -> { + try { + Client client = + createStreamingClient( + agentCard, + "Sub-1", + (event, card) -> { + sub1Events.add(event); + logEvent("Sub-1", event); + if (taskIdRef.get() == null) { + String id = extractTaskId(event); + if (id != null) { + taskIdRef.set(id); + taskIdReady.countDown(); + } + } + }, + sub1StreamDone, + protocol); - System.out.println("[Sub-1] Sending message to create task..."); - client.sendMessage(A2A.toUserMessage("Start streaming demo")); - } catch (Exception e) { - System.out.println("[Sub-1] Error: " + e.getMessage()); - } finally { - sub1StreamDone.countDown(); - } - }, "subscriber-1"); + System.out.println("[Sub-1] Sending message to create task..."); + client.sendMessage(A2A.toUserMessage("Start streaming demo")); + } catch (Exception e) { + System.out.println("[Sub-1] Error: " + e.getMessage()); + } finally { + sub1StreamDone.countDown(); + } + }, + "subscriber-1"); sub1Thread.start(); assertTrue(taskIdReady.await(10, TimeUnit.SECONDS), "Task should be created"); @@ -122,41 +130,62 @@ public static void runAndVerify(AgentCard agentCard, String protocol) throws Exc Thread.sleep(1500); // --- Subscriber 2: Subscribe to the existing task --- - Thread sub2Thread = new Thread(() -> { - try { - Client client = createStreamingClient(agentCard, "Sub-2", (event, card) -> { - sub2Events.add(event); - logEvent("Sub-2", event); - }, sub2StreamDone, protocol); + Thread sub2Thread = + new Thread( + () -> { + try { + Client client = + createStreamingClient( + agentCard, + "Sub-2", + (event, card) -> { + sub2Events.add(event); + logEvent("Sub-2", event); + }, + sub2StreamDone, + protocol); - System.out.println("[Sub-2] Subscribing to task " + taskId + "..."); - client.subscribeToTask(new TaskIdParams(taskId)); - } catch (Exception e) { - System.out.println("[Sub-2] Error: " + e.getMessage()); - } finally { - sub2StreamDone.countDown(); - } - }, "subscriber-2"); + System.out.println("[Sub-2] Subscribing to task " + taskId + "..."); + client.subscribeToTask(new TaskIdParams(taskId)); + } catch (Exception e) { + System.out.println("[Sub-2] Error: " + e.getMessage()); + } finally { + sub2StreamDone.countDown(); + } + }, + "subscriber-2"); sub2Thread.start(); System.out.println("\n=== Subscribers 1 and 2 are active — receiving events... ===\n"); Thread.sleep(2000); // --- Subscriber 3: Triggers the hook (closes all streams) --- - Thread sub3Thread = new Thread(() -> { - try { - Client client = createStreamingClient(agentCard, "Sub-3", (event, card) -> { - logEvent("Sub-3", event); - }, sub3StreamDone, protocol); + Thread sub3Thread = + new Thread( + () -> { + try { + Client client = + createStreamingClient( + agentCard, + "Sub-3", + (event, card) -> { + logEvent("Sub-3", event); + }, + sub3StreamDone, + protocol); - System.out.println("[Sub-3] Subscribing to task " + taskId + " (will trigger stream close)..."); - client.subscribeToTask(new TaskIdParams(taskId)); - } catch (Exception e) { - System.out.println("[Sub-3] Error: " + e.getMessage()); - } finally { - sub3StreamDone.countDown(); - } - }, "subscriber-3"); + System.out.println( + "[Sub-3] Subscribing to task " + + taskId + + " (will trigger stream close)..."); + client.subscribeToTask(new TaskIdParams(taskId)); + } catch (Exception e) { + System.out.println("[Sub-3] Error: " + e.getMessage()); + } finally { + sub3StreamDone.countDown(); + } + }, + "subscriber-3"); sub3Thread.start(); // Wait for all streams to close @@ -167,10 +196,14 @@ public static void runAndVerify(AgentCard agentCard, String protocol) throws Exc System.out.println("\n=== All streams closed. ==="); // Verify events were received - assertTrue(sub1Events.size() >= 2, - "Subscriber 1 should have received at least a status update and one artifact, got: " + sub1Events.size()); - assertTrue(sub2Events.size() >= 1, - "Subscriber 2 should have received at least a task snapshot, got: " + sub2Events.size()); + assertTrue( + sub1Events.size() >= 2, + "Subscriber 1 should have received at least a status update and one artifact, got: " + + sub1Events.size()); + assertTrue( + sub2Events.size() >= 1, + "Subscriber 2 should have received at least a task snapshot, got: " + + sub2Events.size()); } public static void shutdownGrpcChannels() { @@ -185,17 +218,22 @@ public static void shutdownGrpcChannels() { grpcChannels.clear(); } - private static Client createStreamingClient(AgentCard agentCard, String name, - BiConsumer consumer, - CountDownLatch streamDone, - String protocol) throws Exception { - ClientBuilder builder = Client.builder(agentCard) - .addConsumer(consumer) - .streamingErrorHandler(e -> { - System.out.println("[" + name + "] Stream closed."); - streamDone.countDown(); - }) - .clientConfig(ClientConfig.builder().setStreaming(true).build()); + private static Client createStreamingClient( + AgentCard agentCard, + String name, + BiConsumer consumer, + CountDownLatch streamDone, + String protocol) + throws Exception { + ClientBuilder builder = + Client.builder(agentCard) + .addConsumer(consumer) + .streamingErrorHandler( + e -> { + System.out.println("[" + name + "] Stream closed."); + streamDone.countDown(); + }) + .clientConfig(ClientConfig.builder().setStreaming(true).build()); configureTransport(builder, protocol); return builder.build(); } @@ -204,13 +242,16 @@ private static void configureTransport(ClientBuilder clientBuilder, String proto ClientTransportConfig transportConfig; switch (protocol) { case "GRPC": - Function channelFactory = url -> { - String target = url.replaceAll("^https?://", ""); - ManagedChannel channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); - grpcChannels.add(channel); - return channel; - }; - transportConfig = new GrpcTransportConfigBuilder().channelFactory(channelFactory).build(); + Function channelFactory = + url -> { + String target = url.replaceAll("^https?://", ""); + ManagedChannel channel = + ManagedChannelBuilder.forTarget(target).usePlaintext().build(); + grpcChannels.add(channel); + return channel; + }; + transportConfig = + new GrpcTransportConfigBuilder().channelFactory(channelFactory).build(); clientBuilder.withTransport(GrpcTransport.class, transportConfig); break; case "HTTP+JSON": @@ -227,14 +268,16 @@ private static void configureTransport(ClientBuilder clientBuilder, String proto public static void logEvent(String subscriber, ClientEvent event) { if (event instanceof TaskEvent te) { - System.out.printf("[%s] TaskEvent — state: %s, id: %s%n", + System.out.printf( + "[%s] TaskEvent — state: %s, id: %s%n", subscriber, te.getTask().status().state(), te.getTask().id()); } else if (event instanceof TaskUpdateEvent tue) { if (tue.getUpdateEvent() instanceof TaskArtifactUpdateEvent artifact) { String text = extractText(artifact); System.out.printf("[%s] ArtifactEvent — %s%n", subscriber, text); } else { - System.out.printf("[%s] StatusUpdate — %s%n", subscriber, tue.getTask().status().state()); + System.out.printf( + "[%s] StatusUpdate — %s%n", subscriber, tue.getTask().status().state()); } } else if (event instanceof MessageEvent me) { String text = extractText(me.getMessage()); diff --git a/examples/stream-lifecycle/pom.xml b/examples/stream-lifecycle/pom.xml index f0cb0f539..a5425f930 100644 --- a/examples/stream-lifecycle/pom.xml +++ b/examples/stream-lifecycle/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -15,11 +13,16 @@ pom Java SDK A2A Examples: Stream Lifecycle + Example demonstrating TaskStreamLifecycleHook for managing streaming connections + + + client + server + true - Example demonstrating TaskStreamLifecycleHook for managing streaming connections @@ -36,9 +39,4 @@ - - - client - server - diff --git a/examples/stream-lifecycle/server/pom.xml b/examples/stream-lifecycle/server/pom.xml index 64706bcba..191894141 100644 --- a/examples/stream-lifecycle/server/pom.xml +++ b/examples/stream-lifecycle/server/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -67,6 +65,9 @@ io.quarkus quarkus-maven-plugin true + + --add-opens=java.base/java.lang=ALL-UNNAMED + @@ -76,9 +77,6 @@ - - --add-opens=java.base/java.lang=ALL-UNNAMED - diff --git a/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/AgentCardProducer.java b/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/AgentCardProducer.java index 807a26fa8..024f52f6b 100644 --- a/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/AgentCardProducer.java +++ b/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/AgentCardProducer.java @@ -1,11 +1,9 @@ package org.a2aproject.sdk.examples.streamlifecycle.server; -import java.util.Collections; -import java.util.List; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - +import java.util.Collections; +import java.util.List; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; @@ -25,29 +23,34 @@ public class AgentCardProducer { public AgentCard agentCard() { return AgentCard.builder() .name("Stream Lifecycle Demo Agent") - .description("Demonstrates TaskStreamLifecycleHook — closes all streams when 3 subscribers connect") + .description( + "Demonstrates TaskStreamLifecycleHook — closes all streams when 3" + + " subscribers connect") .supportedInterfaces(Collections.singletonList(getAgentInterface())) .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .build()) + .capabilities(AgentCapabilities.builder().streaming(true).build()) .defaultInputModes(Collections.singletonList("text")) .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(AgentSkill.builder() - .id("stream_lifecycle") - .name("Stream lifecycle demo") - .description("Sends progress messages while subscribers connect and disconnect") - .tags(List.of("streaming", "lifecycle")) - .build())) + .skills( + Collections.singletonList( + AgentSkill.builder() + .id("stream_lifecycle") + .name("Stream lifecycle demo") + .description( + "Sends progress messages while subscribers connect" + + " and disconnect") + .tags(List.of("streaming", "lifecycle")) + .build())) .build(); } private AgentInterface getAgentInterface() { TransportProtocol protocol = TransportProtocol.fromString(protocolStr); - String url = switch (protocol) { - case GRPC -> "localhost:9000"; - case JSONRPC, HTTP_JSON -> "http://localhost:9999"; - }; + String url = + switch (protocol) { + case GRPC -> "localhost:9000"; + case JSONRPC, HTTP_JSON -> "http://localhost:9999"; + }; return new AgentInterface(protocol.asString(), url); } } diff --git a/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/AgentExecutorProducer.java b/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/AgentExecutorProducer.java index fe95deb21..43a01045b 100644 --- a/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/AgentExecutorProducer.java +++ b/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/AgentExecutorProducer.java @@ -2,7 +2,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; diff --git a/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/CloseStreamsHook.java b/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/CloseStreamsHook.java index 5ac50a084..0ac2ed40c 100644 --- a/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/CloseStreamsHook.java +++ b/examples/stream-lifecycle/server/src/main/java/org/a2aproject/sdk/examples/streamlifecycle/server/CloseStreamsHook.java @@ -3,7 +3,6 @@ import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Alternative; - import org.a2aproject.sdk.server.events.StreamCloseHandle; import org.a2aproject.sdk.server.events.TaskStreamLifecycleHook; import org.a2aproject.sdk.spec.Event; @@ -32,21 +31,28 @@ public void onSubscribe(String taskId, StreamCloseHandle handle) { LOGGER.info("[HOOK] Subscriber added for task {}. Active subscribers: {}", taskId, count); if (count >= MAX_SUBSCRIBERS) { - LOGGER.info("[HOOK] Subscriber count reached {} for task {} — closing all streams", - MAX_SUBSCRIBERS, taskId); + LOGGER.info( + "[HOOK] Subscriber count reached {} for task {} — closing all streams", + MAX_SUBSCRIBERS, + taskId); handle.closeStreams(); } } @Override public void onUnsubscribe(String taskId, StreamCloseHandle handle) { - LOGGER.info("[HOOK] Subscriber removed for task {}. Active subscribers: {}", - taskId, handle.getActiveSubscriberCount()); + LOGGER.info( + "[HOOK] Subscriber removed for task {}. Active subscribers: {}", + taskId, + handle.getActiveSubscriberCount()); } @Override public void onEvent(String taskId, Event event, StreamCloseHandle handle) { - LOGGER.info("[HOOK] Event distributed for task {}: {} (subscribers: {})", - taskId, event.getClass().getSimpleName(), handle.getActiveSubscriberCount()); + LOGGER.info( + "[HOOK] Event distributed for task {}: {} (subscribers: {})", + taskId, + event.getClass().getSimpleName(), + handle.getActiveSubscriberCount()); } } diff --git a/examples/stream-lifecycle/server/src/test/java/org/a2aproject/sdk/examples/streamlifecycle/server/StreamLifecycleTest.java b/examples/stream-lifecycle/server/src/test/java/org/a2aproject/sdk/examples/streamlifecycle/server/StreamLifecycleTest.java index 07258a975..9e327fc54 100644 --- a/examples/stream-lifecycle/server/src/test/java/org/a2aproject/sdk/examples/streamlifecycle/server/StreamLifecycleTest.java +++ b/examples/stream-lifecycle/server/src/test/java/org/a2aproject/sdk/examples/streamlifecycle/server/StreamLifecycleTest.java @@ -1,15 +1,13 @@ package org.a2aproject.sdk.examples.streamlifecycle.server; +import io.quarkus.test.junit.QuarkusTest; import java.util.List; import java.util.concurrent.TimeUnit; - import org.a2aproject.sdk.examples.streamlifecycle.client.StreamLifecycleClient; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.TransportProtocol; - -import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -28,13 +26,15 @@ void cleanupGrpcChannels() { @Test @Timeout(value = 60, unit = TimeUnit.SECONDS) void testStreamLifecycleWithJsonRpc() throws Exception { - StreamLifecycleClient.runAndVerify(buildCard(TransportProtocol.JSONRPC, HTTP_URL), "JSONRPC"); + StreamLifecycleClient.runAndVerify( + buildCard(TransportProtocol.JSONRPC, HTTP_URL), "JSONRPC"); } @Test @Timeout(value = 60, unit = TimeUnit.SECONDS) void testStreamLifecycleWithRest() throws Exception { - StreamLifecycleClient.runAndVerify(buildCard(TransportProtocol.HTTP_JSON, HTTP_URL), "HTTP+JSON"); + StreamLifecycleClient.runAndVerify( + buildCard(TransportProtocol.HTTP_JSON, HTTP_URL), "HTTP+JSON"); } @Test @@ -51,8 +51,7 @@ private AgentCard buildCard(TransportProtocol protocol, String url) { .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) .skills(List.of()) - .supportedInterfaces(List.of( - new AgentInterface(protocol.asString(), url))) + .supportedInterfaces(List.of(new AgentInterface(protocol.asString(), url))) .capabilities(AgentCapabilities.builder().streaming(true).build()) .build(); } diff --git a/extras/common/pom.xml b/extras/common/pom.xml index df300152c..ff50c338f 100644 --- a/extras/common/pom.xml +++ b/extras/common/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/common/src/main/java/org/a2aproject/sdk/extras/common/events/TaskFinalizedEvent.java b/extras/common/src/main/java/org/a2aproject/sdk/extras/common/events/TaskFinalizedEvent.java index ebf1ba4a3..4cb960cde 100644 --- a/extras/common/src/main/java/org/a2aproject/sdk/extras/common/events/TaskFinalizedEvent.java +++ b/extras/common/src/main/java/org/a2aproject/sdk/extras/common/events/TaskFinalizedEvent.java @@ -10,7 +10,8 @@ */ public class TaskFinalizedEvent { private final String taskId; - private final Object task; // Task type from org.a2aproject.sdk.spec - using Object to avoid dependency + private final Object + task; // Task type from org.a2aproject.sdk.spec - using Object to avoid dependency public TaskFinalizedEvent(String taskId, Object task) { this.taskId = taskId; diff --git a/extras/http-client-android/pom.xml b/extras/http-client-android/pom.xml index 4b3bd839f..9c682b3b0 100644 --- a/extras/http-client-android/pom.xml +++ b/extras/http-client-android/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClient.java b/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClient.java index bafd62f8b..8480656c9 100644 --- a/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClient.java +++ b/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClient.java @@ -1,11 +1,10 @@ package org.a2aproject.sdk.client.http.android; -import static org.a2aproject.sdk.util.Assert.checkNotNullParam; - import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_MULT_CHOICE; import static java.net.HttpURLConnection.HTTP_OK; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; +import static org.a2aproject.sdk.util.Assert.checkNotNullParam; import java.io.BufferedReader; import java.io.IOException; @@ -25,14 +24,13 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.function.Consumer; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpHeaders; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.client.http.BoundedLineAccumulator; import org.a2aproject.sdk.client.http.RedirectHeaderPolicy; -import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.client.http.SSEParserConfig; +import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.client.http.ServerSentEventParser; import org.a2aproject.sdk.common.A2AErrorMessages; import org.a2aproject.sdk.spec.A2AClientHTTPError; @@ -51,502 +49,541 @@ */ public class AndroidA2AHttpClient implements A2AHttpClient { - private static final Executor NET_EXECUTOR = Executors.newCachedThreadPool(r -> { - Thread t = new Thread(r, "A2A-Android-Net"); - t.setDaemon(true); - return t; - }); - - private final SSEParserConfig sseParserConfig; - private final RedirectHeaderPolicy redirectHeaderPolicy; - - public AndroidA2AHttpClient() { - this(SSEParserConfig.DEFAULT, RedirectHeaderPolicy.DEFAULT); - } - - /** - * Creates a new Android HTTP client with a custom redirect header policy. - * - * @param redirectHeaderPolicy policy applied to explicitly enabled POST redirects - * @return a client with default SSE parser limits and the supplied redirect policy - */ - public static AndroidA2AHttpClient withRedirectHeaderPolicy( - RedirectHeaderPolicy redirectHeaderPolicy) { - return new AndroidA2AHttpClient(SSEParserConfig.DEFAULT, redirectHeaderPolicy); - } - - /** - * Creates a new Android HTTP client with custom SSE parser limits. - * - * @param sseParserConfig the SSE parser configuration to use for streaming responses - */ - public AndroidA2AHttpClient(SSEParserConfig sseParserConfig) { - this(sseParserConfig, RedirectHeaderPolicy.DEFAULT); - } - - /** - * Creates a new Android HTTP client with custom parser limits and redirect policy. - * - * @param sseParserConfig the SSE parser configuration to use for streaming responses - * @param redirectHeaderPolicy policy applied to explicitly enabled POST redirects - */ - public AndroidA2AHttpClient( - SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { - this.sseParserConfig = checkNotNullParam("sseParserConfig", sseParserConfig); - this.redirectHeaderPolicy = checkNotNullParam("redirectHeaderPolicy", redirectHeaderPolicy); - } - - @Override - public GetBuilder createGet() { - return new AndroidGetBuilder(sseParserConfig, redirectHeaderPolicy); - } - - @Override - public PostBuilder createPost() { - return new AndroidPostBuilder(sseParserConfig, redirectHeaderPolicy); - } - - @Override - public DeleteBuilder createDelete() { - return new AndroidDeleteBuilder(sseParserConfig, redirectHeaderPolicy); - } - - private abstract static class AndroidBuilder> implements Builder { - protected String url = ""; - protected Map headers = new HashMap<>(); - protected final SSEParserConfig sseParserConfig; - protected final RedirectHeaderPolicy redirectHeaderPolicy; - - AndroidBuilder(SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { - this.sseParserConfig = sseParserConfig; - this.redirectHeaderPolicy = redirectHeaderPolicy; - } + private static final Executor NET_EXECUTOR = + Executors.newCachedThreadPool( + r -> { + Thread t = new Thread(r, "A2A-Android-Net"); + t.setDaemon(true); + return t; + }); - @Override - public T url(String url) { - this.url = url; - return self(); + private final SSEParserConfig sseParserConfig; + private final RedirectHeaderPolicy redirectHeaderPolicy; + + public AndroidA2AHttpClient() { + this(SSEParserConfig.DEFAULT, RedirectHeaderPolicy.DEFAULT); } - @Override - public T addHeader(String name, String value) { - headers.put(name, value); - return self(); + /** + * Creates a new Android HTTP client with a custom redirect header policy. + * + * @param redirectHeaderPolicy policy applied to explicitly enabled POST redirects + * @return a client with default SSE parser limits and the supplied redirect policy + */ + public static AndroidA2AHttpClient withRedirectHeaderPolicy( + RedirectHeaderPolicy redirectHeaderPolicy) { + return new AndroidA2AHttpClient(SSEParserConfig.DEFAULT, redirectHeaderPolicy); } - @Override - public T addHeaders(Map headers) { - if (headers != null) { - this.headers.putAll(headers); - } - return self(); + /** + * Creates a new Android HTTP client with custom SSE parser limits. + * + * @param sseParserConfig the SSE parser configuration to use for streaming responses + */ + public AndroidA2AHttpClient(SSEParserConfig sseParserConfig) { + this(sseParserConfig, RedirectHeaderPolicy.DEFAULT); } - @SuppressWarnings("unchecked") - protected T self() { - return (T) this; + /** + * Creates a new Android HTTP client with custom parser limits and redirect policy. + * + * @param sseParserConfig the SSE parser configuration to use for streaming responses + * @param redirectHeaderPolicy policy applied to explicitly enabled POST redirects + */ + public AndroidA2AHttpClient( + SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { + this.sseParserConfig = checkNotNullParam("sseParserConfig", sseParserConfig); + this.redirectHeaderPolicy = checkNotNullParam("redirectHeaderPolicy", redirectHeaderPolicy); } - protected HttpURLConnection createConnection(String method, boolean isSSE) throws IOException { - return createConnection(url, method, headers, isSSE); + @Override + public GetBuilder createGet() { + return new AndroidGetBuilder(sseParserConfig, redirectHeaderPolicy); } - protected HttpURLConnection createConnection( - String requestUrl, String method, Map requestHeaders, boolean isSSE) - throws IOException { - URL urlObj; - try { - urlObj = new URI(requestUrl).toURL(); - } catch (URISyntaxException e) { - throw new MalformedURLException("Invalid URL: " + url); - } - HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection(); - connection.setRequestMethod(method); - connection.setConnectTimeout(15000); // 15 seconds - connection.setReadTimeout(60000); // 60 seconds - // Security: Disable automatic redirect following to prevent credential leakage - connection.setInstanceFollowRedirects(false); - for (Map.Entry header : requestHeaders.entrySet()) { - connection.setRequestProperty(header.getKey(), header.getValue()); - } - if (isSSE) { - connection.setRequestProperty(A2AHttpClient.ACCEPT, A2AHttpClient.EVENT_STREAM); - } - return connection; + @Override + public PostBuilder createPost() { + return new AndroidPostBuilder(sseParserConfig, redirectHeaderPolicy); } - protected static String readStreamWithLimit(InputStream is) throws IOException { - if (is == null) { - return ""; - } - int maxResponseSize = 10 * 1024 * 1024; // 10 MB - try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { - StringBuilder sb = new StringBuilder(); - String line; - boolean first = true; - while ((line = reader.readLine()) != null) { - if (sb.length() + line.length() > maxResponseSize) { - throw new IOException("Response size exceeds limit"); - } - if (!first) { - sb.append('\n'); - } - sb.append(line); - first = false; - } - return sb.toString(); - } + @Override + public DeleteBuilder createDelete() { + return new AndroidDeleteBuilder(sseParserConfig, redirectHeaderPolicy); } - /** - * Reads the next line from {@code is}, treating LF, CRLF, and bare CR as terminators - * per the SSE specification, and enforcing the byte limit while accumulating - * bytes — not after a full line has been materialised. - * - *

    The caller must provide a {@link BoundedLineAccumulator} and call - * {@link BoundedLineAccumulator#reset()} between lines. After this method returns - * {@code true}, check {@link BoundedLineAccumulator#isTooLong()} to distinguish - * between a valid line and one that exceeded the limit. - * - *

    The {@code is} parameter must support {@link InputStream#mark(int)} (e.g. - * {@link java.io.BufferedInputStream}) so that CR at end-of-buffer can be resolved - * without consuming the following byte. - * - * @param is the input stream to read from (must support mark/reset) - * @param accumulator the accumulator to use for byte collection and limit enforcement - * @return {@code true} if a line was read, {@code false} at end-of-stream - */ - static boolean readBoundedLine(InputStream is, BoundedLineAccumulator accumulator) throws IOException { - boolean hasReadAnyBytes = false; + private abstract static class AndroidBuilder> implements Builder { + protected String url = ""; + protected Map headers = new HashMap<>(); + protected final SSEParserConfig sseParserConfig; + protected final RedirectHeaderPolicy redirectHeaderPolicy; - while (true) { - int b = is.read(); - if (b == -1) { - return hasReadAnyBytes || accumulator.isTooLong(); + AndroidBuilder(SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { + this.sseParserConfig = sseParserConfig; + this.redirectHeaderPolicy = redirectHeaderPolicy; } - hasReadAnyBytes = true; - if (b == '\n') { - return true; + + @Override + public T url(String url) { + this.url = url; + return self(); } - if (b == '\r') { - consumeOptionalLF(is); - return true; + + @Override + public T addHeader(String name, String value) { + headers.put(name, value); + return self(); } - accumulator.addByte(b); - } - } - private static void consumeOptionalLF(InputStream is) throws IOException { - is.mark(1); - int next = is.read(); - if (next != '\n' && next != -1) { - is.reset(); - } - } + @Override + public T addHeaders(Map headers) { + if (headers != null) { + this.headers.putAll(headers); + } + return self(); + } - protected A2AHttpResponse execute(HttpURLConnection connection) throws IOException { - int status = connection.getResponseCode(); - A2AHttpHeaders responseHeaders = fromConnectionHeaders(connection.getHeaderFields()); - if (status == HTTP_UNAUTHORIZED) { - throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED, - new A2AClientHTTPError(HTTP_UNAUTHORIZED, A2AErrorMessages.AUTHENTICATION_FAILED, - null, responseHeaders.toMap())); - } else if (status == HTTP_FORBIDDEN) { - throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED, - new A2AClientHTTPError(HTTP_FORBIDDEN, A2AErrorMessages.AUTHORIZATION_FAILED, - null, responseHeaders.toMap())); - } - - String body = ""; - try (InputStream is = - (status >= HTTP_OK && status < HTTP_MULT_CHOICE) - ? connection.getInputStream() - : connection.getErrorStream()) { - body = readStreamWithLimit(is); - } - - return new AndroidHttpResponse(status, body, responseHeaders); - } + @SuppressWarnings("unchecked") + protected T self() { + return (T) this; + } - protected void processSSEResponse( - HttpURLConnection connection, - Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) { - try { - int status = connection.getResponseCode(); - if (!(status >= HTTP_OK && status < HTTP_MULT_CHOICE)) { - if (status == HTTP_UNAUTHORIZED || status == HTTP_FORBIDDEN) { - A2AHttpHeaders responseHeaders = fromConnectionHeaders(connection.getHeaderFields()); - String msg = status == HTTP_UNAUTHORIZED - ? A2AErrorMessages.AUTHENTICATION_FAILED - : A2AErrorMessages.AUTHORIZATION_FAILED; - errorConsumer.accept(new IOException(msg, - new A2AClientHTTPError(status, msg, null, responseHeaders.toMap()))); - return; - } - - String errorBody = ""; - try (InputStream es = connection.getErrorStream()) { - errorBody = readStreamWithLimit(es); - } - // Pass the error body through messageConsumer so higher-level listeners - // (e.g. RestErrorMapper in SSEEventListener) can produce a typed error. - // Do not also call errorConsumer here — the messageConsumer path is responsible - // for signalling the error, matching the async JDK client's behaviour. - if (!errorBody.isEmpty()) { - messageConsumer.accept(new ServerSentEvent(errorBody)); - } else { - errorConsumer.accept( - new IOException("Request failed with status " + status)); - } - return; + protected HttpURLConnection createConnection(String method, boolean isSSE) + throws IOException { + return createConnection(url, method, headers, isSSE); } - String contentType = connection.getContentType(); - boolean isSse = contentType != null && contentType.contains(EVENT_STREAM); + protected HttpURLConnection createConnection( + String requestUrl, String method, Map requestHeaders, boolean isSSE) + throws IOException { + URL urlObj; + try { + urlObj = new URI(requestUrl).toURL(); + } catch (URISyntaxException e) { + throw new MalformedURLException("Invalid URL: " + url); + } + HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection(); + connection.setRequestMethod(method); + connection.setConnectTimeout(15000); // 15 seconds + connection.setReadTimeout(60000); // 60 seconds + // Security: Disable automatic redirect following to prevent credential leakage + connection.setInstanceFollowRedirects(false); + for (Map.Entry header : requestHeaders.entrySet()) { + connection.setRequestProperty(header.getKey(), header.getValue()); + } + if (isSSE) { + connection.setRequestProperty(A2AHttpClient.ACCEPT, A2AHttpClient.EVENT_STREAM); + } + return connection; + } - try (InputStream is = connection.getInputStream()) { - if (isSse) { - readSSEStream(is, sseParserConfig, messageConsumer, errorConsumer); - } else { - readNonSSEStream(is, messageConsumer); - } - completeRunnable.run(); + protected static String readStreamWithLimit(InputStream is) throws IOException { + if (is == null) { + return ""; + } + int maxResponseSize = 10 * 1024 * 1024; // 10 MB + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + StringBuilder sb = new StringBuilder(); + String line; + boolean first = true; + while ((line = reader.readLine()) != null) { + if (sb.length() + line.length() > maxResponseSize) { + throw new IOException("Response size exceeds limit"); + } + if (!first) { + sb.append('\n'); + } + sb.append(line); + first = false; + } + return sb.toString(); + } } - } catch (Exception e) { - errorConsumer.accept(e); - } finally { - connection.disconnect(); - } - } - private static void readSSEStream( - InputStream is, - SSEParserConfig config, - Consumer messageConsumer, - Consumer errorConsumer) throws IOException { - InputStream buffered = new java.io.BufferedInputStream(is); - BoundedLineAccumulator accumulator = new BoundedLineAccumulator(config.maxLineLength()); - ServerSentEventParser sseParser = new ServerSentEventParser(messageConsumer, errorConsumer, config); - while (readBoundedLine(buffered, accumulator)) { - if (accumulator.isTooLong()) { - sseParser.processLineTooLong(); - } else { - sseParser.processLine(accumulator.toLine()); + /** + * Reads the next line from {@code is}, treating LF, CRLF, and bare CR as terminators + * per the SSE specification, and enforcing the byte limit while accumulating + * bytes — not after a full line has been materialised. + * + *

    The caller must provide a {@link BoundedLineAccumulator} and call + * {@link BoundedLineAccumulator#reset()} between lines. After this method returns + * {@code true}, check {@link BoundedLineAccumulator#isTooLong()} to distinguish + * between a valid line and one that exceeded the limit. + * + *

    The {@code is} parameter must support {@link InputStream#mark(int)} (e.g. + * {@link java.io.BufferedInputStream}) so that CR at end-of-buffer can be resolved + * without consuming the following byte. + * + * @param is the input stream to read from (must support mark/reset) + * @param accumulator the accumulator to use for byte collection and limit enforcement + * @return {@code true} if a line was read, {@code false} at end-of-stream + */ + static boolean readBoundedLine(InputStream is, BoundedLineAccumulator accumulator) + throws IOException { + boolean hasReadAnyBytes = false; + + while (true) { + int b = is.read(); + if (b == -1) { + return hasReadAnyBytes || accumulator.isTooLong(); + } + hasReadAnyBytes = true; + if (b == '\n') { + return true; + } + if (b == '\r') { + consumeOptionalLF(is); + return true; + } + accumulator.addByte(b); + } } - accumulator.reset(); - } - sseParser.flush(); - } - private static void readNonSSEStream( - InputStream is, - Consumer messageConsumer) throws IOException { - StringBuilder bodyBuffer = new StringBuilder(); - BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); - String line; - while ((line = reader.readLine()) != null) { - if (!line.isEmpty()) { - if (bodyBuffer.length() > 0) { - bodyBuffer.append('\n'); - } - bodyBuffer.append(line); + private static void consumeOptionalLF(InputStream is) throws IOException { + is.mark(1); + int next = is.read(); + if (next != '\n' && next != -1) { + is.reset(); + } } - } - String body = bodyBuffer.toString(); - if (!body.isEmpty()) { - messageConsumer.accept(new ServerSentEvent(body)); - } - } - protected CompletableFuture executeAsyncSSE( - HttpURLConnection connection, - Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) { - return CompletableFuture.runAsync( - () -> processSSEResponse(connection, messageConsumer, errorConsumer, completeRunnable), - NET_EXECUTOR); - } - } + protected A2AHttpResponse execute(HttpURLConnection connection) throws IOException { + int status = connection.getResponseCode(); + A2AHttpHeaders responseHeaders = fromConnectionHeaders(connection.getHeaderFields()); + if (status == HTTP_UNAUTHORIZED) { + throw new IOException( + A2AErrorMessages.AUTHENTICATION_FAILED, + new A2AClientHTTPError( + HTTP_UNAUTHORIZED, + A2AErrorMessages.AUTHENTICATION_FAILED, + null, + responseHeaders.toMap())); + } else if (status == HTTP_FORBIDDEN) { + throw new IOException( + A2AErrorMessages.AUTHORIZATION_FAILED, + new A2AClientHTTPError( + HTTP_FORBIDDEN, + A2AErrorMessages.AUTHORIZATION_FAILED, + null, + responseHeaders.toMap())); + } - private static class AndroidGetBuilder extends AndroidBuilder implements GetBuilder { - private boolean followRedirects = false; + String body = ""; + try (InputStream is = + (status >= HTTP_OK && status < HTTP_MULT_CHOICE) + ? connection.getInputStream() + : connection.getErrorStream()) { + body = readStreamWithLimit(is); + } -AndroidGetBuilder(SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { - super(sseParserConfig, redirectHeaderPolicy); - } + return new AndroidHttpResponse(status, body, responseHeaders); + } - @Override - public GetBuilder followRedirects(boolean follow) { - this.followRedirects = follow; - return self(); - } + protected void processSSEResponse( + HttpURLConnection connection, + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) { + try { + int status = connection.getResponseCode(); + if (!(status >= HTTP_OK && status < HTTP_MULT_CHOICE)) { + if (status == HTTP_UNAUTHORIZED || status == HTTP_FORBIDDEN) { + A2AHttpHeaders responseHeaders = + fromConnectionHeaders(connection.getHeaderFields()); + String msg = + status == HTTP_UNAUTHORIZED + ? A2AErrorMessages.AUTHENTICATION_FAILED + : A2AErrorMessages.AUTHORIZATION_FAILED; + errorConsumer.accept( + new IOException( + msg, + new A2AClientHTTPError( + status, msg, null, responseHeaders.toMap()))); + return; + } + + String errorBody = ""; + try (InputStream es = connection.getErrorStream()) { + errorBody = readStreamWithLimit(es); + } + // Pass the error body through messageConsumer so higher-level listeners + // (e.g. RestErrorMapper in SSEEventListener) can produce a typed error. + // Do not also call errorConsumer here — the messageConsumer path is responsible + // for signalling the error, matching the async JDK client's behaviour. + if (!errorBody.isEmpty()) { + messageConsumer.accept(new ServerSentEvent(errorBody)); + } else { + errorConsumer.accept( + new IOException("Request failed with status " + status)); + } + return; + } + + String contentType = connection.getContentType(); + boolean isSse = contentType != null && contentType.contains(EVENT_STREAM); + + try (InputStream is = connection.getInputStream()) { + if (isSse) { + readSSEStream(is, sseParserConfig, messageConsumer, errorConsumer); + } else { + readNonSSEStream(is, messageConsumer); + } + completeRunnable.run(); + } + } catch (Exception e) { + errorConsumer.accept(e); + } finally { + connection.disconnect(); + } + } - @Override - public A2AHttpResponse get() throws IOException { - HttpURLConnection connection = createConnection("GET", false); - try { - A2AHttpResponse response = execute(connection); - if (!followRedirects) { - return response; + private static void readSSEStream( + InputStream is, + SSEParserConfig config, + Consumer messageConsumer, + Consumer errorConsumer) + throws IOException { + InputStream buffered = new java.io.BufferedInputStream(is); + BoundedLineAccumulator accumulator = new BoundedLineAccumulator(config.maxLineLength()); + ServerSentEventParser sseParser = + new ServerSentEventParser(messageConsumer, errorConsumer, config); + while (readBoundedLine(buffered, accumulator)) { + if (accumulator.isTooLong()) { + sseParser.processLineTooLong(); + } else { + sseParser.processLine(accumulator.toLine()); + } + accumulator.reset(); + } + sseParser.flush(); } - String location = response.headers().firstValue("Location"); - RedirectHeaderPolicy.RedirectedRequest redirect = redirectHeaderPolicy - .redirectedRequest(URI.create(url), response.status(), location, headers) - .orElse(null); - if (redirect == null) { - return response; + + private static void readNonSSEStream( + InputStream is, Consumer messageConsumer) throws IOException { + StringBuilder bodyBuffer = new StringBuilder(); + BufferedReader reader = + new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + if (!line.isEmpty()) { + if (bodyBuffer.length() > 0) { + bodyBuffer.append('\n'); + } + bodyBuffer.append(line); + } + } + String body = bodyBuffer.toString(); + if (!body.isEmpty()) { + messageConsumer.accept(new ServerSentEvent(body)); + } } - if (!RedirectHeaderPolicy.sameOrigin(URI.create(url), redirect.target())) { - // HttpURLConnection cannot isolate process-wide CookieHandler or Authenticator state - // for an individual request. Never follow across an origin boundary. - return response; + + protected CompletableFuture executeAsyncSSE( + HttpURLConnection connection, + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) { + return CompletableFuture.runAsync( + () -> + processSSEResponse( + connection, messageConsumer, errorConsumer, completeRunnable), + NET_EXECUTOR); } - connection.disconnect(); - connection = createConnection(redirect.target().toString(), "GET", redirect.headers(), false); - return execute(connection); - } catch (IOException e) { - connection.disconnect(); - throw e; - } } - @Override - public CompletableFuture getAsyncSSE( - Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) - throws IOException { - HttpURLConnection connection = createConnection("GET", true); - return executeAsyncSSE(connection, messageConsumer, errorConsumer, completeRunnable); - } - } + private static class AndroidGetBuilder extends AndroidBuilder + implements GetBuilder { + private boolean followRedirects = false; - private static class AndroidPostBuilder extends AndroidBuilder - implements PostBuilder { - private String body = ""; - private boolean followRedirects = false; + AndroidGetBuilder( + SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { + super(sseParserConfig, redirectHeaderPolicy); + } -AndroidPostBuilder(SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { - super(sseParserConfig, redirectHeaderPolicy); - } + @Override + public GetBuilder followRedirects(boolean follow) { + this.followRedirects = follow; + return self(); + } - @Override - public PostBuilder body(String body) { - this.body = body; - return this; - } + @Override + public A2AHttpResponse get() throws IOException { + HttpURLConnection connection = createConnection("GET", false); + try { + A2AHttpResponse response = execute(connection); + if (!followRedirects) { + return response; + } + String location = response.headers().firstValue("Location"); + RedirectHeaderPolicy.RedirectedRequest redirect = + redirectHeaderPolicy + .redirectedRequest( + URI.create(url), response.status(), location, headers) + .orElse(null); + if (redirect == null) { + return response; + } + if (!RedirectHeaderPolicy.sameOrigin(URI.create(url), redirect.target())) { + // HttpURLConnection cannot isolate process-wide CookieHandler or Authenticator + // state + // for an individual request. Never follow across an origin boundary. + return response; + } + connection.disconnect(); + connection = + createConnection( + redirect.target().toString(), "GET", redirect.headers(), false); + return execute(connection); + } catch (IOException e) { + connection.disconnect(); + throw e; + } + } - @Override - public PostBuilder followRedirects(boolean follow) { - this.followRedirects = follow; - return self(); + @Override + public CompletableFuture getAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) + throws IOException { + HttpURLConnection connection = createConnection("GET", true); + return executeAsyncSSE(connection, messageConsumer, errorConsumer, completeRunnable); + } } - @Override - public A2AHttpResponse post() throws IOException { - HttpURLConnection connection = createConnection("POST", false); - connection.setDoOutput(true); - try { - try (OutputStream os = connection.getOutputStream()) { - os.write(body.getBytes(StandardCharsets.UTF_8)); + private static class AndroidPostBuilder extends AndroidBuilder + implements PostBuilder { + private String body = ""; + private boolean followRedirects = false; + + AndroidPostBuilder( + SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { + super(sseParserConfig, redirectHeaderPolicy); } - A2AHttpResponse response = execute(connection); - if (!followRedirects) { - return response; + + @Override + public PostBuilder body(String body) { + this.body = body; + return this; } - String location = response.headers().firstValue("Location"); - RedirectHeaderPolicy.RedirectedRequest redirect = redirectHeaderPolicy - .redirectedRequest(URI.create(url), response.status(), location, headers) - .orElse(null); - if (redirect == null) { - return response; + @Override + public PostBuilder followRedirects(boolean follow) { + this.followRedirects = follow; + return self(); } - if (!RedirectHeaderPolicy.sameOrigin(URI.create(url), redirect.target())) { - // HttpURLConnection cannot isolate process-wide CookieHandler or Authenticator state - // for an individual request. Never follow across an origin boundary. - return response; + + @Override + public A2AHttpResponse post() throws IOException { + HttpURLConnection connection = createConnection("POST", false); + connection.setDoOutput(true); + try { + try (OutputStream os = connection.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + A2AHttpResponse response = execute(connection); + if (!followRedirects) { + return response; + } + + String location = response.headers().firstValue("Location"); + RedirectHeaderPolicy.RedirectedRequest redirect = + redirectHeaderPolicy + .redirectedRequest( + URI.create(url), response.status(), location, headers) + .orElse(null); + if (redirect == null) { + return response; + } + if (!RedirectHeaderPolicy.sameOrigin(URI.create(url), redirect.target())) { + // HttpURLConnection cannot isolate process-wide CookieHandler or Authenticator + // state + // for an individual request. Never follow across an origin boundary. + return response; + } + + connection.disconnect(); + connection = + createConnection( + redirect.target().toString(), + redirect.redirectAsGet() ? "GET" : "POST", + redirect.headers(), + false); + if (!redirect.redirectAsGet()) { + connection.setDoOutput(true); + try (OutputStream os = connection.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + } + return execute(connection); + } catch (IOException e) { + connection.disconnect(); + throw e; + } } - connection.disconnect(); - connection = createConnection( - redirect.target().toString(), - redirect.redirectAsGet() ? "GET" : "POST", - redirect.headers(), - false); - if (!redirect.redirectAsGet()) { - connection.setDoOutput(true); - try (OutputStream os = connection.getOutputStream()) { - os.write(body.getBytes(StandardCharsets.UTF_8)); - } + @Override + public CompletableFuture postAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) + throws IOException { + HttpURLConnection connection = createConnection("POST", true); + connection.setDoOutput(true); + + return CompletableFuture.runAsync( + () -> { + try { + try (OutputStream os = connection.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + processSSEResponse( + connection, messageConsumer, errorConsumer, completeRunnable); + } catch (Exception e) { + errorConsumer.accept(e); + connection.disconnect(); + } + }, + NET_EXECUTOR); } - return execute(connection); - } catch (IOException e) { - connection.disconnect(); - throw e; - } } - @Override - public CompletableFuture postAsyncSSE( - Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) - throws IOException { - HttpURLConnection connection = createConnection("POST", true); - connection.setDoOutput(true); - - return CompletableFuture.runAsync( - () -> { + private static class AndroidDeleteBuilder extends AndroidBuilder + implements DeleteBuilder { + AndroidDeleteBuilder( + SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { + super(sseParserConfig, redirectHeaderPolicy); + } + + @Override + public A2AHttpResponse delete() throws IOException { + HttpURLConnection connection = createConnection("DELETE", false); try { - try (OutputStream os = connection.getOutputStream()) { - os.write(body.getBytes(StandardCharsets.UTF_8)); - } - processSSEResponse(connection, messageConsumer, errorConsumer, completeRunnable); - } catch (Exception e) { - errorConsumer.accept(e); - connection.disconnect(); + return execute(connection); + } catch (IOException e) { + connection.disconnect(); + throw e; } - }, NET_EXECUTOR); - } - } - - private static class AndroidDeleteBuilder extends AndroidBuilder - implements DeleteBuilder { -AndroidDeleteBuilder(SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { - super(sseParserConfig, redirectHeaderPolicy); + } } - @Override - public A2AHttpResponse delete() throws IOException { - HttpURLConnection connection = createConnection("DELETE", false); - try { - return execute(connection); - } catch (IOException e) { - connection.disconnect(); - throw e; - } + private static A2AHttpHeaders fromConnectionHeaders( + @Nullable Map> headerFields) { + // HttpURLConnection.getHeaderFields() may include a null key for the HTTP status line; + // A2AHttpHeaders.of() filters those out automatically. + return A2AHttpHeaders.of(headerFields != null ? headerFields : Map.of()); } - } - private static A2AHttpHeaders fromConnectionHeaders(@Nullable Map> headerFields) { - // HttpURLConnection.getHeaderFields() may include a null key for the HTTP status line; - // A2AHttpHeaders.of() filters those out automatically. - return A2AHttpHeaders.of(headerFields != null ? headerFields : Map.of()); - } - - private record AndroidHttpResponse(int status, String body, A2AHttpHeaders headers) implements A2AHttpResponse { - @Override - public boolean success() { - return status >= HTTP_OK && status < HTTP_MULT_CHOICE; - } + private record AndroidHttpResponse(int status, String body, A2AHttpHeaders headers) + implements A2AHttpResponse { + @Override + public boolean success() { + return status >= HTTP_OK && status < HTTP_MULT_CHOICE; + } - @Override - public A2AHttpHeaders headers() { - return headers; + @Override + public A2AHttpHeaders headers() { + return headers; + } } - } } diff --git a/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientProvider.java b/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientProvider.java index 28014e207..3c4e641d7 100644 --- a/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientProvider.java +++ b/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientProvider.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.client.http.android; import org.a2aproject.sdk.client.http.A2AHttpClient; -import org.a2aproject.sdk.client.http.A2AHttpClientProvider; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; -import org.a2aproject.sdk.client.http.RedirectHeaderPolicy; +import org.a2aproject.sdk.client.http.A2AHttpClientProvider; import org.a2aproject.sdk.client.http.SSEParserConfig; /** @@ -15,14 +14,16 @@ public final class AndroidA2AHttpClientProvider implements A2AHttpClientProvider private static boolean isAndroidAvailable() { String runtimeName = System.getProperty("java.runtime.name"); - return runtimeName != null && runtimeName.toLowerCase(java.util.Locale.ENGLISH).contains("android"); + return runtimeName != null + && runtimeName.toLowerCase(java.util.Locale.ENGLISH).contains("android"); } @Override public A2AHttpClient create() { if (!ANDROID_AVAILABLE) { throw new IllegalStateException( - "Android classes are not available. This provider is only supported on Android."); + "Android classes are not available. This provider is only supported on" + + " Android."); } return new AndroidA2AHttpClient(); } @@ -36,7 +37,8 @@ public A2AHttpClient create() { public A2AHttpClient createWithSseConfig(SSEParserConfig sseParserConfig) { if (!ANDROID_AVAILABLE) { throw new IllegalStateException( - "Android classes are not available. This provider is only supported on Android."); + "Android classes are not available. This provider is only supported on" + + " Android."); } return new AndroidA2AHttpClient(sseParserConfig); } @@ -45,10 +47,12 @@ public A2AHttpClient createWithSseConfig(SSEParserConfig sseParserConfig) { public A2AHttpClient create(A2AHttpClientConfiguration configuration) { if (!ANDROID_AVAILABLE) { throw new IllegalStateException( - "Android classes are not available. This provider is only supported on Android."); + "Android classes are not available. This provider is only supported on" + + " Android."); } return new AndroidA2AHttpClient( - configuration.sseParserConfigOrDefault(), configuration.redirectHeaderPolicyOrDefault()); + configuration.sseParserConfigOrDefault(), + configuration.redirectHeaderPolicyOrDefault()); } @Override diff --git a/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/package-info.java b/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/package-info.java index 29a20c3f3..7ac542a0b 100644 --- a/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/package-info.java +++ b/extras/http-client-android/src/main/java/org/a2aproject/sdk/client/http/android/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.http.android; import org.jspecify.annotations.NullMarked; - diff --git a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientFactoryTest.java b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientFactoryTest.java index e3ef6e23a..8aff883a9 100644 --- a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientFactoryTest.java +++ b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientFactoryTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.List; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; import org.a2aproject.sdk.client.http.A2AHttpClientFactory; @@ -15,23 +14,27 @@ public class AndroidA2AHttpClientFactoryTest { @Test public void testCreateReturnsAndroidClient() { - // When both JDK and Android are on classpath, Android should be preferred due to higher priority (100) + // When both JDK and Android are on classpath, Android should be preferred due to higher + // priority (100) A2AHttpClient client = A2AHttpClientFactory.create(); assertNotNull(client); - assertInstanceOf(AndroidA2AHttpClient.class, client, - "Factory should return AndroidA2AHttpClient when Android provider is available"); + assertInstanceOf( + AndroidA2AHttpClient.class, + client, + "Factory should return AndroidA2AHttpClient when Android provider is available"); } @Test public void testCreateExcludingAndroidFallsBackToJdkClient() { - A2AHttpClientConfiguration configuration = A2AHttpClientConfiguration.builder() - .excludedProviderNames(List.of("android")) - .build(); + A2AHttpClientConfiguration configuration = + A2AHttpClientConfiguration.builder() + .excludedProviderNames(List.of("android")) + .build(); A2AHttpClient client = A2AHttpClientFactory.create(configuration); assertNotNull(client); - assertInstanceOf(JdkA2AHttpClient.class, client, - "Excluding Android should select the JDK fallback"); + assertInstanceOf( + JdkA2AHttpClient.class, client, "Excluding Android should select the JDK fallback"); } } diff --git a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientProviderTest.java b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientProviderTest.java index f46d96a51..a7f0dcc5d 100644 --- a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientProviderTest.java +++ b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientProviderTest.java @@ -25,7 +25,9 @@ public void testCreateWithSseConfigReturnsAndroidClient() { A2AHttpClient client = provider.createWithSseConfig(config); assertNotNull(client); - assertInstanceOf(AndroidA2AHttpClient.class, client, + assertInstanceOf( + AndroidA2AHttpClient.class, + client, "Provider should return AndroidA2AHttpClient when Android runtime is available"); } @@ -38,7 +40,8 @@ public void testSupportsSseConfig() { @Test public void testDoesNotSupportRedirectHeaderPolicy() { AndroidA2AHttpClientProvider provider = new AndroidA2AHttpClientProvider(); - assertFalse(provider.supportsRedirectHeaderPolicy(), + assertFalse( + provider.supportsRedirectHeaderPolicy(), "Android does not safely support cross-origin redirect header filtering"); } @@ -51,7 +54,9 @@ public void testProviderName() { @Test public void testPriorityOnAndroid() { AndroidA2AHttpClientProvider provider = new AndroidA2AHttpClientProvider(); - assertEquals(110, provider.priority(), + assertEquals( + 110, + provider.priority(), "Android provider should have priority 110 when Android runtime is available"); } } diff --git a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientRedirectTest.java b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientRedirectTest.java index 00326e908..ba94e0755 100644 --- a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientRedirectTest.java +++ b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientRedirectTest.java @@ -10,7 +10,6 @@ import java.net.URI; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.client.http.AbstractA2AHttpClientRedirectTest; @@ -47,15 +46,16 @@ protected boolean followsCrossOriginPostToGetRedirects() { public void synchronousPostFollowsRedirectWhenExplicitlyEnabled() throws Exception { startRedirectPair(); - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent") - .addHeader("X-API-Key", "FULCRUM-SYNTHETIC-EXPLICIT") - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent") + .addHeader("X-API-Key", "FULCRUM-SYNTHETIC-EXPLICIT") + .body("{}") + .followRedirects(true) + .post(); - assertEquals(302, result.status(), - "Android client must not follow cross-origin redirects"); + assertEquals(302, result.status(), "Android client must not follow cross-origin redirects"); getCollector().verify(request().withPath("/collect"), exactly(0)); } @@ -63,14 +63,16 @@ public void synchronousPostFollowsRedirectWhenExplicitlyEnabled() throws Excepti public void crossOriginRedirectDoesNotContactTargetEvenWithSafeHeaders() throws Exception { startRedirectPair(); - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent") - .addHeader("Authorization", "Bearer secret") - .addHeader("X-Custom-Token", "secret") - .addHeader("A2A-Version", "1.0") - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent") + .addHeader("Authorization", "Bearer secret") + .addHeader("X-Custom-Token", "secret") + .addHeader("A2A-Version", "1.0") + .body("{}") + .followRedirects(true) + .post(); assertEquals(302, result.status()); getCollector().verify(request().withPath("/collect"), exactly(0)); @@ -84,17 +86,23 @@ public void crossOriginRedirectDoesNotUseProcessWideCookieHandler() throws Excep cookieManager.put( URI.create("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent"), Map.of("Set-Cookie", List.of("session=secret; Path=/"))); - URI targetUri = URI.create("http://127.0.0.1:" + getCollector().getLocalPort() + "/collect"); - assertEquals(List.of("session=secret"), cookieManager.get(targetUri, Map.of()).get("Cookie"), - "The process-wide CookieHandler must be capable of injecting the cross-port cookie"); + URI targetUri = + URI.create("http://127.0.0.1:" + getCollector().getLocalPort() + "/collect"); + assertEquals( + List.of("session=secret"), + cookieManager.get(targetUri, Map.of()).get("Cookie"), + "The process-wide CookieHandler must be capable of injecting the cross-port" + + " cookie"); CookieHandler.setDefault(cookieManager); cookieHandlerReplaced = true; - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent") - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent") + .body("{}") + .followRedirects(true) + .post(); assertEquals(302, result.status()); getCollector().verify(request().withPath("/collect"), exactly(0)); @@ -107,11 +115,13 @@ public void crossOriginRedirectDoesNotUseProcessWideAuthenticator() throws Excep Authenticator.setDefault(new Authenticator() {}); authenticatorReplaced = true; - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent") - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent") + .body("{}") + .followRedirects(true) + .post(); assertEquals(302, result.status()); getCollector().verify(request().withPath("/collect"), exactly(0)); diff --git a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientSSETest.java b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientSSETest.java index a1f2523ec..2db86cf08 100644 --- a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientSSETest.java +++ b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidA2AHttpClientSSETest.java @@ -14,8 +14,10 @@ protected A2AHttpClient createClient() { @Override protected A2AHttpClient createClient(SSEParserConfig sseParserConfig) { - return new AndroidA2AHttpClientProvider().create(A2AHttpClientConfiguration.builder() - .sseParserConfig(sseParserConfig) - .build()); + return new AndroidA2AHttpClientProvider() + .create( + A2AHttpClientConfiguration.builder() + .sseParserConfig(sseParserConfig) + .build()); } } diff --git a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidBoundedLineReaderTest.java b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidBoundedLineReaderTest.java index f96d5387e..851ab1f5f 100644 --- a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidBoundedLineReaderTest.java +++ b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/AndroidBoundedLineReaderTest.java @@ -24,17 +24,20 @@ public class AndroidBoundedLineReaderTest { static { try { - Class builderClass = Class.forName( - "org.a2aproject.sdk.client.http.android.AndroidA2AHttpClient$AndroidBuilder"); - READ_BOUNDED_LINE = builderClass.getDeclaredMethod( - "readBoundedLine", InputStream.class, BoundedLineAccumulator.class); + Class builderClass = + Class.forName( + "org.a2aproject.sdk.client.http.android.AndroidA2AHttpClient$AndroidBuilder"); + READ_BOUNDED_LINE = + builderClass.getDeclaredMethod( + "readBoundedLine", InputStream.class, BoundedLineAccumulator.class); READ_BOUNDED_LINE.setAccessible(true); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } - private static boolean readBoundedLineRaw(InputStream is, BoundedLineAccumulator accumulator) throws Exception { + private static boolean readBoundedLineRaw(InputStream is, BoundedLineAccumulator accumulator) + throws Exception { try { return (Boolean) READ_BOUNDED_LINE.invoke(null, is, accumulator); } catch (java.lang.reflect.InvocationTargetException e) { diff --git a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/OkHttpA2AHttpClient.java b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/OkHttpA2AHttpClient.java index f239747a0..88bf1070d 100644 --- a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/OkHttpA2AHttpClient.java +++ b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/OkHttpA2AHttpClient.java @@ -12,7 +12,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.function.Consumer; - import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -23,8 +22,8 @@ import org.a2aproject.sdk.client.http.A2AHttpHeaders; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.client.http.RedirectHeaderPolicy; -import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.client.http.SSEParserConfig; +import org.a2aproject.sdk.client.http.ServerSentEvent; import org.a2aproject.sdk.client.http.ServerSentEventParser; import org.a2aproject.sdk.common.A2AErrorMessages; import org.a2aproject.sdk.spec.A2AClientHTTPError; @@ -46,11 +45,13 @@ class OkHttpA2AHttpClient implements A2AHttpClient { private static final int HTTP_UNAUTHORIZED = 401; private static final int HTTP_FORBIDDEN = 403; - private static final Executor NET_EXECUTOR = Executors.newCachedThreadPool(r -> { - Thread t = new Thread(r, "A2A-OkHttp-Test-Net"); - t.setDaemon(true); - return t; - }); + private static final Executor NET_EXECUTOR = + Executors.newCachedThreadPool( + r -> { + Thread t = new Thread(r, "A2A-OkHttp-Test-Net"); + t.setDaemon(true); + return t; + }); private final SSEParserConfig sseParserConfig; @@ -135,13 +136,21 @@ protected A2AHttpResponse toResponse(Response response) throws IOException { A2AHttpHeaders responseHeaders = fromOkHttpHeaders(response); if (status == HTTP_UNAUTHORIZED) { - throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED, - new A2AClientHTTPError(HTTP_UNAUTHORIZED, A2AErrorMessages.AUTHENTICATION_FAILED, - null, responseHeaders.toMap())); + throw new IOException( + A2AErrorMessages.AUTHENTICATION_FAILED, + new A2AClientHTTPError( + HTTP_UNAUTHORIZED, + A2AErrorMessages.AUTHENTICATION_FAILED, + null, + responseHeaders.toMap())); } else if (status == HTTP_FORBIDDEN) { - throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED, - new A2AClientHTTPError(HTTP_FORBIDDEN, A2AErrorMessages.AUTHORIZATION_FAILED, - null, responseHeaders.toMap())); + throw new IOException( + A2AErrorMessages.AUTHORIZATION_FAILED, + new A2AClientHTTPError( + HTTP_FORBIDDEN, + A2AErrorMessages.AUTHORIZATION_FAILED, + null, + responseHeaders.toMap())); } ResponseBody body = response.body(); @@ -179,15 +188,20 @@ protected void processSSEResponse( private boolean handleErrorStatus( Response response, Consumer messageConsumer, - Consumer errorConsumer) throws IOException { + Consumer errorConsumer) + throws IOException { int status = response.code(); if (status == HTTP_UNAUTHORIZED || status == HTTP_FORBIDDEN) { A2AHttpHeaders responseHeaders = fromOkHttpHeaders(response); - String msg = status == HTTP_UNAUTHORIZED - ? A2AErrorMessages.AUTHENTICATION_FAILED - : A2AErrorMessages.AUTHORIZATION_FAILED; - errorConsumer.accept(new IOException(msg, - new A2AClientHTTPError(status, msg, null, responseHeaders.toMap()))); + String msg = + status == HTTP_UNAUTHORIZED + ? A2AErrorMessages.AUTHENTICATION_FAILED + : A2AErrorMessages.AUTHORIZATION_FAILED; + errorConsumer.accept( + new IOException( + msg, + new A2AClientHTTPError( + status, msg, null, responseHeaders.toMap()))); return true; } @@ -208,12 +222,16 @@ private void parseResponseBody( ResponseBody body, boolean isSse, Consumer messageConsumer, - Consumer errorConsumer) throws IOException { - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(body.byteStream(), StandardCharsets.UTF_8))) { + Consumer errorConsumer) + throws IOException { + try (BufferedReader reader = + new BufferedReader( + new InputStreamReader(body.byteStream(), StandardCharsets.UTF_8))) { String line; if (isSse) { - ServerSentEventParser sseParser = new ServerSentEventParser(messageConsumer, errorConsumer, sseParserConfig); + ServerSentEventParser sseParser = + new ServerSentEventParser( + messageConsumer, errorConsumer, sseParserConfig); while ((line = reader.readLine()) != null) { sseParser.processLine(line); } @@ -240,7 +258,7 @@ private void parseResponseBody( private static class OkHttpGetBuilder extends OkHttpBuilder implements GetBuilder { private boolean followRedirects = false; -OkHttpGetBuilder(SSEParserConfig sseParserConfig) { + OkHttpGetBuilder(SSEParserConfig sseParserConfig) { super(sseParserConfig); } @@ -260,17 +278,27 @@ public A2AHttpResponse get() throws IOException { if (location != null) { RedirectHeaderPolicy.@Nullable RedirectedRequest redirect; try { - redirect = RedirectHeaderPolicy.DEFAULT - .redirectedRequest(URI.create(url), response.code(), location, headers) - .orElse(null); + redirect = + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create(url), + response.code(), + location, + headers) + .orElse(null); } catch (IllegalArgumentException e) { redirect = null; } if (redirect != null) { - Request followUpRequest = applyHeaders( - new Request.Builder().url(redirect.target().toString()), redirect.headers()) - .get().build(); - try (Response followUpResponse = client.newCall(followUpRequest).execute()) { + Request followUpRequest = + applyHeaders( + new Request.Builder() + .url(redirect.target().toString()), + redirect.headers()) + .get() + .build(); + try (Response followUpResponse = + client.newCall(followUpRequest).execute()) { return toResponse(followUpResponse); } } @@ -285,27 +313,34 @@ public CompletableFuture getAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) { - return CompletableFuture.runAsync(() -> { - try { - OkHttpClient client = buildClient(false); - Request request = applyHeaders( - new Request.Builder().url(url).get() - .addHeader(ACCEPT, EVENT_STREAM)) - .build(); - Response response = client.newCall(request).execute(); - processSSEResponse(response, messageConsumer, errorConsumer, completeRunnable); - } catch (Exception e) { - errorConsumer.accept(e); - } - }, NET_EXECUTOR); + return CompletableFuture.runAsync( + () -> { + try { + OkHttpClient client = buildClient(false); + Request request = + applyHeaders( + new Request.Builder() + .url(url) + .get() + .addHeader(ACCEPT, EVENT_STREAM)) + .build(); + Response response = client.newCall(request).execute(); + processSSEResponse( + response, messageConsumer, errorConsumer, completeRunnable); + } catch (Exception e) { + errorConsumer.accept(e); + } + }, + NET_EXECUTOR); } } - private static class OkHttpPostBuilder extends OkHttpBuilder implements PostBuilder { + private static class OkHttpPostBuilder extends OkHttpBuilder + implements PostBuilder { private String body = ""; private boolean followRedirects = false; -OkHttpPostBuilder(SSEParserConfig sseParserConfig) { + OkHttpPostBuilder(SSEParserConfig sseParserConfig) { super(sseParserConfig); } @@ -324,18 +359,23 @@ public PostBuilder followRedirects(boolean follow) { @Override public A2AHttpResponse post() throws IOException { OkHttpClient client = buildClient(false); - Request request = applyHeaders( - new Request.Builder().url(url) - .post(RequestBody.create(body, JSON))) - .build(); + Request request = + applyHeaders( + new Request.Builder() + .url(url) + .post(RequestBody.create(body, JSON))) + .build(); try (Response response = client.newCall(request).execute()) { RedirectHeaderPolicy.RedirectedRequest redirect = redirectedRequest(response); if (redirect != null) { - Request.Builder redirectRequest = applyHeaders( - new Request.Builder().url(redirect.target().toString()), redirect.headers()); - Request followUpRequest = redirect.redirectAsGet() - ? redirectRequest.get().build() - : redirectRequest.post(RequestBody.create(body, JSON)).build(); + Request.Builder redirectRequest = + applyHeaders( + new Request.Builder().url(redirect.target().toString()), + redirect.headers()); + Request followUpRequest = + redirect.redirectAsGet() + ? redirectRequest.get().build() + : redirectRequest.post(RequestBody.create(body, JSON)).build(); try (Response followUpResponse = client.newCall(followUpRequest).execute()) { return toResponse(followUpResponse); } @@ -344,7 +384,8 @@ public A2AHttpResponse post() throws IOException { } } - private RedirectHeaderPolicy.@Nullable RedirectedRequest redirectedRequest(Response response) { + private RedirectHeaderPolicy.@Nullable RedirectedRequest redirectedRequest( + Response response) { if (!followRedirects) { return null; } @@ -366,25 +407,31 @@ public CompletableFuture postAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) { - return CompletableFuture.runAsync(() -> { - try { - OkHttpClient client = buildClient(false); - Request request = applyHeaders( - new Request.Builder().url(url) - .post(RequestBody.create(body, JSON)) - .addHeader(ACCEPT, EVENT_STREAM)) - .build(); - Response response = client.newCall(request).execute(); - processSSEResponse(response, messageConsumer, errorConsumer, completeRunnable); - } catch (Exception e) { - errorConsumer.accept(e); - } - }, NET_EXECUTOR); + return CompletableFuture.runAsync( + () -> { + try { + OkHttpClient client = buildClient(false); + Request request = + applyHeaders( + new Request.Builder() + .url(url) + .post(RequestBody.create(body, JSON)) + .addHeader(ACCEPT, EVENT_STREAM)) + .build(); + Response response = client.newCall(request).execute(); + processSSEResponse( + response, messageConsumer, errorConsumer, completeRunnable); + } catch (Exception e) { + errorConsumer.accept(e); + } + }, + NET_EXECUTOR); } } - private static class OkHttpDeleteBuilder extends OkHttpBuilder implements DeleteBuilder { -OkHttpDeleteBuilder(SSEParserConfig sseParserConfig) { + private static class OkHttpDeleteBuilder extends OkHttpBuilder + implements DeleteBuilder { + OkHttpDeleteBuilder(SSEParserConfig sseParserConfig) { super(sseParserConfig); } @@ -406,7 +453,8 @@ private static A2AHttpHeaders fromOkHttpHeaders(Response response) { return A2AHttpHeaders.of(headerMap); } - private record OkHttpResponse(int status, String body, A2AHttpHeaders headers) implements A2AHttpResponse { + private record OkHttpResponse(int status, String body, A2AHttpHeaders headers) + implements A2AHttpResponse { @Override public boolean success() { return status >= HTTP_OK && status < HTTP_MULT_CHOICE; diff --git a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/OkHttpPostRedirectHeaderLeakTest.java b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/OkHttpPostRedirectHeaderLeakTest.java index 9383c36d3..79232ca18 100644 --- a/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/OkHttpPostRedirectHeaderLeakTest.java +++ b/extras/http-client-android/src/test/java/org/a2aproject/sdk/client/http/android/OkHttpPostRedirectHeaderLeakTest.java @@ -7,7 +7,6 @@ import static org.mockserver.verify.VerificationTimes.exactly; import java.util.stream.Stream; - import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -61,15 +60,14 @@ public void okHttpLeaksNonstandardHeaderOnCrossOriginPostRedirect(String headerN throws Exception { startRedirectPair(); - OkHttpClient client = new OkHttpClient.Builder() - .followRedirects(true) - .build(); + OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build(); - Request request = new Request.Builder() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .post(RequestBody.create("{}", JSON)) - .addHeader(headerName, "FULCRUM-SYNTHETIC-LEAK") - .build(); + Request request = + new Request.Builder() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .post(RequestBody.create("{}", JSON)) + .addHeader(headerName, "FULCRUM-SYNTHETIC-LEAK") + .build(); try (Response response = client.newCall(request).execute()) { assertEquals(200, response.code()); @@ -77,11 +75,14 @@ public void okHttpLeaksNonstandardHeaderOnCrossOriginPostRedirect(String headerN collector.verify(request().withPath("/collect"), exactly(1)); - org.mockserver.model.HttpRequest[] recorded = collector.retrieveRecordedRequests( - request().withPath("/collect")); + org.mockserver.model.HttpRequest[] recorded = + collector.retrieveRecordedRequests(request().withPath("/collect")); assertEquals(1, recorded.length); - assertNotEquals("", recorded[0].getFirstHeader(headerName), - headerName + " must survive cross-origin redirect in OkHttp — " + assertNotEquals( + "", + recorded[0].getFirstHeader(headerName), + headerName + + " must survive cross-origin redirect in OkHttp — " + "this is why PostBuilder must default to followRedirects=false"); assertEquals("FULCRUM-SYNTHETIC-LEAK", recorded[0].getFirstHeader(headerName)); } @@ -91,15 +92,14 @@ public void okHttpLeaksNonstandardHeaderOnCrossOriginPostRedirect(String headerN public void okHttpDoesNotLeakWhenRedirectsDisabled(String headerName) throws Exception { startRedirectPair(); - OkHttpClient client = new OkHttpClient.Builder() - .followRedirects(false) - .build(); + OkHttpClient client = new OkHttpClient.Builder().followRedirects(false).build(); - Request request = new Request.Builder() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .post(RequestBody.create("{}", JSON)) - .addHeader(headerName, "FULCRUM-SYNTHETIC-NOLEAK") - .build(); + Request request = + new Request.Builder() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .post(RequestBody.create("{}", JSON)) + .addHeader(headerName, "FULCRUM-SYNTHETIC-NOLEAK") + .build(); try (Response response = client.newCall(request).execute()) { assertEquals(302, response.code()); @@ -112,29 +112,30 @@ public void okHttpDoesNotLeakWhenRedirectsDisabled(String headerName) throws Exc public void okHttpStripsAuthorizationButKeepsNonstandardHeadersOnRedirect() throws Exception { startRedirectPair(); - OkHttpClient client = new OkHttpClient.Builder() - .followRedirects(true) - .build(); + OkHttpClient client = new OkHttpClient.Builder().followRedirects(true).build(); - Request request = new Request.Builder() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .post(RequestBody.create("{}", JSON)) - .addHeader("Authorization", "Bearer FULCRUM-SYNTHETIC-BEARER") - .addHeader("X-API-Key", "FULCRUM-SYNTHETIC-APIKEY") - .addHeader("API-Key", "FULCRUM-SYNTHETIC-APIKEY2") - .addHeader("X-Auth-Token", "FULCRUM-SYNTHETIC-TOKEN") - .addHeader("X-Authentication", "FULCRUM-SYNTHETIC-AUTH") - .build(); + Request request = + new Request.Builder() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .post(RequestBody.create("{}", JSON)) + .addHeader("Authorization", "Bearer FULCRUM-SYNTHETIC-BEARER") + .addHeader("X-API-Key", "FULCRUM-SYNTHETIC-APIKEY") + .addHeader("API-Key", "FULCRUM-SYNTHETIC-APIKEY2") + .addHeader("X-Auth-Token", "FULCRUM-SYNTHETIC-TOKEN") + .addHeader("X-Authentication", "FULCRUM-SYNTHETIC-AUTH") + .build(); try (Response response = client.newCall(request).execute()) { assertEquals(200, response.code()); } - org.mockserver.model.HttpRequest[] recorded = collector.retrieveRecordedRequests( - request().withPath("/collect")); + org.mockserver.model.HttpRequest[] recorded = + collector.retrieveRecordedRequests(request().withPath("/collect")); assertEquals(1, recorded.length); - assertEquals("", recorded[0].getFirstHeader("Authorization"), + assertEquals( + "", + recorded[0].getFirstHeader("Authorization"), "OkHttp strips Authorization on cross-origin redirect"); assertEquals("FULCRUM-SYNTHETIC-APIKEY", recorded[0].getFirstHeader("X-API-Key")); @@ -147,12 +148,18 @@ private void startRedirectPair() { redirector = ClientAndServer.startClientAndServer(0); collector = ClientAndServer.startClientAndServer(0); - redirector.when(request().withMethod("POST").withPath("/agent")) - .respond(response() - .withStatusCode(302) - .withHeader("Location", - "http://127.0.0.1:" + collector.getLocalPort() + "/collect")); - collector.when(request().withPath("/collect")) + redirector + .when(request().withMethod("POST").withPath("/agent")) + .respond( + response() + .withStatusCode(302) + .withHeader( + "Location", + "http://127.0.0.1:" + + collector.getLocalPort() + + "/collect")); + collector + .when(request().withPath("/collect")) .respond(response().withStatusCode(200).withBody("redirected")); } } diff --git a/extras/http-client-cdi/pom.xml b/extras/http-client-cdi/pom.xml index eb13c9944..6146dc03f 100644 --- a/extras/http-client-cdi/pom.xml +++ b/extras/http-client-cdi/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/extras/http-client-cdi/src/main/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientProvider.java b/extras/http-client-cdi/src/main/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientProvider.java index f1c9df366..cbd3a7a94 100644 --- a/extras/http-client-cdi/src/main/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientProvider.java +++ b/extras/http-client-cdi/src/main/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientProvider.java @@ -2,7 +2,6 @@ import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.CDI; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientProvider; diff --git a/extras/http-client-cdi/src/test/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientFactoryTest.java b/extras/http-client-cdi/src/test/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientFactoryTest.java index 2f4f3fcda..41bdf2952 100644 --- a/extras/http-client-cdi/src/test/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientFactoryTest.java +++ b/extras/http-client-cdi/src/test/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientFactoryTest.java @@ -24,20 +24,31 @@ public class CdiA2AHttpClientFactoryTest { private static void assertCdiProviderSkippedLogged(List records, String context) { - assertTrue(records.stream() - .anyMatch(r -> r.getLevel() == Level.WARNING - && r.getMessage().equals("Provider cdi skipped")), + assertTrue( + records.stream() + .anyMatch( + r -> + r.getLevel() == Level.WARNING + && r.getMessage().equals("Provider cdi skipped")), "Expected WARNING 'Provider cdi skipped' " + context); } private static List captureFactoryWarnings(Runnable action) { Logger logger = Logger.getLogger(A2AHttpClientFactory.class.getName()); List records = new ArrayList<>(); - Handler captureHandler = new Handler() { - @Override public void publish(LogRecord r) { records.add(r); } - @Override public void flush() {} - @Override public void close() {} - }; + Handler captureHandler = + new Handler() { + @Override + public void publish(LogRecord r) { + records.add(r); + } + + @Override + public void flush() {} + + @Override + public void close() {} + }; Level saved = logger.getLevel(); logger.setLevel(Level.WARNING); logger.addHandler(captureHandler); @@ -57,10 +68,11 @@ class WithCdiContainer { @BeforeEach void startContainer() { - container = SeContainerInitializer.newInstance() - .disableDiscovery() - .addBeanClasses(DummyA2AHttpClient.class) - .initialize(); + container = + SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses(DummyA2AHttpClient.class) + .initialize(); } @AfterEach @@ -72,14 +84,17 @@ void closeContainer() { public void testCreateUsesCdiBeanAtPriority200() { A2AHttpClient result = A2AHttpClientFactory.create(); assertNotNull(result); - assertInstanceOf(DummyA2AHttpClient.class, result, + assertInstanceOf( + DummyA2AHttpClient.class, + result, "Factory should return CDI-provided client (priority 200) over JDK/Vert.x"); } @Test public void testCreateDoesNotLogWarningWhenCdiSucceeds() { List records = captureFactoryWarnings(A2AHttpClientFactory::create); - assertFalse(records.stream().anyMatch(r -> r.getLevel() == Level.WARNING), + assertFalse( + records.stream().anyMatch(r -> r.getLevel() == Level.WARNING), "No warning should be logged when CDI provider succeeds"); } @@ -88,11 +103,14 @@ public void testCreateExcludingCdiSkipsTheCdiBeanAndFallsBack() { // Regression guard for the recursive CDI self-delegation bug: a CDI producer that // produces an A2AHttpClient must exclude "cdi" so the factory does not // re-enter the container and return the proxy being constructed as its own delegate. - A2AHttpClient client = A2AHttpClientFactory.create(A2AHttpClientConfiguration.builder() - .excludedProviderNames(List.of("cdi")) - .build()); + A2AHttpClient client = + A2AHttpClientFactory.create( + A2AHttpClientConfiguration.builder() + .excludedProviderNames(List.of("cdi")) + .build()); assertNotNull(client); - assertFalse(client instanceof DummyA2AHttpClient, + assertFalse( + client instanceof DummyA2AHttpClient, "Excluding 'cdi' must not return the CDI-registered bean"); } } @@ -104,10 +122,11 @@ class WithCdiContainerButNoBeanRegistered { @BeforeEach void startContainer() { - container = SeContainerInitializer.newInstance() - .disableDiscovery() - .addBeanClasses(FillerBean.class) - .initialize(); + container = + SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses(FillerBean.class) + .initialize(); } @AfterEach @@ -119,7 +138,8 @@ void closeContainer() { public void testCreateFallsBackWhenNoBeanRegistered() { A2AHttpClient client = A2AHttpClientFactory.create(); assertNotNull(client, "Factory should fall back when CDI has no A2AHttpClient bean"); - assertFalse(client instanceof DummyA2AHttpClient, + assertFalse( + client instanceof DummyA2AHttpClient, "Fallback client must not be the CDI bean"); } @@ -137,10 +157,12 @@ class WithAmbiguousBeansInContainer { @BeforeEach void startContainer() { - container = SeContainerInitializer.newInstance() - .disableDiscovery() - .addBeanClasses(DummyA2AHttpClient.class, AnotherDummyA2AHttpClient.class) - .initialize(); + container = + SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses( + DummyA2AHttpClient.class, AnotherDummyA2AHttpClient.class) + .initialize(); } @AfterEach @@ -152,7 +174,9 @@ void closeContainer() { public void testCreateFallsBackWhenBeansAreAmbiguous() { A2AHttpClient client = A2AHttpClientFactory.create(); assertNotNull(client, "Factory should fall back when CDI resolution is ambiguous"); - assertFalse(client instanceof DummyA2AHttpClient || client instanceof AnotherDummyA2AHttpClient, + assertFalse( + client instanceof DummyA2AHttpClient + || client instanceof AnotherDummyA2AHttpClient, "Fallback client must not be any of the ambiguous CDI beans"); } @@ -169,7 +193,8 @@ class WithoutCdiContainer { @Test public void testCreateFallsBackWhenNoCdiContainer() { A2AHttpClient client = A2AHttpClientFactory.create(); - assertNotNull(client, "Factory should fall back to next provider when CDI is unavailable"); + assertNotNull( + client, "Factory should fall back to next provider when CDI is unavailable"); } @Test diff --git a/extras/http-client-cdi/src/test/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientProviderTest.java b/extras/http-client-cdi/src/test/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientProviderTest.java index ba11c7bb8..834a1b623 100644 --- a/extras/http-client-cdi/src/test/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientProviderTest.java +++ b/extras/http-client-cdi/src/test/java/org/a2aproject/sdk/client/http/cdi/CdiA2AHttpClientProviderTest.java @@ -21,10 +21,11 @@ class WithBeanInContainer { @BeforeEach void startContainer() { - container = SeContainerInitializer.newInstance() - .disableDiscovery() - .addBeanClasses(DummyA2AHttpClient.class) - .initialize(); + container = + SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses(DummyA2AHttpClient.class) + .initialize(); } @AfterEach @@ -47,10 +48,11 @@ class WithoutBeanInContainer { @BeforeEach void startContainer() { - container = SeContainerInitializer.newInstance() - .disableDiscovery() - .addBeanClasses(FillerBean.class) - .initialize(); + container = + SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses(FillerBean.class) + .initialize(); } @AfterEach @@ -71,10 +73,12 @@ class WithAmbiguousBeansInContainer { @BeforeEach void startContainer() { - container = SeContainerInitializer.newInstance() - .disableDiscovery() - .addBeanClasses(DummyA2AHttpClient.class, AnotherDummyA2AHttpClient.class) - .initialize(); + container = + SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses( + DummyA2AHttpClient.class, AnotherDummyA2AHttpClient.class) + .initialize(); } @AfterEach diff --git a/extras/http-client-vertx/pom.xml b/extras/http-client-vertx/pom.xml index 46514cdbc..e377cfe73 100644 --- a/extras/http-client-vertx/pom.xml +++ b/extras/http-client-vertx/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClient.java b/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClient.java index 92501e534..d77b15096 100644 --- a/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClient.java +++ b/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClient.java @@ -38,7 +38,6 @@ import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpHeaders; import org.a2aproject.sdk.client.http.A2AHttpResponse; @@ -178,24 +177,27 @@ public VertxA2AHttpClient() { * @param redirectHeaderPolicy policy applied to explicitly enabled POST redirects * @return a client with an internally managed Vert.x instance and supplied redirect policy */ - public static VertxA2AHttpClient withRedirectHeaderPolicy(RedirectHeaderPolicy redirectHeaderPolicy) { + public static VertxA2AHttpClient withRedirectHeaderPolicy( + RedirectHeaderPolicy redirectHeaderPolicy) { return new VertxA2AHttpClient(createVertx(), redirectHeaderPolicy); } - private VertxA2AHttpClient(ManagedVertx managedVertx, RedirectHeaderPolicy redirectHeaderPolicy) { + private VertxA2AHttpClient( + ManagedVertx managedVertx, RedirectHeaderPolicy redirectHeaderPolicy) { this(managedVertx.vertx(), managedVertx.ownsVertx(), redirectHeaderPolicy); } - private VertxA2AHttpClient(Vertx vertx, boolean ownsVertx, RedirectHeaderPolicy redirectHeaderPolicy) { + private VertxA2AHttpClient( + Vertx vertx, boolean ownsVertx, RedirectHeaderPolicy redirectHeaderPolicy) { this.vertx = Assert.checkNotNullParam("vertx", vertx); this.ownsVertx = ownsVertx; - this.redirectHeaderPolicy = Assert.checkNotNullParam("redirectHeaderPolicy", redirectHeaderPolicy); - WebClientOptions options = new WebClientOptions() - .setFollowRedirects(false) - .setKeepAlive(true); + this.redirectHeaderPolicy = + Assert.checkNotNullParam("redirectHeaderPolicy", redirectHeaderPolicy); + WebClientOptions options = + new WebClientOptions().setFollowRedirects(false).setKeepAlive(true); this.webClient = WebClient.create(vertx, options); this.httpClient = vertx.createHttpClient(new HttpClientOptions().setKeepAlive(true)); - LOGGER.fine("Vert.x client is ready."); + LOGGER.fine("Vert.x client is ready."); } private static ManagedVertx createVertx() { @@ -323,9 +325,8 @@ T self() { * @throws InterruptedException if the thread is interrupted while waiting */ private A2AHttpResponse executeSyncRequest( - HttpRequest request, - Map headers, - @Nullable Buffer bodyBuffer) throws IOException, InterruptedException { + HttpRequest request, Map headers, @Nullable Buffer bodyBuffer) + throws IOException, InterruptedException { // Add headers for (Map.Entry entry : headers.entrySet()) { @@ -349,14 +350,14 @@ private A2AHttpResponse executeSyncRequest( Throwable error = errorRef.get(); if (error instanceof IOException) { throw (IOException) error; - } + } if (error instanceof InterruptedException) { throw (InterruptedException) error; } throw new IOException("Request failed", error); } A2AHttpResponse finalResponse = responseRef.get(); - if(finalResponse == null) { + if (finalResponse == null) { throw new IllegalStateException("No response from http request"); } return finalResponse; @@ -378,20 +379,31 @@ private void handleResponse( switch (status) { case HTTP_UNAUTHORIZED -> { A2AHttpHeaders headers = fromVertxHeaders(response.headers()); - errorRef.set(new IOException(A2AErrorMessages.AUTHENTICATION_FAILED, - new org.a2aproject.sdk.spec.A2AClientHTTPError( - HTTP_UNAUTHORIZED, A2AErrorMessages.AUTHENTICATION_FAILED, null, headers.toMap()))); + errorRef.set( + new IOException( + A2AErrorMessages.AUTHENTICATION_FAILED, + new org.a2aproject.sdk.spec.A2AClientHTTPError( + HTTP_UNAUTHORIZED, + A2AErrorMessages.AUTHENTICATION_FAILED, + null, + headers.toMap()))); } case HTTP_FORBIDDEN -> { A2AHttpHeaders headers = fromVertxHeaders(response.headers()); - errorRef.set(new IOException(A2AErrorMessages.AUTHORIZATION_FAILED, - new org.a2aproject.sdk.spec.A2AClientHTTPError( - HTTP_FORBIDDEN, A2AErrorMessages.AUTHORIZATION_FAILED, null, headers.toMap()))); + errorRef.set( + new IOException( + A2AErrorMessages.AUTHORIZATION_FAILED, + new org.a2aproject.sdk.spec.A2AClientHTTPError( + HTTP_FORBIDDEN, + A2AErrorMessages.AUTHORIZATION_FAILED, + null, + headers.toMap()))); } default -> { String body = response.bodyAsString(); A2AHttpHeaders headers = fromVertxHeaders(response.headers()); - responseRef.set(new VertxHttpResponse(status, body != null ? body : "", headers)); + responseRef.set( + new VertxHttpResponse(status, body != null ? body : "", headers)); } } } else { @@ -429,111 +441,180 @@ private CompletableFuture executeAsyncSSE( CompletableFuture future = new CompletableFuture<>(); AtomicBoolean futureCompleted = new AtomicBoolean(false); - RequestOptions options = new RequestOptions() - .setAbsoluteURI(url) - .setMethod(httpMethod) - .addHeader(ACCEPT, EVENT_STREAM); + RequestOptions options = + new RequestOptions() + .setAbsoluteURI(url) + .setMethod(httpMethod) + .addHeader(ACCEPT, EVENT_STREAM); for (Map.Entry entry : headers.entrySet()) { options.addHeader(entry.getKey(), entry.getValue()); } - httpClient.request(options) + httpClient + .request(options) .compose(req -> bodyBuffer != null ? req.send(bodyBuffer) : req.send()) - .onSuccess(response -> { - // Pause before inspecting headers so no body bytes are lost while we - // set up the appropriate handler. pipe().to(...) will resume the response. - response.pause(); - int statusCode = response.statusCode(); - if (statusCode == HTTP_UNAUTHORIZED || statusCode == HTTP_FORBIDDEN) { - if (futureCompleted.compareAndSet(false, true)) { - A2AHttpHeaders respHeaders = fromVertxHeaders(response.headers()); - String msg = statusCode == HTTP_UNAUTHORIZED - ? A2AErrorMessages.AUTHENTICATION_FAILED - : A2AErrorMessages.AUTHORIZATION_FAILED; - errorConsumer.accept(new IOException(msg, - new org.a2aproject.sdk.spec.A2AClientHTTPError( - statusCode, msg, null, respHeaders.toMap()))); - future.complete(null); - } - return; - } - String contentType = response.getHeader("Content-Type"); - boolean isSse = statusCode >= HTTP_OK && statusCode < HTTP_MULT_CHOICE - && contentType != null && contentType.contains(EVENT_STREAM); - if (isSse) { - BodyCodec.sseStream(readStream -> - readStream.handler(sseEvent -> { - String data = sseEvent.data(); - if (data != null && !data.isEmpty()) { - String eventType = sseEvent.event() != null ? sseEvent.event() : ServerSentEvent.DEFAULT_EVENT_TYPE; - // Vert.x SseEvent.retry() defaults to 0 when no retry field is present, so - // retry:0 (a valid SSE directive meaning "reconnect immediately") is - // indistinguishable from "not set" and is silently treated as absent. - Long retry = sseEvent.retry() != 0 ? (long) sseEvent.retry() : null; - messageConsumer.accept(new ServerSentEvent(data, eventType, sseEvent.id(), retry)); - } - }) - ).create(ar -> { - if (ar.failed()) { + .onSuccess( + response -> { + // Pause before inspecting headers so no body bytes are lost while we + // set up the appropriate handler. pipe().to(...) will resume the + // response. + response.pause(); + int statusCode = response.statusCode(); + if (statusCode == HTTP_UNAUTHORIZED || statusCode == HTTP_FORBIDDEN) { if (futureCompleted.compareAndSet(false, true)) { - errorConsumer.accept(ar.cause()); + A2AHttpHeaders respHeaders = + fromVertxHeaders(response.headers()); + String msg = + statusCode == HTTP_UNAUTHORIZED + ? A2AErrorMessages.AUTHENTICATION_FAILED + : A2AErrorMessages.AUTHORIZATION_FAILED; + errorConsumer.accept( + new IOException( + msg, + new org.a2aproject.sdk.spec.A2AClientHTTPError( + statusCode, + msg, + null, + respHeaders.toMap()))); future.complete(null); } return; } - response.pipe().to(ar.result()) - .onSuccess(v -> { - if (futureCompleted.compareAndSet(false, true)) { - completeRunnable.run(); - future.complete(null); - } - }) - .onFailure(cause -> { - if (futureCompleted.compareAndSet(false, true)) { - errorConsumer.accept(cause); - future.complete(null); - } - }); + String contentType = response.getHeader("Content-Type"); + boolean isSse = + statusCode >= HTTP_OK + && statusCode < HTTP_MULT_CHOICE + && contentType != null + && contentType.contains(EVENT_STREAM); + if (isSse) { + BodyCodec.sseStream( + readStream -> + readStream.handler( + sseEvent -> { + String data = sseEvent.data(); + if (data != null + && !data.isEmpty()) { + String eventType = + sseEvent.event() + != null + ? sseEvent + .event() + : ServerSentEvent + .DEFAULT_EVENT_TYPE; + // Vert.x SseEvent.retry() + // defaults to 0 when no + // retry field is present, + // so + // retry:0 (a valid SSE + // directive meaning + // "reconnect immediately") + // is + // indistinguishable from + // "not set" and is silently + // treated as absent. + Long retry = + sseEvent.retry() + != 0 + ? (long) + sseEvent + .retry() + : null; + messageConsumer.accept( + new ServerSentEvent( + data, + eventType, + sseEvent + .id(), + retry)); + } + })) + .create( + ar -> { + if (ar.failed()) { + if (futureCompleted.compareAndSet( + false, true)) { + errorConsumer.accept(ar.cause()); + future.complete(null); + } + return; + } + response.pipe() + .to(ar.result()) + .onSuccess( + v -> { + if (futureCompleted + .compareAndSet( + false, + true)) { + completeRunnable.run(); + future.complete(null); + } + }) + .onFailure( + cause -> { + if (futureCompleted + .compareAndSet( + false, + true)) { + errorConsumer.accept( + cause); + future.complete(null); + } + }); + }); + } else { + // Non-SSE response (error body): collect the full body and deliver + // it + // to messageConsumer so the SSEEventListener can parse the JSON-RPC + // error. + // Using body() instead of pipe() avoids a race where small + // responses + // are already ended before pipe() can attach, causing + // "Response already ended" IllegalStateException. + response.resume(); + response.body() + .onSuccess( + body -> { + if (body != null && body.length() > 0) { + String text = + body.toString( + StandardCharsets + .UTF_8) + .trim(); + if (!text.isEmpty()) { + messageConsumer.accept( + new ServerSentEvent(text)); + } + } + if (futureCompleted.compareAndSet( + false, true)) { + completeRunnable.run(); + future.complete(null); + } + }) + .onFailure( + cause -> { + if (futureCompleted.compareAndSet( + false, true)) { + errorConsumer.accept(cause); + future.complete(null); + } + }); + } + }) + .onFailure( + cause -> { + if (futureCompleted.compareAndSet(false, true)) { + errorConsumer.accept(cause); + future.complete(null); + } }); - } else { - // Non-SSE response (error body): collect the full body and deliver it - // to messageConsumer so the SSEEventListener can parse the JSON-RPC error. - // Using body() instead of pipe() avoids a race where small responses - // are already ended before pipe() can attach, causing - // "Response already ended" IllegalStateException. - response.resume(); - response.body() - .onSuccess(body -> { - if (body != null && body.length() > 0) { - String text = body.toString(StandardCharsets.UTF_8).trim(); - if (!text.isEmpty()) { - messageConsumer.accept(new ServerSentEvent(text)); - } - } - if (futureCompleted.compareAndSet(false, true)) { - completeRunnable.run(); - future.complete(null); - } - }) - .onFailure(cause -> { - if (futureCompleted.compareAndSet(false, true)) { - errorConsumer.accept(cause); - future.complete(null); - } - }); - } - }) - .onFailure(cause -> { - if (futureCompleted.compareAndSet(false, true)) { - errorConsumer.accept(cause); - future.complete(null); - } - }); return future; } - private class VertxGetBuilder extends VertxBuilder implements A2AHttpClient.GetBuilder { + private class VertxGetBuilder extends VertxBuilder + implements A2AHttpClient.GetBuilder { private boolean followRedirects = false; @@ -563,16 +644,23 @@ public A2AHttpResponse get() throws IOException, InterruptedException { if (location != null) { RedirectHeaderPolicy.RedirectedRequest redirect; try { - redirect = redirectHeaderPolicy - .redirectedRequest(URI.create(url), response.status(), location, headers) - .orElse(null); + redirect = + redirectHeaderPolicy + .redirectedRequest( + URI.create(url), + response.status(), + location, + headers) + .orElse(null); } catch (IllegalArgumentException e) { LOGGER.fine("Not following redirect with malformed Location: " + location); return response; } if (redirect != null) { return executeSyncRequest( - webClient.getAbs(redirect.target().toString()), redirect.headers(), null); + webClient.getAbs(redirect.target().toString()), + redirect.headers(), + null); } } } @@ -583,13 +671,22 @@ public A2AHttpResponse get() throws IOException, InterruptedException { public CompletableFuture getAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, - Runnable completeRunnable) throws IOException, InterruptedException { - - return executeAsyncSSE(HttpMethod.GET, url, headers, null, messageConsumer, errorConsumer, completeRunnable); + Runnable completeRunnable) + throws IOException, InterruptedException { + + return executeAsyncSSE( + HttpMethod.GET, + url, + headers, + null, + messageConsumer, + errorConsumer, + completeRunnable); } } - private class VertxPostBuilder extends VertxBuilder implements A2AHttpClient.PostBuilder { + private class VertxPostBuilder extends VertxBuilder + implements A2AHttpClient.PostBuilder { private String body = ""; private boolean followRedirects = false; @@ -629,8 +726,11 @@ public A2AHttpResponse post() throws IOException, InterruptedException { if (location != null) { RedirectHeaderPolicy.RedirectedRequest redirect; try { - redirect = redirectHeaderPolicy.redirectedRequest(URI.create(url), statusCode, location, headers) - .orElse(null); + redirect = + redirectHeaderPolicy + .redirectedRequest( + URI.create(url), statusCode, location, headers) + .orElse(null); } catch (IllegalArgumentException e) { LOGGER.fine("Not following redirect with malformed Location: " + location); return response; @@ -639,13 +739,19 @@ public A2AHttpResponse post() throws IOException, InterruptedException { return response; } if (redirect.redirectAsGet()) { - // RFC 7231: 301/302/303 redirect POST as GET; 307/308 would preserve the method + // RFC 7231: 301/302/303 redirect POST as GET; 307/308 would preserve the + // method return executeSyncRequest( - webClient.getAbs(redirect.target().toString()), redirect.headers(), null); + webClient.getAbs(redirect.target().toString()), + redirect.headers(), + null); } else { - // RFC 7538: 307/308 must repeat the request with the original method and body + // RFC 7538: 307/308 must repeat the request with the original method and + // body return executeSyncRequest( - webClient.postAbs(redirect.target().toString()), redirect.headers(), bodyBuffer); + webClient.postAbs(redirect.target().toString()), + redirect.headers(), + bodyBuffer); } } } @@ -656,14 +762,23 @@ public A2AHttpResponse post() throws IOException, InterruptedException { public CompletableFuture postAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, - Runnable completeRunnable) throws IOException, InterruptedException { + Runnable completeRunnable) + throws IOException, InterruptedException { Buffer bodyBuffer = Buffer.buffer(body, StandardCharsets.UTF_8.name()); - return executeAsyncSSE(HttpMethod.POST, url, headers, bodyBuffer, messageConsumer, errorConsumer, completeRunnable); + return executeAsyncSSE( + HttpMethod.POST, + url, + headers, + bodyBuffer, + messageConsumer, + errorConsumer, + completeRunnable); } } - private class VertxDeleteBuilder extends VertxBuilder implements A2AHttpClient.DeleteBuilder { + private class VertxDeleteBuilder extends VertxBuilder + implements A2AHttpClient.DeleteBuilder { /** * {@inheritDoc} @@ -707,8 +822,11 @@ private static class PlainBodyWriteStream implements WriteStream { @Override public Future write(Buffer data) { if (rawBuffer.length() + data.length() > MAX_BUFFER_BYTES) { - IllegalStateException ex = new IllegalStateException( - "Response body exceeded maximum allowed size of " + MAX_BUFFER_BYTES + " bytes"); + IllegalStateException ex = + new IllegalStateException( + "Response body exceeded maximum allowed size of " + + MAX_BUFFER_BYTES + + " bytes"); Handler eh = exceptionHandler; if (eh != null) { eh.handle(ex); @@ -771,7 +889,8 @@ private static A2AHttpHeaders fromVertxHeaders(io.vertx.core.MultiMap headers) { return A2AHttpHeaders.of(snapshot); } - private record VertxHttpResponse(int status, String body, A2AHttpHeaders headers) implements A2AHttpResponse { + private record VertxHttpResponse(int status, String body, A2AHttpHeaders headers) + implements A2AHttpResponse { @Override public int status() { diff --git a/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientProvider.java b/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientProvider.java index 5b672820d..d42158470 100644 --- a/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientProvider.java +++ b/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientProvider.java @@ -2,10 +2,9 @@ import java.util.logging.Level; import java.util.logging.Logger; - import org.a2aproject.sdk.client.http.A2AHttpClient; -import org.a2aproject.sdk.client.http.A2AHttpClientProvider; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; +import org.a2aproject.sdk.client.http.A2AHttpClientProvider; import org.a2aproject.sdk.client.http.RedirectHeaderPolicy; /** @@ -23,7 +22,8 @@ */ public final class VertxA2AHttpClientProvider implements A2AHttpClientProvider { - private static final Logger LOGGER = Logger.getLogger(VertxA2AHttpClientProvider.class.getName()); + private static final Logger LOGGER = + Logger.getLogger(VertxA2AHttpClientProvider.class.getName()); private static final boolean VERTX_AVAILABLE = isVertxAvailable(); private static boolean isVertxAvailable() { @@ -32,7 +32,11 @@ private static boolean isVertxAvailable() { Class.forName("io.vertx.ext.web.client.WebClient"); return true; } catch (ClassNotFoundException ex) { - LOGGER.log(Level.FINE, "Vert.x classes are not available on the classpath. Falling back to other providers.", ex); + LOGGER.log( + Level.FINE, + "Vert.x classes are not available on the classpath. Falling back to other" + + " providers.", + ex); return false; } } @@ -41,15 +45,17 @@ private static boolean isVertxAvailable() { public A2AHttpClient create() { if (!VERTX_AVAILABLE) { throw new IllegalStateException( - "Vert.x classes are not available on the classpath. " - + "Add io.vertx:vertx-web-client dependency or use the JDK HTTP client implementation."); + "Vert.x classes are not available on the classpath. Add" + + " io.vertx:vertx-web-client dependency or use the JDK HTTP client" + + " implementation."); } // Reflection avoids a compile-time reference to VertxA2AHttpClient so that // this provider class can be loaded by ServiceLoader even when Vert.x jars // are absent from the classpath. try { - Class clientClass = Class.forName("org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient"); + Class clientClass = + Class.forName("org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient"); return (A2AHttpClient) clientClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new IllegalStateException("Failed to create VertxA2AHttpClient instance", e); @@ -63,14 +69,17 @@ public A2AHttpClient create(A2AHttpClientConfiguration configuration) { } if (!VERTX_AVAILABLE) { throw new IllegalStateException( - "Vert.x classes are not available on the classpath. " - + "Add io.vertx:vertx-web-client dependency or use the JDK HTTP client implementation."); + "Vert.x classes are not available on the classpath. Add" + + " io.vertx:vertx-web-client dependency or use the JDK HTTP client" + + " implementation."); } try { - Class clientClass = Class.forName("org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient"); - return (A2AHttpClient) clientClass - .getMethod("withRedirectHeaderPolicy", RedirectHeaderPolicy.class) - .invoke(null, configuration.redirectHeaderPolicy()); + Class clientClass = + Class.forName("org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient"); + return (A2AHttpClient) + clientClass + .getMethod("withRedirectHeaderPolicy", RedirectHeaderPolicy.class) + .invoke(null, configuration.redirectHeaderPolicy()); } catch (Exception e) { throw new IllegalStateException("Failed to create VertxA2AHttpClient instance", e); } diff --git a/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/package-info.java b/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/package-info.java index cd522eabd..16d9ac39b 100644 --- a/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/package-info.java +++ b/extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.client.http.vertx; import org.jspecify.annotations.NullMarked; - diff --git a/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientFactoryTest.java b/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientFactoryTest.java index cda763a1e..6d03921d5 100644 --- a/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientFactoryTest.java +++ b/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientFactoryTest.java @@ -5,7 +5,6 @@ import static org.junit.jupiter.api.Assertions.fail; import java.util.List; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; import org.a2aproject.sdk.client.http.A2AHttpClientFactory; @@ -16,11 +15,14 @@ public class VertxA2AHttpClientFactoryTest { @Test public void testCreateReturnsVertxClient() { - // When both JDK and Vertx are on classpath, Vertx should be preferred due to higher priority + // When both JDK and Vertx are on classpath, Vertx should be preferred due to higher + // priority A2AHttpClient client = A2AHttpClientFactory.create(); assertNotNull(client); - assertInstanceOf(VertxA2AHttpClient.class, client, - "Factory should return VertxA2AHttpClient when Vertx is available"); + assertInstanceOf( + VertxA2AHttpClient.class, + client, + "Factory should return VertxA2AHttpClient when Vertx is available"); // Clean up if (client instanceof AutoCloseable) { try { @@ -33,14 +35,15 @@ public void testCreateReturnsVertxClient() { @Test public void testCreateExcludingVertxFallsBackToJdkClient() { - A2AHttpClientConfiguration configuration = A2AHttpClientConfiguration.builder() - .excludedProviderNames(List.of("vertx")) - .build(); + A2AHttpClientConfiguration configuration = + A2AHttpClientConfiguration.builder() + .excludedProviderNames(List.of("vertx")) + .build(); A2AHttpClient client = A2AHttpClientFactory.create(configuration); assertNotNull(client); - assertInstanceOf(JdkA2AHttpClient.class, client, - "Excluding Vert.x should select the JDK fallback"); + assertInstanceOf( + JdkA2AHttpClient.class, client, "Excluding Vert.x should select the JDK fallback"); } } diff --git a/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientIntegrationTest.java b/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientIntegrationTest.java index ffd4b4d42..7b46ed614 100644 --- a/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientIntegrationTest.java +++ b/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientIntegrationTest.java @@ -22,7 +22,7 @@ public class VertxA2AHttpClientIntegrationTest { @BeforeEach public void setup() { - mockServer = ClientAndServer.startClientAndServer(0); // Use random port + mockServer = ClientAndServer.startClientAndServer(0); // Use random port client = new VertxA2AHttpClient(); } @@ -46,9 +46,7 @@ public void testGetRequestSuccess() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(200).withBody("success")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .get(); + A2AHttpResponse response = client.createGet().url(getBaseUrl() + "/test").get(); assertEquals(200, response.status()); assertTrue(response.success()); @@ -58,16 +56,15 @@ public void testGetRequestSuccess() throws Exception { @Test public void testPostRequestSuccess() throws Exception { mockServer - .when(request() - .withMethod("POST") - .withPath("/test") - .withBody("{\"key\":\"value\"}")) + .when( + request() + .withMethod("POST") + .withPath("/test") + .withBody("{\"key\":\"value\"}")) .respond(response().withStatusCode(201).withBody("created")); - A2AHttpResponse response = client.createPost() - .url(getBaseUrl() + "/test") - .body("{\"key\":\"value\"}") - .post(); + A2AHttpResponse response = + client.createPost().url(getBaseUrl() + "/test").body("{\"key\":\"value\"}").post(); assertEquals(201, response.status()); assertTrue(response.success()); @@ -80,9 +77,7 @@ public void testDeleteRequestSuccess() throws Exception { .when(request().withMethod("DELETE").withPath("/test")) .respond(response().withStatusCode(204)); - A2AHttpResponse response = client.createDelete() - .url(getBaseUrl() + "/test") - .delete(); + A2AHttpResponse response = client.createDelete().url(getBaseUrl() + "/test").delete(); assertEquals(204, response.status()); assertTrue(response.success()); @@ -99,10 +94,11 @@ public void testCustomHostHeaderOverride() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(200).withBody("ok")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .addHeader("Host", "custom-logical-host.example.com") - .get(); + A2AHttpResponse response = + client.createGet() + .url(getBaseUrl() + "/test") + .addHeader("Host", "custom-logical-host.example.com") + .get(); assertEquals(200, response.status()); mockServer.verify( @@ -116,11 +112,12 @@ public void test401AuthenticationErrorOnGet() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(401)); - Exception exception = assertThrows(java.io.IOException.class, () -> { - client.createGet() - .url(getBaseUrl() + "/test") - .get(); - }); + Exception exception = + assertThrows( + java.io.IOException.class, + () -> { + client.createGet().url(getBaseUrl() + "/test").get(); + }); assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage()); } @@ -131,11 +128,12 @@ public void test403AuthorizationErrorOnGet() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(403)); - Exception exception = assertThrows(java.io.IOException.class, () -> { - client.createGet() - .url(getBaseUrl() + "/test") - .get(); - }); + Exception exception = + assertThrows( + java.io.IOException.class, + () -> { + client.createGet().url(getBaseUrl() + "/test").get(); + }); assertEquals(A2AErrorMessages.AUTHORIZATION_FAILED, exception.getMessage()); } @@ -146,12 +144,12 @@ public void test401AuthenticationErrorOnPost() throws Exception { .when(request().withMethod("POST").withPath("/test")) .respond(response().withStatusCode(401)); - Exception exception = assertThrows(java.io.IOException.class, () -> { - client.createPost() - .url(getBaseUrl() + "/test") - .body("{}") - .post(); - }); + Exception exception = + assertThrows( + java.io.IOException.class, + () -> { + client.createPost().url(getBaseUrl() + "/test").body("{}").post(); + }); assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage()); } @@ -162,12 +160,12 @@ public void test403AuthorizationErrorOnPost() throws Exception { .when(request().withMethod("POST").withPath("/test")) .respond(response().withStatusCode(403)); - Exception exception = assertThrows(java.io.IOException.class, () -> { - client.createPost() - .url(getBaseUrl() + "/test") - .body("{}") - .post(); - }); + Exception exception = + assertThrows( + java.io.IOException.class, + () -> { + client.createPost().url(getBaseUrl() + "/test").body("{}").post(); + }); assertEquals(A2AErrorMessages.AUTHORIZATION_FAILED, exception.getMessage()); } @@ -178,11 +176,12 @@ public void test401AuthenticationErrorOnDelete() throws Exception { .when(request().withMethod("DELETE").withPath("/test")) .respond(response().withStatusCode(401)); - Exception exception = assertThrows(java.io.IOException.class, () -> { - client.createDelete() - .url(getBaseUrl() + "/test") - .delete(); - }); + Exception exception = + assertThrows( + java.io.IOException.class, + () -> { + client.createDelete().url(getBaseUrl() + "/test").delete(); + }); assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage()); } @@ -190,18 +189,20 @@ public void test401AuthenticationErrorOnDelete() throws Exception { @Test public void testHeaderPropagation() throws Exception { mockServer - .when(request() - .withMethod("GET") - .withPath("/test") - .withHeader("Authorization", "Bearer token") - .withHeader("X-Custom-Header", "custom-value")) + .when( + request() + .withMethod("GET") + .withPath("/test") + .withHeader("Authorization", "Bearer token") + .withHeader("X-Custom-Header", "custom-value")) .respond(response().withStatusCode(200).withBody("ok")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .addHeader("Authorization", "Bearer token") - .addHeader("X-Custom-Header", "custom-value") - .get(); + A2AHttpResponse response = + client.createGet() + .url(getBaseUrl() + "/test") + .addHeader("Authorization", "Bearer token") + .addHeader("X-Custom-Header", "custom-value") + .get(); assertEquals(200, response.status()); assertEquals("ok", response.body()); @@ -213,9 +214,7 @@ public void testNonSuccessStatusCode() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(500).withBody("Internal Server Error")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .get(); + A2AHttpResponse response = client.createGet().url(getBaseUrl() + "/test").get(); assertEquals(500, response.status()); assertFalse(response.success()); @@ -228,9 +227,7 @@ public void test404NotFound() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(404).withBody("Not Found")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .get(); + A2AHttpResponse response = client.createGet().url(getBaseUrl() + "/test").get(); assertEquals(404, response.status()); assertFalse(response.success()); diff --git a/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientRedirectTest.java b/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientRedirectTest.java index 6ff1eb91d..946380c8d 100644 --- a/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientRedirectTest.java +++ b/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientRedirectTest.java @@ -9,7 +9,6 @@ import java.net.URI; import java.util.Map; import java.util.Set; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; import org.a2aproject.sdk.client.http.A2AHttpResponse; @@ -53,26 +52,34 @@ public void stopRelativeServer() { @Test public void synchronousPostFollowsRelativeRedirectLocation() throws Exception { relativeServer = ClientAndServer.startClientAndServer(0); - relativeServer.when(request().withMethod("POST").withPath("/agent")) + relativeServer + .when(request().withMethod("POST").withPath("/agent")) .respond(response().withStatusCode(302).withHeader("Location", "/collect")); - relativeServer.when(request().withPath("/collect")) + relativeServer + .when(request().withPath("/collect")) .respond(response().withStatusCode(200).withBody("redirected")); - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") - .addHeader("X-API-Key", "FULCRUM-SYNTHETIC-RELATIVE") - .body("{}") - .followRedirects(true) - .post(); - - assertEquals(200, result.status(), - "Synchronous POST must resolve and follow a relative Location against the request URI"); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") + .addHeader("X-API-Key", "FULCRUM-SYNTHETIC-RELATIVE") + .body("{}") + .followRedirects(true) + .post(); + + assertEquals( + 200, + result.status(), + "Synchronous POST must resolve and follow a relative Location against the request" + + " URI"); relativeServer.verify(request().withPath("/collect"), exactly(1)); } @ParameterizedTest @ValueSource(ints = {301, 302, 303}) - public void absoluteCrossOriginRedirectDoesNotForwardSensitiveHeaders(int redirectStatus) throws Exception { + public void absoluteCrossOriginRedirectDoesNotForwardSensitiveHeaders(int redirectStatus) + throws Exception { assertCrossOriginRedirectDoesNotForwardSensitiveHeaders(false, redirectStatus); } @@ -88,38 +95,56 @@ public void providerConfigurationAppliesCustomRedirectPolicy() throws Exception } @Test - public void providerConfigurationDoesNotForwardCustomHeaderToUntrustedOrigin() throws Exception { + public void providerConfigurationDoesNotForwardCustomHeaderToUntrustedOrigin() + throws Exception { assertProviderConfigurationAppliesCustomRedirectPolicy(false); } - private void assertProviderConfigurationAppliesCustomRedirectPolicy(boolean trustCollector) throws Exception { + private void assertProviderConfigurationAppliesCustomRedirectPolicy(boolean trustCollector) + throws Exception { relativeServer = ClientAndServer.startClientAndServer(0); redirectCollector = ClientAndServer.startClientAndServer(0); - relativeServer.when(request().withMethod("POST").withPath("/agent")) - .respond(response().withStatusCode(302).withHeader( - "Location", "http://127.0.0.1:" + redirectCollector.getLocalPort() + "/collect")); - redirectCollector.when(request().withPath("/collect")) + relativeServer + .when(request().withMethod("POST").withPath("/agent")) + .respond( + response() + .withStatusCode(302) + .withHeader( + "Location", + "http://127.0.0.1:" + + redirectCollector.getLocalPort() + + "/collect")); + redirectCollector + .when(request().withPath("/collect")) .respond(response().withStatusCode(200).withBody("redirected")); - int trustedPort = trustCollector ? redirectCollector.getLocalPort() : relativeServer.getLocalPort(); - A2AHttpClient client = new VertxA2AHttpClientProvider().create( - A2AHttpClientConfiguration.builder() - .redirectHeaderPolicy(new RedirectHeaderPolicy( - Set.of("X-Trusted"), - Set.of(URI.create("http://127.0.0.1:" + trustedPort)))) - .build()); + int trustedPort = + trustCollector ? redirectCollector.getLocalPort() : relativeServer.getLocalPort(); + A2AHttpClient client = + new VertxA2AHttpClientProvider() + .create( + A2AHttpClientConfiguration.builder() + .redirectHeaderPolicy( + new RedirectHeaderPolicy( + Set.of("X-Trusted"), + Set.of( + URI.create( + "http://127.0.0.1:" + + trustedPort)))) + .build()); try { - A2AHttpResponse result = client.createPost() - .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") - .addHeader("Authorization", "Bearer secret") - .addHeader("X-Trusted", "allowed") - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + client.createPost() + .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") + .addHeader("Authorization", "Bearer secret") + .addHeader("X-Trusted", "allowed") + .body("{}") + .followRedirects(true) + .post(); assertEquals(200, result.status()); - org.mockserver.model.HttpRequest[] requests = redirectCollector.retrieveRecordedRequests( - request().withPath("/collect")); + org.mockserver.model.HttpRequest[] requests = + redirectCollector.retrieveRecordedRequests(request().withPath("/collect")); assertEquals(1, requests.length); if (trustCollector) { assertEquals("allowed", requests[0].getFirstHeader("X-Trusted")); @@ -132,29 +157,34 @@ private void assertProviderConfigurationAppliesCustomRedirectPolicy(boolean trus } } - private void assertCrossOriginRedirectDoesNotForwardSensitiveHeaders(boolean protocolRelative, int redirectStatus) - throws Exception { + private void assertCrossOriginRedirectDoesNotForwardSensitiveHeaders( + boolean protocolRelative, int redirectStatus) throws Exception { relativeServer = ClientAndServer.startClientAndServer(0); redirectCollector = ClientAndServer.startClientAndServer(0); String target = "127.0.0.1:" + redirectCollector.getLocalPort() + "/collect"; String location = protocolRelative ? "//" + target : "http://" + target; - relativeServer.when(request().withMethod("POST").withPath("/agent")) - .respond(response().withStatusCode(redirectStatus).withHeader("Location", location)); - redirectCollector.when(request().withPath("/collect")) + relativeServer + .when(request().withMethod("POST").withPath("/agent")) + .respond( + response().withStatusCode(redirectStatus).withHeader("Location", location)); + redirectCollector + .when(request().withPath("/collect")) .respond(response().withStatusCode(200).withBody("redirected")); - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") - .addHeaders(requestHeaders()) - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") + .addHeaders(requestHeaders()) + .body("{}") + .followRedirects(true) + .post(); assertEquals(200, result.status()); redirectCollector.verify(request().withPath("/collect"), exactly(1)); - org.mockserver.model.HttpRequest[] recorded = redirectCollector.retrieveRecordedRequests( - request().withPath("/collect")); + org.mockserver.model.HttpRequest[] recorded = + redirectCollector.retrieveRecordedRequests(request().withPath("/collect")); assertEquals(1, recorded.length); assertEquals("GET", recorded[0].getMethod().getValue()); @Nullable String redirectedBody = recorded[0].getBodyAsString(); @@ -169,21 +199,26 @@ private void assertCrossOriginRedirectDoesNotForwardSensitiveHeaders(boolean pro @Test public void sameOriginRedirectRetainsSensitiveHeaders() throws Exception { relativeServer = ClientAndServer.startClientAndServer(0); - relativeServer.when(request().withMethod("POST").withPath("/agent")) + relativeServer + .when(request().withMethod("POST").withPath("/agent")) .respond(response().withStatusCode(307).withHeader("Location", "/collect")); - relativeServer.when(request().withMethod("POST").withPath("/collect")) + relativeServer + .when(request().withMethod("POST").withPath("/collect")) .respond(response().withStatusCode(200).withBody("redirected")); - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") - .addHeaders(requestHeaders()) - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") + .addHeaders(requestHeaders()) + .body("{}") + .followRedirects(true) + .post(); assertEquals(200, result.status()); - org.mockserver.model.HttpRequest[] recorded = relativeServer.retrieveRecordedRequests( - request().withMethod("POST").withPath("/collect")); + org.mockserver.model.HttpRequest[] recorded = + relativeServer.retrieveRecordedRequests( + request().withMethod("POST").withPath("/collect")); assertEquals(1, recorded.length); assertHeadersPresent(recorded[0], requestHeaders()); } @@ -191,21 +226,26 @@ public void sameOriginRedirectRetainsSensitiveHeaders() throws Exception { @Test public void sameOriginPostToGetRedirectDropsEntityHeaders() throws Exception { relativeServer = ClientAndServer.startClientAndServer(0); - relativeServer.when(request().withMethod("POST").withPath("/agent")) + relativeServer + .when(request().withMethod("POST").withPath("/agent")) .respond(response().withStatusCode(302).withHeader("Location", "/collect")); - relativeServer.when(request().withMethod("GET").withPath("/collect")) + relativeServer + .when(request().withMethod("GET").withPath("/collect")) .respond(response().withStatusCode(200).withBody("redirected")); - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") - .addHeaders(requestHeaders()) - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") + .addHeaders(requestHeaders()) + .body("{}") + .followRedirects(true) + .post(); assertEquals(200, result.status()); - org.mockserver.model.HttpRequest[] recorded = relativeServer.retrieveRecordedRequests( - request().withMethod("GET").withPath("/collect")); + org.mockserver.model.HttpRequest[] recorded = + relativeServer.retrieveRecordedRequests( + request().withMethod("GET").withPath("/collect")); assertEquals(1, recorded.length); assertHeadersPresent(recorded[0], credentialHeaders()); assertEntityHeadersRemoved(recorded[0]); @@ -214,14 +254,17 @@ public void sameOriginPostToGetRedirectDropsEntityHeaders() throws Exception { @Test public void malformedRedirectLocationReturnsOriginalResponse() throws Exception { relativeServer = ClientAndServer.startClientAndServer(0); - relativeServer.when(request().withMethod("POST").withPath("/agent")) + relativeServer + .when(request().withMethod("POST").withPath("/agent")) .respond(response().withStatusCode(302).withHeader("Location", "/pa th")); - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent") + .body("{}") + .followRedirects(true) + .post(); assertEquals(302, result.status()); } @@ -275,7 +318,8 @@ private static void assertHeadersAbsent( org.mockserver.model.HttpRequest request, Map headers) { for (String headerName : headers.keySet()) { @Nullable String value = request.getFirstHeader(headerName); - assertTrue(value == null || value.isEmpty(), + assertTrue( + value == null || value.isEmpty(), headerName + " must not be forwarded on this redirect"); } } @@ -283,19 +327,23 @@ private static void assertHeadersAbsent( private static void assertHeadersPresent( org.mockserver.model.HttpRequest request, Map headers) { for (Map.Entry header : headers.entrySet()) { - assertEquals(header.getValue(), request.getFirstHeader(header.getKey()), + assertEquals( + header.getValue(), + request.getFirstHeader(header.getKey()), header.getKey() + " must be retained on this redirect"); } } private static void assertEntityHeadersRemoved(org.mockserver.model.HttpRequest request) { @Nullable String contentLength = request.getFirstHeader("cOnTeNt-LeNgTh"); - assertTrue(contentLength == null || !contentLength.equals("2"), + assertTrue( + contentLength == null || !contentLength.equals("2"), "Content-Length must not retain the POST body length on a redirected GET"); for (String headerName : entityHeaders().keySet()) { if (!headerName.equalsIgnoreCase("Content-Length")) { @Nullable String value = request.getFirstHeader(headerName); - assertTrue(value == null || value.isEmpty(), + assertTrue( + value == null || value.isEmpty(), headerName + " must not be forwarded on a redirected GET"); } } diff --git a/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientSSETest.java b/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientSSETest.java index 342c862b1..7555ef857 100644 --- a/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientSSETest.java +++ b/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientSSETest.java @@ -31,12 +31,12 @@ public void closeVertxClient() { @Override @Test @Disabled("Vert.x BodyCodec.sseStream() does not propagate last event ID to subsequent events") - public void testSSEEventIdAndLastEventId() { - } + public void testSSEEventIdAndLastEventId() {} @Override @Test - @Disabled("Vert.x BodyCodec.sseStream() has no flush() equivalent for streams ending without a trailing blank line") - public void testSSEStreamEndingWithoutTrailingEmptyLine() { - } + @Disabled( + "Vert.x BodyCodec.sseStream() has no flush() equivalent for streams ending without a" + + " trailing blank line") + public void testSSEStreamEndingWithoutTrailingEmptyLine() {} } diff --git a/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientTest.java b/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientTest.java index 71683f2c7..4f073d3fe 100644 --- a/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientTest.java +++ b/extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientTest.java @@ -28,9 +28,11 @@ public void testVertxParameterConstructor() { @Test public void testVertxParameterConstructorNullThrows() { - assertThrows(IllegalArgumentException.class, () -> { - new VertxA2AHttpClient(null); - }); + assertThrows( + IllegalArgumentException.class, + () -> { + new VertxA2AHttpClient(null); + }); } @Test @@ -78,10 +80,11 @@ public void testBuilderHeaderSetting() { @Test public void testBuilderMethodChaining() { try (VertxA2AHttpClient client = new VertxA2AHttpClient()) { - A2AHttpClient.GetBuilder builder = client.createGet() - .url("https://example.com") - .addHeader("Accept", "application/json") - .addHeader("Authorization", "Bearer token"); + A2AHttpClient.GetBuilder builder = + client.createGet() + .url("https://example.com") + .addHeader("Accept", "application/json") + .addHeader("Authorization", "Bearer token"); assertNotNull(builder); } } diff --git a/extras/multitenancy/pom.xml b/extras/multitenancy/pom.xml index ecdc6312a..dc0a514f7 100644 --- a/extras/multitenancy/pom.xml +++ b/extras/multitenancy/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouter.java b/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouter.java index 50e16fa4d..7bf967744 100644 --- a/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouter.java +++ b/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouter.java @@ -1,15 +1,13 @@ package org.a2aproject.sdk.extras.multitenancy; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; - import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Any; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; - +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; import org.a2aproject.sdk.server.ExtendedAgentCard; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.server.multitenancy.AgentCardRouter; @@ -42,19 +40,19 @@ @ApplicationScoped public class CdiAgentCardRouter implements AgentCardRouter { - @Inject - @Any - Instance allCards; + @Inject @Any Instance allCards; private @Nullable AgentCard defaultExtendedCard; private @Nullable AgentCard defaultPublicCard; @PostConstruct void init() { - defaultExtendedCard = CdiUtils.resolveDefaultBean( - allCards, Tenant.class, ExtendedAgentCard.class, "@ExtendedAgentCard"); - defaultPublicCard = CdiUtils.resolveDefaultBean( - allCards, Tenant.class, PublicAgentCard.class, "@PublicAgentCard"); + defaultExtendedCard = + CdiUtils.resolveDefaultBean( + allCards, Tenant.class, ExtendedAgentCard.class, "@ExtendedAgentCard"); + defaultPublicCard = + CdiUtils.resolveDefaultBean( + allCards, Tenant.class, PublicAgentCard.class, "@PublicAgentCard"); } @Override @@ -62,8 +60,8 @@ void init() { if (tenant == null || tenant.isBlank()) { return defaultExtendedCard; } - Instance selected = allCards.select( - new Tenant.Literal(tenant), ExtendedAgentCard.Literal.INSTANCE); + Instance selected = + allCards.select(new Tenant.Literal(tenant), ExtendedAgentCard.Literal.INSTANCE); if (selected.isResolvable()) { return selected.get(); } @@ -125,6 +123,7 @@ void init() { private static IllegalStateException duplicateTenantBean(String cardType, String tenant) { return new IllegalStateException( - String.format("Multiple tenant %s cards detected for tenant '%s'", cardType, tenant)); + String.format( + "Multiple tenant %s cards detected for tenant '%s'", cardType, tenant)); } } diff --git a/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouter.java b/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouter.java index dd4cb1a23..0fc266246 100644 --- a/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouter.java +++ b/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouter.java @@ -5,7 +5,6 @@ import jakarta.enterprise.inject.Any; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; - import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.multitenancy.AgentExecutorRouter; import org.a2aproject.sdk.server.multitenancy.Tenant; @@ -22,16 +21,14 @@ @ApplicationScoped public class CdiAgentExecutorRouter implements AgentExecutorRouter { - @Inject - @Any - Instance allExecutors; + @Inject @Any Instance allExecutors; private @Nullable AgentExecutor defaultExecutor; @PostConstruct void init() { - defaultExecutor = CdiUtils.resolveDefaultBean( - allExecutors, Tenant.class, null, "AgentExecutor"); + defaultExecutor = + CdiUtils.resolveDefaultBean(allExecutors, Tenant.class, null, "AgentExecutor"); } @Override @@ -55,7 +52,8 @@ public AgentExecutor resolve(@Nullable String tenant) { private AgentExecutor requireDefaultExecutor() { if (defaultExecutor == null) { throw new IllegalStateException( - "No default AgentExecutor bean found — provide an AgentExecutor bean without @Tenant qualifier"); + "No default AgentExecutor bean found — provide an AgentExecutor bean without" + + " @Tenant qualifier"); } return defaultExecutor; } diff --git a/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/package-info.java b/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/package-info.java index 8b08494af..f7676a617 100644 --- a/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/package-info.java +++ b/extras/multitenancy/src/main/java/org/a2aproject/sdk/extras/multitenancy/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.extras.multitenancy; import org.jspecify.annotations.NullMarked; - diff --git a/extras/multitenancy/tests/grpc/pom.xml b/extras/multitenancy/tests/grpc/pom.xml index 553d8122a..6cc9ed59d 100644 --- a/extras/multitenancy/tests/grpc/pom.xml +++ b/extras/multitenancy/tests/grpc/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/multitenancy/tests/grpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/grpc/MultiTenantGrpcTest.java b/extras/multitenancy/tests/grpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/grpc/MultiTenantGrpcTest.java index e92e071df..708b9e3af 100644 --- a/extras/multitenancy/tests/grpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/grpc/MultiTenantGrpcTest.java +++ b/extras/multitenancy/tests/grpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/grpc/MultiTenantGrpcTest.java @@ -1,12 +1,11 @@ package org.a2aproject.sdk.extras.multitenancy.tests.grpc; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; - import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.quarkus.test.junit.QuarkusTest; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.transport.grpc.GrpcTransport; import org.a2aproject.sdk.client.transport.grpc.GrpcTransportConfigBuilder; @@ -37,11 +36,18 @@ protected String getTransportUrl() { @Override protected void configureTransport(ClientBuilder builder) { - builder.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(target -> { - ManagedChannel channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); - channels.add(channel); - return channel; - })); + builder.withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder() + .channelFactory( + target -> { + ManagedChannel channel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + channels.add(channel); + return channel; + })); } @AfterAll diff --git a/extras/multitenancy/tests/jsonrpc/pom.xml b/extras/multitenancy/tests/jsonrpc/pom.xml index 618f2ab24..2a72c7081 100644 --- a/extras/multitenancy/tests/jsonrpc/pom.xml +++ b/extras/multitenancy/tests/jsonrpc/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/multitenancy/tests/jsonrpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/jsonrpc/MultiTenantJSONRPCTest.java b/extras/multitenancy/tests/jsonrpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/jsonrpc/MultiTenantJSONRPCTest.java index a030f07b3..8bc71bb94 100644 --- a/extras/multitenancy/tests/jsonrpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/jsonrpc/MultiTenantJSONRPCTest.java +++ b/extras/multitenancy/tests/jsonrpc/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/jsonrpc/MultiTenantJSONRPCTest.java @@ -27,7 +27,8 @@ protected String getTransportUrl() { @Override protected void configureTransport(ClientBuilder builder) { - builder.withTransport(JSONRPCTransport.class, + builder.withTransport( + JSONRPCTransport.class, new JSONRPCTransportConfigBuilder().httpClient(new JdkA2AHttpClient())); } } diff --git a/extras/multitenancy/tests/rest/pom.xml b/extras/multitenancy/tests/rest/pom.xml index e50406211..74b877033 100644 --- a/extras/multitenancy/tests/rest/pom.xml +++ b/extras/multitenancy/tests/rest/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/multitenancy/tests/rest/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/rest/MultiTenantRestTest.java b/extras/multitenancy/tests/rest/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/rest/MultiTenantRestTest.java index a54740705..74e4d28f8 100644 --- a/extras/multitenancy/tests/rest/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/rest/MultiTenantRestTest.java +++ b/extras/multitenancy/tests/rest/src/test/java/org/a2aproject/sdk/extras/multitenancy/tests/rest/MultiTenantRestTest.java @@ -27,7 +27,8 @@ protected String getTransportUrl() { @Override protected void configureTransport(ClientBuilder builder) { - builder.withTransport(RestTransport.class, + builder.withTransport( + RestTransport.class, new RestTransportConfigBuilder().httpClient(new JdkA2AHttpClient())); } } diff --git a/extras/multitenancy/tests/router-unit/pom.xml b/extras/multitenancy/tests/router-unit/pom.xml index 56be1db21..0510301ed 100644 --- a/extras/multitenancy/tests/router-unit/pom.xml +++ b/extras/multitenancy/tests/router-unit/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouterTest.java b/extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouterTest.java index ddcec2afd..487c445f9 100644 --- a/extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouterTest.java +++ b/extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentCardRouterTest.java @@ -4,13 +4,11 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.util.Collections; -import java.util.List; - import jakarta.enterprise.inject.Produces; import jakarta.enterprise.inject.se.SeContainer; import jakarta.enterprise.inject.se.SeContainerInitializer; - +import java.util.Collections; +import java.util.List; import org.a2aproject.sdk.server.ExtendedAgentCard; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.server.multitenancy.Tenant; @@ -25,9 +23,10 @@ class CdiAgentCardRouterTest { private SeContainer container; private void startContainer(Class... beanClasses) { - SeContainerInitializer initializer = SeContainerInitializer.newInstance() - .disableDiscovery() - .addBeanClasses(CdiAgentCardRouter.class); + SeContainerInitializer initializer = + SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses(CdiAgentCardRouter.class); for (Class beanClass : beanClasses) { initializer.addBeanClasses(beanClass); } @@ -117,49 +116,62 @@ void publicCardNoDefaultReturnsNull() { @Test void publicCardWithPublicQualifierResolvesToTenantSpecific() { - startContainer(DefaultPublicAndExtendedCardProducer.class, PublicQualifiedTenantCardProducer.class); + startContainer( + DefaultPublicAndExtendedCardProducer.class, + PublicQualifiedTenantCardProducer.class); CdiAgentCardRouter router = container.select(CdiAgentCardRouter.class).get(); assertEquals("acme-public-qualified", router.resolvePublicCard("acme").name()); } @Test void publicCardWithBothPublicAndExtendedQualifierResolvesToTenantSpecific() { - startContainer(DefaultPublicAndExtendedCardProducer.class, PublicAndExtendedTenantCardProducer.class); + startContainer( + DefaultPublicAndExtendedCardProducer.class, + PublicAndExtendedTenantCardProducer.class); CdiAgentCardRouter router = container.select(CdiAgentCardRouter.class).get(); assertEquals("acme-both", router.resolvePublicCard("acme").name()); } @Test void explicitPublicCardTakesPrecedenceOverBareTenantCard() { - startContainer(DefaultPublicAndExtendedCardProducer.class, BareAndPublicQualifiedTenantCardProducer.class); + startContainer( + DefaultPublicAndExtendedCardProducer.class, + BareAndPublicQualifiedTenantCardProducer.class); CdiAgentCardRouter router = container.select(CdiAgentCardRouter.class).get(); assertEquals("acme-public-qualified", router.resolvePublicCard("acme").name()); } @Test void duplicatePublicQualifiedCardsThrows() { - startContainer(DefaultPublicAndExtendedCardProducer.class, DuplicatePublicQualifiedTenantCardProducer.class); + startContainer( + DefaultPublicAndExtendedCardProducer.class, + DuplicatePublicQualifiedTenantCardProducer.class); CdiAgentCardRouter router = container.select(CdiAgentCardRouter.class).get(); assertThrows(IllegalStateException.class, () -> router.resolvePublicCard("acme")); } @Test void duplicateBareTenantCardsThrows() { - startContainer(DefaultPublicAndExtendedCardProducer.class, DuplicateBareTenantCardProducer.class); + startContainer( + DefaultPublicAndExtendedCardProducer.class, DuplicateBareTenantCardProducer.class); CdiAgentCardRouter router = container.select(CdiAgentCardRouter.class).get(); assertThrows(IllegalStateException.class, () -> router.resolvePublicCard("acme")); } @Test void duplicateExtendedTenantCardsThrows() { - startContainer(DefaultPublicAndExtendedCardProducer.class, DuplicateExtendedTenantCardProducer.class); + startContainer( + DefaultPublicAndExtendedCardProducer.class, + DuplicateExtendedTenantCardProducer.class); CdiAgentCardRouter router = container.select(CdiAgentCardRouter.class).get(); assertThrows(IllegalStateException.class, () -> router.resolveExtendedCard("acme")); } @Test void dualQualifiedCardResolvedByBothPublicAndExtended() { - startContainer(DefaultPublicAndExtendedCardProducer.class, PublicAndExtendedTenantCardProducer.class); + startContainer( + DefaultPublicAndExtendedCardProducer.class, + PublicAndExtendedTenantCardProducer.class); CdiAgentCardRouter router = container.select(CdiAgentCardRouter.class).get(); AgentCard publicCard = router.resolvePublicCard("acme"); AgentCard extendedCard = router.resolveExtendedCard("acme"); @@ -172,7 +184,9 @@ private static AgentCard buildCard(String name) { .name(name) .description(name) .version("1.0.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("jsonrpc", "http://localhost:8080"))) + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("jsonrpc", "http://localhost:8080"))) .capabilities(AgentCapabilities.builder().build()) .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) diff --git a/extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouterTest.java b/extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouterTest.java index 4799952c6..a8acc7ab5 100644 --- a/extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouterTest.java +++ b/extras/multitenancy/tests/router-unit/src/test/java/org/a2aproject/sdk/extras/multitenancy/CdiAgentExecutorRouterTest.java @@ -3,15 +3,13 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; -import java.util.List; - import jakarta.enterprise.inject.Produces; import jakarta.enterprise.inject.se.SeContainer; import jakarta.enterprise.inject.se.SeContainerInitializer; - -import org.a2aproject.sdk.server.multitenancy.Tenant; +import java.util.List; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; +import org.a2aproject.sdk.server.multitenancy.Tenant; import org.a2aproject.sdk.server.tasks.AgentEmitter; import org.a2aproject.sdk.spec.A2AError; import org.a2aproject.sdk.spec.TextPart; @@ -23,9 +21,10 @@ class CdiAgentExecutorRouterTest { private SeContainer container; private void startContainer(Class... beanClasses) { - SeContainerInitializer initializer = SeContainerInitializer.newInstance() - .disableDiscovery() - .addBeanClasses(CdiAgentExecutorRouter.class); + SeContainerInitializer initializer = + SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses(CdiAgentExecutorRouter.class); for (Class beanClass : beanClasses) { initializer.addBeanClasses(beanClass); } diff --git a/extras/multitenancy/tests/test-common/pom.xml b/extras/multitenancy/tests/test-common/pom.xml index 7763f8218..beb3b2b04 100644 --- a/extras/multitenancy/tests/test-common/pom.xml +++ b/extras/multitenancy/tests/test-common/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/AbstractMultiTenantServerTest.java b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/AbstractMultiTenantServerTest.java index 1717a992e..a83448074 100644 --- a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/AbstractMultiTenantServerTest.java +++ b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/AbstractMultiTenantServerTest.java @@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.restassured.RestAssured; +import io.restassured.path.json.JsonPath; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -14,9 +16,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; - -import io.restassured.RestAssured; -import io.restassured.path.json.JsonPath; import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.ClientEvent; @@ -30,7 +29,6 @@ import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.MessageSendParams; import org.a2aproject.sdk.spec.Task; -import org.a2aproject.sdk.spec.TaskState; import org.a2aproject.sdk.spec.TextPart; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; @@ -107,9 +105,12 @@ public void streamingWithUnknownTenantUsesDefault() throws Exception { public void concurrentRequestsForDifferentTenants() throws Exception { ExecutorService pool = Executors.newFixedThreadPool(3); try { - CompletableFuture acme = CompletableFuture.supplyAsync(() -> sendQuietly(Tenants.ACME), pool); - CompletableFuture beta = CompletableFuture.supplyAsync(() -> sendQuietly(Tenants.BETA), pool); - CompletableFuture defaultFuture = CompletableFuture.supplyAsync(() -> sendQuietly(null), pool); + CompletableFuture acme = + CompletableFuture.supplyAsync(() -> sendQuietly(Tenants.ACME), pool); + CompletableFuture beta = + CompletableFuture.supplyAsync(() -> sendQuietly(Tenants.BETA), pool); + CompletableFuture defaultFuture = + CompletableFuture.supplyAsync(() -> sendQuietly(null), pool); assertEquals(Tenants.ACME, acme.get(30, TimeUnit.SECONDS)); assertEquals(Tenants.BETA, beta.get(30, TimeUnit.SECONDS)); assertEquals(Tenants.DEFAULT_LABEL, defaultFuture.get(30, TimeUnit.SECONDS)); @@ -122,22 +123,30 @@ public void concurrentRequestsForDifferentTenants() throws Exception { @Test public void extendedCardKnownTenantResolvesToAcme() throws Exception { - assertEquals("acme-extended", getNonStreamingClient().getExtendedAgentCard(Tenants.ACME, null).name()); + assertEquals( + "acme-extended", + getNonStreamingClient().getExtendedAgentCard(Tenants.ACME, null).name()); } @Test public void extendedCardSecondTenantResolvesToBeta() throws Exception { - assertEquals("beta-extended", getNonStreamingClient().getExtendedAgentCard(Tenants.BETA, null).name()); + assertEquals( + "beta-extended", + getNonStreamingClient().getExtendedAgentCard(Tenants.BETA, null).name()); } @Test public void extendedCardUnknownTenantFallsBackToDefault() throws Exception { - assertEquals("default-extended", getNonStreamingClient().getExtendedAgentCard(Tenants.UNKNOWN, null).name()); + assertEquals( + "default-extended", + getNonStreamingClient().getExtendedAgentCard(Tenants.UNKNOWN, null).name()); } @Test public void extendedCardNullTenantUsesDefault() throws Exception { - assertEquals("default-extended", getNonStreamingClient().getExtendedAgentCard((String) null, null).name()); + assertEquals( + "default-extended", + getNonStreamingClient().getExtendedAgentCard((String) null, null).name()); } // ---------- public well-known card (HTTP only; gRPC overrides to no-ops) ---------- @@ -159,8 +168,11 @@ public void publicCardWithBetaTenant() { @Test public void publicCardUnknownTenantReturns404() { - RestAssured.given().when().get(wellKnownUrl("/.well-known/unknown-corp/agent-card.json")) - .then().statusCode(404); + RestAssured.given() + .when() + .get(wellKnownUrl("/.well-known/unknown-corp/agent-card.json")) + .then() + .statusCode(404); } // ---------- lifecycle ---------- @@ -185,12 +197,14 @@ private String sendQuietly(@Nullable String tenant) { } } - private String sendAndGetArtifactText(@Nullable String tenant, boolean streaming) throws Exception { - Message message = Message.builder() - .messageId(UUID.randomUUID().toString()) - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("hello"))) - .build(); + private String sendAndGetArtifactText(@Nullable String tenant, boolean streaming) + throws Exception { + Message message = + Message.builder() + .messageId(UUID.randomUUID().toString()) + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("hello"))) + .build(); MessageSendParams.Builder params = MessageSendParams.builder().message(message); if (tenant != null) { params.tenant(tenant); @@ -198,23 +212,25 @@ private String sendAndGetArtifactText(@Nullable String tenant, boolean streaming List artifactTexts = new ArrayList<>(); CountDownLatch done = new CountDownLatch(1); - BiConsumer consumer = (event, card) -> { - Task task = null; - if (event instanceof TaskEvent te) { - task = te.getTask(); - } else if (event instanceof TaskUpdateEvent tue) { - task = tue.getTask(); - } - if (task != null && task.artifacts() != null && !task.artifacts().isEmpty()) { - Artifact artifact = task.artifacts().get(0); - if (!artifact.parts().isEmpty() && artifact.parts().get(0) instanceof TextPart tp) { - artifactTexts.add(tp.text()); - } - } - if (task != null && task.status() != null && task.status().state().isFinal()) { - done.countDown(); - } - }; + BiConsumer consumer = + (event, card) -> { + Task task = null; + if (event instanceof TaskEvent te) { + task = te.getTask(); + } else if (event instanceof TaskUpdateEvent tue) { + task = tue.getTask(); + } + if (task != null && task.artifacts() != null && !task.artifacts().isEmpty()) { + Artifact artifact = task.artifacts().get(0); + if (!artifact.parts().isEmpty() + && artifact.parts().get(0) instanceof TextPart tp) { + artifactTexts.add(tp.text()); + } + } + if (task != null && task.status() != null && task.status().state().isFinal()) { + done.countDown(); + } + }; Client client = streaming ? getStreamingClient() : getNonStreamingClient(); client.sendMessage(params.build(), List.of(consumer), null, null); @@ -231,8 +247,14 @@ private String sendAndGetArtifactText(@Nullable String tenant, boolean streaming } private String getWellKnownCardName(String path) { - String body = RestAssured.given().when().get(wellKnownUrl(path)) - .then().statusCode(200).extract().asString(); + String body = + RestAssured.given() + .when() + .get(wellKnownUrl(path)) + .then() + .statusCode(200) + .extract() + .asString(); String name = JsonPath.from(body).getString("name"); assertNotNull(name, "Expected a card name in: " + body); return name; @@ -257,18 +279,27 @@ protected Client getNonStreamingClient() { } private Client buildClient(boolean streaming) { - AgentCard card = AgentCard.builder() - .name("multitenant-client-card") - .description("client-side card") - .version("1.0.0") - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder().streaming(true).extendedAgentCard(true).build()) - .skills(List.of()) - .supportedInterfaces(List.of(new AgentInterface(getTransportProtocol(), getTransportUrl()))) - .build(); - ClientBuilder builder = Client.builder(card) - .clientConfig(new ClientConfig.Builder().setStreaming(streaming).build()); + AgentCard card = + AgentCard.builder() + .name("multitenant-client-card") + .description("client-side card") + .version("1.0.0") + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .extendedAgentCard(true) + .build()) + .skills(List.of()) + .supportedInterfaces( + List.of( + new AgentInterface( + getTransportProtocol(), getTransportUrl()))) + .build(); + ClientBuilder builder = + Client.builder(card) + .clientConfig(new ClientConfig.Builder().setStreaming(streaming).build()); configureTransport(builder); try { return builder.build(); diff --git a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentCardProducer.java b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentCardProducer.java index fcd45ae21..ebbb9a30f 100644 --- a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentCardProducer.java +++ b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentCardProducer.java @@ -1,17 +1,15 @@ package org.a2aproject.sdk.extras.multitenancy.tests; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.List; import java.util.Properties; - -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Singleton; - -import org.a2aproject.sdk.server.multitenancy.Tenant; import org.a2aproject.sdk.server.ExtendedAgentCard; import org.a2aproject.sdk.server.PublicAgentCard; +import org.a2aproject.sdk.server.multitenancy.Tenant; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; @@ -30,7 +28,9 @@ @Singleton public class MultiTenantAgentCardProducer { - @ConfigProperty(name = "quarkus.http.port", defaultValue = "" + AbstractMultiTenantServerTest.TEST_PORT) + @ConfigProperty( + name = "quarkus.http.port", + defaultValue = "" + AbstractMultiTenantServerTest.TEST_PORT) int serverPort; private final String preferredTransport = loadPreferredTransport(); @@ -80,9 +80,10 @@ public AgentCard betaExtendedCard() { } private AgentCard card(String name) { - String url = TransportProtocol.GRPC.asString().equals(preferredTransport) - ? "localhost:" + serverPort - : "http://localhost:" + serverPort; + String url = + TransportProtocol.GRPC.asString().equals(preferredTransport) + ? "localhost:" + serverPort + : "http://localhost:" + serverPort; List interfaces = List.of(new AgentInterface(preferredTransport, url)); return AgentCard.builder() .name(name) @@ -90,26 +91,32 @@ private AgentCard card(String name) { .version("1.0.0") .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder().streaming(true).extendedAgentCard(true).build()) + .capabilities( + AgentCapabilities.builder().streaming(true).extendedAgentCard(true).build()) .skills(List.of()) .supportedInterfaces(interfaces) .build(); } private static String loadPreferredTransport() { - URL url = MultiTenantAgentCardProducer.class.getResource("/a2a-requesthandler-test.properties"); + URL url = + MultiTenantAgentCardProducer.class.getResource( + "/a2a-requesthandler-test.properties"); if (url == null) { - throw new IllegalStateException("Missing /a2a-requesthandler-test.properties on the test classpath"); + throw new IllegalStateException( + "Missing /a2a-requesthandler-test.properties on the test classpath"); } Properties properties = new Properties(); try (InputStream in = url.openStream()) { properties.load(in); } catch (IOException e) { - throw new IllegalStateException("Failed to read /a2a-requesthandler-test.properties", e); + throw new IllegalStateException( + "Failed to read /a2a-requesthandler-test.properties", e); } String preferredTransport = properties.getProperty("preferred-transport"); if (preferredTransport == null || preferredTransport.isBlank()) { - throw new IllegalStateException("preferred-transport not set in /a2a-requesthandler-test.properties"); + throw new IllegalStateException( + "preferred-transport not set in /a2a-requesthandler-test.properties"); } return preferredTransport; } diff --git a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentExecutorProducer.java b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentExecutorProducer.java index b1b4f63bc..a501b05b9 100644 --- a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentExecutorProducer.java +++ b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/MultiTenantAgentExecutorProducer.java @@ -1,12 +1,10 @@ package org.a2aproject.sdk.extras.multitenancy.tests; -import java.util.List; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - -import org.a2aproject.sdk.server.multitenancy.Tenant; +import java.util.List; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; +import org.a2aproject.sdk.server.multitenancy.Tenant; import org.a2aproject.sdk.spec.TextPart; /** diff --git a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/Tenants.java b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/Tenants.java index b6af7d3f2..cdae55440 100644 --- a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/Tenants.java +++ b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/Tenants.java @@ -8,6 +8,5 @@ public final class Tenants { public static final String UNKNOWN = "unknown-corp"; public static final String DEFAULT_LABEL = "default"; - private Tenants() { - } + private Tenants() {} } diff --git a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/TestAuthorizationController.java b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/TestAuthorizationController.java index 35bae7f4e..a01a61863 100644 --- a/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/TestAuthorizationController.java +++ b/extras/multitenancy/tests/test-common/src/main/java/org/a2aproject/sdk/extras/multitenancy/tests/TestAuthorizationController.java @@ -1,14 +1,12 @@ package org.a2aproject.sdk.extras.multitenancy.tests; +import io.quarkus.security.spi.runtime.AuthorizationController; import jakarta.annotation.Priority; import jakarta.enterprise.inject.Alternative; import jakarta.inject.Singleton; import jakarta.interceptor.Interceptor; - import org.eclipse.microprofile.config.inject.ConfigProperty; -import io.quarkus.security.spi.runtime.AuthorizationController; - /** * Disables authorization for multitenancy tests, across every transport. *

    diff --git a/extras/opentelemetry/client-propagation/pom.xml b/extras/opentelemetry/client-propagation/pom.xml index 50102779c..9b7a48938 100644 --- a/extras/opentelemetry/client-propagation/pom.xml +++ b/extras/opentelemetry/client-propagation/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/OpenTelemetryClientPropagatorTransport.java b/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/OpenTelemetryClientPropagatorTransport.java index 898ed7b40..46d14942c 100644 --- a/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/OpenTelemetryClientPropagatorTransport.java +++ b/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/OpenTelemetryClientPropagatorTransport.java @@ -1,5 +1,11 @@ package org.a2aproject.sdk.extras.opentelemetry.client.propagation; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; import org.a2aproject.sdk.client.transport.spi.ClientTransport; import org.a2aproject.sdk.client.transport.spi.interceptors.ClientCallContext; import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; @@ -19,12 +25,6 @@ import org.a2aproject.sdk.spec.TaskIdParams; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.a2aproject.sdk.spec.TaskQueryParams; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.context.Context; -import io.opentelemetry.context.propagation.TextMapSetter; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Consumer; import org.jspecify.annotations.Nullable; public class OpenTelemetryClientPropagatorTransport implements ClientTransport { @@ -32,16 +32,18 @@ public class OpenTelemetryClientPropagatorTransport implements ClientTransport { private final OpenTelemetry openTelemetry; private final ClientTransport delegate; - private static final TextMapSetter> MAP_SETTER = new TextMapSetter>() { - @Override - public void set(@Nullable Map carrier, String key, String value) { - if (carrier != null) { - carrier.put(key, value); - } - } - }; - - public OpenTelemetryClientPropagatorTransport(ClientTransport delegate, OpenTelemetry openTelemetry) { + private static final TextMapSetter> MAP_SETTER = + new TextMapSetter>() { + @Override + public void set(@Nullable Map carrier, String key, String value) { + if (carrier != null) { + carrier.put(key, value); + } + } + }; + + public OpenTelemetryClientPropagatorTransport( + ClientTransport delegate, OpenTelemetry openTelemetry) { this.delegate = delegate; this.openTelemetry = openTelemetry; } @@ -51,70 +53,93 @@ private ClientCallContext propagateContext(@Nullable ClientCallContext context) if (context == null) { clientContext = new ClientCallContext(Map.of(), new HashMap<>()); } else { - clientContext = new ClientCallContext(context.getState(), new HashMap<>(context.getHeaders())); + clientContext = + new ClientCallContext(context.getState(), new HashMap<>(context.getHeaders())); } - openTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), clientContext.getHeaders(), MAP_SETTER); + openTelemetry + .getPropagators() + .getTextMapPropagator() + .inject(Context.current(), clientContext.getHeaders(), MAP_SETTER); return clientContext; } @Override - public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) throws A2AClientException { + public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) + throws A2AClientException { return delegate.sendMessage(request, propagateContext(context)); } @Override - public void sendMessageStreaming(MessageSendParams request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException { - delegate.sendMessageStreaming(request, eventConsumer, errorConsumer, propagateContext(context)); + public void sendMessageStreaming( + MessageSendParams request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException { + delegate.sendMessageStreaming( + request, eventConsumer, errorConsumer, propagateContext(context)); } @Override - public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) throws A2AClientException { + public Task getTask(TaskQueryParams request, @Nullable ClientCallContext context) + throws A2AClientException { return delegate.getTask(request, propagateContext(context)); } @Override - public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) throws A2AClientException { + public Task cancelTask(CancelTaskParams request, @Nullable ClientCallContext context) + throws A2AClientException { return delegate.cancelTask(request, propagateContext(context)); } @Override - public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) throws A2AClientException { + public ListTasksResult listTasks(ListTasksParams request, @Nullable ClientCallContext context) + throws A2AClientException { return delegate.listTasks(request, propagateContext(context)); } @Override - public TaskPushNotificationConfig createTaskPushNotificationConfiguration(TaskPushNotificationConfig request, - @Nullable ClientCallContext context) throws A2AClientException { + public TaskPushNotificationConfig createTaskPushNotificationConfiguration( + TaskPushNotificationConfig request, @Nullable ClientCallContext context) + throws A2AClientException { return delegate.createTaskPushNotificationConfiguration(request, propagateContext(context)); } @Override - public TaskPushNotificationConfig getTaskPushNotificationConfiguration(GetTaskPushNotificationConfigParams request, - @Nullable ClientCallContext context) throws A2AClientException { + public TaskPushNotificationConfig getTaskPushNotificationConfiguration( + GetTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) + throws A2AClientException { return delegate.getTaskPushNotificationConfiguration(request, propagateContext(context)); } @Override - public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations(ListTaskPushNotificationConfigsParams request, - @Nullable ClientCallContext context) throws A2AClientException { + public ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigurations( + ListTaskPushNotificationConfigsParams request, @Nullable ClientCallContext context) + throws A2AClientException { return delegate.listTaskPushNotificationConfigurations(request, propagateContext(context)); } @Override - public void deleteTaskPushNotificationConfigurations(DeleteTaskPushNotificationConfigParams request, - @Nullable ClientCallContext context) throws A2AClientException { + public void deleteTaskPushNotificationConfigurations( + DeleteTaskPushNotificationConfigParams request, @Nullable ClientCallContext context) + throws A2AClientException { delegate.deleteTaskPushNotificationConfigurations(request, propagateContext(context)); } @Override - public void subscribeToTask(TaskIdParams request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException { + public void subscribeToTask( + TaskIdParams request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException { delegate.subscribeToTask(request, eventConsumer, errorConsumer, propagateContext(context)); } @Override - public AgentCard getExtendedAgentCard(GetExtendedAgentCardParams request, @Nullable ClientCallContext context) throws A2AClientException { + public AgentCard getExtendedAgentCard( + GetExtendedAgentCardParams request, @Nullable ClientCallContext context) + throws A2AClientException { return delegate.getExtendedAgentCard(request, propagateContext(context)); } diff --git a/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/OpenTelemetryClientPropagatorTransportWrapper.java b/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/OpenTelemetryClientPropagatorTransportWrapper.java index 8be158b32..f094fa25a 100644 --- a/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/OpenTelemetryClientPropagatorTransportWrapper.java +++ b/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/OpenTelemetryClientPropagatorTransportWrapper.java @@ -1,9 +1,9 @@ package org.a2aproject.sdk.extras.opentelemetry.client.propagation; +import io.opentelemetry.api.OpenTelemetry; import org.a2aproject.sdk.client.transport.spi.ClientTransport; import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig; import org.a2aproject.sdk.client.transport.spi.ClientTransportWrapper; -import io.opentelemetry.api.OpenTelemetry; /** * OpenTelemetry client transport wrapper that injects W3C Trace Context headers into outbound A2A client requests. @@ -23,7 +23,8 @@ public class OpenTelemetryClientPropagatorTransportWrapper implements ClientTran /** * Configuration key for the {@link OpenTelemetry} instance used for context propagation. */ - public static final String OTEL_OPEN_TELEMETRY_KEY = "org.a2aproject.sdk.extras.opentelemetry.OpenTelemetry"; + public static final String OTEL_OPEN_TELEMETRY_KEY = + "org.a2aproject.sdk.extras.opentelemetry.OpenTelemetry"; @Override public ClientTransport wrap(ClientTransport transport, ClientTransportConfig config) { diff --git a/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/package-info.java b/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/package-info.java index 62412a953..5890b09bc 100644 --- a/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/package-info.java +++ b/extras/opentelemetry/client-propagation/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/propagation/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.extras.opentelemetry.client.propagation; import org.jspecify.annotations.NullMarked; - diff --git a/extras/opentelemetry/client/pom.xml b/extras/opentelemetry/client/pom.xml index 447577e2d..7acf330e7 100644 --- a/extras/opentelemetry/client/pom.xml +++ b/extras/opentelemetry/client/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransport.java b/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransport.java index 638fa3ba2..463da6815 100644 --- a/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransport.java +++ b/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransport.java @@ -68,14 +68,17 @@ public OpenTelemetryClientTransport(ClientTransport delegate, Tracer tracer) { /** * @param meter optional meter for recording {@code gen_ai.client.operation.duration}; null disables metrics */ - public OpenTelemetryClientTransport(ClientTransport delegate, Tracer tracer, @Nullable Meter meter) { + public OpenTelemetryClientTransport( + ClientTransport delegate, Tracer tracer, @Nullable Meter meter) { this.delegate = delegate; this.tracer = tracer; - this.operationDurationHistogram = meter == null ? null - : meter.histogramBuilder(GENAI_CLIENT_OPERATION_DURATION) - .setUnit("s") - .setDescription("Duration of A2A client operations") - .build(); + this.operationDurationHistogram = + meter == null + ? null + : meter.histogramBuilder(GENAI_CLIENT_OPERATION_DURATION) + .setUnit("s") + .setDescription("Duration of A2A client operations") + .build(); } private boolean extractRequest() { @@ -87,10 +90,12 @@ private boolean extractResponse() { } @Override - public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) throws A2AClientException { + public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallContext context) + throws A2AClientException { ClientCallContext clientContext = createContext(context); long startNanos = System.nanoTime(); - SpanBuilder spanBuilder = tracer.spanBuilder(A2AMethods.SEND_MESSAGE_METHOD).setSpanKind(SpanKind.CLIENT); + SpanBuilder spanBuilder = + tracer.spanBuilder(A2AMethods.SEND_MESSAGE_METHOD).setSpanKind(SpanKind.CLIENT); spanBuilder.setAttribute(GENAI_OPERATION_NAME, A2AMethods.SEND_MESSAGE_METHOD); if (request.message() != null) { if (request.message().taskId() != null) { @@ -105,8 +110,10 @@ public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallCont if (request.message().role() != null) { spanBuilder.setAttribute(GENAI_ROLE, request.message().role().name()); } - if (request.message().extensions() != null && !request.message().extensions().isEmpty()) { - spanBuilder.setAttribute(GENAI_EXTENSIONS, String.join(",", request.message().extensions())); + if (request.message().extensions() != null + && !request.message().extensions().isEmpty()) { + spanBuilder.setAttribute( + GENAI_EXTENSIONS, String.join(",", request.message().extensions())); } spanBuilder.setAttribute(GENAI_PARTS_NUMBER, request.message().parts().size()); } @@ -135,11 +142,17 @@ public EventKind sendMessage(MessageSendParams request, @Nullable ClientCallCont } @Override - public void sendMessageStreaming(MessageSendParams request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException { + public void sendMessageStreaming( + MessageSendParams request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException { ClientCallContext clientContext = createContext(context); long startNanos = System.nanoTime(); - SpanBuilder spanBuilder = tracer.spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD).setSpanKind(SpanKind.CLIENT); + SpanBuilder spanBuilder = + tracer.spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD) + .setSpanKind(SpanKind.CLIENT); spanBuilder.setAttribute(GENAI_OPERATION_NAME, A2AMethods.SEND_STREAMING_MESSAGE_METHOD); if (request.message() != null) { if (request.message().taskId() != null) { @@ -154,8 +167,10 @@ public void sendMessageStreaming(MessageSendParams request, Consumer eventConsumer, - Consumer errorConsumer, @Nullable ClientCallContext context) throws A2AClientException { + public void subscribeToTask( + TaskIdParams request, + Consumer eventConsumer, + Consumer errorConsumer, + @Nullable ClientCallContext context) + throws A2AClientException { ClientCallContext clientContext = createContext(context); long startNanos = System.nanoTime(); - SpanBuilder spanBuilder = tracer.spanBuilder(A2AMethods.SUBSCRIBE_TO_TASK_METHOD).setSpanKind(SpanKind.CLIENT); + SpanBuilder spanBuilder = + tracer.spanBuilder(A2AMethods.SUBSCRIBE_TO_TASK_METHOD) + .setSpanKind(SpanKind.CLIENT); spanBuilder.setAttribute(GENAI_OPERATION_NAME, A2AMethods.SUBSCRIBE_TO_TASK_METHOD); if (request.id() != null) { spanBuilder.setAttribute(GENAI_TASK_ID, request.id()); @@ -448,14 +508,22 @@ public void subscribeToTask(TaskIdParams request, Consumer e SpanContext spanContext = span.getSpanContext(); delegate.subscribeToTask( request, - new OpenTelemetryEventConsumer(A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_EVENT_SUFFIX, - eventConsumer, tracer, spanContext), - new OpenTelemetryTerminalConsumer(errorConsumer, - new TerminalConsumerConfig(A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_END_SUFFIX, - tracer, spanContext, startNanos, A2AMethods.SUBSCRIBE_TO_TASK_METHOD, - operationDurationHistogram, terminated)), - clientContext - ); + new OpenTelemetryEventConsumer( + A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_EVENT_SUFFIX, + eventConsumer, + tracer, + spanContext), + new OpenTelemetryTerminalConsumer( + errorConsumer, + new TerminalConsumerConfig( + A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_END_SUFFIX, + tracer, + spanContext, + startNanos, + A2AMethods.SUBSCRIBE_TO_TASK_METHOD, + operationDurationHistogram, + terminated)), + clientContext); span.setStatus(StatusCode.OK); } catch (Exception ex) { span.setStatus(StatusCode.ERROR, ex.getMessage()); @@ -469,10 +537,14 @@ public void subscribeToTask(TaskIdParams request, Consumer e } @Override - public AgentCard getExtendedAgentCard(GetExtendedAgentCardParams params, @Nullable ClientCallContext context) throws A2AClientException { + public AgentCard getExtendedAgentCard( + GetExtendedAgentCardParams params, @Nullable ClientCallContext context) + throws A2AClientException { ClientCallContext clientContext = createContext(context); long startNanos = System.nanoTime(); - SpanBuilder spanBuilder = tracer.spanBuilder(A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD).setSpanKind(SpanKind.CLIENT); + SpanBuilder spanBuilder = + tracer.spanBuilder(A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD) + .setSpanKind(SpanKind.CLIENT); spanBuilder.setAttribute(GENAI_OPERATION_NAME, A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD); Span span = spanBuilder.startSpan(); boolean success = false; @@ -496,7 +568,8 @@ public AgentCard getExtendedAgentCard(GetExtendedAgentCardParams params, @Nullab } private void recordOperationDuration(long startNanos, String operationName, boolean success) { - StreamingEventAttributes.recordDuration(operationDurationHistogram, startNanos, operationName, success); + StreamingEventAttributes.recordDuration( + operationDurationHistogram, startNanos, operationName, success); } private ClientCallContext createContext(@Nullable ClientCallContext context) { @@ -518,8 +591,11 @@ private static class OpenTelemetryEventConsumer implements Consumer delegate, - Tracer tracer, SpanContext parentSpanContext) { + OpenTelemetryEventConsumer( + String name, + Consumer delegate, + Tracer tracer, + SpanContext parentSpanContext) { this.delegate = delegate; this.tracer = tracer; this.parentSpanContext = parentSpanContext; @@ -528,10 +604,11 @@ private static class OpenTelemetryEventConsumer implements Consumer { @@ -566,10 +648,12 @@ public void accept(@Nullable Throwable t) { if (!config.terminated().compareAndSet(false, true)) { return; } - Span closingSpan = config.tracer().spanBuilder(config.name()) - .setSpanKind(SpanKind.CLIENT) - .addLink(config.parentSpanContext()) - .startSpan(); + Span closingSpan = + config.tracer() + .spanBuilder(config.name()) + .setSpanKind(SpanKind.CLIENT) + .addLink(config.parentSpanContext()) + .startSpan(); try (Scope ignored = closingSpan.makeCurrent()) { if (t == null) { closingSpan.setStatus(StatusCode.OK); @@ -581,8 +665,11 @@ public void accept(@Nullable Throwable t) { } } finally { closingSpan.end(); - StreamingEventAttributes.recordDuration(config.operationDurationHistogram(), - config.startNanos(), config.operationName(), t == null); + StreamingEventAttributes.recordDuration( + config.operationDurationHistogram(), + config.startNanos(), + config.operationName(), + t == null); } } } diff --git a/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransportWrapper.java b/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransportWrapper.java index fce3a8c78..ebeabe441 100644 --- a/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransportWrapper.java +++ b/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransportWrapper.java @@ -1,10 +1,10 @@ package org.a2aproject.sdk.extras.opentelemetry.client; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.trace.Tracer; import org.a2aproject.sdk.client.transport.spi.ClientTransport; import org.a2aproject.sdk.client.transport.spi.ClientTransportConfig; import org.a2aproject.sdk.client.transport.spi.ClientTransportWrapper; -import io.opentelemetry.api.metrics.Meter; -import io.opentelemetry.api.trace.Tracer; /** * OpenTelemetry client transport wrapper that adds distributed tracing to A2A client calls. diff --git a/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/package-info.java b/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/package-info.java index a78ff7fc9..9a466f613 100644 --- a/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/package-info.java +++ b/extras/opentelemetry/client/src/main/java/org/a2aproject/sdk/extras/opentelemetry/client/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.extras.opentelemetry.client; import org.jspecify.annotations.NullMarked; - diff --git a/extras/opentelemetry/client/src/test/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransportTest.java b/extras/opentelemetry/client/src/test/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransportTest.java index 65927735d..953bfa960 100644 --- a/extras/opentelemetry/client/src/test/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransportTest.java +++ b/extras/opentelemetry/client/src/test/java/org/a2aproject/sdk/extras/opentelemetry/client/OpenTelemetryClientTransportTest.java @@ -76,26 +76,19 @@ @MockitoSettings(strictness = Strictness.LENIENT) class OpenTelemetryClientTransportTest { - @Mock - private ClientTransport delegate; + @Mock private ClientTransport delegate; - @Mock - private Tracer tracer; + @Mock private Tracer tracer; - @Mock - private SpanBuilder spanBuilder; + @Mock private SpanBuilder spanBuilder; - @Mock - private Span span; + @Mock private Span span; - @Mock - private Scope scope; + @Mock private Scope scope; - @Mock - private SpanContext spanContext; + @Mock private SpanContext spanContext; - @Mock - private ClientCallContext context; + @Mock private ClientCallContext context; private OpenTelemetryClientTransport transport; @@ -116,14 +109,14 @@ void setUp() { transport = new OpenTelemetryClientTransport(delegate, tracer); } - @Test void testSendMessage_Success() throws A2AClientException { MessageSendParams request = mock(MessageSendParams.class); EventKind expectedResult = mock(EventKind.class); when(request.toString()).thenReturn("request-string"); when(expectedResult.toString()).thenReturn("response-string"); - when(delegate.sendMessage(eq(request), any(ClientCallContext.class))).thenReturn(expectedResult); + when(delegate.sendMessage(eq(request), any(ClientCallContext.class))) + .thenReturn(expectedResult); EventKind result = transport.sendMessage(request, context); @@ -157,10 +150,12 @@ void testSendMessage_ThrowsException() throws A2AClientException { MessageSendParams request = mock(MessageSendParams.class); when(request.toString()).thenReturn("request-string"); A2AClientException expectedException = new A2AClientException("Test error"); - when(delegate.sendMessage(eq(request), any(ClientCallContext.class))).thenThrow(expectedException); + when(delegate.sendMessage(eq(request), any(ClientCallContext.class))) + .thenThrow(expectedException); - A2AClientException exception = assertThrows(A2AClientException.class, - () -> transport.sendMessage(request, context)); + A2AClientException exception = + assertThrows( + A2AClientException.class, () -> transport.sendMessage(request, context)); assertEquals(expectedException, exception); verify(span).setStatus(StatusCode.ERROR, "Test error"); @@ -184,10 +179,16 @@ void testSendMessageStreaming() throws A2AClientException { verify(span).setStatus(StatusCode.OK); verify(span).end(); - ArgumentCaptor> eventConsumerCaptor = ArgumentCaptor.forClass(Consumer.class); - ArgumentCaptor> errorConsumerCaptor = ArgumentCaptor.forClass(Consumer.class); - verify(delegate).sendMessageStreaming(eq(request), eventConsumerCaptor.capture(), - errorConsumerCaptor.capture(), any(ClientCallContext.class)); + ArgumentCaptor> eventConsumerCaptor = + ArgumentCaptor.forClass(Consumer.class); + ArgumentCaptor> errorConsumerCaptor = + ArgumentCaptor.forClass(Consumer.class); + verify(delegate) + .sendMessageStreaming( + eq(request), + eventConsumerCaptor.capture(), + errorConsumerCaptor.capture(), + any(ClientCallContext.class)); assertNotNull(eventConsumerCaptor.getValue()); assertNotNull(errorConsumerCaptor.getValue()); @@ -199,7 +200,8 @@ void testGetTask_Success() throws A2AClientException { Task expectedResult = mock(Task.class); when(request.toString()).thenReturn("request-string"); when(expectedResult.toString()).thenReturn("response-string"); - when(delegate.getTask(eq(request), any(ClientCallContext.class))).thenReturn(expectedResult); + when(delegate.getTask(eq(request), any(ClientCallContext.class))) + .thenReturn(expectedResult); Task result = transport.getTask(request, context); @@ -217,7 +219,8 @@ void testCancelTask_Success() throws A2AClientException { Task expectedResult = mock(Task.class); when(request.toString()).thenReturn("request-string"); when(expectedResult.toString()).thenReturn("response-string"); - when(delegate.cancelTask(eq(request), any(ClientCallContext.class))).thenReturn(expectedResult); + when(delegate.cancelTask(eq(request), any(ClientCallContext.class))) + .thenReturn(expectedResult); Task result = transport.cancelTask(request, context); @@ -235,7 +238,8 @@ void testListTasks_Success() throws A2AClientException { ListTasksResult expectedResult = mock(ListTasksResult.class); when(request.toString()).thenReturn("request-string"); when(expectedResult.toString()).thenReturn("response-string"); - when(delegate.listTasks(eq(request), any(ClientCallContext.class))).thenReturn(expectedResult); + when(delegate.listTasks(eq(request), any(ClientCallContext.class))) + .thenReturn(expectedResult); ListTasksResult result = transport.listTasks(request, context); @@ -253,9 +257,12 @@ void testCreateTaskPushNotificationConfiguration_Success() throws A2AClientExcep TaskPushNotificationConfig expectedResult = mock(TaskPushNotificationConfig.class); when(request.toString()).thenReturn("request-string"); when(expectedResult.toString()).thenReturn("response-string"); - when(delegate.createTaskPushNotificationConfiguration(eq(request), any(ClientCallContext.class))).thenReturn(expectedResult); + when(delegate.createTaskPushNotificationConfiguration( + eq(request), any(ClientCallContext.class))) + .thenReturn(expectedResult); - TaskPushNotificationConfig result = transport.createTaskPushNotificationConfiguration(request, context); + TaskPushNotificationConfig result = + transport.createTaskPushNotificationConfiguration(request, context); assertEquals(expectedResult, result); verify(tracer).spanBuilder(A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); @@ -267,13 +274,17 @@ void testCreateTaskPushNotificationConfiguration_Success() throws A2AClientExcep @Test void testGetTaskPushNotificationConfiguration_Success() throws A2AClientException { - GetTaskPushNotificationConfigParams request = mock(GetTaskPushNotificationConfigParams.class); + GetTaskPushNotificationConfigParams request = + mock(GetTaskPushNotificationConfigParams.class); TaskPushNotificationConfig expectedResult = mock(TaskPushNotificationConfig.class); when(request.toString()).thenReturn("request-string"); when(expectedResult.toString()).thenReturn("response-string"); - when(delegate.getTaskPushNotificationConfiguration(eq(request), any(ClientCallContext.class))).thenReturn(expectedResult); + when(delegate.getTaskPushNotificationConfiguration( + eq(request), any(ClientCallContext.class))) + .thenReturn(expectedResult); - TaskPushNotificationConfig result = transport.getTaskPushNotificationConfiguration(request, context); + TaskPushNotificationConfig result = + transport.getTaskPushNotificationConfiguration(request, context); assertEquals(expectedResult, result); verify(tracer).spanBuilder(A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); @@ -285,16 +296,21 @@ void testGetTaskPushNotificationConfiguration_Success() throws A2AClientExceptio @Test void testListTaskPushNotificationConfigurations_Success() throws A2AClientException { - ListTaskPushNotificationConfigsParams request = mock(ListTaskPushNotificationConfigsParams.class); + ListTaskPushNotificationConfigsParams request = + mock(ListTaskPushNotificationConfigsParams.class); TaskPushNotificationConfig config1 = mock(TaskPushNotificationConfig.class); TaskPushNotificationConfig config2 = mock(TaskPushNotificationConfig.class); when(config1.toString()).thenReturn("config1"); when(config2.toString()).thenReturn("config2"); - ListTaskPushNotificationConfigsResult expectedResult = new ListTaskPushNotificationConfigsResult(List.of(config1, config2)); + ListTaskPushNotificationConfigsResult expectedResult = + new ListTaskPushNotificationConfigsResult(List.of(config1, config2)); when(request.toString()).thenReturn("request-string"); - when(delegate.listTaskPushNotificationConfigurations(eq(request), any(ClientCallContext.class))).thenReturn(expectedResult); + when(delegate.listTaskPushNotificationConfigurations( + eq(request), any(ClientCallContext.class))) + .thenReturn(expectedResult); - ListTaskPushNotificationConfigsResult result = transport.listTaskPushNotificationConfigurations(request, context); + ListTaskPushNotificationConfigsResult result = + transport.listTaskPushNotificationConfigurations(request, context); assertEquals(expectedResult, result); verify(tracer).spanBuilder(A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); @@ -307,9 +323,13 @@ void testListTaskPushNotificationConfigurations_Success() throws A2AClientExcept @Test void testDeleteTaskPushNotificationConfigurations_Success() throws A2AClientException { - DeleteTaskPushNotificationConfigParams request = mock(DeleteTaskPushNotificationConfigParams.class); + DeleteTaskPushNotificationConfigParams request = + mock(DeleteTaskPushNotificationConfigParams.class); when(request.toString()).thenReturn("request-string"); - doNothing().when(delegate).deleteTaskPushNotificationConfigurations(eq(request), any(ClientCallContext.class)); + doNothing() + .when(delegate) + .deleteTaskPushNotificationConfigurations( + eq(request), any(ClientCallContext.class)); transport.deleteTaskPushNotificationConfigurations(request, context); @@ -317,7 +337,9 @@ void testDeleteTaskPushNotificationConfigurations_Success() throws A2AClientExce verify(spanBuilder).setAttribute(GENAI_REQUEST, "request-string"); verify(span).setStatus(StatusCode.OK); verify(span).end(); - verify(delegate).deleteTaskPushNotificationConfigurations(eq(request), any(ClientCallContext.class)); + verify(delegate) + .deleteTaskPushNotificationConfigurations( + eq(request), any(ClientCallContext.class)); } @Test @@ -336,10 +358,16 @@ void testSubscribeToTask() throws A2AClientException { verify(span).setStatus(StatusCode.OK); verify(span).end(); - ArgumentCaptor> eventConsumerCaptor = ArgumentCaptor.forClass(Consumer.class); - ArgumentCaptor> errorConsumerCaptor = ArgumentCaptor.forClass(Consumer.class); - verify(delegate).subscribeToTask(eq(request), eventConsumerCaptor.capture(), - errorConsumerCaptor.capture(), any(ClientCallContext.class)); + ArgumentCaptor> eventConsumerCaptor = + ArgumentCaptor.forClass(Consumer.class); + ArgumentCaptor> errorConsumerCaptor = + ArgumentCaptor.forClass(Consumer.class); + verify(delegate) + .subscribeToTask( + eq(request), + eventConsumerCaptor.capture(), + errorConsumerCaptor.capture(), + any(ClientCallContext.class)); assertNotNull(eventConsumerCaptor.getValue()); assertNotNull(errorConsumerCaptor.getValue()); @@ -349,9 +377,11 @@ void testSubscribeToTask() throws A2AClientException { void testGetAgentCard_Success() throws A2AClientException { AgentCard expectedResult = mock(AgentCard.class); when(expectedResult.toString()).thenReturn("response-string"); - when(delegate.getExtendedAgentCard(any(GetExtendedAgentCardParams.class), any(ClientCallContext.class))).thenReturn(expectedResult); + when(delegate.getExtendedAgentCard( + any(GetExtendedAgentCardParams.class), any(ClientCallContext.class))) + .thenReturn(expectedResult); - GetExtendedAgentCardParams params = mock(GetExtendedAgentCardParams.class); + GetExtendedAgentCardParams params = mock(GetExtendedAgentCardParams.class); AgentCard result = transport.getExtendedAgentCard(params, context); assertEquals(expectedResult, result); @@ -364,7 +394,9 @@ void testGetAgentCard_Success() throws A2AClientException { @Test void testGetAgentCard_NullResponse() throws A2AClientException { - when(delegate.getExtendedAgentCard(any(GetExtendedAgentCardParams.class), any(ClientCallContext.class))).thenReturn(null); + when(delegate.getExtendedAgentCard( + any(GetExtendedAgentCardParams.class), any(ClientCallContext.class))) + .thenReturn(null); GetExtendedAgentCardParams params = mock(GetExtendedAgentCardParams.class); AgentCard result = transport.getExtendedAgentCard(params, context); @@ -390,29 +422,43 @@ void testEventConsumer_ThroughSendMessageStreaming() throws A2AClientException { when(request.toString()).thenReturn("request-string"); Span eventSpan = mock(Span.class); - SpanBuilder eventSpanBuilder = mockEventSpanBuilder(tracer, - A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX, eventSpan); + SpanBuilder eventSpanBuilder = + mockEventSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX, + eventSpan); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, + closingSpan); - ArgumentCaptor> eventConsumerCaptor = ArgumentCaptor.forClass(Consumer.class); + ArgumentCaptor> eventConsumerCaptor = + ArgumentCaptor.forClass(Consumer.class); Consumer originalConsumer = mock(Consumer.class); transport.sendMessageStreaming(request, originalConsumer, mock(Consumer.class), context); - verify(delegate).sendMessageStreaming(eq(request), eventConsumerCaptor.capture(), any(Consumer.class), any(ClientCallContext.class)); - - Message event = Message.builder() - .messageId("test-id") - .taskId("task-id") - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test content"))) - .build(); + verify(delegate) + .sendMessageStreaming( + eq(request), + eventConsumerCaptor.capture(), + any(Consumer.class), + any(ClientCallContext.class)); + + Message event = + Message.builder() + .messageId("test-id") + .taskId("task-id") + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test content"))) + .build(); eventConsumerCaptor.getValue().accept(event); - verify(tracer).spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX); + verify(tracer) + .spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX); verify(eventSpanBuilder).addLink(spanContext); verify(eventSpanBuilder).setAttribute(GENAI_STREAMING_EVENT, event.kind()); verify(eventSpan).end(); @@ -426,14 +472,23 @@ void testErrorConsumer_ThroughSendMessageStreaming() throws A2AClientException { when(request.toString()).thenReturn("request-string"); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, + closingSpan); - ArgumentCaptor> errorConsumerCaptor = ArgumentCaptor.forClass(Consumer.class); + ArgumentCaptor> errorConsumerCaptor = + ArgumentCaptor.forClass(Consumer.class); Consumer originalConsumer = mock(Consumer.class); transport.sendMessageStreaming(request, mock(Consumer.class), originalConsumer, context); - verify(delegate).sendMessageStreaming(eq(request), any(Consumer.class), errorConsumerCaptor.capture(), any(ClientCallContext.class)); + verify(delegate) + .sendMessageStreaming( + eq(request), + any(Consumer.class), + errorConsumerCaptor.capture(), + any(ClientCallContext.class)); Throwable error = new RuntimeException("Test error"); errorConsumerCaptor.getValue().accept(error); @@ -451,14 +506,23 @@ void testTerminalConsumer_WithNullThrowable_CreatesOkClosingSpan() throws A2ACli when(request.toString()).thenReturn("request-string"); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, + closingSpan); - ArgumentCaptor> terminalConsumerCaptor = ArgumentCaptor.forClass(Consumer.class); + ArgumentCaptor> terminalConsumerCaptor = + ArgumentCaptor.forClass(Consumer.class); Consumer originalConsumer = mock(Consumer.class); transport.sendMessageStreaming(request, mock(Consumer.class), originalConsumer, context); - verify(delegate).sendMessageStreaming(eq(request), any(Consumer.class), terminalConsumerCaptor.capture(), any(ClientCallContext.class)); + verify(delegate) + .sendMessageStreaming( + eq(request), + any(Consumer.class), + terminalConsumerCaptor.capture(), + any(ClientCallContext.class)); terminalConsumerCaptor.getValue().accept(null); @@ -469,13 +533,19 @@ void testTerminalConsumer_WithNullThrowable_CreatesOkClosingSpan() throws A2ACli @Test void testDeleteTaskPushNotificationConfigurations_ThrowsException() throws A2AClientException { - DeleteTaskPushNotificationConfigParams request = mock(DeleteTaskPushNotificationConfigParams.class); + DeleteTaskPushNotificationConfigParams request = + mock(DeleteTaskPushNotificationConfigParams.class); when(request.toString()).thenReturn("request-string"); A2AClientException expectedException = new A2AClientException("Delete failed"); - doThrow(expectedException).when(delegate).deleteTaskPushNotificationConfigurations(eq(request), any(ClientCallContext.class)); + doThrow(expectedException) + .when(delegate) + .deleteTaskPushNotificationConfigurations( + eq(request), any(ClientCallContext.class)); - A2AClientException exception = assertThrows(A2AClientException.class, - () -> transport.deleteTaskPushNotificationConfigurations(request, context)); + A2AClientException exception = + assertThrows( + A2AClientException.class, + () -> transport.deleteTaskPushNotificationConfigurations(request, context)); assertEquals(expectedException, exception); verify(span).setStatus(StatusCode.ERROR, "Delete failed"); @@ -489,11 +559,20 @@ void testResubscribe_ThrowsException() throws A2AClientException { Consumer eventConsumer = mock(Consumer.class); Consumer errorConsumer = mock(Consumer.class); A2AClientException expectedException = new A2AClientException("Resubscribe failed"); - doThrow(expectedException).when(delegate).subscribeToTask(any(TaskIdParams.class), any(Consumer.class), - any(Consumer.class), any(ClientCallContext.class)); - - A2AClientException exception = assertThrows(A2AClientException.class, - () -> transport.subscribeToTask(request, eventConsumer, errorConsumer, context)); + doThrow(expectedException) + .when(delegate) + .subscribeToTask( + any(TaskIdParams.class), + any(Consumer.class), + any(Consumer.class), + any(ClientCallContext.class)); + + A2AClientException exception = + assertThrows( + A2AClientException.class, + () -> + transport.subscribeToTask( + request, eventConsumer, errorConsumer, context)); assertEquals(expectedException, exception); verify(span).setStatus(StatusCode.ERROR, "Resubscribe failed"); @@ -503,35 +582,37 @@ void testResubscribe_ThrowsException() throws A2AClientException { @Nested class MetricsTests { - @Mock - private Meter meter; + @Mock private Meter meter; - @Mock - private DoubleHistogramBuilder histogramBuilder; + @Mock private DoubleHistogramBuilder histogramBuilder; - @Mock - private DoubleHistogram histogram; + @Mock private DoubleHistogram histogram; @BeforeEach void setUpMeter() { lenient().when(meter.histogramBuilder(anyString())).thenReturn(histogramBuilder); lenient().when(histogramBuilder.setUnit(anyString())).thenReturn(histogramBuilder); - lenient().when(histogramBuilder.setDescription(anyString())).thenReturn(histogramBuilder); + lenient() + .when(histogramBuilder.setDescription(anyString())) + .thenReturn(histogramBuilder); lenient().when(histogramBuilder.build()).thenReturn(histogram); } @Test void sendMessage_recordsOperationDuration() throws A2AClientException { - OpenTelemetryClientTransport transportWithMeter = new OpenTelemetryClientTransport(delegate, tracer, meter); + OpenTelemetryClientTransport transportWithMeter = + new OpenTelemetryClientTransport(delegate, tracer, meter); MessageSendParams request = mock(MessageSendParams.class); EventKind expectedResult = mock(EventKind.class); when(request.toString()).thenReturn("request-string"); when(expectedResult.toString()).thenReturn("response-string"); - when(delegate.sendMessage(eq(request), any(ClientCallContext.class))).thenReturn(expectedResult); + when(delegate.sendMessage(eq(request), any(ClientCallContext.class))) + .thenReturn(expectedResult); transportWithMeter.sendMessage(request, context); - verify(histogram).record(anyDouble(), any(io.opentelemetry.api.common.Attributes.class)); + verify(histogram) + .record(anyDouble(), any(io.opentelemetry.api.common.Attributes.class)); } @Test @@ -541,26 +622,38 @@ void sendMessage_noHistogramWhenNoMeter() throws A2AClientException { EventKind expectedResult = mock(EventKind.class); when(request.toString()).thenReturn("request-string"); when(expectedResult.toString()).thenReturn("response-string"); - when(delegate.sendMessage(eq(request), any(ClientCallContext.class))).thenReturn(expectedResult); + when(delegate.sendMessage(eq(request), any(ClientCallContext.class))) + .thenReturn(expectedResult); transport.sendMessage(request, context); - verify(histogram, never()).record(anyDouble(), any(io.opentelemetry.api.common.Attributes.class)); + verify(histogram, never()) + .record(anyDouble(), any(io.opentelemetry.api.common.Attributes.class)); } @Test @SuppressWarnings("unchecked") void sendMessageStreaming_recordsFailedOperationDuration() throws A2AClientException { - OpenTelemetryClientTransport transportWithMeter = new OpenTelemetryClientTransport(delegate, tracer, meter); + OpenTelemetryClientTransport transportWithMeter = + new OpenTelemetryClientTransport(delegate, tracer, meter); MessageSendParams request = mock(MessageSendParams.class); A2AClientException expectedException = new A2AClientException("Streaming failed"); - doThrow(expectedException).when(delegate).sendMessageStreaming(any(MessageSendParams.class), any(Consumer.class), - any(Consumer.class), any(ClientCallContext.class)); - - assertThrows(A2AClientException.class, - () -> transportWithMeter.sendMessageStreaming(request, mock(Consumer.class), mock(Consumer.class), context)); - - verify(histogram).record(anyDouble(), any(io.opentelemetry.api.common.Attributes.class)); + doThrow(expectedException) + .when(delegate) + .sendMessageStreaming( + any(MessageSendParams.class), + any(Consumer.class), + any(Consumer.class), + any(ClientCallContext.class)); + + assertThrows( + A2AClientException.class, + () -> + transportWithMeter.sendMessageStreaming( + request, mock(Consumer.class), mock(Consumer.class), context)); + + verify(histogram) + .record(anyDouble(), any(io.opentelemetry.api.common.Attributes.class)); } } @@ -570,9 +663,11 @@ void closingSpan_notCreatedDuringStreamingInitiation() throws A2AClientException MessageSendParams request = mock(MessageSendParams.class); when(request.toString()).thenReturn("request-string"); - transport.sendMessageStreaming(request, mock(Consumer.class), mock(Consumer.class), context); + transport.sendMessageStreaming( + request, mock(Consumer.class), mock(Consumer.class), context); - verify(tracer, never()).spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX); + verify(tracer, never()) + .spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX); } @Test @@ -582,11 +677,20 @@ void testSendMessageStreaming_ThrowsException() throws A2AClientException { Consumer eventConsumer = mock(Consumer.class); Consumer errorConsumer = mock(Consumer.class); A2AClientException expectedException = new A2AClientException("Streaming failed"); - doThrow(expectedException).when(delegate).sendMessageStreaming(any(MessageSendParams.class), any(Consumer.class), - any(Consumer.class), any(ClientCallContext.class)); - - A2AClientException exception = assertThrows(A2AClientException.class, - () -> transport.sendMessageStreaming(request, eventConsumer, errorConsumer, context)); + doThrow(expectedException) + .when(delegate) + .sendMessageStreaming( + any(MessageSendParams.class), + any(Consumer.class), + any(Consumer.class), + any(ClientCallContext.class)); + + A2AClientException exception = + assertThrows( + A2AClientException.class, + () -> + transport.sendMessageStreaming( + request, eventConsumer, errorConsumer, context)); assertEquals(expectedException, exception); verify(span).setStatus(StatusCode.ERROR, "Streaming failed"); diff --git a/extras/opentelemetry/common/pom.xml b/extras/opentelemetry/common/pom.xml index e1b4e7672..6b8f59965 100644 --- a/extras/opentelemetry/common/pom.xml +++ b/extras/opentelemetry/common/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/opentelemetry/common/src/main/java/org/a2aproject/sdk/extras/opentelemetry/A2AObservabilityNames.java b/extras/opentelemetry/common/src/main/java/org/a2aproject/sdk/extras/opentelemetry/A2AObservabilityNames.java index db94826af..dad770066 100644 --- a/extras/opentelemetry/common/src/main/java/org/a2aproject/sdk/extras/opentelemetry/A2AObservabilityNames.java +++ b/extras/opentelemetry/common/src/main/java/org/a2aproject/sdk/extras/opentelemetry/A2AObservabilityNames.java @@ -6,6 +6,7 @@ public interface A2AObservabilityNames { String EXTRACT_RESPONSE_SYS_PROPERTY = "org.a2aproject.sdk.server.extract.response"; String ERROR_TYPE = "error.type"; + /** * Fixed error marker recorded in histogram attributes on failure. * @@ -16,15 +17,15 @@ public interface A2AObservabilityNames { String GENAI_PREFIX = "gen_ai.agent.a2a"; String GENAI_CONFIG_ID = GENAI_PREFIX + ".config_id"; - String GENAI_CONTEXT_ID = GENAI_PREFIX + ".context_id"; //gen_ai.conversation.id + String GENAI_CONTEXT_ID = GENAI_PREFIX + ".context_id"; // gen_ai.conversation.id String GENAI_EXTENSIONS = GENAI_PREFIX + ".extensions"; String GENAI_MESSAGE_ID = GENAI_PREFIX + ".message_id"; - String GENAI_OPERATION_NAME = GENAI_PREFIX + ".operation.name"; //gen_ai.agent.operation.name ? + String GENAI_OPERATION_NAME = GENAI_PREFIX + ".operation.name"; // gen_ai.agent.operation.name ? String GENAI_PARTS_NUMBER = GENAI_PREFIX + ".parts.number"; String GENAI_TRANSPORT = GENAI_PREFIX + ".transport"; String GENAI_STATUS = GENAI_PREFIX + ".status"; String GENAI_PROTOCOL_VERSION = GENAI_PREFIX + ".protocol.version"; - String GENAI_REQUEST = GENAI_PREFIX + ".request"; //gen_ai.input.messages ? + String GENAI_REQUEST = GENAI_PREFIX + ".request"; // gen_ai.input.messages ? String GENAI_RESPONSE = GENAI_PREFIX + ".response"; // gen_ai.output.messages ? String GENAI_ROLE = GENAI_PREFIX + ".role"; String GENAI_TASK_ID = GENAI_PREFIX + ".task_id"; diff --git a/extras/opentelemetry/common/src/main/java/org/a2aproject/sdk/extras/opentelemetry/StreamingEventAttributes.java b/extras/opentelemetry/common/src/main/java/org/a2aproject/sdk/extras/opentelemetry/StreamingEventAttributes.java index 2d3252b2f..20d20e097 100644 --- a/extras/opentelemetry/common/src/main/java/org/a2aproject/sdk/extras/opentelemetry/StreamingEventAttributes.java +++ b/extras/opentelemetry/common/src/main/java/org/a2aproject/sdk/extras/opentelemetry/StreamingEventAttributes.java @@ -21,8 +21,7 @@ public final class StreamingEventAttributes { - private StreamingEventAttributes() { - } + private StreamingEventAttributes() {} /** * Applies task-specific attributes derived from the streaming event to the span builder. @@ -53,8 +52,7 @@ public static void apply(SpanBuilder builder, StreamingEventKind item) { builder.setAttribute(GENAI_TASK_ID, message.taskId()); } } - default -> { - } + default -> {} } } @@ -70,15 +68,19 @@ public static void apply(SpanBuilder builder, StreamingEventKind item) { * @param operationName the A2A operation name recorded as {@code gen_ai.agent.a2a.operation.name} * @param success {@code false} adds the {@code error.type} attribute */ - public static void recordDuration(@Nullable DoubleHistogram histogram, long startNanos, - String operationName, boolean success) { + public static void recordDuration( + @Nullable DoubleHistogram histogram, + long startNanos, + String operationName, + boolean success) { if (histogram == null) { return; } double seconds = (System.nanoTime() - startNanos) / 1_000_000_000.0; - AttributesBuilder builder = Attributes.builder() - .put(GENAI_OPERATION_NAME, operationName) - .put(GENAI_SYSTEM, GENAI_SYSTEM_VALUE); + AttributesBuilder builder = + Attributes.builder() + .put(GENAI_OPERATION_NAME, operationName) + .put(GENAI_SYSTEM, GENAI_SYSTEM_VALUE); if (!success) { builder.put(ERROR_TYPE, ERROR_TYPE_VALUE); } diff --git a/extras/opentelemetry/common/src/test/java/org/a2aproject/sdk/extras/opentelemetry/OtelSpanMocks.java b/extras/opentelemetry/common/src/test/java/org/a2aproject/sdk/extras/opentelemetry/OtelSpanMocks.java index e1266db24..5bc950cf0 100644 --- a/extras/opentelemetry/common/src/test/java/org/a2aproject/sdk/extras/opentelemetry/OtelSpanMocks.java +++ b/extras/opentelemetry/common/src/test/java/org/a2aproject/sdk/extras/opentelemetry/OtelSpanMocks.java @@ -17,14 +17,14 @@ /** Shared Mockito helpers for OTel span builder stubs used in client and server tests. */ public final class OtelSpanMocks { - private OtelSpanMocks() { - } + private OtelSpanMocks() {} /** * Stubs {@code tracer.spanBuilder(spanName)} to return a builder that produces {@code closingSpan}, * with status and scope stubs that cover both success and error paths. */ - public static SpanBuilder mockClosingSpanBuilder(Tracer tracer, String spanName, Span closingSpan) { + public static SpanBuilder mockClosingSpanBuilder( + Tracer tracer, String spanName, Span closingSpan) { SpanBuilder closingSpanBuilder = mock(SpanBuilder.class); when(tracer.spanBuilder(spanName)).thenReturn(closingSpanBuilder); when(closingSpanBuilder.setSpanKind(any(SpanKind.class))).thenReturn(closingSpanBuilder); @@ -32,7 +32,9 @@ public static SpanBuilder mockClosingSpanBuilder(Tracer tracer, String spanName, when(closingSpanBuilder.startSpan()).thenReturn(closingSpan); // lenient: success/error path coverage varies per test lenient().when(closingSpan.setStatus(any(StatusCode.class))).thenReturn(closingSpan); - lenient().when(closingSpan.setStatus(any(StatusCode.class), anyString())).thenReturn(closingSpan); + lenient() + .when(closingSpan.setStatus(any(StatusCode.class), anyString())) + .thenReturn(closingSpan); when(closingSpan.makeCurrent()).thenReturn(mock(Scope.class)); return closingSpanBuilder; } diff --git a/extras/opentelemetry/common/src/test/java/org/a2aproject/sdk/extras/opentelemetry/StreamingEventAttributesTest.java b/extras/opentelemetry/common/src/test/java/org/a2aproject/sdk/extras/opentelemetry/StreamingEventAttributesTest.java index 53f4c541a..cdc445480 100644 --- a/extras/opentelemetry/common/src/test/java/org/a2aproject/sdk/extras/opentelemetry/StreamingEventAttributesTest.java +++ b/extras/opentelemetry/common/src/test/java/org/a2aproject/sdk/extras/opentelemetry/StreamingEventAttributesTest.java @@ -40,21 +40,29 @@ void recordDuration_recordsPositiveValueForSubSecondOperation() { void recordDuration_setsErrorTypeAttributeOnFailure() { DoubleHistogram histogram = mock(DoubleHistogram.class); - StreamingEventAttributes.recordDuration(histogram, System.nanoTime() - 500_000L, "test-op", false); + StreamingEventAttributes.recordDuration( + histogram, System.nanoTime() - 500_000L, "test-op", false); ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); verify(histogram).record(anyDouble(), attrsCaptor.capture()); - assertNotNull(attrsCaptor.getValue().get(AttributeKey.stringKey(A2AObservabilityNames.ERROR_TYPE))); + assertNotNull( + attrsCaptor + .getValue() + .get(AttributeKey.stringKey(A2AObservabilityNames.ERROR_TYPE))); } @Test void recordDuration_omitsErrorTypeAttributeOnSuccess() { DoubleHistogram histogram = mock(DoubleHistogram.class); - StreamingEventAttributes.recordDuration(histogram, System.nanoTime() - 500_000L, "test-op", true); + StreamingEventAttributes.recordDuration( + histogram, System.nanoTime() - 500_000L, "test-op", true); ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); verify(histogram).record(anyDouble(), attrsCaptor.capture()); - assertNull(attrsCaptor.getValue().get(AttributeKey.stringKey(A2AObservabilityNames.ERROR_TYPE))); + assertNull( + attrsCaptor + .getValue() + .get(AttributeKey.stringKey(A2AObservabilityNames.ERROR_TYPE))); } } diff --git a/extras/opentelemetry/integration-tests/pom.xml b/extras/opentelemetry/integration-tests/pom.xml index beccbc3ae..fd1c5122f 100644 --- a/extras/opentelemetry/integration-tests/pom.xml +++ b/extras/opentelemetry/integration-tests/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 @@ -101,6 +99,9 @@ io.quarkus quarkus-maven-plugin true + + --add-opens=java.base/java.lang=ALL-UNNAMED + @@ -110,24 +111,18 @@ - - --add-opens=java.base/java.lang=ALL-UNNAMED - org.apache.maven.plugins - maven-surefire-plugin + maven-failsafe-plugin + ${skipTests} org.jboss.logmanager.LogManager ${maven.home} --add-opens java.base/java.lang=ALL-UNNAMED - - - org.apache.maven.plugins - maven-failsafe-plugin @@ -136,8 +131,11 @@ + + + org.apache.maven.plugins + maven-surefire-plugin - ${skipTests} org.jboss.logmanager.LogManager ${maven.home} diff --git a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/A2ATestRoutes.java b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/A2ATestRoutes.java index 9774a73f8..f427c323b 100644 --- a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/A2ATestRoutes.java +++ b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/A2ATestRoutes.java @@ -8,11 +8,6 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; -import org.a2aproject.sdk.spec.Task; -import org.a2aproject.sdk.spec.TaskArtifactUpdateEvent; -import org.a2aproject.sdk.spec.TaskPushNotificationConfig; -import org.a2aproject.sdk.spec.TaskStatusUpdateEvent; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; @@ -36,6 +31,11 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; +import org.a2aproject.sdk.spec.Task; +import org.a2aproject.sdk.spec.TaskArtifactUpdateEvent; +import org.a2aproject.sdk.spec.TaskPushNotificationConfig; +import org.a2aproject.sdk.spec.TaskStatusUpdateEvent; /** * Test routes for OpenTelemetry integration testing. @@ -53,107 +53,106 @@ public class A2ATestRoutes { */ private final List> webhookRequests = new CopyOnWriteArrayList<>(); - @Inject - TestUtilsBean testUtilsBean; - @Inject - InMemorySpanExporter inMemorySpanExporter; - @Inject - InMemoryMetricExporter inMemoryMetricExporter; - @Inject - OpenTelemetry openTelemetry; - @Inject - CustomDispatchExecutorProducer customDispatchExecutorProducer; + @Inject TestUtilsBean testUtilsBean; + @Inject InMemorySpanExporter inMemorySpanExporter; + @Inject InMemoryMetricExporter inMemoryMetricExporter; + @Inject OpenTelemetry openTelemetry; + @Inject CustomDispatchExecutorProducer customDispatchExecutorProducer; - @Inject - Tracer tracer; + @Inject Tracer tracer; void setupRoutes(@Observes Router router) { router.post("/test/task") - .consumes(APPLICATION_JSON) - .handler(BodyHandler.create()) - .blockingHandler(ctx -> saveTask(ctx.body().asString(), ctx)); + .consumes(APPLICATION_JSON) + .handler(BodyHandler.create()) + .blockingHandler(ctx -> saveTask(ctx.body().asString(), ctx)); router.get("/test/task/:taskId") - .produces(APPLICATION_JSON) - .blockingHandler(ctx -> getTask(ctx.pathParam("taskId"), ctx)); + .produces(APPLICATION_JSON) + .blockingHandler(ctx -> getTask(ctx.pathParam("taskId"), ctx)); router.delete("/test/task/:taskId") - .blockingHandler(ctx -> deleteTask(ctx.pathParam("taskId"), ctx)); + .blockingHandler(ctx -> deleteTask(ctx.pathParam("taskId"), ctx)); router.post("/test/queue/ensure/:taskId") - .handler(ctx -> ensureTaskQueue(ctx.pathParam("taskId"), ctx)); + .handler(ctx -> ensureTaskQueue(ctx.pathParam("taskId"), ctx)); router.post("/test/queue/enqueueTaskStatusUpdateEvent/:taskId") - .handler(BodyHandler.create()) - .handler(ctx -> enqueueTaskStatusUpdateEvent(ctx.pathParam("taskId"), ctx.body().asString(), ctx)); + .handler(BodyHandler.create()) + .handler( + ctx -> + enqueueTaskStatusUpdateEvent( + ctx.pathParam("taskId"), ctx.body().asString(), ctx)); router.post("/test/queue/enqueueTaskArtifactUpdateEvent/:taskId") - .handler(BodyHandler.create()) - .handler(ctx -> enqueueTaskArtifactUpdateEvent(ctx.pathParam("taskId"), ctx.body().asString(), ctx)); + .handler(BodyHandler.create()) + .handler( + ctx -> + enqueueTaskArtifactUpdateEvent( + ctx.pathParam("taskId"), ctx.body().asString(), ctx)); router.get("/test/queue/childCount/:taskId") - .produces(TEXT_PLAIN) - .handler(ctx -> getChildQueueCount(ctx.pathParam("taskId"), ctx)); + .produces(TEXT_PLAIN) + .handler(ctx -> getChildQueueCount(ctx.pathParam("taskId"), ctx)); router.post("/test/pushconfig/:taskId") - .consumes(APPLICATION_JSON) - .handler(BodyHandler.create()) - .blockingHandler(ctx -> savePushNotificationConfig(ctx.pathParam("taskId"), ctx.body().asString(), ctx)); + .consumes(APPLICATION_JSON) + .handler(BodyHandler.create()) + .blockingHandler( + ctx -> + savePushNotificationConfig( + ctx.pathParam("taskId"), ctx.body().asString(), ctx)); router.delete("/test/pushconfig/:taskId/:configId") - .blockingHandler(ctx -> deletePushNotificationConfig( - ctx.pathParam("taskId"), ctx.pathParam("configId"), ctx)); + .blockingHandler( + ctx -> + deletePushNotificationConfig( + ctx.pathParam("taskId"), ctx.pathParam("configId"), ctx)); router.post("/test/webhook") - .handler(BodyHandler.create()) - .handler(ctx -> captureWebhook(ctx)); + .handler(BodyHandler.create()) + .handler(ctx -> captureWebhook(ctx)); router.get("/test/webhook/requests") - .produces(APPLICATION_JSON) - .handler(ctx -> getWebhookRequests(ctx)); + .produces(APPLICATION_JSON) + .handler(ctx -> getWebhookRequests(ctx)); - router.delete("/test/webhook/requests") - .handler(ctx -> clearWebhookRequests(ctx)); + router.delete("/test/webhook/requests").handler(ctx -> clearWebhookRequests(ctx)); router.get("/test/dispatch-executor/count") - .produces(TEXT_PLAIN) - .handler(ctx -> ctx.response().setStatusCode(HTTP_OK) - .end(String.valueOf(customDispatchExecutorProducer.getExecutionCount()))); + .produces(TEXT_PLAIN) + .handler( + ctx -> + ctx.response() + .setStatusCode(HTTP_OK) + .end( + String.valueOf( + customDispatchExecutorProducer + .getExecutionCount()))); router.delete("/test/dispatch-executor/count") - .handler(ctx -> { - customDispatchExecutorProducer.resetExecutionCount(); - ctx.response().setStatusCode(HTTP_OK).end(); - }); - - router.get("/hello") - .produces(TEXT_PLAIN) - .handler(ctx -> hello(ctx)); - - router.get("/export") - .produces(APPLICATION_JSON) - .handler(ctx -> exportSpans(ctx)); - - router.get("/export-metrics") - .produces(APPLICATION_JSON) - .handler(ctx -> exportMetrics(ctx)); - - router.get("/reset") - .produces(TEXT_PLAIN) - .handler(ctx -> reset(ctx)); - - router.get("/reset-metrics") - .produces(TEXT_PLAIN) - .handler(ctx -> resetMetrics(ctx)); + .handler( + ctx -> { + customDispatchExecutorProducer.resetExecutionCount(); + ctx.response().setStatusCode(HTTP_OK).end(); + }); + + router.get("/hello").produces(TEXT_PLAIN).handler(ctx -> hello(ctx)); + + router.get("/export").produces(APPLICATION_JSON).handler(ctx -> exportSpans(ctx)); + + router.get("/export-metrics").produces(APPLICATION_JSON).handler(ctx -> exportMetrics(ctx)); + + router.get("/reset").produces(TEXT_PLAIN).handler(ctx -> reset(ctx)); + + router.get("/reset-metrics").produces(TEXT_PLAIN).handler(ctx -> resetMetrics(ctx)); } public void saveTask(String body, RoutingContext rc) { try { Task task = JsonUtil.fromJson(body, Task.class); testUtilsBean.saveTask(task); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -163,9 +162,7 @@ public void getTask(String taskId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } rc.response() @@ -182,15 +179,11 @@ public void deleteTask(String taskId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } testUtilsBean.deleteTask(taskId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -199,9 +192,7 @@ public void deleteTask(String taskId, RoutingContext rc) { public void ensureTaskQueue(String taskId, RoutingContext rc) { try { testUtilsBean.ensureQueue(taskId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -211,9 +202,7 @@ public void enqueueTaskStatusUpdateEvent(String taskId, String body, RoutingCont try { TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -223,9 +212,7 @@ public void enqueueTaskArtifactUpdateEvent(String taskId, String body, RoutingCo try { TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -233,18 +220,15 @@ public void enqueueTaskArtifactUpdateEvent(String taskId, String body, RoutingCo public void getChildQueueCount(String taskId, RoutingContext rc) { int count = testUtilsBean.getChildQueueCount(taskId); - rc.response() - .setStatusCode(200) - .end(String.valueOf(count)); + rc.response().setStatusCode(200).end(String.valueOf(count)); } public void savePushNotificationConfig(String taskId, String body, RoutingContext rc) { try { - TaskPushNotificationConfig config = JsonUtil.fromJson(body, TaskPushNotificationConfig.class); + TaskPushNotificationConfig config = + JsonUtil.fromJson(body, TaskPushNotificationConfig.class); testUtilsBean.savePushNotificationConfig(taskId, config); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -253,9 +237,7 @@ public void savePushNotificationConfig(String taskId, String body, RoutingContex public void deletePushNotificationConfig(String taskId, String configId, RoutingContext rc) { try { testUtilsBean.deletePushNotificationConfig(taskId, configId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -293,15 +275,15 @@ public void hello(RoutingContext rc) { } public void exportSpans(RoutingContext rc) { - List spans = inMemorySpanExporter.getFinishedSpanItems() - .stream() - .filter(sd -> !sd.getName().contains("export") && !sd.getName().contains("reset")) - .toList(); + List spans = + inMemorySpanExporter.getFinishedSpanItems().stream() + .filter( + sd -> + !sd.getName().contains("export") + && !sd.getName().contains("reset")) + .toList(); String json = gson.toJson(serialize(spans)); - rc.response() - .setStatusCode(200) - .putHeader(CONTENT_TYPE, APPLICATION_JSON) - .end(json); + rc.response().setStatusCode(200).putHeader(CONTENT_TYPE, APPLICATION_JSON).end(json); } private JsonElement serialize(List spanDatas) { @@ -321,13 +303,18 @@ private JsonElement serialize(List spanDatas) { jsonObject.addProperty("parent_remote", spanData.getParentSpanContext().isRemote()); jsonObject.addProperty("parent_valid", spanData.getParentSpanContext().isValid()); - spanData.getAttributes().forEach((k, v) -> { - jsonObject.addProperty("attr_" + k.getKey(), v.toString()); - }); + spanData.getAttributes() + .forEach( + (k, v) -> { + jsonObject.addProperty("attr_" + k.getKey(), v.toString()); + }); - spanData.getResource().getAttributes().forEach((k, v) -> { - jsonObject.addProperty("resource_" + k.getKey(), v.toString()); - }); + spanData.getResource() + .getAttributes() + .forEach( + (k, v) -> { + jsonObject.addProperty("resource_" + k.getKey(), v.toString()); + }); serializeSpanEvents(spanData, jsonObject); serializeSpanLinks(spanData, jsonObject); @@ -340,19 +327,27 @@ private JsonElement serialize(List spanDatas) { private void serializeSpanEvents(SpanData spanData, JsonObject jsonObject) { jsonObject.addProperty("events_count", spanData.getEvents().size()); - spanData.getEvents().forEach(event -> { - JsonObject eventObject = new JsonObject(); - eventObject.addProperty("name", event.getName()); - event.getAttributes().forEach((k, v) -> eventObject.addProperty("attr_" + k.getKey(), v.toString())); - jsonObject.add("event_" + event.getName(), eventObject); - }); + spanData.getEvents() + .forEach( + event -> { + JsonObject eventObject = new JsonObject(); + eventObject.addProperty("name", event.getName()); + event.getAttributes() + .forEach( + (k, v) -> + eventObject.addProperty( + "attr_" + k.getKey(), v.toString())); + jsonObject.add("event_" + event.getName(), eventObject); + }); } private void serializeSpanLinks(SpanData spanData, JsonObject jsonObject) { jsonObject.addProperty("links_count", spanData.getLinks().size()); if (!spanData.getLinks().isEmpty()) { - jsonObject.addProperty("link_0_spanId", spanData.getLinks().get(0).getSpanContext().getSpanId()); - jsonObject.addProperty("link_0_traceId", spanData.getLinks().get(0).getSpanContext().getTraceId()); + jsonObject.addProperty( + "link_0_spanId", spanData.getLinks().get(0).getSpanContext().getSpanId()); + jsonObject.addProperty( + "link_0_traceId", spanData.getLinks().get(0).getSpanContext().getTraceId()); } } @@ -360,9 +355,10 @@ public void exportMetrics(RoutingContext rc) { if (openTelemetry instanceof OpenTelemetrySdk sdk) { sdk.getSdkMeterProvider().forceFlush(); } - List metrics = inMemoryMetricExporter.getFinishedMetricItems().stream() - .filter(m -> m.getName().startsWith("gen_ai.")) - .toList(); + List metrics = + inMemoryMetricExporter.getFinishedMetricItems().stream() + .filter(m -> m.getName().startsWith("gen_ai.")) + .toList(); rc.response() .setStatusCode(200) .putHeader(CONTENT_TYPE, APPLICATION_JSON) @@ -381,13 +377,21 @@ JsonElement serializeMetrics(List metricDataList) { HistogramData histogram = m.getHistogramData(); obj.addProperty("data_count", histogram.getPoints().size()); JsonArray points = new JsonArray(histogram.getPoints().size()); - histogram.getPoints().forEach(point -> { - JsonObject pointObj = new JsonObject(); - pointObj.addProperty("count", point.getCount()); - pointObj.addProperty("sum", point.getSum()); - point.getAttributes().forEach((k, v) -> pointObj.addProperty("attr_" + k.getKey(), v.toString())); - points.add(pointObj); - }); + histogram + .getPoints() + .forEach( + point -> { + JsonObject pointObj = new JsonObject(); + pointObj.addProperty("count", point.getCount()); + pointObj.addProperty("sum", point.getSum()); + point.getAttributes() + .forEach( + (k, v) -> + pointObj.addProperty( + "attr_" + k.getKey(), + v.toString())); + points.add(pointObj); + }); obj.add("points", points); } result.add(obj); @@ -407,10 +411,7 @@ public void reset(RoutingContext rc) { private void errorResponse(Throwable t, RoutingContext rc) { t.printStackTrace(); - rc.response() - .setStatusCode(500) - .putHeader(CONTENT_TYPE, TEXT_PLAIN) - .end(); + rc.response().setStatusCode(500).putHeader(CONTENT_TYPE, TEXT_PLAIN).end(); } @ApplicationScoped diff --git a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/CustomDispatchExecutorProducer.java b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/CustomDispatchExecutorProducer.java index c3eda9fe8..9cbc76102 100644 --- a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/CustomDispatchExecutorProducer.java +++ b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/CustomDispatchExecutorProducer.java @@ -1,15 +1,14 @@ package org.a2aproject.sdk.extras.opentelemetry.it; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Named; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; - -import jakarta.annotation.PreDestroy; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Named; import org.a2aproject.sdk.server.tasks.BasePushNotificationSender; /** @@ -31,11 +30,13 @@ public class CustomDispatchExecutorProducer { public CustomDispatchExecutorProducer() { AtomicInteger threadCounter = new AtomicInteger(); - ThreadFactory threadFactory = r -> { - Thread t = new Thread(r, CUSTOM_THREAD_PREFIX + threadCounter.getAndIncrement()); - t.setDaemon(true); - return t; - }; + ThreadFactory threadFactory = + r -> { + Thread t = + new Thread(r, CUSTOM_THREAD_PREFIX + threadCounter.getAndIncrement()); + t.setDaemon(true); + return t; + }; this.pool = Executors.newFixedThreadPool(2, threadFactory); } diff --git a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/CustomHttpClientProducer.java b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/CustomHttpClientProducer.java index fb3f27c32..f8cb7f481 100644 --- a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/CustomHttpClientProducer.java +++ b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/CustomHttpClientProducer.java @@ -20,9 +20,11 @@ public class CustomHttpClientProducer { @Produces @ApplicationScoped A2AHttpClient customClient() { - return new MarkerHeaderA2AHttpClient(A2AHttpClientFactory.create(A2AHttpClientConfiguration.builder() - .excludedProviderNames(java.util.List.of("cdi")) - .build())); + return new MarkerHeaderA2AHttpClient( + A2AHttpClientFactory.create( + A2AHttpClientConfiguration.builder() + .excludedProviderNames(java.util.List.of("cdi")) + .build())); } /** diff --git a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/SimpleAgentExecutor.java b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/SimpleAgentExecutor.java index c10d28a03..398bb2189 100644 --- a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/SimpleAgentExecutor.java +++ b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/SimpleAgentExecutor.java @@ -1,12 +1,12 @@ package org.a2aproject.sdk.extras.opentelemetry.it; +import jakarta.enterprise.context.ApplicationScoped; import org.a2aproject.sdk.A2A; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; import org.a2aproject.sdk.spec.A2AError; import org.a2aproject.sdk.spec.TextPart; -import jakarta.enterprise.context.ApplicationScoped; /** * Simple AgentExecutor for integration testing. @@ -23,11 +23,12 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro } // Get the user's message - String userText = context.getMessage().parts().stream() - .filter(part -> part instanceof TextPart) - .map(part -> ((TextPart) part).text()) - .findFirst() - .orElse(""); + String userText = + context.getMessage().parts().stream() + .filter(part -> part instanceof TextPart) + .map(part -> ((TextPart) part).text()) + .findFirst() + .orElse(""); // Echo it back String response = "Echo: " + userText; diff --git a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/TestAgentCardProducer.java b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/TestAgentCardProducer.java index 9ea7a3b9c..7114b3715 100644 --- a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/TestAgentCardProducer.java +++ b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/TestAgentCardProducer.java @@ -1,16 +1,14 @@ package org.a2aproject.sdk.extras.opentelemetry.it; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import java.util.Collections; +import java.util.List; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.AgentSkill; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Produces; - -import java.util.Collections; -import java.util.List; - /** * Produces the AgentCard for integration testing. @@ -24,24 +22,24 @@ public AgentCard agentCard() { return AgentCard.builder() .name("OpenTelemetry Test Agent") .description("Test agent for OpenTelemetry integration tests") - .supportedInterfaces(Collections.singletonList( - new AgentInterface("JSONRPC", "http://localhost:8081") - )) + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("JSONRPC", "http://localhost:8081"))) .version("1.0.0-TEST") .documentationUrl("http://example.com/test") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .build()) + .capabilities( + AgentCapabilities.builder().streaming(true).pushNotifications(true).build()) .defaultInputModes(Collections.singletonList("text")) .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(AgentSkill.builder() - .id("echo") - .name("Echo") - .description("Echoes back the user's message") - .tags(Collections.singletonList("test")) - .examples(List.of("hello", "test message")) - .build())) + .skills( + Collections.singletonList( + AgentSkill.builder() + .id("echo") + .name("Echo") + .description("Echoes back the user's message") + .tags(Collections.singletonList("test")) + .examples(List.of("hello", "test message")) + .build())) .build(); } } diff --git a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/TestUtilsBean.java b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/TestUtilsBean.java index 4db0b2611..e037b6e80 100644 --- a/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/TestUtilsBean.java +++ b/extras/opentelemetry/integration-tests/src/main/java/org/a2aproject/sdk/extras/opentelemetry/it/TestUtilsBean.java @@ -1,13 +1,13 @@ package org.a2aproject.sdk.extras.opentelemetry.it; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; import org.a2aproject.sdk.server.events.QueueManager; import org.a2aproject.sdk.server.tasks.PushNotificationConfigStore; import org.a2aproject.sdk.server.tasks.TaskStore; import org.a2aproject.sdk.spec.Event; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; /** * Test utilities for OpenTelemetry integration tests. @@ -16,14 +16,11 @@ @ApplicationScoped public class TestUtilsBean { - @Inject - TaskStore taskStore; + @Inject TaskStore taskStore; - @Inject - QueueManager queueManager; + @Inject QueueManager queueManager; - @Inject - PushNotificationConfigStore pushNotificationConfigStore; + @Inject PushNotificationConfigStore pushNotificationConfigStore; public void saveTask(Task task) { taskStore.save(task, false); @@ -51,8 +48,7 @@ public int getChildQueueCount(String taskId) { public void savePushNotificationConfig(String taskId, TaskPushNotificationConfig config) { pushNotificationConfigStore.setInfo( - TaskPushNotificationConfig.builder(config).taskId(taskId).build(), - null); + TaskPushNotificationConfig.builder(config).taskId(taskId).build(), null); } public void deletePushNotificationConfig(String taskId, String configId) { diff --git a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/A2ATestRoutesTest.java b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/A2ATestRoutesTest.java index dbc30cd3e..b1ae792f7 100644 --- a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/A2ATestRoutesTest.java +++ b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/A2ATestRoutesTest.java @@ -1,5 +1,12 @@ package org.a2aproject.sdk.extras.opentelemetry.it; +import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.GENAI_CLIENT_OPERATION_DURATION; +import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.GENAI_STREAMING_DURATION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -8,31 +15,19 @@ import io.opentelemetry.sdk.metrics.data.HistogramPointData; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.data.MetricDataType; +import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.List; - -import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.GENAI_CLIENT_OPERATION_DURATION; -import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.GENAI_STREAMING_DURATION; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class A2ATestRoutesTest { - @Mock - private MetricData metricData; - @Mock - private HistogramData histogramData; - @Mock - private HistogramPointData point1; - @Mock - private HistogramPointData point2; + @Mock private MetricData metricData; + @Mock private HistogramData histogramData; + @Mock private HistogramPointData point1; + @Mock private HistogramPointData point2; private final A2ATestRoutes routes = new A2ATestRoutes(); diff --git a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/BaseTest.java b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/BaseTest.java index c15b66ff8..d43f015e8 100644 --- a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/BaseTest.java +++ b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/BaseTest.java @@ -2,24 +2,21 @@ import static io.restassured.RestAssured.get; +import io.restassured.common.mapper.TypeRef; import java.util.List; import java.util.Map; -import io.restassured.common.mapper.TypeRef; - public class BaseTest { protected List> getSpans() { - return get("/export").body().as(new TypeRef<>() { - }); + return get("/export").body().as(new TypeRef<>() {}); } protected List> getMetrics() { - return get("/export-metrics").body().as(new TypeRef<>() { - }); + return get("/export-metrics").body().as(new TypeRef<>() {}); } protected void buildGlobalTelemetryInstance() { // Do nothing in JVM mode } -} \ No newline at end of file +} diff --git a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2ABaseTest.java b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2ABaseTest.java index 6813976c7..886e2abff 100644 --- a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2ABaseTest.java +++ b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2ABaseTest.java @@ -11,6 +11,17 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import io.opentelemetry.api.trace.SpanKind; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; import org.a2aproject.sdk.A2A; import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.config.ClientConfig; @@ -18,32 +29,20 @@ import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransportConfigBuilder; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.spec.A2AClientException; +import org.a2aproject.sdk.spec.A2AMethods; import org.a2aproject.sdk.spec.CancelTaskParams; import org.a2aproject.sdk.spec.ListTasksParams; +import org.a2aproject.sdk.spec.Message; +import org.a2aproject.sdk.spec.MessageSendParams; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.a2aproject.sdk.spec.TaskQueryParams; import org.a2aproject.sdk.spec.TaskState; import org.a2aproject.sdk.spec.TaskStatus; -import io.opentelemetry.api.trace.SpanKind; +import org.a2aproject.sdk.spec.TextPart; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.io.IOException; -import java.lang.reflect.Type; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.a2aproject.sdk.spec.A2AMethods; -import org.a2aproject.sdk.spec.Message; -import org.a2aproject.sdk.spec.MessageSendParams; -import org.a2aproject.sdk.spec.TextPart; - /** * Abstract base class for OpenTelemetry A2A integration tests. * Contains common test logic shared between unit and integration test modes. @@ -51,9 +50,8 @@ abstract class OpenTelemetryA2ABaseTest extends BaseTest { private static final Gson GSON = new Gson(); - private static final HttpClient TEST_HTTP_CLIENT = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); + private static final HttpClient TEST_HTTP_CLIENT = + HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); private static final String PUSH_TEST_TASK_ID = "push-propagation-task"; private static final String PUSH_TEST_CONFIG_ID = "otel-push-config"; @@ -63,14 +61,13 @@ abstract class OpenTelemetryA2ABaseTest extends BaseTest { @BeforeEach void setUp() throws A2AClientException { - ClientConfig clientConfig = new ClientConfig.Builder() - .setStreaming(false) - .build(); - - client = Client.builder(A2A.getAgentCard("http://localhost:" + serverPort)) - .clientConfig(clientConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); + ClientConfig clientConfig = new ClientConfig.Builder().setStreaming(false).build(); + + client = + Client.builder(A2A.getAgentCard("http://localhost:" + serverPort)) + .clientConfig(clientConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); } @BeforeEach @@ -86,15 +83,17 @@ void reset() { } protected void clearExportedSpans() { - await().atMost(5, SECONDS).until(() -> { - List> spans = getSpans(); - if (spans.isEmpty()) { - return true; - } else { - given().get("/reset").then().statusCode(HTTP_OK); - return false; - } - }); + await().atMost(5, SECONDS) + .until( + () -> { + List> spans = getSpans(); + if (spans.isEmpty()) { + return true; + } else { + given().get("/reset").then().statusCode(HTTP_OK); + return false; + } + }); } @Test @@ -102,13 +101,14 @@ void testGetTaskCreatesSpans() throws Exception { String taskId = "span-test-task-1"; String contextId = "span-test-ctx-1"; - Task task = Task.builder() - .id(taskId) - .contextId(contextId) - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + Task task = + Task.builder() + .id(taskId) + .contextId(contextId) + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); saveTaskInTaskStore(task); clearExportedSpans(); @@ -124,20 +124,33 @@ void testGetTaskCreatesSpans() throws Exception { System.out.println("We have created spans " + spans); assertFalse(spans.isEmpty(), "Should have created spans for getTask operation"); - long serverSpanCount = spans.stream() - .filter(span -> SpanKind.valueOf((span.get("kind").toString())) == SpanKind.SERVER) - .count(); + long serverSpanCount = + spans.stream() + .filter( + span -> + SpanKind.valueOf((span.get("kind").toString())) + == SpanKind.SERVER) + .count(); assertTrue(serverSpanCount > 0, "Should have at least one SERVER span"); - Map serverSpan = spans.stream() - .filter(span -> SpanKind.valueOf(span.get("kind").toString()) == SpanKind.SERVER) - .filter(span -> "GetTask".equals(span.get("name"))) - .findFirst() - .orElseThrow(() -> new AssertionError("No SERVER span found for GetTask")); - - assertEquals("GetTask", serverSpan.get("attr_gen_ai.agent.a2a.operation.name"), + Map serverSpan = + spans.stream() + .filter( + span -> + SpanKind.valueOf(span.get("kind").toString()) + == SpanKind.SERVER) + .filter(span -> "GetTask".equals(span.get("name"))) + .findFirst() + .orElseThrow( + () -> new AssertionError("No SERVER span found for GetTask")); + + assertEquals( + "GetTask", + serverSpan.get("attr_gen_ai.agent.a2a.operation.name"), "Operation name attribute should be set"); - assertEquals(taskId, serverSpan.get("attr_gen_ai.agent.a2a.task_id"), + assertEquals( + taskId, + serverSpan.get("attr_gen_ai.agent.a2a.task_id"), "Task ID attribute should be set"); } finally { @@ -149,11 +162,10 @@ void testGetTaskCreatesSpans() throws Exception { void testListTasksCreatesSpans() throws Exception { clearExportedSpans(); - ListTasksParams params = new ListTasksParams( - null, null, null, null, null, null, null, "" - ); + ListTasksParams params = new ListTasksParams(null, null, null, null, null, null, null, ""); - org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult result = client.listTasks(params, null); + org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult result = + client.listTasks(params, null); assertNotNull(result); @@ -162,13 +174,20 @@ void testListTasksCreatesSpans() throws Exception { System.out.println("We have created spans " + spans); assertFalse(spans.isEmpty(), "Should have created spans for listTasks"); - Map serverSpan = spans.stream() - .filter(span -> SpanKind.valueOf(span.get("kind").toString()) == SpanKind.SERVER) - .filter(span -> "ListTasks".equals(span.get("name"))) - .findFirst() - .orElseThrow(() -> new AssertionError("No SERVER span found for ListTasks")); - - assertEquals("ListTasks", serverSpan.get("attr_gen_ai.agent.a2a.operation.name"), + Map serverSpan = + spans.stream() + .filter( + span -> + SpanKind.valueOf(span.get("kind").toString()) + == SpanKind.SERVER) + .filter(span -> "ListTasks".equals(span.get("name"))) + .findFirst() + .orElseThrow( + () -> new AssertionError("No SERVER span found for ListTasks")); + + assertEquals( + "ListTasks", + serverSpan.get("attr_gen_ai.agent.a2a.operation.name"), "Operation name attribute should be set"); } @@ -177,13 +196,14 @@ void testCancelTaskCreatesSpans() throws Exception { String taskId = "cancel-test-task-1"; String contextId = "cancel-test-ctx-1"; - Task task = Task.builder() - .id(taskId) - .contextId(contextId) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + Task task = + Task.builder() + .id(taskId) + .contextId(contextId) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); saveTaskInTaskStore(task); ensureQueueForTask(taskId); @@ -200,15 +220,26 @@ void testCancelTaskCreatesSpans() throws Exception { System.out.println("We have created spans " + spans); assertFalse(spans.isEmpty(), "Should have created spans for cancelTask"); - Map serverSpan = spans.stream() - .filter(span -> SpanKind.valueOf(span.get("kind").toString()) == SpanKind.SERVER) - .filter(span -> "CancelTask".equals(span.get("name"))) - .findFirst() - .orElseThrow(() -> new AssertionError("No SERVER span found for CancelTask")); - - assertEquals("CancelTask", serverSpan.get("attr_gen_ai.agent.a2a.operation.name"), + Map serverSpan = + spans.stream() + .filter( + span -> + SpanKind.valueOf(span.get("kind").toString()) + == SpanKind.SERVER) + .filter(span -> "CancelTask".equals(span.get("name"))) + .findFirst() + .orElseThrow( + () -> + new AssertionError( + "No SERVER span found for CancelTask")); + + assertEquals( + "CancelTask", + serverSpan.get("attr_gen_ai.agent.a2a.operation.name"), "Operation name attribute should be set"); - assertEquals(taskId, serverSpan.get("attr_gen_ai.agent.a2a.task_id"), + assertEquals( + taskId, + serverSpan.get("attr_gen_ai.agent.a2a.task_id"), "Task ID attribute should be set"); } finally { @@ -221,13 +252,14 @@ void testSpanAttributes() throws Exception { String taskId = "attr-test-task-1"; String contextId = "attr-test-ctx-1"; - Task task = Task.builder() - .id(taskId) - .contextId(contextId) - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + Task task = + Task.builder() + .id(taskId) + .contextId(contextId) + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); saveTaskInTaskStore(task); clearExportedSpans(); @@ -240,11 +272,16 @@ void testSpanAttributes() throws Exception { System.out.println("We have created spans " + spans); assertFalse(spans.isEmpty()); - Map serverSpan = spans.stream() - .filter(span -> SpanKind.valueOf(span.get("kind").toString()) == SpanKind.SERVER) - .filter(span -> "GetTask".equals(span.get("name"))) - .findFirst() - .orElseThrow(() -> new AssertionError("No SERVER span found for GetTask")); + Map serverSpan = + spans.stream() + .filter( + span -> + SpanKind.valueOf(span.get("kind").toString()) + == SpanKind.SERVER) + .filter(span -> "GetTask".equals(span.get("name"))) + .findFirst() + .orElseThrow( + () -> new AssertionError("No SERVER span found for GetTask")); assertNotNull(serverSpan.get("spanId"), "Span should have a span ID"); assertNotNull(serverSpan.get("traceId"), "Span should have a trace ID"); @@ -252,12 +289,17 @@ void testSpanAttributes() throws Exception { assertEquals("SERVER", serverSpan.get("kind"), "Span kind should be SERVER"); assertEquals(Boolean.TRUE, serverSpan.get("ended"), "Span should be ended"); - assertEquals("GetTask", serverSpan.get("attr_gen_ai.agent.a2a.operation.name"), + assertEquals( + "GetTask", + serverSpan.get("attr_gen_ai.agent.a2a.operation.name"), "Operation name attribute should be set"); - assertEquals(taskId, serverSpan.get("attr_gen_ai.agent.a2a.task_id"), + assertEquals( + taskId, + serverSpan.get("attr_gen_ai.agent.a2a.task_id"), "Task ID attribute should be set"); - assertNotNull(serverSpan.get("resource_service.name"), + assertNotNull( + serverSpan.get("resource_service.name"), "Service name resource attribute should be set"); System.out.println("Service name: " + serverSpan.get("resource_service.name")); @@ -273,36 +315,58 @@ void testSendMessageStreamCreatesClosingSpan() throws Exception { reset(); given().get("/reset-metrics").then().statusCode(HTTP_OK); - Client streamingClient = Client.builder(A2A.getAgentCard("http://localhost:" + serverPort)) - .clientConfig(new ClientConfig.Builder().setStreaming(true).build()) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); - - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("stream test"))) - .messageId("stream-msg-1") - .build(); + Client streamingClient = + Client.builder(A2A.getAgentCard("http://localhost:" + serverPort)) + .clientConfig(new ClientConfig.Builder().setStreaming(true).build()) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); + + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("stream test"))) + .messageId("stream-msg-1") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); streamingClient.sendMessage(params, List.of(), null, null); - await().atMost(10, SECONDS).until(() -> { - List> spans = getSpans(); - return spans.stream().anyMatch(s -> (A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX).equals(s.get("name"))); - }); + await().atMost(10, SECONDS) + .until( + () -> { + List> spans = getSpans(); + return spans.stream() + .anyMatch( + s -> + (A2AMethods.SEND_STREAMING_MESSAGE_METHOD + + STREAMING_END_SUFFIX) + .equals(s.get("name"))); + }); List> spans = getSpans(); - Map initialSpan = spans.stream() - .filter(s -> A2AMethods.SEND_STREAMING_MESSAGE_METHOD.equals(s.get("name"))) - .findFirst() - .orElseThrow(() -> new AssertionError("No initial SendStreamingMessage span found")); - - Map closingSpan = spans.stream() - .filter(s -> (A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX).equals(s.get("name"))) - .findFirst() - .orElseThrow(() -> new AssertionError("No SendStreamingMessage-stream closing span found")); + Map initialSpan = + spans.stream() + .filter(s -> A2AMethods.SEND_STREAMING_MESSAGE_METHOD.equals(s.get("name"))) + .findFirst() + .orElseThrow( + () -> + new AssertionError( + "No initial SendStreamingMessage span found")); + + Map closingSpan = + spans.stream() + .filter( + s -> + (A2AMethods.SEND_STREAMING_MESSAGE_METHOD + + STREAMING_END_SUFFIX) + .equals(s.get("name"))) + .findFirst() + .orElseThrow( + () -> + new AssertionError( + "No SendStreamingMessage-stream closing span" + + " found")); assertEquals(Boolean.TRUE, initialSpan.get("ended"), "Initial span should be ended"); assertEquals("SERVER", initialSpan.get("kind"), "Initial span should be SERVER"); @@ -311,11 +375,20 @@ void testSendMessageStreamCreatesClosingSpan() throws Exception { assertEquals("SERVER", closingSpan.get("kind"), "Closing span should be SERVER"); assertTrue( - spans.stream().anyMatch(s -> (A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX).equals(s.get("name"))), + spans.stream() + .anyMatch( + s -> + (A2AMethods.SEND_STREAMING_MESSAGE_METHOD + + STREAMING_EVENT_SUFFIX) + .equals(s.get("name"))), "Should have at least one streaming event span"); - assertEquals(1, ((Number) closingSpan.get("links_count")).intValue(), + assertEquals( + 1, + ((Number) closingSpan.get("links_count")).intValue(), "Closing span should have exactly one link"); - assertEquals(initialSpan.get("spanId"), closingSpan.get("link_0_spanId"), + assertEquals( + initialSpan.get("spanId"), + closingSpan.get("link_0_spanId"), "Closing span should link to the initial span"); } @@ -324,34 +397,44 @@ void testSendMessageStreamRecordsStreamingDuration() throws Exception { reset(); given().get("/reset-metrics").then().statusCode(HTTP_OK); - Client streamingClient = Client.builder(A2A.getAgentCard("http://localhost:" + serverPort)) - .clientConfig(new ClientConfig.Builder().setStreaming(true).build()) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); - - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("metric stream test"))) - .messageId("metric-stream-msg-1") - .build(); + Client streamingClient = + Client.builder(A2A.getAgentCard("http://localhost:" + serverPort)) + .clientConfig(new ClientConfig.Builder().setStreaming(true).build()) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); + + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("metric stream test"))) + .messageId("metric-stream-msg-1") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); streamingClient.sendMessage(params, List.of(), null, null); - await().atMost(10, SECONDS).until(() -> { - List> metrics = getMetrics(); - return metrics.stream().anyMatch(m -> GENAI_STREAMING_DURATION.equals(m.get("name"))); - }); + await().atMost(10, SECONDS) + .until( + () -> { + List> metrics = getMetrics(); + return metrics.stream() + .anyMatch(m -> GENAI_STREAMING_DURATION.equals(m.get("name"))); + }); List> metrics = getMetrics(); - Map streamingMetric = metrics.stream() - .filter(m -> GENAI_STREAMING_DURATION.equals(m.get("name"))) - .findFirst() - .orElseThrow(() -> new AssertionError("No streaming duration metric found")); - - assertEquals("HISTOGRAM", streamingMetric.get("type"), + Map streamingMetric = + metrics.stream() + .filter(m -> GENAI_STREAMING_DURATION.equals(m.get("name"))) + .findFirst() + .orElseThrow( + () -> new AssertionError("No streaming duration metric found")); + + assertEquals( + "HISTOGRAM", + streamingMetric.get("type"), "Streaming duration metric should be a histogram"); - assertTrue(((Number) streamingMetric.get("data_count")).intValue() > 0, + assertTrue( + ((Number) streamingMetric.get("data_count")).intValue() > 0, "Histogram should have at least one data point"); } @@ -360,17 +443,19 @@ void testPushNotificationCarriesTraceContext() throws Exception { String taskId = PUSH_TEST_TASK_ID; String contextId = "push-propagation-ctx"; - Task task = Task.builder() - .id(taskId) - .contextId(contextId) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + Task task = + Task.builder() + .id(taskId) + .contextId(contextId) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); saveTaskInTaskStore(task); clearExportedSpans(); - savePushNotificationConfig(taskId, + savePushNotificationConfig( + taskId, TaskPushNotificationConfig.builder() .id(PUSH_TEST_CONFIG_ID) .url("http://localhost:" + serverPort + "/test/webhook") @@ -386,24 +471,44 @@ void testPushNotificationCarriesTraceContext() throws Exception { String traceparent = assertTraceparentHeader(webhookHeaders); // Wait for the SendPushNotification CLIENT span to be exported - await().atMost(10, SECONDS).until(() -> getSpans().stream() - .anyMatch(span -> "SendPushNotification".equals(span.get("name")) - && SpanKind.CLIENT.name().equals(span.get("kind")))); + await().atMost(10, SECONDS) + .until( + () -> + getSpans().stream() + .anyMatch( + span -> + "SendPushNotification" + .equals( + span.get( + "name")) + && SpanKind.CLIENT + .name() + .equals( + span.get( + "kind")))); List> spans = getSpans(); - Map pushSpan = spans.stream() - .filter(span -> "SendPushNotification".equals(span.get("name"))) - .filter(span -> SpanKind.CLIENT.name().equals(span.get("kind"))) - .findFirst() - .orElseThrow(() -> new AssertionError( - "No CLIENT span found for SendPushNotification. Spans: " - + spans.stream().map(s -> s.get("name")).toList())); + Map pushSpan = + spans.stream() + .filter(span -> "SendPushNotification".equals(span.get("name"))) + .filter(span -> SpanKind.CLIENT.name().equals(span.get("kind"))) + .findFirst() + .orElseThrow( + () -> + new AssertionError( + "No CLIENT span found for SendPushNotification." + + " Spans: " + + spans.stream() + .map(s -> s.get("name")) + .toList())); assertNotNull(pushSpan.get("spanId")); assertNotNull(pushSpan.get("traceId")); String traceIdFromHeader = traceparent.split("-")[1]; - assertEquals(traceIdFromHeader, pushSpan.get("traceId"), + assertEquals( + traceIdFromHeader, + pushSpan.get("traceId"), "traceparent trace ID should match the SendPushNotification span's trace ID"); } finally { deletePushNotificationConfig(taskId, PUSH_TEST_CONFIG_ID); @@ -416,17 +521,19 @@ void testPushNotificationPreservesCustomHttpClient() throws Exception { String taskId = "push-custom-client-task"; String contextId = "push-custom-client-ctx"; - Task task = Task.builder() - .id(taskId) - .contextId(contextId) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + Task task = + Task.builder() + .id(taskId) + .contextId(contextId) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); saveTaskInTaskStore(task); clearExportedSpans(); - savePushNotificationConfig(taskId, + savePushNotificationConfig( + taskId, TaskPushNotificationConfig.builder() .id("custom-client-config") .url("http://localhost:" + serverPort + "/test/webhook") @@ -440,11 +547,15 @@ void testPushNotificationPreservesCustomHttpClient() throws Exception { Map webhookHeaders = fetchWebhookRequests().get(0); - String customHeader = getHeader(webhookHeaders, CustomHttpClientProducer.CUSTOM_HEADER_NAME); - assertNotNull(customHeader, - "Push notification must carry the custom client's marker header (" + - CustomHttpClientProducer.CUSTOM_HEADER_NAME + "), proving the OTel module " + - "wraps the application's HTTP client. Received headers: " + webhookHeaders.keySet()); + String customHeader = + getHeader(webhookHeaders, CustomHttpClientProducer.CUSTOM_HEADER_NAME); + assertNotNull( + customHeader, + "Push notification must carry the custom client's marker header (" + + CustomHttpClientProducer.CUSTOM_HEADER_NAME + + "), proving the OTel module " + + "wraps the application's HTTP client. Received headers: " + + webhookHeaders.keySet()); assertEquals(CustomHttpClientProducer.CUSTOM_HEADER_VALUE, customHeader); assertTraceparentHeader(webhookHeaders); @@ -459,17 +570,19 @@ void testPushNotificationUsesCustomDispatchExecutorWithTraceContext() throws Exc String taskId = "push-custom-executor-task"; String contextId = "push-custom-executor-ctx"; - Task task = Task.builder() - .id(taskId) - .contextId(contextId) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + Task task = + Task.builder() + .id(taskId) + .contextId(contextId) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); saveTaskInTaskStore(task); clearExportedSpans(); - savePushNotificationConfig(taskId, + savePushNotificationConfig( + taskId, TaskPushNotificationConfig.builder() .id("custom-executor-config") .url("http://localhost:" + serverPort + "/test/webhook") @@ -484,13 +597,18 @@ void testPushNotificationUsesCustomDispatchExecutorWithTraceContext() throws Exc Map webhookHeaders = fetchWebhookRequests().get(0); assertTraceparentHeader(webhookHeaders); - int executionCount = Integer.parseInt( - given().get("/test/dispatch-executor/count") - .then().statusCode(HTTP_OK) - .extract().body().asString()); - assertTrue(executionCount > 0, - "Custom @Named dispatch executor must be used by the OTel-wrapped executor, " + - "but execution count was 0"); + int executionCount = + Integer.parseInt( + given().get("/test/dispatch-executor/count") + .then() + .statusCode(HTTP_OK) + .extract() + .body() + .asString()); + assertTrue( + executionCount > 0, + "Custom @Named dispatch executor must be used by the OTel-wrapped executor, " + + "but execution count was 0"); } finally { deletePushNotificationConfig(taskId, "custom-executor-config"); deleteTaskInTaskStore(taskId); @@ -512,9 +630,12 @@ private static String getHeader(Map headers, String name) { */ private static String assertTraceparentHeader(Map webhookHeaders) { String traceparent = getHeader(webhookHeaders, "traceparent"); - assertNotNull(traceparent, - "Push notification should carry traceparent header. Received headers: " + webhookHeaders.keySet()); - assertTrue(traceparent.matches("00-[a-f0-9]{32}-[a-f0-9]{16}-[a-f0-9]{2}"), + assertNotNull( + traceparent, + "Push notification should carry traceparent header. Received headers: " + + webhookHeaders.keySet()); + assertTrue( + traceparent.matches("00-[a-f0-9]{32}-[a-f0-9]{16}-[a-f0-9]{2}"), "traceparent should be in W3C format, got: " + traceparent); return traceparent; } @@ -523,87 +644,98 @@ private URI testUri(String path) { return URI.create("http://localhost:" + serverPort + "/test/" + path); } - private HttpResponse sendTestRequest(HttpRequest request) throws IOException, InterruptedException { - return TEST_HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + private HttpResponse sendTestRequest(HttpRequest request) + throws IOException, InterruptedException { + return TEST_HTTP_CLIENT.send( + request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); } - private void sendTestRequestExpecting200(HttpRequest request, String operation) throws IOException, InterruptedException { + private void sendTestRequestExpecting200(HttpRequest request, String operation) + throws IOException, InterruptedException { HttpResponse response = sendTestRequest(request); if (response.statusCode() != HTTP_OK) { - throw new RuntimeException(String.format("%s failed! Status: %d, Body: %s", - operation, response.statusCode(), response.body())); + throw new RuntimeException( + String.format( + "%s failed! Status: %d, Body: %s", + operation, response.statusCode(), response.body())); } } protected void saveTaskInTaskStore(Task task) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(testUri("task")) - .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(task))) - .header("Content-Type", "application/json") - .build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(testUri("task")) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(task))) + .header("Content-Type", "application/json") + .build(); sendTestRequestExpecting200(request, "Saving task"); } protected Task getTaskFromTaskStore(String taskId) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(testUri("task/" + taskId)) - .GET() - .build(); + HttpRequest request = HttpRequest.newBuilder().uri(testUri("task/" + taskId)).GET().build(); HttpResponse response = sendTestRequest(request); if (response.statusCode() == 404) { return null; } if (response.statusCode() != HTTP_OK) { - throw new RuntimeException(String.format("Getting task failed! Status: %d, Body: %s", - response.statusCode(), response.body())); + throw new RuntimeException( + String.format( + "Getting task failed! Status: %d, Body: %s", + response.statusCode(), response.body())); } return JsonUtil.fromJson(response.body(), Task.class); } protected void deleteTaskInTaskStore(String taskId) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(testUri("task/" + taskId)) - .DELETE() - .build(); + HttpRequest request = + HttpRequest.newBuilder().uri(testUri("task/" + taskId)).DELETE().build(); sendTestRequestExpecting200(request, "Deleting task"); } - protected void savePushNotificationConfig(String taskId, TaskPushNotificationConfig config) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(testUri("pushconfig/" + taskId)) - .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(config))) - .header("Content-Type", "application/json") - .build(); + protected void savePushNotificationConfig(String taskId, TaskPushNotificationConfig config) + throws Exception { + HttpRequest request = + HttpRequest.newBuilder() + .uri(testUri("pushconfig/" + taskId)) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(config))) + .header("Content-Type", "application/json") + .build(); sendTestRequestExpecting200(request, "Saving push config"); } protected void deletePushNotificationConfig(String taskId, String configId) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(testUri("pushconfig/" + taskId + "/" + configId)) - .DELETE() - .build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(testUri("pushconfig/" + taskId + "/" + configId)) + .DELETE() + .build(); sendTestRequestExpecting200(request, "Deleting push config"); } - private static final Type WEBHOOK_LIST_TYPE = new TypeToken>>() {}.getType(); + private static final Type WEBHOOK_LIST_TYPE = + new TypeToken>>() {}.getType(); /** * Fetches captured webhook requests via HTTP, so it works across JVM * boundaries in {@code @QuarkusIntegrationTest} mode. */ protected List> fetchWebhookRequests() { - String json = given().get("/test/webhook/requests") - .then().statusCode(HTTP_OK) - .extract().body().asString(); + String json = + given().get("/test/webhook/requests") + .then() + .statusCode(HTTP_OK) + .extract() + .body() + .asString(); return GSON.fromJson(json, WEBHOOK_LIST_TYPE); } protected void ensureQueueForTask(String taskId) throws Exception { - HttpRequest request = HttpRequest.newBuilder() - .uri(testUri("queue/ensure/" + taskId)) - .POST(HttpRequest.BodyPublishers.noBody()) - .build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(testUri("queue/ensure/" + taskId)) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); sendTestRequestExpecting200(request, "Ensuring queue"); } - } diff --git a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2AIT.java b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2AIT.java index 272acfb1d..a50550879 100644 --- a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2AIT.java +++ b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2AIT.java @@ -7,5 +7,4 @@ * Runs in native mode with @QuarkusIntegrationTest annotation. */ @QuarkusIntegrationTest -class OpenTelemetryA2AIT extends OpenTelemetryA2ABaseTest { -} +class OpenTelemetryA2AIT extends OpenTelemetryA2ABaseTest {} diff --git a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2ATest.java b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2ATest.java index 928f91c5d..79f364e4a 100644 --- a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2ATest.java +++ b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryA2ATest.java @@ -7,5 +7,4 @@ * Runs in JVM mode with @QuarkusTest annotation. */ @QuarkusTest -class OpenTelemetryA2ATest extends OpenTelemetryA2ABaseTest { -} +class OpenTelemetryA2ATest extends OpenTelemetryA2ABaseTest {} diff --git a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryTest.java b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryTest.java index 4aa10341e..d7efb5394 100644 --- a/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryTest.java +++ b/extras/opentelemetry/integration-tests/src/test/java/org/a2aproject/sdk/extras/opentelemetry/it/OpenTelemetryTest.java @@ -6,37 +6,33 @@ import static org.awaitility.Awaitility.await; import static org.hamcrest.CoreMatchers.is; +import io.quarkus.test.junit.QuarkusTest; import java.util.List; import java.util.Map; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import io.quarkus.test.junit.QuarkusTest; - @QuarkusTest public class OpenTelemetryTest extends BaseTest { @BeforeEach void reset() { - await().atMost(5, SECONDS).until(() -> { - List> spans = getSpans(); - if (spans.isEmpty()) { - return true; - } else { - given().get("/reset").then().statusCode(HTTP_OK); - return false; - } - }); + await().atMost(5, SECONDS) + .until( + () -> { + List> spans = getSpans(); + if (spans.isEmpty()) { + return true; + } else { + given().get("/reset").then().statusCode(HTTP_OK); + return false; + } + }); } @Test void buildTimeEnabled() { - given() - .when().get("/hello") - .then() - .statusCode(200) - .body(is("Hello from Quarkus REST")); + given().when().get("/hello").then().statusCode(200).body(is("Hello from Quarkus REST")); await().atMost(5, SECONDS).until(() -> getSpans().size() == 1); } -} \ No newline at end of file +} diff --git a/extras/opentelemetry/pom.xml b/extras/opentelemetry/pom.xml index 01a9d6267..2941f928e 100644 --- a/extras/opentelemetry/pom.xml +++ b/extras/opentelemetry/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 @@ -17,6 +15,14 @@ A2A Java SDK :: Extras :: Opentelemetry :: Parent Java SDK for the Agent2Agent Protocol (A2A) - Extras - Opentelemetry + + common + client-propagation + client + server + integration-tests + + 2.0.1 @@ -30,12 +36,4 @@ - - common - client-propagation - client - server - integration-tests - - - \ No newline at end of file + diff --git a/extras/opentelemetry/server/pom.xml b/extras/opentelemetry/server/pom.xml index c07a23353..e035b7598 100644 --- a/extras/opentelemetry/server/pom.xml +++ b/extras/opentelemetry/server/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryA2AHttpClient.java b/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryA2AHttpClient.java index 47fef1df2..11edeb7db 100644 --- a/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryA2AHttpClient.java +++ b/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryA2AHttpClient.java @@ -1,13 +1,12 @@ package org.a2aproject.sdk.extras.opentelemetry.server; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; - -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.context.Context; import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.client.http.ServerSentEvent; @@ -64,7 +63,8 @@ public DeleteBuilder createDelete() { * @param the concrete propagating builder type (for method chaining) * @param the delegate builder type */ - private abstract static class AbstractPropagatingBuilder, D extends Builder> { + private abstract static class AbstractPropagatingBuilder< + S extends Builder, D extends Builder> { final D delegate; private final OpenTelemetry openTelemetry; @@ -96,19 +96,23 @@ public S addHeader(String name, String value) { protected void injectContextIntoDelegate() { Map propagationHeaders = new HashMap<>(); - openTelemetry.getPropagators().getTextMapPropagator() - .inject(Context.current(), propagationHeaders, (carrier, key, value) -> { - if (carrier != null) { - carrier.put(key, value); - } - }); + openTelemetry + .getPropagators() + .getTextMapPropagator() + .inject( + Context.current(), + propagationHeaders, + (carrier, key, value) -> { + if (carrier != null) { + carrier.put(key, value); + } + }); delegate.addHeaders(propagationHeaders); } } private static class PropagatingGetBuilder - extends AbstractPropagatingBuilder - implements GetBuilder { + extends AbstractPropagatingBuilder implements GetBuilder { PropagatingGetBuilder(GetBuilder delegate, OpenTelemetry openTelemetry) { super(delegate, openTelemetry); @@ -130,15 +134,15 @@ public A2AHttpResponse get() throws IOException, InterruptedException { public CompletableFuture getAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, - Runnable completeRunnable) throws IOException, InterruptedException { + Runnable completeRunnable) + throws IOException, InterruptedException { injectContextIntoDelegate(); return delegate.getAsyncSSE(messageConsumer, errorConsumer, completeRunnable); } } private static class PropagatingPostBuilder - extends AbstractPropagatingBuilder - implements PostBuilder { + extends AbstractPropagatingBuilder implements PostBuilder { PropagatingPostBuilder(PostBuilder delegate, OpenTelemetry openTelemetry) { super(delegate, openTelemetry); @@ -166,7 +170,8 @@ public A2AHttpResponse post() throws IOException, InterruptedException { public CompletableFuture postAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, - Runnable completeRunnable) throws IOException, InterruptedException { + Runnable completeRunnable) + throws IOException, InterruptedException { injectContextIntoDelegate(); return delegate.postAsyncSSE(messageConsumer, errorConsumer, completeRunnable); } diff --git a/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryHttpClientProducer.java b/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryHttpClientProducer.java index 1e1c5fe87..5353be96b 100644 --- a/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryHttpClientProducer.java +++ b/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryHttpClientProducer.java @@ -1,12 +1,5 @@ package org.a2aproject.sdk.extras.opentelemetry.server; -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.context.Context; import jakarta.annotation.PreDestroy; @@ -15,6 +8,12 @@ import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; import jakarta.inject.Named; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpClientConfiguration; import org.a2aproject.sdk.client.http.A2AHttpClientFactory; @@ -91,11 +90,13 @@ public OpenTelemetryHttpClientProducer(OpenTelemetry openTelemetry) { @ApplicationScoped @Instrumented A2AHttpClient createPropagatingClient(Instance httpClient) { - A2AHttpClient delegate = httpClient.isResolvable() - ? httpClient.get() - : A2AHttpClientFactory.create(A2AHttpClientConfiguration.builder() - .excludedProviderNames(java.util.List.of(CDI_PROVIDER_NAME)) - .build()); + A2AHttpClient delegate = + httpClient.isResolvable() + ? httpClient.get() + : A2AHttpClientFactory.create( + A2AHttpClientConfiguration.builder() + .excludedProviderNames(java.util.List.of(CDI_PROVIDER_NAME)) + .build()); return new OpenTelemetryA2AHttpClient(delegate, openTelemetry); } @@ -126,17 +127,19 @@ A2AHttpClient createPropagatingClient(Instance httpClient) { @ApplicationScoped @Instrumented Executor createContextPropagatingExecutor( - @Named(BasePushNotificationSender.DISPATCH_EXECUTOR_NAME) Instance userExecutor) { + @Named(BasePushNotificationSender.DISPATCH_EXECUTOR_NAME) + Instance userExecutor) { if (userExecutor.isResolvable()) { return Context.taskWrapping(userExecutor.get()); } int threads = Math.max(4, Runtime.getRuntime().availableProcessors() * 2); AtomicInteger counter = new AtomicInteger(); - ThreadFactory threadFactory = r -> { - Thread t = new Thread(r, "a2a-push-dispatch-" + counter.getAndIncrement()); - t.setDaemon(true); - return t; - }; + ThreadFactory threadFactory = + r -> { + Thread t = new Thread(r, "a2a-push-dispatch-" + counter.getAndIncrement()); + t.setDaemon(true); + return t; + }; ExecutorService pool = Executors.newFixedThreadPool(threads, threadFactory); dispatchPool = pool; return Context.taskWrapping(pool); diff --git a/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryPushNotificationSenderDecorator.java b/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryPushNotificationSenderDecorator.java index 44217ed8e..e3feb6f33 100644 --- a/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryPushNotificationSenderDecorator.java +++ b/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryPushNotificationSenderDecorator.java @@ -42,17 +42,14 @@ */ @Decorator @Priority(100) -public abstract class OpenTelemetryPushNotificationSenderDecorator implements PushNotificationSender { +public abstract class OpenTelemetryPushNotificationSenderDecorator + implements PushNotificationSender { static final String SEND_PUSH_NOTIFICATION = "SendPushNotification"; - @Inject - @Delegate - @Any - private PushNotificationSender delegate; + @Inject @Delegate @Any private PushNotificationSender delegate; - @Inject - private Tracer tracer; + @Inject private Tracer tracer; private final boolean extractResponse; @@ -71,8 +68,8 @@ public OpenTelemetryPushNotificationSenderDecorator() { * @param tracer the tracer to use * @param extractResponse whether to capture the full event payload as a span attribute */ - OpenTelemetryPushNotificationSenderDecorator(PushNotificationSender delegate, Tracer tracer, - boolean extractResponse) { + OpenTelemetryPushNotificationSenderDecorator( + PushNotificationSender delegate, Tracer tracer, boolean extractResponse) { this.delegate = delegate; this.tracer = tracer; this.extractResponse = extractResponse; @@ -80,10 +77,12 @@ public OpenTelemetryPushNotificationSenderDecorator() { @Override public boolean sendNotification(StreamingEventKind event, @Nullable Task taskSnapshot) { - var spanBuilder = tracer.spanBuilder(SEND_PUSH_NOTIFICATION) - .setSpanKind(SpanKind.CLIENT) - .setAttribute(GENAI_OPERATION_NAME, SEND_PUSH_NOTIFICATION) - .setAttribute(PUSH_NOTIFICATION_EVENT_KIND, event.getClass().getSimpleName()); + var spanBuilder = + tracer.spanBuilder(SEND_PUSH_NOTIFICATION) + .setSpanKind(SpanKind.CLIENT) + .setAttribute(GENAI_OPERATION_NAME, SEND_PUSH_NOTIFICATION) + .setAttribute( + PUSH_NOTIFICATION_EVENT_KIND, event.getClass().getSimpleName()); EventIds ids = extractIds(event, taskSnapshot); if (ids.taskId != null) { @@ -107,12 +106,14 @@ public boolean sendNotification(StreamingEventKind event, @Nullable Task taskSna } return result; } catch (Error err) { - String errorDescription = err.getMessage() != null ? err.getMessage() : err.getClass().getName(); + String errorDescription = + err.getMessage() != null ? err.getMessage() : err.getClass().getName(); span.setAttribute(ERROR_TYPE, errorDescription); span.setStatus(StatusCode.ERROR, errorDescription); throw err; } catch (Exception ex) { - String errorDescription = ex.getMessage() != null ? ex.getMessage() : ex.getClass().getName(); + String errorDescription = + ex.getMessage() != null ? ex.getMessage() : ex.getClass().getName(); span.setAttribute(ERROR_TYPE, errorDescription); span.setStatus(StatusCode.ERROR, errorDescription); throw ex; @@ -121,8 +122,7 @@ public boolean sendNotification(StreamingEventKind event, @Nullable Task taskSna } } - private record EventIds(@Nullable String taskId, @Nullable String contextId) { - } + private record EventIds(@Nullable String taskId, @Nullable String contextId) {} private static EventIds extractIds(StreamingEventKind event, @Nullable Task taskSnapshot) { if (taskSnapshot != null) { @@ -142,5 +142,4 @@ private static EventIds extractIds(StreamingEventKind event, @Nullable Task task } return new EventIds(null, null); } - } diff --git a/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryRequestHandlerDecorator.java b/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryRequestHandlerDecorator.java index ba1f7a615..50aee9242 100644 --- a/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryRequestHandlerDecorator.java +++ b/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryRequestHandlerDecorator.java @@ -86,25 +86,18 @@ @Priority(100) public abstract class OpenTelemetryRequestHandlerDecorator implements RequestHandler { - @Inject - @Delegate - @Any - private RequestHandler delegate; + @Inject @Delegate @Any private RequestHandler delegate; - @Inject - private Tracer tracer; + @Inject private Tracer tracer; - @Inject - private Instance meterInstance; + @Inject private Instance meterInstance; - @Nullable - private volatile DoubleHistogram streamingDurationHistogram; + @Nullable private volatile DoubleHistogram streamingDurationHistogram; /** * Default constructor for CDI. */ - public OpenTelemetryRequestHandlerDecorator() { - } + public OpenTelemetryRequestHandlerDecorator() {} /** * Constructor for testing without metrics. @@ -123,7 +116,8 @@ public OpenTelemetryRequestHandlerDecorator(RequestHandler delegate, Tracer trac * @param tracer the tracer to use * @param meter optional meter for streaming duration histogram; null disables metrics */ - public OpenTelemetryRequestHandlerDecorator(RequestHandler delegate, Tracer tracer, @Nullable Meter meter) { + public OpenTelemetryRequestHandlerDecorator( + RequestHandler delegate, Tracer tracer, @Nullable Meter meter) { this.delegate = delegate; this.tracer = tracer; this.streamingDurationHistogram = buildStreamingDurationHistogram(meter); @@ -131,10 +125,13 @@ public OpenTelemetryRequestHandlerDecorator(RequestHandler delegate, Tracer trac @Nullable private DoubleHistogram streamingDurationHistogram() { - if (streamingDurationHistogram == null && meterInstance != null && meterInstance.isResolvable()) { + if (streamingDurationHistogram == null + && meterInstance != null + && meterInstance.isResolvable()) { synchronized (this) { if (streamingDurationHistogram == null) { - streamingDurationHistogram = buildStreamingDurationHistogram(meterInstance.get()); + streamingDurationHistogram = + buildStreamingDurationHistogram(meterInstance.get()); } } } @@ -148,15 +145,17 @@ private static DoubleHistogram buildStreamingDurationHistogram(@Nullable Meter m } return meter.histogramBuilder(GENAI_STREAMING_DURATION) .setUnit("s") - .setDescription("Duration of A2A streaming operations from initiation to last event") + .setDescription( + "Duration of A2A streaming operations from initiation to last event") .build(); } @Override public Task onGetTask(TaskQueryParams params, ServerCallContext context) throws A2AError { - var spanBuilder = tracer.spanBuilder(A2AMethods.GET_TASK_METHOD) - .setSpanKind(SpanKind.SERVER) - .setAttribute(GENAI_OPERATION_NAME, A2AMethods.GET_TASK_METHOD); + var spanBuilder = + tracer.spanBuilder(A2AMethods.GET_TASK_METHOD) + .setSpanKind(SpanKind.SERVER) + .setAttribute(GENAI_OPERATION_NAME, A2AMethods.GET_TASK_METHOD); if (params.id() != null) { spanBuilder.setAttribute(GENAI_TASK_ID, params.id()); @@ -187,10 +186,12 @@ public Task onGetTask(TaskQueryParams params, ServerCallContext context) throws } @Override - public ListTasksResult onListTasks(ListTasksParams params, ServerCallContext context) throws A2AError { - var spanBuilder = tracer.spanBuilder(A2AMethods.LIST_TASK_METHOD) - .setSpanKind(SpanKind.SERVER) - .setAttribute(GENAI_OPERATION_NAME, A2AMethods.LIST_TASK_METHOD); + public ListTasksResult onListTasks(ListTasksParams params, ServerCallContext context) + throws A2AError { + var spanBuilder = + tracer.spanBuilder(A2AMethods.LIST_TASK_METHOD) + .setSpanKind(SpanKind.SERVER) + .setAttribute(GENAI_OPERATION_NAME, A2AMethods.LIST_TASK_METHOD); if (extractRequest()) { spanBuilder.setAttribute(GENAI_REQUEST, params.toString()); @@ -221,9 +222,10 @@ public ListTasksResult onListTasks(ListTasksParams params, ServerCallContext con @Override public Task onCancelTask(CancelTaskParams params, ServerCallContext context) throws A2AError { - var spanBuilder = tracer.spanBuilder(A2AMethods.CANCEL_TASK_METHOD) - .setSpanKind(SpanKind.SERVER) - .setAttribute(GENAI_OPERATION_NAME, A2AMethods.CANCEL_TASK_METHOD); + var spanBuilder = + tracer.spanBuilder(A2AMethods.CANCEL_TASK_METHOD) + .setSpanKind(SpanKind.SERVER) + .setAttribute(GENAI_OPERATION_NAME, A2AMethods.CANCEL_TASK_METHOD); if (params.id() != null) { spanBuilder.setAttribute(GENAI_TASK_ID, params.id()); @@ -254,10 +256,12 @@ public Task onCancelTask(CancelTaskParams params, ServerCallContext context) thr } @Override - public EventKind onMessageSend(MessageSendParams params, ServerCallContext context) throws A2AError { - var spanBuilder = tracer.spanBuilder(A2AMethods.SEND_MESSAGE_METHOD) - .setSpanKind(SpanKind.SERVER) - .setAttribute(GENAI_OPERATION_NAME, A2AMethods.SEND_MESSAGE_METHOD); + public EventKind onMessageSend(MessageSendParams params, ServerCallContext context) + throws A2AError { + var spanBuilder = + tracer.spanBuilder(A2AMethods.SEND_MESSAGE_METHOD) + .setSpanKind(SpanKind.SERVER) + .setAttribute(GENAI_OPERATION_NAME, A2AMethods.SEND_MESSAGE_METHOD); if (params.message() != null) { if (params.message().taskId() != null) { @@ -273,7 +277,8 @@ public EventKind onMessageSend(MessageSendParams params, ServerCallContext conte spanBuilder.setAttribute(GENAI_ROLE, params.message().role().name()); } if (params.message().extensions() != null && !params.message().extensions().isEmpty()) { - spanBuilder.setAttribute(GENAI_EXTENSIONS, String.join(",", params.message().extensions())); + spanBuilder.setAttribute( + GENAI_EXTENSIONS, String.join(",", params.message().extensions())); } spanBuilder.setAttribute(GENAI_PARTS_NUMBER, params.message().parts().size()); } @@ -303,10 +308,13 @@ public EventKind onMessageSend(MessageSendParams params, ServerCallContext conte } @Override - public Flow.Publisher onMessageSendStream(MessageSendParams params, ServerCallContext context) throws A2AError { - var spanBuilder = tracer.spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD) - .setSpanKind(SpanKind.SERVER) - .setAttribute(GENAI_OPERATION_NAME, A2AMethods.SEND_STREAMING_MESSAGE_METHOD); + public Flow.Publisher onMessageSendStream( + MessageSendParams params, ServerCallContext context) throws A2AError { + var spanBuilder = + tracer.spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD) + .setSpanKind(SpanKind.SERVER) + .setAttribute( + GENAI_OPERATION_NAME, A2AMethods.SEND_STREAMING_MESSAGE_METHOD); if (params.message() != null) { if (params.message().taskId() != null) { @@ -322,7 +330,8 @@ public Flow.Publisher onMessageSendStream(MessageSendParams spanBuilder.setAttribute(GENAI_ROLE, params.message().role().name()); } if (params.message().extensions() != null && !params.message().extensions().isEmpty()) { - spanBuilder.setAttribute(GENAI_EXTENSIONS, String.join(",", params.message().extensions())); + spanBuilder.setAttribute( + GENAI_EXTENSIONS, String.join(",", params.message().extensions())); } spanBuilder.setAttribute(GENAI_PARTS_NUMBER, params.message().parts().size()); } @@ -335,7 +344,8 @@ public Flow.Publisher onMessageSendStream(MessageSendParams long startNanos = System.nanoTime(); try (Scope scope = span.makeCurrent()) { - Flow.Publisher result = delegate.onMessageSendStream(params, context); + Flow.Publisher result = + delegate.onMessageSendStream(params, context); if (extractResponse()) { span.setAttribute(GENAI_RESPONSE, "Stream publisher created"); @@ -343,12 +353,20 @@ public Flow.Publisher onMessageSendStream(MessageSendParams span.setStatus(StatusCode.OK); SpanContext spanContext = span.getSpanContext(); - return new OpenTelemetryStreamPublisher(result, - new StreamingSpanConfig(tracer, A2AMethods.SEND_STREAMING_MESSAGE_METHOD, - spanContext, streamingDurationHistogram(), startNanos)); + return new OpenTelemetryStreamPublisher( + result, + new StreamingSpanConfig( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD, + spanContext, + streamingDurationHistogram(), + startNanos)); } catch (A2AError error) { - StreamingEventAttributes.recordDuration(streamingDurationHistogram(), - startNanos, A2AMethods.SEND_STREAMING_MESSAGE_METHOD, false); + StreamingEventAttributes.recordDuration( + streamingDurationHistogram(), + startNanos, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD, + false); span.setAttribute(ERROR_TYPE, error.getMessage()); span.setStatus(StatusCode.ERROR, error.getMessage()); throw error; @@ -358,10 +376,14 @@ public Flow.Publisher onMessageSendStream(MessageSendParams } @Override - public TaskPushNotificationConfig onCreateTaskPushNotificationConfig(TaskPushNotificationConfig params, ServerCallContext context) throws A2AError { - var spanBuilder = tracer.spanBuilder(A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD) - .setSpanKind(SpanKind.SERVER) - .setAttribute(GENAI_OPERATION_NAME, A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + public TaskPushNotificationConfig onCreateTaskPushNotificationConfig( + TaskPushNotificationConfig params, ServerCallContext context) throws A2AError { + var spanBuilder = + tracer.spanBuilder(A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD) + .setSpanKind(SpanKind.SERVER) + .setAttribute( + GENAI_OPERATION_NAME, + A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); if (params.taskId() != null) { spanBuilder.setAttribute(GENAI_TASK_ID, params.taskId()); @@ -377,7 +399,8 @@ public TaskPushNotificationConfig onCreateTaskPushNotificationConfig(TaskPushNot Span span = spanBuilder.startSpan(); try (Scope scope = span.makeCurrent()) { - TaskPushNotificationConfig result = delegate.onCreateTaskPushNotificationConfig(params, context); + TaskPushNotificationConfig result = + delegate.onCreateTaskPushNotificationConfig(params, context); if (result != null && extractResponse()) { span.setAttribute(GENAI_RESPONSE, result.toString()); @@ -395,10 +418,14 @@ public TaskPushNotificationConfig onCreateTaskPushNotificationConfig(TaskPushNot } @Override - public TaskPushNotificationConfig onGetTaskPushNotificationConfig(GetTaskPushNotificationConfigParams params, ServerCallContext context) throws A2AError { - var spanBuilder = tracer.spanBuilder(A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD) - .setSpanKind(SpanKind.SERVER) - .setAttribute(GENAI_OPERATION_NAME, A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + public TaskPushNotificationConfig onGetTaskPushNotificationConfig( + GetTaskPushNotificationConfigParams params, ServerCallContext context) throws A2AError { + var spanBuilder = + tracer.spanBuilder(A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD) + .setSpanKind(SpanKind.SERVER) + .setAttribute( + GENAI_OPERATION_NAME, + A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); if (params.taskId() != null) { spanBuilder.setAttribute(GENAI_TASK_ID, params.taskId()); @@ -414,7 +441,8 @@ public TaskPushNotificationConfig onGetTaskPushNotificationConfig(GetTaskPushNot Span span = spanBuilder.startSpan(); try (Scope scope = span.makeCurrent()) { - TaskPushNotificationConfig result = delegate.onGetTaskPushNotificationConfig(params, context); + TaskPushNotificationConfig result = + delegate.onGetTaskPushNotificationConfig(params, context); if (result != null && extractResponse()) { span.setAttribute(GENAI_RESPONSE, result.toString()); @@ -432,10 +460,12 @@ public TaskPushNotificationConfig onGetTaskPushNotificationConfig(GetTaskPushNot } @Override - public Flow.Publisher onSubscribeToTask(TaskIdParams params, ServerCallContext context) throws A2AError { - var spanBuilder = tracer.spanBuilder(A2AMethods.SUBSCRIBE_TO_TASK_METHOD) - .setSpanKind(SpanKind.SERVER) - .setAttribute(GENAI_OPERATION_NAME, A2AMethods.SUBSCRIBE_TO_TASK_METHOD); + public Flow.Publisher onSubscribeToTask( + TaskIdParams params, ServerCallContext context) throws A2AError { + var spanBuilder = + tracer.spanBuilder(A2AMethods.SUBSCRIBE_TO_TASK_METHOD) + .setSpanKind(SpanKind.SERVER) + .setAttribute(GENAI_OPERATION_NAME, A2AMethods.SUBSCRIBE_TO_TASK_METHOD); if (params.id() != null) { spanBuilder.setAttribute(GENAI_TASK_ID, params.id()); @@ -457,12 +487,20 @@ public Flow.Publisher onSubscribeToTask(TaskIdParams params, span.setStatus(StatusCode.OK); SpanContext spanContext = span.getSpanContext(); - return new OpenTelemetryStreamPublisher(result, - new StreamingSpanConfig(tracer, A2AMethods.SUBSCRIBE_TO_TASK_METHOD, - spanContext, streamingDurationHistogram(), startNanos)); + return new OpenTelemetryStreamPublisher( + result, + new StreamingSpanConfig( + tracer, + A2AMethods.SUBSCRIBE_TO_TASK_METHOD, + spanContext, + streamingDurationHistogram(), + startNanos)); } catch (A2AError error) { - StreamingEventAttributes.recordDuration(streamingDurationHistogram(), - startNanos, A2AMethods.SUBSCRIBE_TO_TASK_METHOD, false); + StreamingEventAttributes.recordDuration( + streamingDurationHistogram(), + startNanos, + A2AMethods.SUBSCRIBE_TO_TASK_METHOD, + false); span.setAttribute(ERROR_TYPE, error.getMessage()); span.setStatus(StatusCode.ERROR, error.getMessage()); throw error; @@ -472,10 +510,15 @@ public Flow.Publisher onSubscribeToTask(TaskIdParams params, } @Override - public ListTaskPushNotificationConfigsResult onListTaskPushNotificationConfigs(ListTaskPushNotificationConfigsParams params, ServerCallContext context) throws A2AError { - var spanBuilder = tracer.spanBuilder(A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD) - .setSpanKind(SpanKind.SERVER) - .setAttribute(GENAI_OPERATION_NAME, A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + public ListTaskPushNotificationConfigsResult onListTaskPushNotificationConfigs( + ListTaskPushNotificationConfigsParams params, ServerCallContext context) + throws A2AError { + var spanBuilder = + tracer.spanBuilder(A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD) + .setSpanKind(SpanKind.SERVER) + .setAttribute( + GENAI_OPERATION_NAME, + A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); if (extractRequest()) { spanBuilder.setAttribute(GENAI_REQUEST, params.toString()); @@ -488,7 +531,8 @@ public ListTaskPushNotificationConfigsResult onListTaskPushNotificationConfigs(L Span span = spanBuilder.startSpan(); try (Scope scope = span.makeCurrent()) { - ListTaskPushNotificationConfigsResult result = delegate.onListTaskPushNotificationConfigs(params, context); + ListTaskPushNotificationConfigsResult result = + delegate.onListTaskPushNotificationConfigs(params, context); if (result != null && extractResponse()) { span.setAttribute(GENAI_RESPONSE, result.toString()); @@ -506,10 +550,15 @@ public ListTaskPushNotificationConfigsResult onListTaskPushNotificationConfigs(L } @Override - public void onDeleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigParams params, ServerCallContext context) throws A2AError { - var spanBuilder = tracer.spanBuilder(A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD) - .setSpanKind(SpanKind.SERVER) - .setAttribute(GENAI_OPERATION_NAME, A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + public void onDeleteTaskPushNotificationConfig( + DeleteTaskPushNotificationConfigParams params, ServerCallContext context) + throws A2AError { + var spanBuilder = + tracer.spanBuilder(A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD) + .setSpanKind(SpanKind.SERVER) + .setAttribute( + GENAI_OPERATION_NAME, + A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); if (extractRequest()) { spanBuilder.setAttribute(GENAI_REQUEST, params.toString()); @@ -532,8 +581,9 @@ public void onDeleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigP } @Override - public void authorizeTaskAccess(@Nullable String requestedTaskId, ServerCallContext context, - TaskOperation operation) throws A2AError { + public void authorizeTaskAccess( + @Nullable String requestedTaskId, ServerCallContext context, TaskOperation operation) + throws A2AError { delegate.authorizeTaskAccess(requestedTaskId, context, operation); } @@ -545,7 +595,8 @@ private boolean extractResponse() { return Boolean.getBoolean(EXTRACT_RESPONSE_SYS_PROPERTY); } - private static void applyTransportAttributes(SpanBuilder spanBuilder, ServerCallContext context) { + private static void applyTransportAttributes( + SpanBuilder spanBuilder, ServerCallContext context) { Map state = context.getState(); Object transport = state.get(ServerCallContext.TRANSPORT_KEY); if (transport instanceof TransportProtocol tp) { @@ -557,15 +608,21 @@ private static void applyTransportAttributes(SpanBuilder spanBuilder, ServerCall } } - private record StreamingSpanConfig(Tracer tracer, String spanName, SpanContext parentSpanContext, - @Nullable DoubleHistogram streamingDurationHistogram, long startNanos) {} + private record StreamingSpanConfig( + Tracer tracer, + String spanName, + SpanContext parentSpanContext, + @Nullable DoubleHistogram streamingDurationHistogram, + long startNanos) {} - private static class OpenTelemetryStreamPublisher implements Flow.Publisher { + private static class OpenTelemetryStreamPublisher + implements Flow.Publisher { private final Flow.Publisher delegate; private final StreamingSpanConfig config; - OpenTelemetryStreamPublisher(Flow.Publisher delegate, StreamingSpanConfig config) { + OpenTelemetryStreamPublisher( + Flow.Publisher delegate, StreamingSpanConfig config) { this.delegate = delegate; this.config = config; } @@ -576,52 +633,61 @@ public void subscribe(Flow.Subscriber subscriber) { } } - private static class OpenTelemetryStreamSubscriber implements Flow.Subscriber { + private static class OpenTelemetryStreamSubscriber + implements Flow.Subscriber { - private static final Logger LOGGER = Logger.getLogger(OpenTelemetryStreamSubscriber.class.getName()); + private static final Logger LOGGER = + Logger.getLogger(OpenTelemetryStreamSubscriber.class.getName()); private final Flow.Subscriber delegate; private final StreamingSpanConfig config; private final AtomicBoolean terminated = new AtomicBoolean(false); - OpenTelemetryStreamSubscriber(Flow.Subscriber delegate, StreamingSpanConfig config) { + OpenTelemetryStreamSubscriber( + Flow.Subscriber delegate, StreamingSpanConfig config) { this.delegate = delegate; this.config = config; } @Override public void onSubscribe(Flow.Subscription subscription) { - delegate.onSubscribe(new Flow.Subscription() { - @Override - public void request(long n) { - subscription.request(n); - } + delegate.onSubscribe( + new Flow.Subscription() { + @Override + public void request(long n) { + subscription.request(n); + } - @Override - public void cancel() { - if (terminated.compareAndSet(false, true)) { - recordStreamingDuration(true); - Span closingSpan = config.tracer().spanBuilder(config.spanName() + STREAMING_END_SUFFIX) - .setSpanKind(SpanKind.SERVER) - .addLink(config.parentSpanContext()) - .startSpan(); - try (Scope ignored = closingSpan.makeCurrent()) { - closingSpan.setStatus(StatusCode.OK); - } finally { - closingSpan.end(); + @Override + public void cancel() { + if (terminated.compareAndSet(false, true)) { + recordStreamingDuration(true); + Span closingSpan = + config.tracer() + .spanBuilder( + config.spanName() + STREAMING_END_SUFFIX) + .setSpanKind(SpanKind.SERVER) + .addLink(config.parentSpanContext()) + .startSpan(); + try (Scope ignored = closingSpan.makeCurrent()) { + closingSpan.setStatus(StatusCode.OK); + } finally { + closingSpan.end(); + } + } + subscription.cancel(); } - } - subscription.cancel(); - } - }); + }); } @Override public void onNext(StreamingEventKind item) { - SpanBuilder builder = config.tracer().spanBuilder(config.spanName() + STREAMING_EVENT_SUFFIX) - .setSpanKind(SpanKind.SERVER) - .addLink(config.parentSpanContext()) - .setAttribute(GENAI_STREAMING_EVENT, item.kind()); + SpanBuilder builder = + config.tracer() + .spanBuilder(config.spanName() + STREAMING_EVENT_SUFFIX) + .setSpanKind(SpanKind.SERVER) + .addLink(config.parentSpanContext()) + .setAttribute(GENAI_STREAMING_EVENT, item.kind()); StreamingEventAttributes.apply(builder, item); Span span = builder.startSpan(); try (Scope ignored = span.makeCurrent()) { @@ -639,14 +705,19 @@ public void onNext(StreamingEventKind item) { @Override public void onError(Throwable throwable) { if (!terminated.compareAndSet(false, true)) { - LOGGER.warning("onError() received after stream termination (publisher protocol violation); ignoring: " + throwable); + LOGGER.warning( + "onError() received after stream termination (publisher protocol" + + " violation); ignoring: " + + throwable); return; } recordStreamingDuration(false); - Span closingSpan = config.tracer().spanBuilder(config.spanName() + STREAMING_END_SUFFIX) - .setSpanKind(SpanKind.SERVER) - .addLink(config.parentSpanContext()) - .startSpan(); + Span closingSpan = + config.tracer() + .spanBuilder(config.spanName() + STREAMING_END_SUFFIX) + .setSpanKind(SpanKind.SERVER) + .addLink(config.parentSpanContext()) + .startSpan(); try (Scope ignored = closingSpan.makeCurrent()) { closingSpan.setAttribute(ERROR_TYPE, throwable.getClass().getName()); closingSpan.setStatus(StatusCode.ERROR, throwable.getMessage()); @@ -665,10 +736,12 @@ public void onComplete() { return; } recordStreamingDuration(true); - Span closingSpan = config.tracer().spanBuilder(config.spanName() + STREAMING_END_SUFFIX) - .setSpanKind(SpanKind.SERVER) - .addLink(config.parentSpanContext()) - .startSpan(); + Span closingSpan = + config.tracer() + .spanBuilder(config.spanName() + STREAMING_END_SUFFIX) + .setSpanKind(SpanKind.SERVER) + .addLink(config.parentSpanContext()) + .startSpan(); try (Scope ignored = closingSpan.makeCurrent()) { delegate.onComplete(); closingSpan.setStatus(StatusCode.OK); @@ -682,8 +755,11 @@ public void onComplete() { } private void recordStreamingDuration(boolean success) { - StreamingEventAttributes.recordDuration(config.streamingDurationHistogram(), - config.startNanos(), config.spanName(), success); + StreamingEventAttributes.recordDuration( + config.streamingDurationHistogram(), + config.startNanos(), + config.spanName(), + success); } } } diff --git a/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/package-info.java b/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/package-info.java index 5d3e4090a..98a7a43b4 100644 --- a/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/package-info.java +++ b/extras/opentelemetry/server/src/main/java/org/a2aproject/sdk/extras/opentelemetry/server/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.extras.opentelemetry.server; import org.jspecify.annotations.NullMarked; - diff --git a/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryA2AHttpClientTest.java b/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryA2AHttpClientTest.java index b965be187..7a244d27f 100644 --- a/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryA2AHttpClientTest.java +++ b/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryA2AHttpClientTest.java @@ -8,15 +8,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.io.IOException; -import java.util.Map; - import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.context.propagation.TextMapSetter; +import java.io.IOException; +import java.util.Map; import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.junit.jupiter.api.BeforeEach; @@ -32,29 +31,21 @@ @MockitoSettings(strictness = Strictness.LENIENT) class OpenTelemetryA2AHttpClientTest { - @Mock - private A2AHttpClient delegate; + @Mock private A2AHttpClient delegate; - @Mock - private OpenTelemetry openTelemetry; + @Mock private OpenTelemetry openTelemetry; - @Mock - private ContextPropagators propagators; + @Mock private ContextPropagators propagators; - @Mock - private TextMapPropagator textMapPropagator; + @Mock private TextMapPropagator textMapPropagator; - @Mock - private A2AHttpClient.PostBuilder delegatePostBuilder; + @Mock private A2AHttpClient.PostBuilder delegatePostBuilder; - @Mock - private A2AHttpClient.GetBuilder delegateGetBuilder; + @Mock private A2AHttpClient.GetBuilder delegateGetBuilder; - @Mock - private A2AHttpClient.DeleteBuilder delegateDeleteBuilder; + @Mock private A2AHttpClient.DeleteBuilder delegateDeleteBuilder; - @Mock - private A2AHttpResponse response; + @Mock private A2AHttpResponse response; private OpenTelemetryA2AHttpClient client; @@ -69,7 +60,8 @@ void setUp() { stubBuilderDefaults(delegatePostBuilder); when(delegatePostBuilder.body(any())).thenReturn(delegatePostBuilder); - when(delegatePostBuilder.followRedirects(any(boolean.class))).thenReturn(delegatePostBuilder); + when(delegatePostBuilder.followRedirects(any(boolean.class))) + .thenReturn(delegatePostBuilder); stubBuilderDefaults(delegateGetBuilder); stubBuilderDefaults(delegateDeleteBuilder); @@ -89,11 +81,11 @@ void post_injectsContextBeforeDelegating() throws IOException, InterruptedExcept when(delegatePostBuilder.post()).thenReturn(response); A2AHttpClient.PostBuilder builder = client.createPost(); - A2AHttpResponse result = builder - .url("http://example.com/push") - .addHeader("Content-Type", "application/json") - .body("{}") - .post(); + A2AHttpResponse result = + builder.url("http://example.com/push") + .addHeader("Content-Type", "application/json") + .body("{}") + .post(); assertSame(response, result); @@ -107,12 +99,10 @@ void post_injectsContextBeforeDelegating() throws IOException, InterruptedExcept void post_propagatesCurrentSpanContext() throws IOException, InterruptedException { when(delegatePostBuilder.post()).thenReturn(response); - Context testContext = Context.current().with(io.opentelemetry.context.ContextKey.named("test"), "value"); + Context testContext = + Context.current().with(io.opentelemetry.context.ContextKey.named("test"), "value"); try (Scope ignored = testContext.makeCurrent()) { - client.createPost() - .url("http://example.com/push") - .body("{}") - .post(); + client.createPost().url("http://example.com/push").body("{}").post(); } verify(textMapPropagator).inject(any(Context.class), any(), any(TextMapSetter.class)); @@ -124,9 +114,7 @@ void get_injectsContextBeforeDelegating() throws IOException, InterruptedExcepti when(delegateGetBuilder.get()).thenReturn(response); A2AHttpClient.GetBuilder builder = client.createGet(); - A2AHttpResponse result = builder - .url("http://example.com/api") - .get(); + A2AHttpResponse result = builder.url("http://example.com/api").get(); assertSame(response, result); @@ -141,9 +129,7 @@ void delete_injectsContextBeforeDelegating() throws IOException, InterruptedExce when(delegateDeleteBuilder.delete()).thenReturn(response); A2AHttpClient.DeleteBuilder builder = client.createDelete(); - A2AHttpResponse result = builder - .url("http://example.com/resource") - .delete(); + A2AHttpResponse result = builder.url("http://example.com/resource").delete(); assertSame(response, result); @@ -156,11 +142,11 @@ void delete_injectsContextBeforeDelegating() throws IOException, InterruptedExce void postBuilder_chainsCorrectly() { A2AHttpClient.PostBuilder builder = client.createPost(); - A2AHttpClient.PostBuilder chained = builder - .url("http://example.com") - .addHeader("X-Custom", "value") - .body("{}") - .followRedirects(false); + A2AHttpClient.PostBuilder chained = + builder.url("http://example.com") + .addHeader("X-Custom", "value") + .body("{}") + .followRedirects(false); assertNotNull(chained); } @@ -175,20 +161,21 @@ void getBuilder_delegatesRedirectFollowing() { @Test @SuppressWarnings("unchecked") - void contextInjection_propagatesHeadersViaAddHeaders() throws IOException, InterruptedException { + void contextInjection_propagatesHeadersViaAddHeaders() + throws IOException, InterruptedException { when(delegatePostBuilder.post()).thenReturn(response); - doAnswer(invocation -> { - Map carrier = invocation.getArgument(1); - TextMapSetter> setter = invocation.getArgument(2); - setter.set(carrier, "traceparent", "00-abc-def-01"); - return null; - }).when(textMapPropagator).inject(any(Context.class), any(), any(TextMapSetter.class)); - - client.createPost() - .url("http://example.com") - .body("{}") - .post(); + doAnswer( + invocation -> { + Map carrier = invocation.getArgument(1); + TextMapSetter> setter = invocation.getArgument(2); + setter.set(carrier, "traceparent", "00-abc-def-01"); + return null; + }) + .when(textMapPropagator) + .inject(any(Context.class), any(), any(TextMapSetter.class)); + + client.createPost().url("http://example.com").body("{}").post(); verify(delegatePostBuilder).addHeaders(Map.of("traceparent", "00-abc-def-01")); } diff --git a/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryHttpClientProducerTest.java b/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryHttpClientProducerTest.java index 304380012..a49c709e2 100644 --- a/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryHttpClientProducerTest.java +++ b/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryHttpClientProducerTest.java @@ -36,7 +36,9 @@ void createPropagatingClient_noUserBean_fallsBackToFactory() { A2AHttpClient client = producer.createPropagatingClient(emptyInstance); assertNotNull(client); - assertInstanceOf(OpenTelemetryA2AHttpClient.class, client, + assertInstanceOf( + OpenTelemetryA2AHttpClient.class, + client, "Producer must wrap a factory-created delegate in OpenTelemetryA2AHttpClient"); } @@ -58,7 +60,9 @@ void createPropagatingClient_withUserBean_wrapsUserClient() { A2AHttpClient client = producer.createPropagatingClient(userInstance); assertNotNull(client); - assertInstanceOf(OpenTelemetryA2AHttpClient.class, client, + assertInstanceOf( + OpenTelemetryA2AHttpClient.class, + client, "Producer must wrap the user's client in OpenTelemetryA2AHttpClient"); verify(userInstance).get(); } @@ -88,10 +92,11 @@ void createContextPropagatingExecutor_withUserBean_wrapsUserExecutor() { OpenTelemetryHttpClientProducer producer = new OpenTelemetryHttpClientProducer(mockOTel); AtomicInteger executionCount = new AtomicInteger(); - Executor userExecutor = command -> { - executionCount.incrementAndGet(); - command.run(); - }; + Executor userExecutor = + command -> { + executionCount.incrementAndGet(); + command.run(); + }; Instance userInstance = mock(Instance.class); when(userInstance.isResolvable()).thenReturn(true); when(userInstance.get()).thenReturn(userExecutor); @@ -102,7 +107,9 @@ void createContextPropagatingExecutor_withUserBean_wrapsUserExecutor() { verify(userInstance).get(); wrappedExecutor.execute(() -> {}); - assertEquals(1, executionCount.get(), + assertEquals( + 1, + executionCount.get(), "Wrapped executor must delegate to the user-provided executor"); } } diff --git a/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryPushNotificationSenderDecoratorTest.java b/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryPushNotificationSenderDecoratorTest.java index 1596c4882..ff26cded7 100644 --- a/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryPushNotificationSenderDecoratorTest.java +++ b/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryPushNotificationSenderDecoratorTest.java @@ -41,20 +41,15 @@ @ExtendWith(MockitoExtension.class) class OpenTelemetryPushNotificationSenderDecoratorTest { - @Mock - private PushNotificationSender delegate; + @Mock private PushNotificationSender delegate; - @Mock - private Tracer tracer; + @Mock private Tracer tracer; - @Mock - private SpanBuilder spanBuilder; + @Mock private SpanBuilder spanBuilder; - @Mock - private Span span; + @Mock private Span span; - @Mock - private Scope scope; + @Mock private Scope scope; private OpenTelemetryPushNotificationSenderDecorator decorator; @@ -72,18 +67,20 @@ void setUp() { @Test void sendNotification_createsSpanWithTaskIdFromSnapshot() { when(delegate.sendNotification(any(), any())).thenReturn(true); - Task taskSnapshot = Task.builder() - .id("task-1") - .contextId("ctx-1") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId("task-1") - .contextId("ctx-1") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + Task taskSnapshot = + Task.builder() + .id("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId("task-1") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); decorator.sendNotification(event, taskSnapshot); @@ -100,12 +97,14 @@ void sendNotification_createsSpanWithTaskIdFromSnapshot() { @Test void sendNotification_createsSpanWithTaskIdFromEvent() { when(delegate.sendNotification(any(), any())).thenReturn(true); - org.a2aproject.sdk.spec.Artifact artifact = org.a2aproject.sdk.spec.Artifact.builder() - .artifactId("artifact-id") - .parts(List.of(new org.a2aproject.sdk.spec.TextPart("result"))) - .build(); - TaskArtifactUpdateEvent event = new TaskArtifactUpdateEvent( - "task-event-1", artifact, "ctx-event-1", null, null, null); + org.a2aproject.sdk.spec.Artifact artifact = + org.a2aproject.sdk.spec.Artifact.builder() + .artifactId("artifact-id") + .parts(List.of(new org.a2aproject.sdk.spec.TextPart("result"))) + .build(); + TaskArtifactUpdateEvent event = + new TaskArtifactUpdateEvent( + "task-event-1", artifact, "ctx-event-1", null, null, null); decorator.sendNotification(event, null); @@ -118,11 +117,12 @@ void sendNotification_createsSpanWithTaskIdFromEvent() { @Test void sendNotification_setsErrorStatusOnDeliveryFailure() { - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId("task-fail") - .contextId("ctx-fail") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId("task-fail") + .contextId("ctx-fail") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); when(delegate.sendNotification(any(), any())).thenReturn(false); boolean result = decorator.sendNotification(event, null); @@ -135,11 +135,12 @@ void sendNotification_setsErrorStatusOnDeliveryFailure() { @Test void sendNotification_setsErrorStatusOnException() { - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId("task-err") - .contextId("ctx-err") - .status(new TaskStatus(TaskState.TASK_STATE_FAILED)) - .build(); + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId("task-err") + .contextId("ctx-err") + .status(new TaskStatus(TaskState.TASK_STATE_FAILED)) + .build(); RuntimeException ex = new RuntimeException("webhook unreachable"); doThrow(ex).when(delegate).sendNotification(any(), any()); @@ -153,11 +154,12 @@ void sendNotification_setsErrorStatusOnException() { @Test void sendNotification_setsEventKindAttribute() { when(delegate.sendNotification(any(), any())).thenReturn(true); - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId("task-kind") - .contextId("ctx-kind") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId("task-kind") + .contextId("ctx-kind") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); decorator.sendNotification(event, null); @@ -169,11 +171,12 @@ void sendNotification_extractsResponseWhenEnabled() { when(delegate.sendNotification(any(), any())).thenReturn(true); OpenTelemetryPushNotificationSenderDecorator decoratorWithExtract = new TestablePushNotificationSenderDecorator(delegate, tracer, true); - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId("task-resp") - .contextId("ctx-resp") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId("task-resp") + .contextId("ctx-resp") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); decoratorWithExtract.sendNotification(event, null); @@ -183,11 +186,12 @@ void sendNotification_extractsResponseWhenEnabled() { @Test void sendNotification_noResponseAttributeWhenDisabled() { when(delegate.sendNotification(any(), any())).thenReturn(true); - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId("task-noresp") - .contextId("ctx-noresp") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId("task-noresp") + .contextId("ctx-noresp") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); // decorator is constructed with extractResponse=false (setUp default) decorator.sendNotification(event, null); @@ -195,13 +199,14 @@ void sendNotification_noResponseAttributeWhenDisabled() { verify(spanBuilder, never()).setAttribute(eq(GENAI_RESPONSE), anyString()); } - static class TestablePushNotificationSenderDecorator extends OpenTelemetryPushNotificationSenderDecorator { + static class TestablePushNotificationSenderDecorator + extends OpenTelemetryPushNotificationSenderDecorator { TestablePushNotificationSenderDecorator(PushNotificationSender delegate, Tracer tracer) { super(delegate, tracer, false); } - TestablePushNotificationSenderDecorator(PushNotificationSender delegate, Tracer tracer, - boolean extractResponse) { + TestablePushNotificationSenderDecorator( + PushNotificationSender delegate, Tracer tracer, boolean extractResponse) { super(delegate, tracer, extractResponse); } } diff --git a/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryRequestHandlerDecoratorTest.java b/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryRequestHandlerDecoratorTest.java index 8aeb5e3bb..08d68ca23 100644 --- a/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryRequestHandlerDecoratorTest.java +++ b/extras/opentelemetry/server/src/test/java/org/a2aproject/sdk/extras/opentelemetry/server/OpenTelemetryRequestHandlerDecoratorTest.java @@ -3,8 +3,10 @@ import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.ERROR_TYPE; import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.EXTRACT_REQUEST_SYS_PROPERTY; import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.EXTRACT_RESPONSE_SYS_PROPERTY; +import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.GENAI_PROTOCOL_VERSION; import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.GENAI_REQUEST; import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.GENAI_RESPONSE; +import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.GENAI_TRANSPORT; import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.STREAMING_END_SUFFIX; import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.STREAMING_EVENT_SUFFIX; import static org.a2aproject.sdk.extras.opentelemetry.OtelSpanMocks.mockClosingSpanBuilder; @@ -75,41 +77,28 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.GENAI_PROTOCOL_VERSION; -import static org.a2aproject.sdk.extras.opentelemetry.A2AObservabilityNames.GENAI_TRANSPORT; - @ExtendWith(MockitoExtension.class) class OpenTelemetryRequestHandlerDecoratorTest { - @Mock - private Tracer tracer; + @Mock private Tracer tracer; - @Mock - private Span span; + @Mock private Span span; - @Mock - private SpanBuilder spanBuilder; + @Mock private SpanBuilder spanBuilder; - @Mock - private Scope scope; + @Mock private Scope scope; - @Mock - private SpanContext spanContext; + @Mock private SpanContext spanContext; - @Mock - private ServerCallContext context; + @Mock private ServerCallContext context; - @Mock - private RequestHandler delegate; + @Mock private RequestHandler delegate; - @Mock - private Meter meter; + @Mock private Meter meter; - @Mock - private DoubleHistogramBuilder histogramBuilder; + @Mock private DoubleHistogramBuilder histogramBuilder; - @Mock - private DoubleHistogram streamingHistogram; + @Mock private DoubleHistogram streamingHistogram; private TestableOpenTelemetryRequestHandlerDecorator decorator; @@ -148,29 +137,32 @@ void setUp() { /** * Concrete test implementation of the abstract decorator for testing purposes. */ - static class TestableOpenTelemetryRequestHandlerDecorator extends OpenTelemetryRequestHandlerDecorator { - public TestableOpenTelemetryRequestHandlerDecorator(RequestHandler delegate, Tracer tracer) { + static class TestableOpenTelemetryRequestHandlerDecorator + extends OpenTelemetryRequestHandlerDecorator { + public TestableOpenTelemetryRequestHandlerDecorator( + RequestHandler delegate, Tracer tracer) { super(delegate, tracer); } - public TestableOpenTelemetryRequestHandlerDecorator(RequestHandler delegate, Tracer tracer, Meter meter) { + public TestableOpenTelemetryRequestHandlerDecorator( + RequestHandler delegate, Tracer tracer, Meter meter) { super(delegate, tracer, meter); } } - @Nested class GetTaskTests { @Test void onGetTask_createsSpanAndDelegatesToHandler() throws A2AError { TaskQueryParams params = new TaskQueryParams("task-123", null); - Task result = Task.builder() - .id("task-123") - .contextId("ctx-1") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + Task result = + Task.builder() + .id("task-123") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); when(delegate.onGetTask(params, context)).thenReturn(result); Task actualResult = decorator.onGetTask(params, context); @@ -205,7 +197,8 @@ void onGetTask_withError_setsErrorStatusAndRethrows() throws A2AError { class ListTasksTests { @Test void onListTasks_createsSpanAndDelegatesToHandler() throws A2AError { - ListTasksParams params = new ListTasksParams(null, null, null, null, null, null, null, "test-tenant"); + ListTasksParams params = + new ListTasksParams(null, null, null, null, null, null, null, "test-tenant"); ListTasksResult result = new ListTasksResult(Collections.emptyList(), 0, 0, null); when(delegate.onListTasks(params, context)).thenReturn(result); @@ -221,7 +214,8 @@ void onListTasks_createsSpanAndDelegatesToHandler() throws A2AError { @Test void onListTasks_withError_setsErrorStatus() throws A2AError { - ListTasksParams params = new ListTasksParams(null, null, null, null, null, null, null, "test-tenant"); + ListTasksParams params = + new ListTasksParams(null, null, null, null, null, null, null, "test-tenant"); A2AError error = new InvalidRequestError("Invalid parameters"); when(delegate.onListTasks(params, context)).thenThrow(error); @@ -238,13 +232,14 @@ class CancelTaskTests { @Test void onCancelTask_createsSpanAndDelegatesToHandler() throws A2AError { CancelTaskParams params = new CancelTaskParams("task-123"); - Task result = Task.builder() - .id("task-123") - .contextId("ctx-1") - .status(new TaskStatus(TaskState.TASK_STATE_CANCELED)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + Task result = + Task.builder() + .id("task-123") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.TASK_STATE_CANCELED)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); when(delegate.onCancelTask(params, context)).thenReturn(result); Task actualResult = decorator.onCancelTask(params, context); @@ -275,21 +270,23 @@ void onCancelTask_withError_setsErrorStatus() throws A2AError { class MessageSendTests { @Test void onMessageSend_createsSpanAndDelegatesToHandler() throws A2AError { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-123") - .contextId("ctx-1") - .taskId("task-123") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-123") + .contextId("ctx-1") + .taskId("task-123") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); - EventKind result = Task.builder() - .id("task-123") - .contextId("ctx-1") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + EventKind result = + Task.builder() + .id("task-123") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); when(delegate.onMessageSend(params, context)).thenReturn(result); EventKind actualResult = decorator.onMessageSend(params, context); @@ -303,13 +300,14 @@ void onMessageSend_createsSpanAndDelegatesToHandler() throws A2AError { @Test void onMessageSend_withError_setsErrorStatus() throws A2AError { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-123") - .contextId("ctx-1") - .taskId("task-123") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-123") + .contextId("ctx-1") + .taskId("task-123") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); A2AError error = new InvalidRequestError("Invalid message"); when(delegate.onMessageSend(params, context)).thenThrow(error); @@ -326,18 +324,20 @@ void onMessageSend_withError_setsErrorStatus() throws A2AError { class MessageSendStreamTests { @Test void onMessageSendStream_createsSpanAndWrapsPublisher() throws A2AError { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-123") - .contextId("ctx-1") - .taskId("task-123") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-123") + .contextId("ctx-1") + .taskId("task-123") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); Flow.Publisher publisher = mock(Flow.Publisher.class); when(delegate.onMessageSendStream(params, context)).thenReturn(publisher); - Flow.Publisher actualResult = decorator.onMessageSendStream(params, context); + Flow.Publisher actualResult = + decorator.onMessageSendStream(params, context); assertNotNull(actualResult); verify(tracer).spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD); @@ -349,18 +349,21 @@ void onMessageSendStream_createsSpanAndWrapsPublisher() throws A2AError { @Test void onMessageSendStream_withError_setsErrorStatus() throws A2AError { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-123") - .contextId("ctx-1") - .taskId("task-123") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-123") + .contextId("ctx-1") + .taskId("task-123") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); A2AError error = new InvalidRequestError("Stream error"); when(delegate.onMessageSendStream(params, context)).thenThrow(error); - assertThrows(InvalidRequestError.class, () -> decorator.onMessageSendStream(params, context)); + assertThrows( + InvalidRequestError.class, + () -> decorator.onMessageSendStream(params, context)); verify(span).setAttribute(ERROR_TYPE, error.getMessage()); verify(span).setStatus(StatusCode.ERROR, error.getMessage()); @@ -371,31 +374,39 @@ void onMessageSendStream_withError_setsErrorStatus() throws A2AError { @Test @SuppressWarnings("unchecked") void onMessageSendStream_closingSpan_createdOnComplete() throws A2AError { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-123") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-123") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); Flow.Publisher publisher = mock(Flow.Publisher.class); when(delegate.onMessageSendStream(params, context)).thenReturn(publisher); Span closingSpan = mock(Span.class); - SpanBuilder closingSpanBuilder = mockClosingSpanBuilder(tracer, - A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, closingSpan); + SpanBuilder closingSpanBuilder = + mockClosingSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, + closingSpan); - Flow.Publisher result = decorator.onMessageSendStream(params, context); + Flow.Publisher result = + decorator.onMessageSendStream(params, context); Flow.Subscriber testSubscriber = mock(Flow.Subscriber.class); result.subscribe(testSubscriber); - ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(Flow.Subscriber.class); + ArgumentCaptor subscriberCaptor = + ArgumentCaptor.forClass(Flow.Subscriber.class); verify(publisher).subscribe(subscriberCaptor.capture()); - Flow.Subscriber otelSubscriber = (Flow.Subscriber) subscriberCaptor.getValue(); + Flow.Subscriber otelSubscriber = + (Flow.Subscriber) subscriberCaptor.getValue(); otelSubscriber.onComplete(); - verify(tracer).spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX); + verify(tracer) + .spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX); verify(closingSpanBuilder).addLink(spanContext); verify(closingSpan).setStatus(StatusCode.OK); verify(closingSpan).end(); @@ -406,42 +417,54 @@ void onMessageSendStream_closingSpan_createdOnComplete() throws A2AError { @Test @SuppressWarnings("unchecked") void onMessageSendStream_closingSpan_recordsEventsAndEndsOnError() throws A2AError { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-123") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-123") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); Flow.Publisher publisher = mock(Flow.Publisher.class); when(delegate.onMessageSendStream(params, context)).thenReturn(publisher); - StreamingEventKind eventItem = Message.builder() - .role(Message.Role.ROLE_AGENT) - .parts(List.of(new TextPart("event-data"))) - .messageId("event-msg-1") - .build(); + StreamingEventKind eventItem = + Message.builder() + .role(Message.Role.ROLE_AGENT) + .parts(List.of(new TextPart("event-data"))) + .messageId("event-msg-1") + .build(); Span eventSpan = mock(Span.class); - SpanBuilder eventSpanBuilder = mockEventSpanBuilder(tracer, - A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX, eventSpan); + SpanBuilder eventSpanBuilder = + mockEventSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX, + eventSpan); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, + closingSpan); - Flow.Publisher result = decorator.onMessageSendStream(params, context); + Flow.Publisher result = + decorator.onMessageSendStream(params, context); Flow.Subscriber testSubscriber = mock(Flow.Subscriber.class); result.subscribe(testSubscriber); - ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(Flow.Subscriber.class); + ArgumentCaptor subscriberCaptor = + ArgumentCaptor.forClass(Flow.Subscriber.class); verify(publisher).subscribe(subscriberCaptor.capture()); - Flow.Subscriber otelSubscriber = (Flow.Subscriber) subscriberCaptor.getValue(); + Flow.Subscriber otelSubscriber = + (Flow.Subscriber) subscriberCaptor.getValue(); otelSubscriber.onNext(eventItem); Throwable error = new RuntimeException("stream failure"); otelSubscriber.onError(error); verify(testSubscriber).onNext(eventItem); - verify(tracer).spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX); + verify(tracer) + .spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX); verify(eventSpanBuilder).setSpanKind(SpanKind.SERVER); verify(eventSpan).end(); verify(closingSpan).setAttribute(ERROR_TYPE, RuntimeException.class.getName()); @@ -454,39 +477,50 @@ void onMessageSendStream_closingSpan_recordsEventsAndEndsOnError() throws A2AErr @Test @SuppressWarnings("unchecked") void onMessageSendStream_closingSpan_createdOnSubscriptionCancel() throws A2AError { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-123") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-123") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); Flow.Publisher publisher = mock(Flow.Publisher.class); when(delegate.onMessageSendStream(params, context)).thenReturn(publisher); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, + closingSpan); - Flow.Publisher result = decorator.onMessageSendStream(params, context); + Flow.Publisher result = + decorator.onMessageSendStream(params, context); Flow.Subscriber testSubscriber = mock(Flow.Subscriber.class); result.subscribe(testSubscriber); - ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(Flow.Subscriber.class); + ArgumentCaptor subscriberCaptor = + ArgumentCaptor.forClass(Flow.Subscriber.class); verify(publisher).subscribe(subscriberCaptor.capture()); - Flow.Subscriber otelSubscriber = (Flow.Subscriber) subscriberCaptor.getValue(); + Flow.Subscriber otelSubscriber = + (Flow.Subscriber) subscriberCaptor.getValue(); - // Simulate onSubscribe — the OTel subscriber wraps the subscription before passing it down + // Simulate onSubscribe — the OTel subscriber wraps the subscription before passing it + // down Flow.Subscription rawSubscription = mock(Flow.Subscription.class); otelSubscriber.onSubscribe(rawSubscription); // Capture the wrapped subscription that was forwarded to the downstream subscriber - ArgumentCaptor subscriptionCaptor = ArgumentCaptor.forClass(Flow.Subscription.class); + ArgumentCaptor subscriptionCaptor = + ArgumentCaptor.forClass(Flow.Subscription.class); verify(testSubscriber).onSubscribe(subscriptionCaptor.capture()); - // Cancel via the wrapped subscription — this must produce the terminal span and histogram + // Cancel via the wrapped subscription — this must produce the terminal span and + // histogram subscriptionCaptor.getValue().cancel(); - verify(tracer).spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX); + verify(tracer) + .spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX); verify(closingSpan).setStatus(StatusCode.OK); verify(closingSpan).end(); verify(streamingHistogram).record(anyDouble(), any(Attributes.class)); @@ -497,31 +531,39 @@ void onMessageSendStream_closingSpan_createdOnSubscriptionCancel() throws A2AErr @Test @SuppressWarnings("unchecked") void onMessageSendStream_closingSpan_cancelledTwiceOnlyEmitsOneSpan() throws A2AError { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-123") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-123") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); Flow.Publisher publisher = mock(Flow.Publisher.class); when(delegate.onMessageSendStream(params, context)).thenReturn(publisher); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, + closingSpan); - Flow.Publisher result = decorator.onMessageSendStream(params, context); + Flow.Publisher result = + decorator.onMessageSendStream(params, context); Flow.Subscriber testSubscriber = mock(Flow.Subscriber.class); result.subscribe(testSubscriber); - ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(Flow.Subscriber.class); + ArgumentCaptor subscriberCaptor = + ArgumentCaptor.forClass(Flow.Subscriber.class); verify(publisher).subscribe(subscriberCaptor.capture()); - Flow.Subscriber otelSubscriber = (Flow.Subscriber) subscriberCaptor.getValue(); + Flow.Subscriber otelSubscriber = + (Flow.Subscriber) subscriberCaptor.getValue(); Flow.Subscription rawSubscription = mock(Flow.Subscription.class); otelSubscriber.onSubscribe(rawSubscription); - ArgumentCaptor subscriptionCaptor = ArgumentCaptor.forClass(Flow.Subscription.class); + ArgumentCaptor subscriptionCaptor = + ArgumentCaptor.forClass(Flow.Subscription.class); verify(testSubscriber).onSubscribe(subscriptionCaptor.capture()); subscriptionCaptor.getValue().cancel(); @@ -536,40 +578,52 @@ void onMessageSendStream_closingSpan_cancelledTwiceOnlyEmitsOneSpan() throws A2A @Test @SuppressWarnings("unchecked") void onMessageSendStream_closingSpan_recordsMultipleEventsOnComplete() throws A2AError { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-123") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-123") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); Flow.Publisher publisher = mock(Flow.Publisher.class); when(delegate.onMessageSendStream(params, context)).thenReturn(publisher); - StreamingEventKind event1 = Message.builder() - .role(Message.Role.ROLE_AGENT) - .parts(List.of(new TextPart("first event"))) - .messageId("event-1") - .build(); - StreamingEventKind event2 = Message.builder() - .role(Message.Role.ROLE_AGENT) - .parts(List.of(new TextPart("second event"))) - .messageId("event-2") - .build(); + StreamingEventKind event1 = + Message.builder() + .role(Message.Role.ROLE_AGENT) + .parts(List.of(new TextPart("first event"))) + .messageId("event-1") + .build(); + StreamingEventKind event2 = + Message.builder() + .role(Message.Role.ROLE_AGENT) + .parts(List.of(new TextPart("second event"))) + .messageId("event-2") + .build(); Span eventSpan = mock(Span.class); - SpanBuilder eventSpanBuilder = mockEventSpanBuilder(tracer, - A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX, eventSpan); + SpanBuilder eventSpanBuilder = + mockEventSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX, + eventSpan); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, + closingSpan); - Flow.Publisher result = decorator.onMessageSendStream(params, context); + Flow.Publisher result = + decorator.onMessageSendStream(params, context); Flow.Subscriber testSubscriber = mock(Flow.Subscriber.class); result.subscribe(testSubscriber); - ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(Flow.Subscriber.class); + ArgumentCaptor subscriberCaptor = + ArgumentCaptor.forClass(Flow.Subscriber.class); verify(publisher).subscribe(subscriberCaptor.capture()); - Flow.Subscriber otelSubscriber = (Flow.Subscriber) subscriberCaptor.getValue(); + Flow.Subscriber otelSubscriber = + (Flow.Subscriber) subscriberCaptor.getValue(); otelSubscriber.onNext(event1); otelSubscriber.onNext(event2); @@ -577,7 +631,8 @@ void onMessageSendStream_closingSpan_recordsMultipleEventsOnComplete() throws A2 verify(testSubscriber).onNext(event1); verify(testSubscriber).onNext(event2); - verify(tracer, times(2)).spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX); + verify(tracer, times(2)) + .spanBuilder(A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_EVENT_SUFFIX); verify(eventSpanBuilder, times(2)).setSpanKind(SpanKind.SERVER); verify(eventSpan, times(2)).end(); verify(closingSpan).setStatus(StatusCode.OK); @@ -588,27 +643,36 @@ void onMessageSendStream_closingSpan_recordsMultipleEventsOnComplete() throws A2 @Test @SuppressWarnings("unchecked") - void onMessageSendStream_closingSpan_delegateThrowOnComplete_recordsExceptionAndSetsErrorStatus() throws A2AError { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-ex") - .build(); + void + onMessageSendStream_closingSpan_delegateThrowOnComplete_recordsExceptionAndSetsErrorStatus() + throws A2AError { + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-ex") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); Flow.Publisher publisher = mock(Flow.Publisher.class); when(delegate.onMessageSendStream(params, context)).thenReturn(publisher); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, + closingSpan); - Flow.Publisher result = decorator.onMessageSendStream(params, context); + Flow.Publisher result = + decorator.onMessageSendStream(params, context); Flow.Subscriber testSubscriber = mock(Flow.Subscriber.class); result.subscribe(testSubscriber); - ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(Flow.Subscriber.class); + ArgumentCaptor subscriberCaptor = + ArgumentCaptor.forClass(Flow.Subscriber.class); verify(publisher).subscribe(subscriberCaptor.capture()); - Flow.Subscriber otelSubscriber = (Flow.Subscriber) subscriberCaptor.getValue(); + Flow.Subscriber otelSubscriber = + (Flow.Subscriber) subscriberCaptor.getValue(); RuntimeException delegateError = new RuntimeException("delegate onComplete failure"); doThrow(delegateError).when(testSubscriber).onComplete(); @@ -625,13 +689,22 @@ void onMessageSendStream_closingSpan_delegateThrowOnComplete_recordsExceptionAnd class SetTaskPushNotificationConfigTests { @Test void onSetTaskPushNotificationConfig_createsSpanAndDelegatesToHandler() throws A2AError { - TaskPushNotificationConfig params = TaskPushNotificationConfig.builder() - .id("config-1").taskId("task-123").url("http://example.com").build(); - TaskPushNotificationConfig result = TaskPushNotificationConfig.builder() - .id("config-1").taskId("task-123").url("http://example.com").build(); + TaskPushNotificationConfig params = + TaskPushNotificationConfig.builder() + .id("config-1") + .taskId("task-123") + .url("http://example.com") + .build(); + TaskPushNotificationConfig result = + TaskPushNotificationConfig.builder() + .id("config-1") + .taskId("task-123") + .url("http://example.com") + .build(); when(delegate.onCreateTaskPushNotificationConfig(params, context)).thenReturn(result); - TaskPushNotificationConfig actualResult = decorator.onCreateTaskPushNotificationConfig(params, context); + TaskPushNotificationConfig actualResult = + decorator.onCreateTaskPushNotificationConfig(params, context); assertEquals(result, actualResult); verify(tracer).spanBuilder(A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); @@ -643,12 +716,18 @@ void onSetTaskPushNotificationConfig_createsSpanAndDelegatesToHandler() throws A @Test void onSetTaskPushNotificationConfig_withError_setsErrorStatus() throws A2AError { - TaskPushNotificationConfig params = TaskPushNotificationConfig.builder() - .id("config-1").taskId("task-123").url("http://example.com").build(); + TaskPushNotificationConfig params = + TaskPushNotificationConfig.builder() + .id("config-1") + .taskId("task-123") + .url("http://example.com") + .build(); A2AError error = new InvalidRequestError("Invalid config"); when(delegate.onCreateTaskPushNotificationConfig(params, context)).thenThrow(error); - assertThrows(InvalidRequestError.class, () -> decorator.onCreateTaskPushNotificationConfig(params, context)); + assertThrows( + InvalidRequestError.class, + () -> decorator.onCreateTaskPushNotificationConfig(params, context)); verify(span).setAttribute(ERROR_TYPE, error.getMessage()); verify(span).setStatus(StatusCode.ERROR, error.getMessage()); @@ -660,12 +739,18 @@ void onSetTaskPushNotificationConfig_withError_setsErrorStatus() throws A2AError class GetTaskPushNotificationConfigTests { @Test void onGetTaskPushNotificationConfig_createsSpanAndDelegatesToHandler() throws A2AError { - GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("task-123", "config-1"); - TaskPushNotificationConfig result = TaskPushNotificationConfig.builder() - .id("config-1").taskId("task-123").url("http://example.com").build(); + GetTaskPushNotificationConfigParams params = + new GetTaskPushNotificationConfigParams("task-123", "config-1"); + TaskPushNotificationConfig result = + TaskPushNotificationConfig.builder() + .id("config-1") + .taskId("task-123") + .url("http://example.com") + .build(); when(delegate.onGetTaskPushNotificationConfig(params, context)).thenReturn(result); - TaskPushNotificationConfig actualResult = decorator.onGetTaskPushNotificationConfig(params, context); + TaskPushNotificationConfig actualResult = + decorator.onGetTaskPushNotificationConfig(params, context); assertEquals(result, actualResult); verify(tracer).spanBuilder(A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); @@ -677,11 +762,14 @@ void onGetTaskPushNotificationConfig_createsSpanAndDelegatesToHandler() throws A @Test void onGetTaskPushNotificationConfig_withError_setsErrorStatus() throws A2AError { - GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("task-123", ""); + GetTaskPushNotificationConfigParams params = + new GetTaskPushNotificationConfigParams("task-123", ""); A2AError error = new TaskNotFoundError(); when(delegate.onGetTaskPushNotificationConfig(params, context)).thenThrow(error); - assertThrows(TaskNotFoundError.class, () -> decorator.onGetTaskPushNotificationConfig(params, context)); + assertThrows( + TaskNotFoundError.class, + () -> decorator.onGetTaskPushNotificationConfig(params, context)); verify(span).setAttribute(ERROR_TYPE, error.getMessage()); verify(span).setStatus(StatusCode.ERROR, error.getMessage()); @@ -697,7 +785,8 @@ void onResubscribeToTask_createsSpanAndWrapsPublisher() throws A2AError { Flow.Publisher publisher = mock(Flow.Publisher.class); when(delegate.onSubscribeToTask(params, context)).thenReturn(publisher); - Flow.Publisher actualResult = decorator.onSubscribeToTask(params, context); + Flow.Publisher actualResult = + decorator.onSubscribeToTask(params, context); assertNotNull(actualResult); verify(tracer).spanBuilder(A2AMethods.SUBSCRIBE_TO_TASK_METHOD); @@ -713,7 +802,8 @@ void onResubscribeToTask_withError_setsErrorStatus() throws A2AError { A2AError error = new TaskNotFoundError(); when(delegate.onSubscribeToTask(params, context)).thenThrow(error); - assertThrows(TaskNotFoundError.class, () -> decorator.onSubscribeToTask(params, context)); + assertThrows( + TaskNotFoundError.class, () -> decorator.onSubscribeToTask(params, context)); verify(span).setAttribute(ERROR_TYPE, error.getMessage()); verify(span).setStatus(StatusCode.ERROR, error.getMessage()); @@ -729,17 +819,23 @@ void onResubscribeToTask_closingSpan_createdOnComplete() throws A2AError { when(delegate.onSubscribeToTask(params, context)).thenReturn(publisher); Span closingSpan = mock(Span.class); - SpanBuilder closingSpanBuilder = mockClosingSpanBuilder(tracer, - A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_END_SUFFIX, closingSpan); + SpanBuilder closingSpanBuilder = + mockClosingSpanBuilder( + tracer, + A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_END_SUFFIX, + closingSpan); - Flow.Publisher result = decorator.onSubscribeToTask(params, context); + Flow.Publisher result = + decorator.onSubscribeToTask(params, context); Flow.Subscriber testSubscriber = mock(Flow.Subscriber.class); result.subscribe(testSubscriber); - ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(Flow.Subscriber.class); + ArgumentCaptor subscriberCaptor = + ArgumentCaptor.forClass(Flow.Subscriber.class); verify(publisher).subscribe(subscriberCaptor.capture()); - Flow.Subscriber otelSubscriber = (Flow.Subscriber) subscriberCaptor.getValue(); + Flow.Subscriber otelSubscriber = + (Flow.Subscriber) subscriberCaptor.getValue(); otelSubscriber.onComplete(); @@ -757,34 +853,45 @@ void onResubscribeToTask_closingSpan_recordsEventsOnCompleteAndError() throws A2 TaskIdParams params = new TaskIdParams("task-123"); Flow.Publisher publisher = mock(Flow.Publisher.class); when(delegate.onSubscribeToTask(params, context)).thenReturn(publisher); - StreamingEventKind eventItem = Message.builder() - .role(Message.Role.ROLE_AGENT) - .parts(List.of(new TextPart("streamed-event"))) - .messageId("evt-1") - .build(); + StreamingEventKind eventItem = + Message.builder() + .role(Message.Role.ROLE_AGENT) + .parts(List.of(new TextPart("streamed-event"))) + .messageId("evt-1") + .build(); Span eventSpan = mock(Span.class); - SpanBuilder eventSpanBuilder = mockEventSpanBuilder(tracer, - A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_EVENT_SUFFIX, eventSpan); + SpanBuilder eventSpanBuilder = + mockEventSpanBuilder( + tracer, + A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_EVENT_SUFFIX, + eventSpan); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_END_SUFFIX, + closingSpan); - Flow.Publisher result = decorator.onSubscribeToTask(params, context); + Flow.Publisher result = + decorator.onSubscribeToTask(params, context); Flow.Subscriber testSubscriber = mock(Flow.Subscriber.class); result.subscribe(testSubscriber); - ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(Flow.Subscriber.class); + ArgumentCaptor subscriberCaptor = + ArgumentCaptor.forClass(Flow.Subscriber.class); verify(publisher).subscribe(subscriberCaptor.capture()); - Flow.Subscriber otelSubscriber = (Flow.Subscriber) subscriberCaptor.getValue(); + Flow.Subscriber otelSubscriber = + (Flow.Subscriber) subscriberCaptor.getValue(); otelSubscriber.onNext(eventItem); Throwable error = new RuntimeException("subscribe failure"); otelSubscriber.onError(error); verify(testSubscriber).onNext(eventItem); - verify(tracer).spanBuilder(A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_EVENT_SUFFIX); + verify(tracer) + .spanBuilder(A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_EVENT_SUFFIX); verify(eventSpanBuilder).setSpanKind(SpanKind.SERVER); verify(eventSpan).end(); verify(closingSpan).setAttribute(ERROR_TYPE, RuntimeException.class.getName()); @@ -802,26 +909,35 @@ void onResubscribeToTask_closingSpan_createdOnSubscriptionCancel() throws A2AErr when(delegate.onSubscribeToTask(params, context)).thenReturn(publisher); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_END_SUFFIX, + closingSpan); - Flow.Publisher result = decorator.onSubscribeToTask(params, context); + Flow.Publisher result = + decorator.onSubscribeToTask(params, context); Flow.Subscriber testSubscriber = mock(Flow.Subscriber.class); result.subscribe(testSubscriber); - ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(Flow.Subscriber.class); + ArgumentCaptor subscriberCaptor = + ArgumentCaptor.forClass(Flow.Subscriber.class); verify(publisher).subscribe(subscriberCaptor.capture()); - Flow.Subscriber otelSubscriber = (Flow.Subscriber) subscriberCaptor.getValue(); + Flow.Subscriber otelSubscriber = + (Flow.Subscriber) subscriberCaptor.getValue(); - // Simulate onSubscribe — the OTel subscriber wraps the subscription before passing it down + // Simulate onSubscribe — the OTel subscriber wraps the subscription before passing it + // down Flow.Subscription rawSubscription = mock(Flow.Subscription.class); otelSubscriber.onSubscribe(rawSubscription); // Capture the wrapped subscription forwarded to the downstream subscriber - ArgumentCaptor subscriptionCaptor = ArgumentCaptor.forClass(Flow.Subscription.class); + ArgumentCaptor subscriptionCaptor = + ArgumentCaptor.forClass(Flow.Subscription.class); verify(testSubscriber).onSubscribe(subscriptionCaptor.capture()); - // Cancel via the wrapped subscription — this must produce the terminal span and histogram + // Cancel via the wrapped subscription — this must produce the terminal span and + // histogram subscriptionCaptor.getValue().cancel(); verify(tracer).spanBuilder(A2AMethods.SUBSCRIBE_TO_TASK_METHOD + STREAMING_END_SUFFIX); @@ -836,11 +952,14 @@ void onResubscribeToTask_closingSpan_createdOnSubscriptionCancel() throws A2AErr class ListTaskPushNotificationConfigsTests { @Test void onListTaskPushNotificationConfigs_createsSpanAndDelegatesToHandler() throws A2AError { - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams("task-123"); - ListTaskPushNotificationConfigsResult result = new ListTaskPushNotificationConfigsResult(Collections.emptyList(), null); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams("task-123"); + ListTaskPushNotificationConfigsResult result = + new ListTaskPushNotificationConfigsResult(Collections.emptyList(), null); when(delegate.onListTaskPushNotificationConfigs(params, context)).thenReturn(result); - ListTaskPushNotificationConfigsResult actualResult = decorator.onListTaskPushNotificationConfigs(params, context); + ListTaskPushNotificationConfigsResult actualResult = + decorator.onListTaskPushNotificationConfigs(params, context); assertEquals(result, actualResult); verify(tracer).spanBuilder(A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); @@ -852,11 +971,14 @@ void onListTaskPushNotificationConfigs_createsSpanAndDelegatesToHandler() throws @Test void onListTaskPushNotificationConfigs_withError_setsErrorStatus() throws A2AError { - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams("task-123"); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams("task-123"); A2AError error = new InvalidRequestError("Invalid request"); when(delegate.onListTaskPushNotificationConfigs(params, context)).thenThrow(error); - assertThrows(InvalidRequestError.class, () -> decorator.onListTaskPushNotificationConfigs(params, context)); + assertThrows( + InvalidRequestError.class, + () -> decorator.onListTaskPushNotificationConfigs(params, context)); verify(span).setAttribute(ERROR_TYPE, error.getMessage()); verify(span).setStatus(StatusCode.ERROR, error.getMessage()); @@ -868,7 +990,8 @@ void onListTaskPushNotificationConfigs_withError_setsErrorStatus() throws A2AErr class DeleteTaskPushNotificationConfigTests { @Test void onDeleteTaskPushNotificationConfig_createsSpanAndDelegatesToHandler() throws A2AError { - DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams("task-123", "config-123"); + DeleteTaskPushNotificationConfigParams params = + new DeleteTaskPushNotificationConfigParams("task-123", "config-123"); doNothing().when(delegate).onDeleteTaskPushNotificationConfig(params, context); decorator.onDeleteTaskPushNotificationConfig(params, context); @@ -882,11 +1005,14 @@ void onDeleteTaskPushNotificationConfig_createsSpanAndDelegatesToHandler() throw @Test void onDeleteTaskPushNotificationConfig_withError_setsErrorStatus() throws A2AError { - DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams("task-123", "config-123"); + DeleteTaskPushNotificationConfigParams params = + new DeleteTaskPushNotificationConfigParams("task-123", "config-123"); A2AError error = new TaskNotFoundError(); doThrow(error).when(delegate).onDeleteTaskPushNotificationConfig(params, context); - assertThrows(TaskNotFoundError.class, () -> decorator.onDeleteTaskPushNotificationConfig(params, context)); + assertThrows( + TaskNotFoundError.class, + () -> decorator.onDeleteTaskPushNotificationConfig(params, context)); verify(span).setAttribute(ERROR_TYPE, error.getMessage()); verify(span).setStatus(StatusCode.ERROR, error.getMessage()); @@ -902,26 +1028,33 @@ void onMessageSendStream_withNullMeter_doesNotRecordHistogram() throws A2AError TestableOpenTelemetryRequestHandlerDecorator decoratorNoMeter = new TestableOpenTelemetryRequestHandlerDecorator(delegate, tracer); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test message"))) - .messageId("msg-123") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test message"))) + .messageId("msg-123") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); Flow.Publisher publisher = mock(Flow.Publisher.class); when(delegate.onMessageSendStream(params, context)).thenReturn(publisher); Span closingSpan = mock(Span.class); - mockClosingSpanBuilder(tracer, A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, closingSpan); + mockClosingSpanBuilder( + tracer, + A2AMethods.SEND_STREAMING_MESSAGE_METHOD + STREAMING_END_SUFFIX, + closingSpan); - Flow.Publisher result = decoratorNoMeter.onMessageSendStream(params, context); + Flow.Publisher result = + decoratorNoMeter.onMessageSendStream(params, context); Flow.Subscriber testSubscriber = mock(Flow.Subscriber.class); result.subscribe(testSubscriber); - ArgumentCaptor subscriberCaptor = ArgumentCaptor.forClass(Flow.Subscriber.class); + ArgumentCaptor subscriberCaptor = + ArgumentCaptor.forClass(Flow.Subscriber.class); verify(publisher).subscribe(subscriberCaptor.capture()); - Flow.Subscriber otelSubscriber = (Flow.Subscriber) subscriberCaptor.getValue(); + Flow.Subscriber otelSubscriber = + (Flow.Subscriber) subscriberCaptor.getValue(); otelSubscriber.onComplete(); @@ -941,11 +1074,12 @@ void onMessageSend_setsGenaiProtocolAttribute_whenTransportKeyPresent() throws A when(context.getState()).thenReturn(state); when(context.getRequestedProtocolVersion()).thenReturn(null); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test"))) - .messageId("msg-1") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test"))) + .messageId("msg-1") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); when(delegate.onMessageSend(params, context)).thenReturn(mock(EventKind.class)); @@ -959,11 +1093,12 @@ void onMessageSend_setsGenaiProtocolAttribute_whenTransportKeyPresent() throws A void onMessageSend_setsGenaiVersionAttribute_whenVersionPresent() throws A2AError { when(context.getRequestedProtocolVersion()).thenReturn("1.0"); - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test"))) - .messageId("msg-1") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test"))) + .messageId("msg-1") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); when(delegate.onMessageSend(params, context)).thenReturn(mock(EventKind.class)); @@ -973,14 +1108,17 @@ void onMessageSend_setsGenaiVersionAttribute_whenVersionPresent() throws A2AErro } @Test - void onMessageSend_doesNotSetGenaiProtocolAttribute_whenTransportKeyAbsent() throws A2AError { - // context.getState() returns empty HashMap and getRequestedProtocolVersion() returns null from setUp - - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("test"))) - .messageId("msg-1") - .build(); + void onMessageSend_doesNotSetGenaiProtocolAttribute_whenTransportKeyAbsent() + throws A2AError { + // context.getState() returns empty HashMap and getRequestedProtocolVersion() returns + // null from setUp + + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("test"))) + .messageId("msg-1") + .build(); MessageSendParams params = new MessageSendParams(message, null, null, ""); when(delegate.onMessageSend(params, context)).thenReturn(mock(EventKind.class)); @@ -996,13 +1134,14 @@ class SpanLifecycleTests { @Test void allMethods_createAndEndSpans() throws A2AError { TaskQueryParams params = new TaskQueryParams("task-123", null); - Task result = Task.builder() - .id("task-123") - .contextId("ctx-1") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + Task result = + Task.builder() + .id("task-123") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); when(delegate.onGetTask(params, context)).thenReturn(result); decorator.onGetTask(params, context); @@ -1014,13 +1153,14 @@ void allMethods_createAndEndSpans() throws A2AError { @Test void spanAttributes_setCorrectly() throws A2AError { TaskQueryParams params = new TaskQueryParams("task-123", null); - Task result = Task.builder() - .id("task-123") - .contextId("ctx-1") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .history(Collections.emptyList()) - .artifacts(Collections.emptyList()) - .build(); + Task result = + Task.builder() + .id("task-123") + .contextId("ctx-1") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .history(Collections.emptyList()) + .artifacts(Collections.emptyList()) + .build(); when(delegate.onGetTask(params, context)).thenReturn(result); decorator.onGetTask(params, context); diff --git a/extras/push-notification-config-store-database-jpa/pom.xml b/extras/push-notification-config-store-database-jpa/pom.xml index 76fb2dfc0..72f731b00 100644 --- a/extras/push-notification-config-store-database-jpa/pom.xml +++ b/extras/push-notification-config-store-database-jpa/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java index 424291974..a71fdaad9 100644 --- a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java +++ b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStore.java @@ -1,17 +1,15 @@ package org.a2aproject.sdk.extras.pushnotificationconfigstore.database.jpa; -import jakarta.persistence.TypedQuery; -import java.time.Instant; -import java.util.List; - import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Alternative; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import jakarta.persistence.TypedQuery; import jakarta.transaction.Transactional; - +import java.time.Instant; +import java.util.List; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; import org.a2aproject.sdk.server.config.A2AConfigProvider; import org.a2aproject.sdk.server.tasks.PushNotificationConfigStore; @@ -29,16 +27,16 @@ @Alternative @Priority(50) public class JpaDatabasePushNotificationConfigStore implements PushNotificationConfigStore { - - private static final Logger LOGGER = LoggerFactory.getLogger(JpaDatabasePushNotificationConfigStore.class); + + private static final Logger LOGGER = + LoggerFactory.getLogger(JpaDatabasePushNotificationConfigStore.class); private static final Instant NULL_TIMESTAMP_SENTINEL = Instant.EPOCH; @PersistenceContext(unitName = "a2a-java") EntityManager em; - @Inject - A2AConfigProvider config; + @Inject A2AConfigProvider config; @Transactional @Override @@ -48,82 +46,127 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio @Transactional @Override - public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificationConfig, @Nullable String protocolVersion) { + public TaskPushNotificationConfig setInfo( + TaskPushNotificationConfig notificationConfig, @Nullable String protocolVersion) { String taskId = Assert.checkNotNullParam("taskId", notificationConfig.taskId()); // Default missing config IDs to the task ID, matching the in-memory store. - boolean configIdIsMissing = notificationConfig.id() == null || notificationConfig.id().isEmpty(); + boolean configIdIsMissing = + notificationConfig.id() == null || notificationConfig.id().isEmpty(); if (configIdIsMissing) { - notificationConfig = TaskPushNotificationConfig.builder(notificationConfig).id(taskId).build(); + notificationConfig = + TaskPushNotificationConfig.builder(notificationConfig).id(taskId).build(); } - String resolvedVersion = PushNotificationConfigStore.resolveProtocolVersion(protocolVersion); - LOGGER.debug("Saving PushNotificationConfig for Task '{}' with ID: {}", taskId, notificationConfig.id()); + String resolvedVersion = + PushNotificationConfigStore.resolveProtocolVersion(protocolVersion); + LOGGER.debug( + "Saving PushNotificationConfig for Task '{}' with ID: {}", + taskId, + notificationConfig.id()); try { TaskConfigId configId = new TaskConfigId(taskId, notificationConfig.id()); // Check if entity already exists - JpaPushNotificationConfig existingJpaConfig = em.find(JpaPushNotificationConfig.class, configId); + JpaPushNotificationConfig existingJpaConfig = + em.find(JpaPushNotificationConfig.class, configId); if (configIdIsMissing && existingJpaConfig != null && !"0.3".equals(resolvedVersion)) { - throw new InvalidParamsError("A push notification config with the default ID already exists for task " - + taskId + "; specify the config ID explicitly to update it"); + throw new InvalidParamsError( + "A push notification config with the default ID already exists for task " + + taskId + + "; specify the config ID explicitly to update it"); } if (existingJpaConfig != null) { // Update existing entity existingJpaConfig.setConfig(notificationConfig); existingJpaConfig.setProtocolVersion(resolvedVersion); - LOGGER.debug("Updated existing PushNotificationConfig for Task '{}' with ID: {}", - taskId, notificationConfig.id()); + LOGGER.debug( + "Updated existing PushNotificationConfig for Task '{}' with ID: {}", + taskId, + notificationConfig.id()); } else { // Enforce the per-task limit (configurable via // a2a.push-notification-config.max-per-task); only genuinely new // configs count against it. int maxPerTask = PushNotificationConfigStore.maxPushConfigsPerTask(config); - Long existingCount = em.createQuery( - "SELECT COUNT(c) FROM JpaPushNotificationConfig c WHERE c.id.taskId = :taskId", - Long.class).setParameter("taskId", taskId).getSingleResult(); + Long existingCount = + em.createQuery( + "SELECT COUNT(c) FROM JpaPushNotificationConfig c WHERE" + + " c.id.taskId = :taskId", + Long.class) + .setParameter("taskId", taskId) + .getSingleResult(); if (existingCount >= maxPerTask) { - throw new InvalidParamsError("Too many push notification configs for task " + taskId - + " (max " + maxPerTask + ")"); + throw new InvalidParamsError( + "Too many push notification configs for task " + + taskId + + " (max " + + maxPerTask + + ")"); } // Create new entity - JpaPushNotificationConfig jpaConfig = JpaPushNotificationConfig.createFromConfig(taskId, notificationConfig, resolvedVersion); + JpaPushNotificationConfig jpaConfig = + JpaPushNotificationConfig.createFromConfig( + taskId, notificationConfig, resolvedVersion); em.persist(jpaConfig); - LOGGER.debug("Persisted new PushNotificationConfig for Task '{}' with ID: {}", - taskId, notificationConfig.id()); + LOGGER.debug( + "Persisted new PushNotificationConfig for Task '{}' with ID: {}", + taskId, + notificationConfig.id()); } } catch (JsonProcessingException e) { - LOGGER.error("Failed to serialize PushNotificationConfig for Task '{}' with ID: {}", - taskId, notificationConfig.id(), e); - throw new RuntimeException("Failed to serialize PushNotificationConfig for Task '" + - taskId + "' with ID: " + notificationConfig.id(), e); + LOGGER.error( + "Failed to serialize PushNotificationConfig for Task '{}' with ID: {}", + taskId, + notificationConfig.id(), + e); + throw new RuntimeException( + "Failed to serialize PushNotificationConfig for Task '" + + taskId + + "' with ID: " + + notificationConfig.id(), + e); } return notificationConfig; } @Transactional @Override - public ListTaskPushNotificationConfigsResult getInfo(ListTaskPushNotificationConfigsParams params) { + public ListTaskPushNotificationConfigsResult getInfo( + ListTaskPushNotificationConfigsParams params) { String taskId = params.id(); - LOGGER.debug("Retrieving PushNotificationConfigs for Task '{}' with params: pageSize={}, pageToken={}", - taskId, params.pageSize(), params.pageToken()); + LOGGER.debug( + "Retrieving PushNotificationConfigs for Task '{}' with params: pageSize={}," + + " pageToken={}", + taskId, + params.pageSize(), + params.pageToken()); // Parse pageToken once at the beginning PageToken pageToken = PageToken.fromString(params.pageToken()); try { - StringBuilder queryBuilder = new StringBuilder("SELECT c FROM JpaPushNotificationConfig c WHERE c.id.taskId = :taskId"); + StringBuilder queryBuilder = + new StringBuilder( + "SELECT c FROM JpaPushNotificationConfig c WHERE c.id.taskId =" + + " :taskId"); if (pageToken != null) { - // Keyset pagination: get tasks where timestamp < tokenTimestamp OR (timestamp = tokenTimestamp AND id > tokenId) + // Keyset pagination: get tasks where timestamp < tokenTimestamp OR (timestamp = + // tokenTimestamp AND id > tokenId) // All tasks have timestamps (TaskStatus canonical constructor ensures this) - queryBuilder.append(" AND (COALESCE(c.createdAt, :nullSentinel) < :tokenTimestamp OR (COALESCE(c.createdAt, :nullSentinel) = :tokenTimestamp AND c.id.configId > :tokenId))"); + queryBuilder.append( + " AND (COALESCE(c.createdAt, :nullSentinel) < :tokenTimestamp OR" + + " (COALESCE(c.createdAt, :nullSentinel) = :tokenTimestamp AND" + + " c.id.configId > :tokenId))"); } - queryBuilder.append(" ORDER BY COALESCE(c.createdAt, :nullSentinel) DESC, c.id.configId ASC"); + queryBuilder.append( + " ORDER BY COALESCE(c.createdAt, :nullSentinel) DESC, c.id.configId ASC"); - TypedQuery query = em.createQuery(queryBuilder.toString(), JpaPushNotificationConfig.class); + TypedQuery query = + em.createQuery(queryBuilder.toString(), JpaPushNotificationConfig.class); query.setParameter("taskId", taskId); query.setParameter("nullSentinel", NULL_TIMESTAMP_SENTINEL); @@ -138,35 +181,57 @@ public ListTaskPushNotificationConfigsResult getInfo(ListTaskPushNotificationCon String nextPageToken = null; if (jpaConfigsPage.size() > pageSize) { - // There are more results than the page size, and in this case, a nextToken should be created with the last item. - // Format: "timestamp_millis:taskId" for keyset pagination - jpaConfigsPage = jpaConfigsPage.subList(0, pageSize); - JpaPushNotificationConfig lastConfig = jpaConfigsPage.get(jpaConfigsPage.size() - 1); - Instant timestamp = lastConfig.getCreatedAt() != null ? lastConfig.getCreatedAt() : NULL_TIMESTAMP_SENTINEL; - nextPageToken = new PageToken(timestamp, lastConfig.getId().getConfigId()).toString(); + // There are more results than the page size, and in this case, a nextToken should + // be created with the last item. + // Format: "timestamp_millis:taskId" for keyset pagination + jpaConfigsPage = jpaConfigsPage.subList(0, pageSize); + JpaPushNotificationConfig lastConfig = + jpaConfigsPage.get(jpaConfigsPage.size() - 1); + Instant timestamp = + lastConfig.getCreatedAt() != null + ? lastConfig.getCreatedAt() + : NULL_TIMESTAMP_SENTINEL; + nextPageToken = + new PageToken(timestamp, lastConfig.getId().getConfigId()).toString(); } - List taskPushNotificationConfigs = jpaConfigsPage.stream() - .map(jpaConfig -> { - try { - TaskPushNotificationConfig config = jpaConfig.getConfig(); - // Set taskId and tenant from the params - return TaskPushNotificationConfig.builder(config) - .taskId(params.id()) - .tenant(params.tenant()) - .build(); - } catch (JsonProcessingException e) { - LOGGER.error("Failed to deserialize PushNotificationConfig for Task '{}' with ID: {}", - taskId, jpaConfig.getId().getConfigId(), e); - throw new RuntimeException("Failed to deserialize PushNotificationConfig for Task '" + - taskId + "' with ID: " + jpaConfig.getId().getConfigId(), e); - } - }) - .toList(); - - LOGGER.debug("Successfully retrieved {} PushNotificationConfigs for Task '{}'", taskPushNotificationConfigs.size(), taskId); - - return new ListTaskPushNotificationConfigsResult(taskPushNotificationConfigs, nextPageToken); + List taskPushNotificationConfigs = + jpaConfigsPage.stream() + .map( + jpaConfig -> { + try { + TaskPushNotificationConfig config = + jpaConfig.getConfig(); + // Set taskId and tenant from the params + return TaskPushNotificationConfig.builder(config) + .taskId(params.id()) + .tenant(params.tenant()) + .build(); + } catch (JsonProcessingException e) { + LOGGER.error( + "Failed to deserialize PushNotificationConfig" + + " for Task '{}' with ID: {}", + taskId, + jpaConfig.getId().getConfigId(), + e); + throw new RuntimeException( + "Failed to deserialize PushNotificationConfig" + + " for Task '" + + taskId + + "' with ID: " + + jpaConfig.getId().getConfigId(), + e); + } + }) + .toList(); + + LOGGER.debug( + "Successfully retrieved {} PushNotificationConfigs for Task '{}'", + taskPushNotificationConfigs.size(), + taskId); + + return new ListTaskPushNotificationConfigsResult( + taskPushNotificationConfigs, nextPageToken); } catch (Exception e) { LOGGER.error("Failed to retrieve PushNotificationConfigs for Task '{}'", taskId, e); throw e; @@ -180,41 +245,53 @@ public void deleteInfo(String taskId, String configId) { configId = taskId; } - LOGGER.debug("Deleting PushNotificationConfig for Task '{}' with Config ID: {}", taskId, configId); - JpaPushNotificationConfig jpaConfig = em.find(JpaPushNotificationConfig.class, - new TaskConfigId(taskId, configId)); + LOGGER.debug( + "Deleting PushNotificationConfig for Task '{}' with Config ID: {}", + taskId, + configId); + JpaPushNotificationConfig jpaConfig = + em.find(JpaPushNotificationConfig.class, new TaskConfigId(taskId, configId)); if (jpaConfig != null) { em.remove(jpaConfig); - LOGGER.debug("Successfully deleted PushNotificationConfig for Task '{}' with Config ID: {}", - taskId, configId); + LOGGER.debug( + "Successfully deleted PushNotificationConfig for Task '{}' with Config ID: {}", + taskId, + configId); } else { - LOGGER.debug("PushNotificationConfig not found for deletion with Task '{}' and Config ID: {}", - taskId, configId); + LOGGER.debug( + "PushNotificationConfig not found for deletion with Task '{}' and Config ID:" + + " {}", + taskId, + configId); } } @Transactional @Override public String getProtocolVersion(String taskId, String configId) { - JpaPushNotificationConfig jpaConfig = em.find(JpaPushNotificationConfig.class, - new TaskConfigId(taskId, configId)); - return jpaConfig != null ? jpaConfig.getProtocolVersion() : PushNotificationConfigStore.resolveProtocolVersion(null); + JpaPushNotificationConfig jpaConfig = + em.find(JpaPushNotificationConfig.class, new TaskConfigId(taskId, configId)); + return jpaConfig != null + ? jpaConfig.getProtocolVersion() + : PushNotificationConfigStore.resolveProtocolVersion(null); } @Transactional @Override public java.util.Map getProtocolVersions(String taskId) { - List results = em.createQuery( - "SELECT c.id.configId, c.protocolVersion FROM JpaPushNotificationConfig c " + - "WHERE c.id.taskId = :taskId AND c.protocolVersion IS NOT NULL", Object[].class) - .setParameter("taskId", taskId) - .getResultList(); + List results = + em.createQuery( + "SELECT c.id.configId, c.protocolVersion FROM" + + " JpaPushNotificationConfig c WHERE c.id.taskId = :taskId AND" + + " c.protocolVersion IS NOT NULL", + Object[].class) + .setParameter("taskId", taskId) + .getResultList(); java.util.Map versions = new java.util.HashMap<>(); for (Object[] row : results) { versions.put((String) row[0], (String) row[1]); } return versions; } - } diff --git a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java index 8867c50f2..ffba140b4 100644 --- a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java +++ b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfig.java @@ -6,20 +6,17 @@ import jakarta.persistence.PrePersist; import jakarta.persistence.Table; import jakarta.persistence.Transient; - +import java.time.Instant; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.server.tasks.PushNotificationConfigStore; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.jspecify.annotations.Nullable; -import java.time.Instant; - @Entity @Table(name = "a2a_push_notification_configs") public class JpaPushNotificationConfig { - @EmbeddedId - private TaskConfigId id; + @EmbeddedId private TaskConfigId id; @Column(name = "task_data", columnDefinition = "TEXT", nullable = false) private String configJson; @@ -30,12 +27,10 @@ public class JpaPushNotificationConfig { @Column(name = "created_at") private Instant createdAt; - @Transient - private TaskPushNotificationConfig config; + @Transient private TaskPushNotificationConfig config; // Default constructor required by JPA - public JpaPushNotificationConfig() { - } + public JpaPushNotificationConfig() {} public JpaPushNotificationConfig(TaskConfigId id, String configJson) { this.id = id; @@ -44,9 +39,9 @@ public JpaPushNotificationConfig(TaskConfigId id, String configJson) { @PrePersist protected void onCreate() { - if (createdAt == null) { - createdAt = Instant.now(); - } + if (createdAt == null) { + createdAt = Instant.now(); + } } public TaskConfigId getId() { @@ -70,19 +65,24 @@ public TaskPushNotificationConfig getConfig() throws JsonProcessingException { public void setConfig(TaskPushNotificationConfig config) throws JsonProcessingException { if (config.id() == null || !config.id().equals(id.getConfigId())) { - throw new IllegalArgumentException("Mismatched config id. " + - "Expected '" + id.getConfigId() + "'. Got: '" + config.id() + "'"); + throw new IllegalArgumentException( + "Mismatched config id. " + + "Expected '" + + id.getConfigId() + + "'. Got: '" + + config.id() + + "'"); } configJson = JsonUtil.toJson(config); this.config = config; } public Instant getCreatedAt() { - return createdAt; + return createdAt; } public void setCreatedAt(Instant createdAt) { - this.createdAt = createdAt; + this.createdAt = createdAt; } public String getProtocolVersion() { @@ -93,7 +93,9 @@ public void setProtocolVersion(String protocolVersion) { this.protocolVersion = protocolVersion; } - static JpaPushNotificationConfig createFromConfig(String taskId, TaskPushNotificationConfig config, @Nullable String protocolVersion) throws JsonProcessingException { + static JpaPushNotificationConfig createFromConfig( + String taskId, TaskPushNotificationConfig config, @Nullable String protocolVersion) + throws JsonProcessingException { String json = JsonUtil.toJson(config); JpaPushNotificationConfig jpaPushNotificationConfig = new JpaPushNotificationConfig(new TaskConfigId(taskId, config.id()), json); diff --git a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/TaskConfigId.java b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/TaskConfigId.java index 0ab0c553f..fbc301120 100644 --- a/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/TaskConfigId.java +++ b/extras/push-notification-config-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/TaskConfigId.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.extras.pushnotificationconfigstore.database.jpa; -import java.io.Serializable; -import java.util.Objects; - import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; @Embeddable public class TaskConfigId implements Serializable { @@ -16,8 +15,7 @@ public class TaskConfigId implements Serializable { private String configId; // No-argument constructor (required by JPA) - public TaskConfigId() { - } + public TaskConfigId() {} public TaskConfigId(String taskId, String configId) { this.taskId = taskId; @@ -48,8 +46,7 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TaskConfigId that = (TaskConfigId) o; - return Objects.equals(taskId, that.taskId) && - Objects.equals(configId, that.configId); + return Objects.equals(taskId, that.taskId) && Objects.equals(configId, that.configId); } @Override diff --git a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreIntegrationTest.java b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreIntegrationTest.java index 0d51bd039..625b88246 100644 --- a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreIntegrationTest.java +++ b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreIntegrationTest.java @@ -7,15 +7,14 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import java.util.List; import java.util.Queue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; - -import jakarta.inject.Inject; -import jakarta.transaction.Transactional; - import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.TaskEvent; import org.a2aproject.sdk.client.TaskUpdateEvent; @@ -35,7 +34,6 @@ import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.a2aproject.sdk.spec.TextPart; -import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -46,15 +44,11 @@ @QuarkusTest public class JpaDatabasePushNotificationConfigStoreIntegrationTest { - @Inject - PushNotificationConfigStore pushNotificationConfigStore; + @Inject PushNotificationConfigStore pushNotificationConfigStore; - @Inject - @PublicAgentCard - AgentCard agentCard; + @Inject @PublicAgentCard AgentCard agentCard; - @Inject - MockPushNotificationSender mockPushNotificationSender; + @Inject MockPushNotificationSender mockPushNotificationSender; private Client client; @@ -64,15 +58,14 @@ public void setup() throws A2AClientException { mockPushNotificationSender.clear(); // Create client configuration - enable streaming for automatic push notifications - ClientConfig clientConfig = new ClientConfig.Builder() - .setStreaming(true) - .build(); + ClientConfig clientConfig = new ClientConfig.Builder().setStreaming(true).build(); // Build client with JSON-RPC transport - client = Client.builder(agentCard) - .clientConfig(clientConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); + client = + Client.builder(agentCard) + .clientConfig(clientConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); } @Test @@ -85,11 +78,14 @@ public void testDirectNotificationTrigger() { // Simple test to verify the mock notification mechanism works mockPushNotificationSender.clear(); - Task testTask = Task.builder() - .id("direct-test-task") - .contextId("test-context") - .status(new org.a2aproject.sdk.spec.TaskStatus(org.a2aproject.sdk.spec.TaskState.TASK_STATE_SUBMITTED)) - .build(); + Task testTask = + Task.builder() + .id("direct-test-task") + .contextId("test-context") + .status( + new org.a2aproject.sdk.spec.TaskStatus( + org.a2aproject.sdk.spec.TaskState.TASK_STATE_SUBMITTED)) + .build(); // Directly trigger the mock mockPushNotificationSender.sendNotification(testTask, null); @@ -103,26 +99,31 @@ public void testDirectNotificationTrigger() { @Test public void testJpaDatabasePushNotificationConfigStoreIntegration() throws Exception { // Step 1: Create the task (no client-provided taskId — server generates it) - Message createMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("create"))) // Send the "create" command - .messageId("test-msg-1") - .build(); + Message createMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("create"))) // Send the "create" command + .messageId("test-msg-1") + .build(); // Use a latch to wait for the first operation to complete and capture server-generated ids CountDownLatch createLatch = new CountDownLatch(1); AtomicReference createdTaskRef = new AtomicReference<>(); - client.sendMessage(createMessage, List.of((event, card) -> { - if (event instanceof TaskEvent taskEvent) { - createdTaskRef.set(taskEvent.getTask()); - createLatch.countDown(); - return; - } - if (event instanceof TaskUpdateEvent taskUpdateEvent) { - createdTaskRef.set(taskUpdateEvent.getTask()); - createLatch.countDown(); - } - }), (e) -> createLatch.countDown()); + client.sendMessage( + createMessage, + List.of( + (event, card) -> { + if (event instanceof TaskEvent taskEvent) { + createdTaskRef.set(taskEvent.getTask()); + createLatch.countDown(); + return; + } + if (event instanceof TaskUpdateEvent taskUpdateEvent) { + createdTaskRef.set(taskUpdateEvent.getTask()); + createLatch.countDown(); + } + }), + (e) -> createLatch.countDown()); assertTrue(createLatch.await(10, TimeUnit.SECONDS), "Timeout waiting for task creation"); assertNotNull(createdTaskRef.get(), "Task should have been created"); @@ -130,19 +131,22 @@ public void testJpaDatabasePushNotificationConfigStoreIntegration() throws Excep final String contextId = createdTaskRef.get().contextId(); // Step 2: Set the push notification configuration - TaskPushNotificationConfig taskPushConfig = TaskPushNotificationConfig.builder() - .id("test-config-1") - .taskId(taskId) - .url("http://localhost:9999/mock-endpoint") - .token("test-token-123") - .tenant("") - .build(); - TaskPushNotificationConfig setResult = client.createTaskPushNotificationConfiguration(taskPushConfig); + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() + .id("test-config-1") + .taskId(taskId) + .url("http://localhost:9999/mock-endpoint") + .token("test-token-123") + .tenant("") + .build(); + TaskPushNotificationConfig setResult = + client.createTaskPushNotificationConfiguration(taskPushConfig); assertNotNull(setResult); // Step 3: Verify the configuration was stored using client API - TaskPushNotificationConfig storedConfig = client.getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams(taskId, "test-config-1")); + TaskPushNotificationConfig storedConfig = + client.getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams(taskId, "test-config-1")); assertNotNull(storedConfig); assertEquals(taskId, storedConfig.taskId()); @@ -151,20 +155,25 @@ public void testJpaDatabasePushNotificationConfigStoreIntegration() throws Excep assertEquals("test-token-123", storedConfig.token()); // Step 4: Update the task to trigger the notification - Message updateMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("update"))) // Send the "update" command - .taskId(taskId) - .messageId("test-msg-2") - .contextId(contextId) - .build(); + Message updateMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("update"))) // Send the "update" command + .taskId(taskId) + .messageId("test-msg-2") + .contextId(contextId) + .build(); CountDownLatch updateLatch = new CountDownLatch(1); - client.sendMessage(updateMessage, List.of((event, card) -> updateLatch.countDown()), (e) -> updateLatch.countDown()); + client.sendMessage( + updateMessage, + List.of((event, card) -> updateLatch.countDown()), + (e) -> updateLatch.countDown()); assertTrue(updateLatch.await(10, TimeUnit.SECONDS), "Timeout waiting for task update"); // Step 5: Poll for the async notification to be captured - // With the new StreamingEventKind support, we receive all event types (Task, Message, TaskArtifactUpdateEvent, etc.) + // With the new StreamingEventKind support, we receive all event types (Task, Message, + // TaskArtifactUpdateEvent, etc.) long end = System.currentTimeMillis() + 5000; boolean notificationReceived = false; @@ -180,38 +189,49 @@ public void testJpaDatabasePushNotificationConfigStoreIntegration() throws Excep // Step 6: Verify the captured notification // Check if we received events for this task (could be Task, TaskArtifactUpdateEvent, etc.) - Queue capturedEvents = mockPushNotificationSender.getCapturedEvents(); + Queue capturedEvents = + mockPushNotificationSender.getCapturedEvents(); // Look for Task events with artifacts OR TaskArtifactUpdateEvent for this task - boolean hasTaskWithArtifact = capturedEvents.stream() - .filter(e -> e instanceof Task) - .map(e -> (Task) e) - .anyMatch(t -> taskId.equals(t.id()) && t.artifacts() != null && t.artifacts().size() > 0); - - boolean hasArtifactUpdateEvent = capturedEvents.stream() - .filter(e -> e instanceof org.a2aproject.sdk.spec.TaskArtifactUpdateEvent) - .map(e -> (org.a2aproject.sdk.spec.TaskArtifactUpdateEvent) e) - .anyMatch(e -> taskId.equals(e.taskId())); - - assertTrue(hasTaskWithArtifact || hasArtifactUpdateEvent, - "Notification should contain either Task with artifacts or TaskArtifactUpdateEvent for task " + taskId); + boolean hasTaskWithArtifact = + capturedEvents.stream() + .filter(e -> e instanceof Task) + .map(e -> (Task) e) + .anyMatch( + t -> + taskId.equals(t.id()) + && t.artifacts() != null + && t.artifacts().size() > 0); + + boolean hasArtifactUpdateEvent = + capturedEvents.stream() + .filter(e -> e instanceof org.a2aproject.sdk.spec.TaskArtifactUpdateEvent) + .map(e -> (org.a2aproject.sdk.spec.TaskArtifactUpdateEvent) e) + .anyMatch(e -> taskId.equals(e.taskId())); + + assertTrue( + hasTaskWithArtifact || hasArtifactUpdateEvent, + "Notification should contain either Task with artifacts or TaskArtifactUpdateEvent" + + " for task " + + taskId); // Step 7: Clean up - delete the push notification configuration client.deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams(taskId, "test-config-1")); + new DeleteTaskPushNotificationConfigParams(taskId, "test-config-1")); // Verify deletion by asserting that getting the config now throws an exception - assertThrows(A2AClientException.class, () -> { - client.getTaskPushNotificationConfiguration(new GetTaskPushNotificationConfigParams(taskId, "test-config-1")); - }, "Getting a deleted config should throw an A2AClientException"); + assertThrows( + A2AClientException.class, + () -> { + client.getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams(taskId, "test-config-1")); + }, + "Getting a deleted config should throw an A2AClientException"); } - private TaskPushNotificationConfig createSamplePushConfig(String url, String configId, String token) { - return TaskPushNotificationConfig.builder() - .url(url) - .id(configId) - .token(token) - .build(); + private TaskPushNotificationConfig createSamplePushConfig( + String url, String configId, String token) { + return TaskPushNotificationConfig.builder().url(url).id(configId).token(token).build(); } @Test @@ -221,7 +241,8 @@ public void testPaginationWithPageSize() { // Create 5 configs createSamples(taskId, 5); // Request first page with pageSize=2 - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams(taskId, 2, "", ""); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 2, "", ""); ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); assertNotNull(result); @@ -237,31 +258,32 @@ public void testPaginationWithPageToken() { createSamples(taskId, 5); // Get first page - ListTaskPushNotificationConfigsParams firstPageParams = new ListTaskPushNotificationConfigsParams(taskId, 2, "", ""); - ListTaskPushNotificationConfigsResult firstPage = pushNotificationConfigStore.getInfo(firstPageParams); + ListTaskPushNotificationConfigsParams firstPageParams = + new ListTaskPushNotificationConfigsParams(taskId, 2, "", ""); + ListTaskPushNotificationConfigsResult firstPage = + pushNotificationConfigStore.getInfo(firstPageParams); assertNotNull(firstPage.nextPageToken()); // Get second page using nextPageToken - ListTaskPushNotificationConfigsParams secondPageParams = new ListTaskPushNotificationConfigsParams( - taskId, 2, firstPage.nextPageToken(), ""); - ListTaskPushNotificationConfigsResult secondPage = pushNotificationConfigStore.getInfo(secondPageParams); + ListTaskPushNotificationConfigsParams secondPageParams = + new ListTaskPushNotificationConfigsParams(taskId, 2, firstPage.nextPageToken(), ""); + ListTaskPushNotificationConfigsResult secondPage = + pushNotificationConfigStore.getInfo(secondPageParams); assertNotNull(secondPage); assertEquals(2, secondPage.configs().size(), "Should return 2 configs for second page"); - assertNotNull(secondPage.nextPageToken(), "Should have nextPageToken when more items exist"); + assertNotNull( + secondPage.nextPageToken(), "Should have nextPageToken when more items exist"); // Verify NO overlap between pages - collect all IDs from both pages - List firstPageIds = firstPage.configs().stream() - .map(c -> c.id()) - .toList(); - List secondPageIds = secondPage.configs().stream() - .map(c -> c.id()) - .toList(); + List firstPageIds = firstPage.configs().stream().map(c -> c.id()).toList(); + List secondPageIds = secondPage.configs().stream().map(c -> c.id()).toList(); // Check that no ID from first page appears in second page for (String id : firstPageIds) { - assertTrue(!secondPageIds.contains(id), - "Config " + id + " appears in both pages - overlap detected!"); + assertTrue( + !secondPageIds.contains(id), + "Config " + id + " appears in both pages - overlap detected!"); } // Also verify the pages are sequential (first page ends before second page starts) @@ -282,18 +304,23 @@ public void testPaginationLastPage() { createSamples(taskId, 5); // Get first page (2 items) - ListTaskPushNotificationConfigsParams firstPageParams = new ListTaskPushNotificationConfigsParams(taskId, 2, "", ""); - ListTaskPushNotificationConfigsResult firstPage = pushNotificationConfigStore.getInfo(firstPageParams); + ListTaskPushNotificationConfigsParams firstPageParams = + new ListTaskPushNotificationConfigsParams(taskId, 2, "", ""); + ListTaskPushNotificationConfigsResult firstPage = + pushNotificationConfigStore.getInfo(firstPageParams); // Get second page (2 items) - ListTaskPushNotificationConfigsParams secondPageParams = new ListTaskPushNotificationConfigsParams( - taskId, 2, firstPage.nextPageToken(), ""); - ListTaskPushNotificationConfigsResult secondPage = pushNotificationConfigStore.getInfo(secondPageParams); + ListTaskPushNotificationConfigsParams secondPageParams = + new ListTaskPushNotificationConfigsParams(taskId, 2, firstPage.nextPageToken(), ""); + ListTaskPushNotificationConfigsResult secondPage = + pushNotificationConfigStore.getInfo(secondPageParams); // Get last page (1 item remaining) - ListTaskPushNotificationConfigsParams lastPageParams = new ListTaskPushNotificationConfigsParams( - taskId, 2, secondPage.nextPageToken(), ""); - ListTaskPushNotificationConfigsResult lastPage = pushNotificationConfigStore.getInfo(lastPageParams); + ListTaskPushNotificationConfigsParams lastPageParams = + new ListTaskPushNotificationConfigsParams( + taskId, 2, secondPage.nextPageToken(), ""); + ListTaskPushNotificationConfigsResult lastPage = + pushNotificationConfigStore.getInfo(lastPageParams); assertNotNull(lastPage); assertEquals(1, lastPage.configs().size(), "Last page should have 1 remaining config"); @@ -308,7 +335,8 @@ public void testPaginationWithZeroPageSize() { createSamples(taskId, 5); // Request with pageSize=0 should return all configs - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams(taskId, 0, "", ""); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 0, "", ""); ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); assertNotNull(result); @@ -324,11 +352,13 @@ public void testPaginationWithNegativePageSize() { createSamples(taskId, 3); // Request with negative pageSize should return all configs - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams(taskId, -1, "", ""); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, -1, "", ""); ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); assertNotNull(result); - assertEquals(3, result.configs().size(), "Should return all configs when pageSize is negative"); + assertEquals( + 3, result.configs().size(), "Should return all configs when pageSize is negative"); assertNull(result.nextPageToken(), "Should not have nextPageToken when returning all"); } @@ -340,12 +370,15 @@ public void testPaginationPageSizeLargerThanConfigs() { createSamples(taskId, 3); // Request with pageSize larger than available configs - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams(taskId, 10, "", ""); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 10, "", ""); ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); assertNotNull(result); assertEquals(3, result.configs().size(), "Should return all 3 configs"); - assertNull(result.nextPageToken(), "Should not have nextPageToken when all configs fit in one page"); + assertNull( + result.nextPageToken(), + "Should not have nextPageToken when all configs fit in one page"); } @Test @@ -356,12 +389,15 @@ public void testPaginationExactlyPageSize() { createSamples(taskId, 3); // Request with pageSize equal to number of configs - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams(taskId, 3, "", ""); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 3, "", ""); ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); assertNotNull(result); assertEquals(3, result.configs().size(), "Should return all 3 configs"); - assertNull(result.nextPageToken(), "Should not have nextPageToken when configs exactly match pageSize"); + assertNull( + result.nextPageToken(), + "Should not have nextPageToken when configs exactly match pageSize"); } @Test @@ -372,11 +408,14 @@ public void testPaginationWithInvalidToken() { createSamples(taskId, 5); // Request with invalid pageToken - should throw InvalidParamsError for invalid format - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams( - taskId, 2, "invalid_token_that_does_not_exist", ""); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams( + taskId, 2, "invalid_token_that_does_not_exist", ""); - assertThrows(org.a2aproject.sdk.spec.InvalidParamsError.class, () -> pushNotificationConfigStore.getInfo(params), - "Should throw InvalidParamsError for invalid pageToken format (missing colon)"); + assertThrows( + org.a2aproject.sdk.spec.InvalidParamsError.class, + () -> pushNotificationConfigStore.getInfo(params), + "Should throw InvalidParamsError for invalid pageToken format (missing colon)"); } @Test @@ -385,7 +424,8 @@ public void testPaginationEmptyTaskWithPageSize() { String taskId = "task_pagination_empty_" + System.currentTimeMillis(); // No configs created - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams(taskId, 2, "", ""); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 2, "", ""); ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); assertNotNull(result); @@ -406,8 +446,10 @@ public void testPaginationFullIteration() { int pageCount = 0; do { - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams(taskId, 3, pageToken, ""); - ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 3, pageToken, ""); + ListTaskPushNotificationConfigsResult result = + pushNotificationConfigStore.getInfo(params); totalCollected += result.configs().size(); pageToken = result.nextPageToken(); @@ -425,152 +467,161 @@ public void testPaginationFullIteration() { @Test @Transactional public void testPageTokenWithNonNumericTimestamp() { - String taskId = "task_malformed_token_" + System.currentTimeMillis(); - createSamples(taskId, 3); + String taskId = "task_malformed_token_" + System.currentTimeMillis(); + createSamples(taskId, 3); - ListTaskPushNotificationConfigsParams params = - new ListTaskPushNotificationConfigsParams(taskId, 2, "not_a_number:cfg1", ""); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 2, "not_a_number:cfg1", ""); - assertThrows(org.a2aproject.sdk.spec.InvalidParamsError.class, - () -> pushNotificationConfigStore.getInfo(params), - "Should throw InvalidParamsError for non-numeric timestamp in pageToken"); + assertThrows( + org.a2aproject.sdk.spec.InvalidParamsError.class, + () -> pushNotificationConfigStore.getInfo(params), + "Should throw InvalidParamsError for non-numeric timestamp in pageToken"); } @Test @Transactional public void testPageTokenWithMissingColon() { - String taskId = "task_missing_colon_" + System.currentTimeMillis(); - createSamples(taskId, 5); + String taskId = "task_missing_colon_" + System.currentTimeMillis(); + createSamples(taskId, 5); - ListTaskPushNotificationConfigsParams params = - new ListTaskPushNotificationConfigsParams(taskId, 2, "123456789cfg1", ""); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 2, "123456789cfg1", ""); - assertThrows(org.a2aproject.sdk.spec.InvalidParamsError.class, () -> pushNotificationConfigStore.getInfo(params), - "Should throw InvalidParamsError for invalid pageToken format (missing colon)"); + assertThrows( + org.a2aproject.sdk.spec.InvalidParamsError.class, + () -> pushNotificationConfigStore.getInfo(params), + "Should throw InvalidParamsError for invalid pageToken format (missing colon)"); } @Test @Transactional public void testPaginationBoundaryExactlyMaxResultsPlusOne() { - String taskId = "task_boundary_" + System.currentTimeMillis(); + String taskId = "task_boundary_" + System.currentTimeMillis(); - createSamples(taskId, 4); + createSamples(taskId, 4); - ListTaskPushNotificationConfigsParams params = - new ListTaskPushNotificationConfigsParams(taskId, 4, "", ""); - ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 4, "", ""); + ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); - assertEquals(4, result.configs().size(), - "Should return all 4 configs when pageSize equals total count"); - assertNull(result.nextPageToken(), - "Should not have nextPageToken when count equals pageSize"); + assertEquals( + 4, + result.configs().size(), + "Should return all 4 configs when pageSize equals total count"); + assertNull( + result.nextPageToken(), "Should not have nextPageToken when count equals pageSize"); } @Test @Transactional public void testMultipleTasksDoNotInterfere() { - String taskId1 = "task1_" + System.currentTimeMillis(); - String taskId2 = "task2_" + System.currentTimeMillis(); - - createSamples(taskId1, 3); - createSamples(taskId2, 2); - - ListTaskPushNotificationConfigsResult result1 = - pushNotificationConfigStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId1)); - ListTaskPushNotificationConfigsResult result2 = - pushNotificationConfigStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId2)); - - assertEquals(3, result1.configs().size(), "Task1 should have 3 configs"); - assertEquals(2, result2.configs().size(), "Task2 should have 2 configs"); - - List task1Ids = result1.configs().stream() - .map(c -> taskId1 + c.id()) - .toList(); - List task2Ids = result2.configs().stream() - .map(c -> taskId2 + c.id()) - .toList(); - - for (String id : task1Ids) { - assertTrue(!task2Ids.contains(id), - "Configs from different tasks should not overlap"); - } + String taskId1 = "task1_" + System.currentTimeMillis(); + String taskId2 = "task2_" + System.currentTimeMillis(); + + createSamples(taskId1, 3); + createSamples(taskId2, 2); + + ListTaskPushNotificationConfigsResult result1 = + pushNotificationConfigStore.getInfo( + new ListTaskPushNotificationConfigsParams(taskId1)); + ListTaskPushNotificationConfigsResult result2 = + pushNotificationConfigStore.getInfo( + new ListTaskPushNotificationConfigsParams(taskId2)); + + assertEquals(3, result1.configs().size(), "Task1 should have 3 configs"); + assertEquals(2, result2.configs().size(), "Task2 should have 2 configs"); + + List task1Ids = result1.configs().stream().map(c -> taskId1 + c.id()).toList(); + List task2Ids = result2.configs().stream().map(c -> taskId2 + c.id()).toList(); + + for (String id : task1Ids) { + assertTrue(!task2Ids.contains(id), "Configs from different tasks should not overlap"); + } } @Test @Transactional public void testGetInfoWithNonExistentTaskIdDoesNotThrow() { - String nonExistentTaskId = "non_existent_task_" + System.currentTimeMillis(); + String nonExistentTaskId = "non_existent_task_" + System.currentTimeMillis(); - ListTaskPushNotificationConfigsParams params = - new ListTaskPushNotificationConfigsParams(nonExistentTaskId, 10, "", ""); - ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(nonExistentTaskId, 10, "", ""); + ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); - assertNotNull(result, "Result should not be null"); - assertTrue(result.configs().isEmpty(), - "Should return empty list for non-existent task"); - assertNull(result.nextPageToken(), - "Should not have nextPageToken for empty result"); + assertNotNull(result, "Result should not be null"); + assertTrue(result.configs().isEmpty(), "Should return empty list for non-existent task"); + assertNull(result.nextPageToken(), "Should not have nextPageToken for empty result"); } @Test @Transactional public void testGetInfoReturnsTenantFromParams() { - String taskId = "task_tenant_" + System.currentTimeMillis(); - String tenant = "test-tenant-123"; + String taskId = "task_tenant_" + System.currentTimeMillis(); + String tenant = "test-tenant-123"; - TaskPushNotificationConfig config = createSamplePushConfig( - "http://url.com/callback", "cfg1", "token"); - pushNotificationConfigStore.setInfo(TaskPushNotificationConfig.builder(config).taskId(taskId).build()); + TaskPushNotificationConfig config = + createSamplePushConfig("http://url.com/callback", "cfg1", "token"); + pushNotificationConfigStore.setInfo( + TaskPushNotificationConfig.builder(config).taskId(taskId).build()); - ListTaskPushNotificationConfigsParams params = - new ListTaskPushNotificationConfigsParams(taskId, 0, "", tenant); - ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 0, "", tenant); + ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); - assertNotNull(result); - assertEquals(1, result.configs().size()); - assertEquals(tenant, result.configs().get(0).tenant(), - "Tenant from params should be returned in result"); + assertNotNull(result); + assertEquals(1, result.configs().size()); + assertEquals( + tenant, + result.configs().get(0).tenant(), + "Tenant from params should be returned in result"); } @Test @Transactional public void testPaginationOrderingConsistency() { - String taskId = "task_ordering_consistency_" + System.currentTimeMillis(); - createSamples(taskId, 15); - - List allConfigIds = new java.util.ArrayList<>(); - String pageToken = ""; - int pageCount = 0; + String taskId = "task_ordering_consistency_" + System.currentTimeMillis(); + createSamples(taskId, 15); - do { - ListTaskPushNotificationConfigsParams params = - new ListTaskPushNotificationConfigsParams(taskId, 3, pageToken, ""); - ListTaskPushNotificationConfigsResult result = pushNotificationConfigStore.getInfo(params); + List allConfigIds = new java.util.ArrayList<>(); + String pageToken = ""; + int pageCount = 0; - result.configs().forEach(c -> - allConfigIds.add(c.id())); - pageToken = result.nextPageToken(); - pageCount++; + do { + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams(taskId, 3, pageToken, ""); + ListTaskPushNotificationConfigsResult result = + pushNotificationConfigStore.getInfo(params); - assertTrue(pageCount <= 20, "Should not have more than 20 pages for 15 configs"); + result.configs().forEach(c -> allConfigIds.add(c.id())); + pageToken = result.nextPageToken(); + pageCount++; - } while (pageToken != null); + assertTrue(pageCount <= 20, "Should not have more than 20 pages for 15 configs"); - assertEquals(15, allConfigIds.size(), "Should retrieve all 15 configs"); - assertEquals(15, new java.util.HashSet<>(allConfigIds).size(), - "All config IDs should be unique - no duplicates"); + } while (pageToken != null); - assertEquals("cfg14", allConfigIds.get(0), - "First config should be most recently created (DESC order)"); - assertEquals("cfg0", allConfigIds.get(14), - "Last config should be oldest created"); + assertEquals(15, allConfigIds.size(), "Should retrieve all 15 configs"); + assertEquals( + 15, + new java.util.HashSet<>(allConfigIds).size(), + "All config IDs should be unique - no duplicates"); + + assertEquals( + "cfg14", + allConfigIds.get(0), + "First config should be most recently created (DESC order)"); + assertEquals("cfg0", allConfigIds.get(14), "Last config should be oldest created"); } - private void createSamples(String taskId, int size) { + + private void createSamples(String taskId, int size) { // Create configs with slight delays to ensure unique timestamps for deterministic ordering for (int i = 0; i < size; i++) { - TaskPushNotificationConfig config = createSamplePushConfig( - "http://url" + i + ".com/callback", "cfg" + i, "token" + i); - pushNotificationConfigStore.setInfo(TaskPushNotificationConfig.builder(config).taskId(taskId).build()); + TaskPushNotificationConfig config = + createSamplePushConfig( + "http://url" + i + ".com/callback", "cfg" + i, "token" + i); + pushNotificationConfigStore.setInfo( + TaskPushNotificationConfig.builder(config).taskId(taskId).build()); // Sleep briefly to ensure each config gets a unique timestamp // This prevents non-deterministic ordering in pagination tests @@ -587,24 +638,33 @@ private void createSamples(String taskId, int size) { @Transactional public void testPushConfigLimitEnforced() { String taskId = "task_limit_" + System.currentTimeMillis(); - int limit = org.a2aproject.sdk.server.tasks.PushNotificationConfigStore.DEFAULT_MAX_PUSH_CONFIGS_PER_TASK; + int limit = + org.a2aproject.sdk.server.tasks.PushNotificationConfigStore + .DEFAULT_MAX_PUSH_CONFIGS_PER_TASK; // Register configs up to the limit. for (int i = 0; i < limit; i++) { - TaskPushNotificationConfig config = createSamplePushConfig( - "http://limit" + i + ".com/callback", "limit-cfg" + i, "token" + i); - pushNotificationConfigStore.setInfo(TaskPushNotificationConfig.builder(config).taskId(taskId).build()); + TaskPushNotificationConfig config = + createSamplePushConfig( + "http://limit" + i + ".com/callback", "limit-cfg" + i, "token" + i); + pushNotificationConfigStore.setInfo( + TaskPushNotificationConfig.builder(config).taskId(taskId).build()); } // The (limit+1)-th distinct config must be rejected. - TaskPushNotificationConfig extra = createSamplePushConfig( - "http://extra.com/callback", "limit-extra", "token-extra"); - assertThrows(InvalidParamsError.class, () -> - pushNotificationConfigStore.setInfo(TaskPushNotificationConfig.builder(extra).taskId(taskId).build())); + TaskPushNotificationConfig extra = + createSamplePushConfig("http://extra.com/callback", "limit-extra", "token-extra"); + assertThrows( + InvalidParamsError.class, + () -> + pushNotificationConfigStore.setInfo( + TaskPushNotificationConfig.builder(extra).taskId(taskId).build())); // Re-registering an existing config ID at the limit is still allowed. - TaskPushNotificationConfig existing = createSamplePushConfig( - "http://limit0.com/callback-updated", "limit-cfg0", "token-updated"); - pushNotificationConfigStore.setInfo(TaskPushNotificationConfig.builder(existing).taskId(taskId).build()); + TaskPushNotificationConfig existing = + createSamplePushConfig( + "http://limit0.com/callback-updated", "limit-cfg0", "token-updated"); + pushNotificationConfigStore.setInfo( + TaskPushNotificationConfig.builder(existing).taskId(taskId).build()); } } diff --git a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreTestAgentCardProducer.java b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreTestAgentCardProducer.java index 3e55a2aa4..40cec30e0 100644 --- a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreTestAgentCardProducer.java +++ b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreTestAgentCardProducer.java @@ -1,17 +1,15 @@ package org.a2aproject.sdk.extras.pushnotificationconfigstore.database.jpa; -import java.util.Collections; -import java.util.List; - +import io.quarkus.arc.profile.IfBuildProfile; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - +import java.util.Collections; +import java.util.List; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.TransportProtocol; -import io.quarkus.arc.profile.IfBuildProfile; /** * Simple test AgentCard producer for our integration test. @@ -30,13 +28,17 @@ public AgentCard agentCard() { .version("1.0.0") .supportedInterfaces( Collections.singletonList( - new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:8081"))) // Port is managed by QuarkusTest + new AgentInterface( + TransportProtocol.JSONRPC.asString(), + "http://localhost:8081"))) // Port is managed by QuarkusTest .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) - .capabilities(AgentCapabilities.builder() - .pushNotifications(true) // Enable push notifications - .streaming(true) // Enable streaming for automatic push notifications - .build()) + .capabilities( + AgentCapabilities.builder() + .pushNotifications(true) // Enable push notifications + .streaming( + true) // Enable streaming for automatic push notifications + .build()) .skills(List.of()) .build(); } diff --git a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreTestAgentExecutor.java b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreTestAgentExecutor.java index 3ecbe53c6..bc7f83233 100644 --- a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreTestAgentExecutor.java +++ b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreTestAgentExecutor.java @@ -1,11 +1,10 @@ package org.a2aproject.sdk.extras.pushnotificationconfigstore.database.jpa; -import java.util.List; - +import io.quarkus.arc.profile.IfBuildProfile; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; - +import java.util.List; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; @@ -15,7 +14,6 @@ import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.Part; import org.a2aproject.sdk.spec.TextPart; -import io.quarkus.arc.profile.IfBuildProfile; /** * Simple test AgentExecutor that updates the task, which in turn @@ -25,8 +23,7 @@ @ApplicationScoped public class JpaDatabasePushNotificationConfigStoreTestAgentExecutor { - @Inject - PushNotificationSender pushNotificationSender; + @Inject PushNotificationSender pushNotificationSender; @Produces public AgentExecutor agentExecutor() { @@ -43,7 +40,8 @@ public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2 case "update": // Perform a meaningful update, like adding an artifact. // This state change is what will trigger the notification. - agentEmitter.addArtifact(List.of(new TextPart("updated-artifact")), "art-1", "test", null); + agentEmitter.addArtifact( + List.of(new TextPart("updated-artifact")), "art-1", "test", null); break; default: // On the first message (which might have no text), just submit. diff --git a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java index 4c96f50f7..43665a76a 100644 --- a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java +++ b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/JpaPushNotificationConfigStoreTest.java @@ -12,11 +12,10 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.List; - +import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import jakarta.transaction.Transactional; - +import java.util.List; import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.server.tasks.BasePushNotificationSender; @@ -29,7 +28,6 @@ import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.a2aproject.sdk.spec.TaskState; import org.a2aproject.sdk.spec.TaskStatus; -import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -40,25 +38,22 @@ @QuarkusTest public class JpaPushNotificationConfigStoreTest { - @Inject - PushNotificationConfigStore configStore; + @Inject PushNotificationConfigStore configStore; private BasePushNotificationSender notificationSender; - @Mock - private A2AHttpClient mockHttpClient; + @Mock private A2AHttpClient mockHttpClient; - @Mock - private A2AHttpClient.PostBuilder mockPostBuilder; + @Mock private A2AHttpClient.PostBuilder mockPostBuilder; - @Mock - private A2AHttpResponse mockHttpResponse; + @Mock private A2AHttpResponse mockHttpResponse; @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); - notificationSender = new BasePushNotificationSender(configStore, mockHttpClient, - PushNotificationUrlValidator.ALLOW_ALL); + notificationSender = + new BasePushNotificationSender( + configStore, mockHttpClient, PushNotificationUrlValidator.ALLOW_ALL); } @Test @@ -67,34 +62,31 @@ public void testIsJpaDatabasePushNotificationConfigStore() { } private Task createSampleTask(String taskId, TaskState state) { - return Task.builder() - .id(taskId) - .contextId("ctx456") - .status(new TaskStatus(state)) - .build(); + return Task.builder().id(taskId).contextId("ctx456").status(new TaskStatus(state)).build(); } - private TaskPushNotificationConfig createSamplePushConfig(String url, String configId, String token) { - return TaskPushNotificationConfig.builder() - .url(url) - .id(configId) - .token(token) - .build(); + private TaskPushNotificationConfig createSamplePushConfig( + String url, String configId, String token) { + return TaskPushNotificationConfig.builder().url(url).id(configId).token(token).build(); } @Test @Transactional public void testSetInfoAddsNewConfig() { String taskId = "task_new"; - TaskPushNotificationConfig config = createSamplePushConfig("http://new.url/callback", "cfg1", null); + TaskPushNotificationConfig config = + createSamplePushConfig("http://new.url/callback", "cfg1", null); - TaskPushNotificationConfig result = configStore.setInfo(TaskPushNotificationConfig.builder(config).taskId(taskId).build()); + TaskPushNotificationConfig result = + configStore.setInfo( + TaskPushNotificationConfig.builder(config).taskId(taskId).build()); assertNotNull(result); assertEquals(config.url(), result.url()); assertEquals(config.id(), result.id()); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertNotNull(configResult); assertEquals(1, configResult.configs().size()); assertEquals(config.url(), configResult.configs().get(0).url()); @@ -105,28 +97,27 @@ public void testSetInfoAddsNewConfig() { @Transactional public void testSetInfoAppendsToExistingConfig() { String taskId = "task_update"; - TaskPushNotificationConfig initialConfig = createSamplePushConfig( - "http://initial.url/callback", "cfg_initial", null); - configStore.setInfo(TaskPushNotificationConfig.builder(initialConfig).taskId(taskId).build()); + TaskPushNotificationConfig initialConfig = + createSamplePushConfig("http://initial.url/callback", "cfg_initial", null); + configStore.setInfo( + TaskPushNotificationConfig.builder(initialConfig).taskId(taskId).build()); - TaskPushNotificationConfig updatedConfig = createSamplePushConfig( - "http://updated.url/callback", "cfg_updated", null); - configStore.setInfo(TaskPushNotificationConfig.builder(updatedConfig).taskId(taskId).build()); + TaskPushNotificationConfig updatedConfig = + createSamplePushConfig("http://updated.url/callback", "cfg_updated", null); + configStore.setInfo( + TaskPushNotificationConfig.builder(updatedConfig).taskId(taskId).build()); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertNotNull(configResult); assertEquals(2, configResult.configs().size()); // Find the configs by ID since order might vary List configs = configResult.configs(); - TaskPushNotificationConfig foundInitial = configs.stream() - .filter(c -> "cfg_initial".equals(c.id())) - .findFirst() - .orElse(null); - TaskPushNotificationConfig foundUpdated = configs.stream() - .filter(c -> "cfg_updated".equals(c.id())) - .findFirst() - .orElse(null); + TaskPushNotificationConfig foundInitial = + configs.stream().filter(c -> "cfg_initial".equals(c.id())).findFirst().orElse(null); + TaskPushNotificationConfig foundUpdated = + configs.stream().filter(c -> "cfg_updated".equals(c.id())).findFirst().orElse(null); assertNotNull(foundInitial); assertNotNull(foundUpdated); @@ -138,24 +129,27 @@ public void testSetInfoAppendsToExistingConfig() { @Transactional public void testSetInfoWithoutConfigId() { String taskId = "task1"; - TaskPushNotificationConfig initialConfig = TaskPushNotificationConfig.builder() - .id("") - .url("http://initial.url/callback") - .taskId(taskId) - .build(); + TaskPushNotificationConfig initialConfig = + TaskPushNotificationConfig.builder() + .id("") + .url("http://initial.url/callback") + .taskId(taskId) + .build(); TaskPushNotificationConfig result = configStore.setInfo(initialConfig); assertEquals(taskId, result.id(), "Config ID should default to taskId when not provided"); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertEquals(1, configResult.configs().size()); assertEquals(taskId, configResult.configs().get(0).id()); - TaskPushNotificationConfig duplicateConfig = TaskPushNotificationConfig.builder() - .id("") - .url("http://initial.url/callback_new") - .taskId(taskId) - .build(); + TaskPushNotificationConfig duplicateConfig = + TaskPushNotificationConfig.builder() + .id("") + .url("http://initial.url/callback_new") + .taskId(taskId) + .build(); assertThrows(InvalidParamsError.class, () -> configStore.setInfo(duplicateConfig)); @@ -168,23 +162,25 @@ public void testSetInfoWithoutConfigId() { @Transactional public void testSetInfoWithNullConfigId() { String taskId = "task_null_config_id"; - TaskPushNotificationConfig config = TaskPushNotificationConfig.builder() - .url("http://null-id.url/callback") - .taskId(taskId) - .build(); + TaskPushNotificationConfig config = + TaskPushNotificationConfig.builder() + .url("http://null-id.url/callback") + .taskId(taskId) + .build(); TaskPushNotificationConfig result = configStore.setInfo(config); assertEquals(taskId, result.id(), "A missing config ID should default to the task ID"); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo( - new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertEquals(1, configResult.configs().size()); assertEquals(taskId, configResult.configs().get(0).id()); - TaskPushNotificationConfig duplicateConfig = TaskPushNotificationConfig.builder() - .url("http://updated.url/callback") - .taskId(taskId) - .build(); + TaskPushNotificationConfig duplicateConfig = + TaskPushNotificationConfig.builder() + .url("http://updated.url/callback") + .taskId(taskId) + .build(); assertThrows(InvalidParamsError.class, () -> configStore.setInfo(duplicateConfig)); } @@ -193,14 +189,16 @@ public void testSetInfoWithNullConfigId() { @Transactional public void testSetInfoAllowsV03DefaultConfigUpdate() { String taskId = "task_v03_default_update"; - TaskPushNotificationConfig initialConfig = TaskPushNotificationConfig.builder() - .taskId(taskId) - .url("http://initial.url/callback") - .build(); - TaskPushNotificationConfig updatedConfig = TaskPushNotificationConfig.builder() - .taskId(taskId) - .url("http://updated.url/callback") - .build(); + TaskPushNotificationConfig initialConfig = + TaskPushNotificationConfig.builder() + .taskId(taskId) + .url("http://initial.url/callback") + .build(); + TaskPushNotificationConfig updatedConfig = + TaskPushNotificationConfig.builder() + .taskId(taskId) + .url("http://updated.url/callback") + .build(); configStore.setInfo(initialConfig, "0.3"); TaskPushNotificationConfig result = configStore.setInfo(updatedConfig, "0.3"); @@ -216,10 +214,12 @@ public void testSetInfoAllowsV03DefaultConfigUpdate() { @Transactional public void testGetInfoExistingConfig() { String taskId = "task_get_exist"; - TaskPushNotificationConfig config = createSamplePushConfig("http://get.this/callback", "cfg1", null); + TaskPushNotificationConfig config = + createSamplePushConfig("http://get.this/callback", "cfg1", null); configStore.setInfo(TaskPushNotificationConfig.builder(config).taskId(taskId).build()); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertNotNull(configResult); assertEquals(1, configResult.configs().size()); assertEquals(config.url(), configResult.configs().get(0).url()); @@ -230,27 +230,35 @@ public void testGetInfoExistingConfig() { @Transactional public void testGetInfoNonExistentConfig() { String taskId = "task_get_non_exist"; - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertNotNull(configResult); - assertTrue(configResult.configs().isEmpty(), "Should return empty list for non-existent task ID"); + assertTrue( + configResult.configs().isEmpty(), + "Should return empty list for non-existent task ID"); } @Test @Transactional public void testDeleteInfoExistingConfig() { String taskId = "task_delete_exist"; - TaskPushNotificationConfig config = createSamplePushConfig("http://delete.this/callback", "cfg1", null); + TaskPushNotificationConfig config = + createSamplePushConfig("http://delete.this/callback", "cfg1", null); configStore.setInfo(TaskPushNotificationConfig.builder(config).taskId(taskId).build()); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertNotNull(configResult); assertEquals(1, configResult.configs().size()); configStore.deleteInfo(taskId, config.id()); - ListTaskPushNotificationConfigsResult configsAfterDelete = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configsAfterDelete = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertNotNull(configsAfterDelete); - assertTrue(configsAfterDelete.configs().isEmpty(), "Should return empty list when no configs remain after deletion"); + assertTrue( + configsAfterDelete.configs().isEmpty(), + "Should return empty list when no configs remain after deletion"); } @Test @@ -260,35 +268,43 @@ public void testDeleteInfoNonExistentConfig() { // Should not throw an error configStore.deleteInfo(taskId, "non_existent_id"); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertNotNull(configResult); - assertTrue(configResult.configs().isEmpty(), "Should return empty list for non-existent task ID"); + assertTrue( + configResult.configs().isEmpty(), + "Should return empty list for non-existent task ID"); } @Test @Transactional public void testDeleteInfoWithNullConfigId() { String taskId = "task_delete_null_config"; - TaskPushNotificationConfig config = TaskPushNotificationConfig.builder() - .id("") - .url("http://delete.this/callback") - .taskId(taskId) - .build(); + TaskPushNotificationConfig config = + TaskPushNotificationConfig.builder() + .id("") + .url("http://delete.this/callback") + .taskId(taskId) + .build(); configStore.setInfo(config); // Delete with null configId should use taskId configStore.deleteInfo(taskId, null); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertNotNull(configResult); - assertTrue(configResult.configs().isEmpty(), "Should return empty list after deletion when using taskId as configId"); + assertTrue( + configResult.configs().isEmpty(), + "Should return empty list after deletion when using taskId as configId"); } private void setupBasicMockHttpResponse() throws Exception { when(mockHttpClient.createPost()).thenReturn(mockPostBuilder); when(mockPostBuilder.followRedirects(false)).thenReturn(mockPostBuilder); when(mockPostBuilder.url(any(String.class))).thenReturn(mockPostBuilder); - when(mockPostBuilder.addHeader(any(String.class), any(String.class))).thenReturn(mockPostBuilder); + when(mockPostBuilder.addHeader(any(String.class), any(String.class))) + .thenReturn(mockPostBuilder); when(mockPostBuilder.body(any(String.class))).thenReturn(mockPostBuilder); when(mockPostBuilder.post()).thenReturn(mockHttpResponse); when(mockHttpResponse.success()).thenReturn(true); @@ -299,7 +315,8 @@ private void setupBasicMockHttpResponse() throws Exception { public void testSendNotificationSuccess() throws Exception { String taskId = "task_send_success"; Task task = createSampleTask(taskId, TaskState.TASK_STATE_COMPLETED); - TaskPushNotificationConfig config = createSamplePushConfig("http://notify.me/here", "cfg1", null); + TaskPushNotificationConfig config = + createSamplePushConfig("http://notify.me/here", "cfg1", null); configStore.setInfo(TaskPushNotificationConfig.builder(config).taskId(taskId).build()); setupBasicMockHttpResponse(); @@ -323,11 +340,13 @@ public void testSendNotificationSuccess() throws Exception { @Test @Transactional - @Disabled("Token authentication is not yet implemented in BasePushNotificationSender (TODO auth)") + @Disabled( + "Token authentication is not yet implemented in BasePushNotificationSender (TODO auth)") public void testSendNotificationWithToken() throws Exception { String taskId = "task_send_with_token"; Task task = createSampleTask(taskId, TaskState.TASK_STATE_COMPLETED); - TaskPushNotificationConfig config = createSamplePushConfig("http://notify.me/here", "cfg1", "unique_token"); + TaskPushNotificationConfig config = + createSamplePushConfig("http://notify.me/here", "cfg1", "unique_token"); configStore.setInfo(TaskPushNotificationConfig.builder(config).taskId(taskId).build()); setupBasicMockHttpResponse(); @@ -368,13 +387,16 @@ public void testSendNotificationNoConfig() throws Exception { @Transactional public void testMultipleConfigsForSameTask() { String taskId = "task_multiple"; - TaskPushNotificationConfig config1 = createSamplePushConfig("http://url1.com/callback", "cfg1", null); - TaskPushNotificationConfig config2 = createSamplePushConfig("http://url2.com/callback", "cfg2", null); + TaskPushNotificationConfig config1 = + createSamplePushConfig("http://url1.com/callback", "cfg1", null); + TaskPushNotificationConfig config2 = + createSamplePushConfig("http://url2.com/callback", "cfg2", null); configStore.setInfo(TaskPushNotificationConfig.builder(config1).taskId(taskId).build()); configStore.setInfo(TaskPushNotificationConfig.builder(config2).taskId(taskId).build()); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertNotNull(configResult); assertEquals(2, configResult.configs().size()); @@ -387,8 +409,10 @@ public void testMultipleConfigsForSameTask() { @Transactional public void testDeleteSpecificConfigFromMultiple() { String taskId = "task_delete_specific"; - TaskPushNotificationConfig config1 = createSamplePushConfig("http://url1.com/callback", "cfg1", null); - TaskPushNotificationConfig config2 = createSamplePushConfig("http://url2.com/callback", "cfg2", null); + TaskPushNotificationConfig config1 = + createSamplePushConfig("http://url1.com/callback", "cfg1", null); + TaskPushNotificationConfig config2 = + createSamplePushConfig("http://url2.com/callback", "cfg2", null); configStore.setInfo(TaskPushNotificationConfig.builder(config1).taskId(taskId).build()); configStore.setInfo(TaskPushNotificationConfig.builder(config2).taskId(taskId).build()); @@ -396,7 +420,8 @@ public void testDeleteSpecificConfigFromMultiple() { // Delete only config1 configStore.deleteInfo(taskId, "cfg1"); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertNotNull(configResult); assertEquals(1, configResult.configs().size()); assertEquals("cfg2", configResult.configs().get(0).id()); @@ -406,20 +431,25 @@ public void testDeleteSpecificConfigFromMultiple() { @Transactional public void testConfigStoreIntegration() { String taskId = "integration_test"; - TaskPushNotificationConfig config = createSamplePushConfig("http://example.com", "test_id", "test_token"); + TaskPushNotificationConfig config = + createSamplePushConfig("http://example.com", "test_id", "test_token"); // Test that we can store and retrieve configurations - TaskPushNotificationConfig storedConfig = configStore.setInfo(TaskPushNotificationConfig.builder(config).taskId(taskId).build()); + TaskPushNotificationConfig storedConfig = + configStore.setInfo( + TaskPushNotificationConfig.builder(config).taskId(taskId).build()); assertEquals(config.url(), storedConfig.url()); assertEquals(config.token(), storedConfig.token()); - ListTaskPushNotificationConfigsResult configResult = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult configResult = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertEquals(1, configResult.configs().size()); assertEquals(config.url(), configResult.configs().get(0).url()); // Test deletion configStore.deleteInfo(taskId, storedConfig.id()); - ListTaskPushNotificationConfigsResult afterDeletion = configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); + ListTaskPushNotificationConfigsResult afterDeletion = + configStore.getInfo(new ListTaskPushNotificationConfigsParams(taskId)); assertTrue(afterDeletion.configs().isEmpty()); } } diff --git a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/MockPushNotificationSender.java b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/MockPushNotificationSender.java index 2460f20c7..563c699d6 100644 --- a/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/MockPushNotificationSender.java +++ b/extras/push-notification-config-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/pushnotificationconfigstore/database/jpa/MockPushNotificationSender.java @@ -1,12 +1,10 @@ package org.a2aproject.sdk.extras.pushnotificationconfigstore.database.jpa; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; - import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Alternative; - +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import org.a2aproject.sdk.server.tasks.PushNotificationSender; import org.a2aproject.sdk.spec.StreamingEventKind; import org.a2aproject.sdk.spec.Task; @@ -43,9 +41,9 @@ public Queue getCapturedEvents() { public Queue getCapturedTasks() { Queue tasks = new ConcurrentLinkedQueue<>(); capturedEvents.stream() - .filter(e -> e instanceof Task) - .map(e -> (Task) e) - .forEach(tasks::add); + .filter(e -> e instanceof Task) + .map(e -> (Task) e) + .forEach(tasks::add); return tasks; } diff --git a/extras/queue-manager-replicated/core/pom.xml b/extras/queue-manager-replicated/core/pom.xml index 734596b6c..f40512cbc 100644 --- a/extras/queue-manager-replicated/core/pom.xml +++ b/extras/queue-manager-replicated/core/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 @@ -57,4 +55,4 @@ - \ No newline at end of file + diff --git a/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java b/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java index 0e6eb9cc3..eed2e0f37 100644 --- a/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java +++ b/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedEventQueueItem.java @@ -15,8 +15,7 @@ public class ReplicatedEventQueueItem implements EventQueueItem { private boolean closedEvent; // Default constructor for JSON deserialization - public ReplicatedEventQueueItem() { - } + public ReplicatedEventQueueItem() {} // Constructor for creating from A2A StreamingEventKind objects public ReplicatedEventQueueItem(String taskId, StreamingEventKind event) { @@ -48,11 +47,12 @@ public ReplicatedEventQueueItem(String taskId, Event event) { this.error = jsonRpcError; this.closedEvent = false; } else { - throw new IllegalArgumentException("Event must be StreamingEventKind, A2AError, or QueueClosedEvent, got: " + event.getClass()); + throw new IllegalArgumentException( + "Event must be StreamingEventKind, A2AError, or QueueClosedEvent, got: " + + event.getClass()); } } - public String getTaskId() { return taskId; } @@ -161,11 +161,16 @@ public boolean isTaskEvent() { @Override public String toString() { - return "ReplicatedEventQueueItem{" + - "taskId='" + taskId + '\'' + - ", event=" + event + - ", error=" + error + - ", closedEvent=" + closedEvent + - '}'; - } -} \ No newline at end of file + return "ReplicatedEventQueueItem{" + + "taskId='" + + taskId + + '\'' + + ", event=" + + event + + ", error=" + + error + + ", closedEvent=" + + closedEvent + + '}'; + } +} diff --git a/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedQueueManager.java b/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedQueueManager.java index 1f1565563..1af6f70c9 100644 --- a/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedQueueManager.java +++ b/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedQueueManager.java @@ -6,7 +6,6 @@ import jakarta.enterprise.event.TransactionPhase; import jakarta.enterprise.inject.Alternative; import jakarta.inject.Inject; - import org.a2aproject.sdk.extras.common.events.TaskFinalizedEvent; import org.a2aproject.sdk.server.events.DefaultTaskStreamLifecycleHook; import org.a2aproject.sdk.server.events.EventEnqueueHook; @@ -49,25 +48,34 @@ protected ReplicatedQueueManager() { this.streamLifecycleHook = null; } - public ReplicatedQueueManager(ReplicationStrategy replicationStrategy, - TaskStateProvider taskStateProvider, - MainEventBus mainEventBus) { - this(replicationStrategy, taskStateProvider, mainEventBus, new DefaultTaskStreamLifecycleHook()); + public ReplicatedQueueManager( + ReplicationStrategy replicationStrategy, + TaskStateProvider taskStateProvider, + MainEventBus mainEventBus) { + this( + replicationStrategy, + taskStateProvider, + mainEventBus, + new DefaultTaskStreamLifecycleHook()); } @Inject - public ReplicatedQueueManager(ReplicationStrategy replicationStrategy, - TaskStateProvider taskStateProvider, - MainEventBus mainEventBus, - TaskStreamLifecycleHook streamLifecycleHook) { + public ReplicatedQueueManager( + ReplicationStrategy replicationStrategy, + TaskStateProvider taskStateProvider, + MainEventBus mainEventBus, + TaskStreamLifecycleHook streamLifecycleHook) { this.replicationStrategy = replicationStrategy; this.taskStateProvider = taskStateProvider; this.streamLifecycleHook = streamLifecycleHook; - this.delegate = new InMemoryQueueManager( - new ReplicatingEventQueueFactory(), taskStateProvider, mainEventBus, streamLifecycleHook); + this.delegate = + new InMemoryQueueManager( + new ReplicatingEventQueueFactory(), + taskStateProvider, + mainEventBus, + streamLifecycleHook); } - @Override public void add(String taskId, EventQueue queue) { delegate.add(taskId, queue); @@ -109,8 +117,10 @@ public void onReplicatedEvent(@Observes ReplicatedEventQueueItem replicatedEvent && !replicatedEvent.isTaskEvent() && !taskStateProvider.isTaskActive(replicatedEvent.getTaskId())) { // Task is no longer active - skip processing this replicated event - // This prevents creating queues for tasks that have been finalized beyond the grace period - LOGGER.debug("Skipping replicated event for inactive task {}", replicatedEvent.getTaskId()); + // This prevents creating queues for tasks that have been finalized beyond the grace + // period + LOGGER.debug( + "Skipping replicated event for inactive task {}", replicatedEvent.getTaskId()); return; } @@ -125,26 +135,32 @@ public void onReplicatedEvent(@Observes ReplicatedEventQueueItem replicatedEvent // // If MainQueue doesn't exist, create it. This handles late-arriving replicated events // for tasks that were created on another instance. - EventQueue childQueue = null; // Track ChildQueue we might create + EventQueue childQueue = null; // Track ChildQueue we might create EventQueue mainQueue = delegate.get(replicatedEvent.getTaskId()); try { if (mainQueue == null) { - LOGGER.debug("Creating MainQueue for replicated event on task {}", replicatedEvent.getTaskId()); - childQueue = delegate.createOrTap(replicatedEvent.getTaskId()); // Creates MainQueue + returns ChildQueue - mainQueue = delegate.get(replicatedEvent.getTaskId()); // Get MainQueue from map + LOGGER.debug( + "Creating MainQueue for replicated event on task {}", + replicatedEvent.getTaskId()); + childQueue = + delegate.createOrTap( + replicatedEvent + .getTaskId()); // Creates MainQueue + returns ChildQueue + mainQueue = delegate.get(replicatedEvent.getTaskId()); // Get MainQueue from map } if (mainQueue != null) { mainQueue.enqueueItem(replicatedEvent); } else { LOGGER.warn( - "MainQueue not found for task {}, cannot enqueue replicated event. This may happen if the queue was already cleaned up.", + "MainQueue not found for task {}, cannot enqueue replicated event. This may" + + " happen if the queue was already cleaned up.", replicatedEvent.getTaskId()); } } finally { if (childQueue != null) { try { - childQueue.close(); // Close the ChildQueue we created (not MainQueue!) + childQueue.close(); // Close the ChildQueue we created (not MainQueue!) } catch (Exception ignore) { // The close is safe, but print a stacktrace just in case if (LOGGER.isDebugEnabled()) { @@ -170,33 +186,39 @@ public void onReplicatedEvent(@Observes ReplicatedEventQueueItem replicatedEvent * * @param event the task finalized event containing the task ID and final Task */ - public void onTaskFinalized(@Observes(during = TransactionPhase.AFTER_SUCCESS) TaskFinalizedEvent event) { + public void onTaskFinalized( + @Observes(during = TransactionPhase.AFTER_SUCCESS) TaskFinalizedEvent event) { String taskId = event.getTaskId(); - org.a2aproject.sdk.spec.Task finalTask = (org.a2aproject.sdk.spec.Task) event.getTask(); // Cast from Object + org.a2aproject.sdk.spec.Task finalTask = + (org.a2aproject.sdk.spec.Task) event.getTask(); // Cast from Object - LOGGER.debug("Task {} finalized - sending TaskStatusUpdateEvent then poison pill (QueueClosedEvent) after transaction commit", taskId); + LOGGER.debug( + "Task {} finalized - sending TaskStatusUpdateEvent then poison pill" + + " (QueueClosedEvent) after transaction commit", + taskId); // Convert final Task to TaskStatusUpdateEvent to match local event distribution // This ensures remote instances receive the same event type as local instances - org.a2aproject.sdk.spec.TaskStatusUpdateEvent finalStatusEvent = org.a2aproject.sdk.spec.TaskStatusUpdateEvent.builder() - .taskId(taskId) - .contextId(finalTask.contextId()) - .status(finalTask.status()) - .build(); + org.a2aproject.sdk.spec.TaskStatusUpdateEvent finalStatusEvent = + org.a2aproject.sdk.spec.TaskStatusUpdateEvent.builder() + .taskId(taskId) + .contextId(finalTask.contextId()) + .status(finalTask.status()) + .build(); // Send TaskStatusUpdateEvent FIRST to ensure it arrives before poison pill replicationStrategy.send(taskId, finalStatusEvent); // Then send poison pill // The transaction has committed, so the final state is guaranteed to be in the database - org.a2aproject.sdk.server.events.QueueClosedEvent closedEvent = new org.a2aproject.sdk.server.events.QueueClosedEvent(taskId); + org.a2aproject.sdk.server.events.QueueClosedEvent closedEvent = + new org.a2aproject.sdk.server.events.QueueClosedEvent(taskId); replicationStrategy.send(taskId, closedEvent); } @Override public EventQueue.EventQueueBuilder getEventQueueBuilder(String taskId) { - return QueueManager.super.getEventQueueBuilder(taskId) - .hook(new ReplicationHook(taskId)); + return QueueManager.super.getEventQueueBuilder(taskId).hook(new ReplicationHook(taskId)); } @Override @@ -213,13 +235,12 @@ public EventQueue.EventQueueBuilder builder(String taskId) { // Call createBaseEventQueueBuilder() directly to avoid infinite recursion // (getEventQueueBuilder() would delegate back to this factory, creating a loop) - // The base builder already includes: taskId, cleanup callback, taskStateProvider, mainEventBus - return delegate.createBaseEventQueueBuilder(taskId) - .hook(new ReplicationHook(taskId)); + // The base builder already includes: taskId, cleanup callback, taskStateProvider, + // mainEventBus + return delegate.createBaseEventQueueBuilder(taskId).hook(new ReplicationHook(taskId)); } } - private class ReplicationHook implements EventEnqueueHook { private final String taskId; @@ -238,4 +259,4 @@ public void onEnqueue(EventQueueItem item) { } } } -} \ No newline at end of file +} diff --git a/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicationStrategy.java b/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicationStrategy.java index 965bcaafd..0cb111f99 100644 --- a/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicationStrategy.java +++ b/extras/queue-manager-replicated/core/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicationStrategy.java @@ -4,4 +4,4 @@ public interface ReplicationStrategy { void send(String taskId, Event event); -} \ No newline at end of file +} diff --git a/extras/queue-manager-replicated/core/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/EventSerializationTest.java b/extras/queue-manager-replicated/core/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/EventSerializationTest.java index ead3a5d7a..c1b591933 100644 --- a/extras/queue-manager-replicated/core/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/EventSerializationTest.java +++ b/extras/queue-manager-replicated/core/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/EventSerializationTest.java @@ -8,7 +8,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; - import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.server.events.QueueClosedEvent; @@ -45,11 +44,12 @@ public class EventSerializationTest { public void testTaskSerialization() throws JsonProcessingException { // Create a Task TaskStatus status = new TaskStatus(TaskState.TASK_STATE_SUBMITTED); - Task originalTask = Task.builder() - .id("test-task-123") - .contextId("test-context-456") - .status(status) - .build(); + Task originalTask = + Task.builder() + .id("test-task-123") + .contextId("test-context-456") + .status(status) + .build(); // Test streaming-event serialization String json = JsonUtil.toJsonStreamingEvent(originalTask); @@ -67,20 +67,25 @@ public void testTaskSerialization() throws JsonProcessingException { assertEquals(originalTask.status().state(), deserializedTask.status().state()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); - assertInstanceOf(Task.class, deserializedAsStreaming, "Should deserialize to Task as StreamingEventKind"); + StreamingEventKind deserializedAsStreaming = + JsonUtil.fromJson(json, StreamingEventKind.class); + assertInstanceOf( + Task.class, + deserializedAsStreaming, + "Should deserialize to Task as StreamingEventKind"); } @Test public void testMessageSerialization() throws JsonProcessingException { // Create a Message - Message originalMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("Hello, world!"))) - .taskId("test-task-789") - .messageId("test-msg-456") - .contextId("test-context-123") - .build(); + Message originalMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("Hello, world!"))) + .taskId("test-task-789") + .messageId("test-msg-456") + .contextId("test-context-123") + .build(); // Test streaming-event serialization String json = JsonUtil.toJsonStreamingEvent(originalMessage); @@ -98,19 +103,24 @@ public void testMessageSerialization() throws JsonProcessingException { assertEquals(originalMessage.parts().size(), deserializedMessage.parts().size()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); - assertInstanceOf(Message.class, deserializedAsStreaming, "Should deserialize to Message as StreamingEventKind"); + StreamingEventKind deserializedAsStreaming = + JsonUtil.fromJson(json, StreamingEventKind.class); + assertInstanceOf( + Message.class, + deserializedAsStreaming, + "Should deserialize to Message as StreamingEventKind"); } @Test public void testTaskStatusUpdateEventSerialization() throws JsonProcessingException { // Create a TaskStatusUpdateEvent TaskStatus status = new TaskStatus(TaskState.TASK_STATE_COMPLETED); - TaskStatusUpdateEvent originalEvent = TaskStatusUpdateEvent.builder() - .taskId("test-task-abc") - .contextId("test-context-def") - .status(status) - .build(); + TaskStatusUpdateEvent originalEvent = + TaskStatusUpdateEvent.builder() + .taskId("test-task-abc") + .contextId("test-context-def") + .status(status) + .build(); // Test streaming-event serialization String json = JsonUtil.toJsonStreamingEvent(originalEvent); @@ -120,7 +130,10 @@ public void testTaskStatusUpdateEventSerialization() throws JsonProcessingExcept // Test deserialization back to StreamingEventKind StreamingEventKind deserializedEvent = JsonUtil.fromJson(json, StreamingEventKind.class); - assertInstanceOf(TaskStatusUpdateEvent.class, deserializedEvent, "Should deserialize to TaskStatusUpdateEvent"); + assertInstanceOf( + TaskStatusUpdateEvent.class, + deserializedEvent, + "Should deserialize to TaskStatusUpdateEvent"); TaskStatusUpdateEvent deserializedStatusEvent = (TaskStatusUpdateEvent) deserializedEvent; assertEquals(originalEvent.taskId(), deserializedStatusEvent.taskId()); @@ -130,41 +143,64 @@ public void testTaskStatusUpdateEventSerialization() throws JsonProcessingExcept assertEquals(originalEvent.isFinal(), deserializedStatusEvent.isFinal()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); - assertInstanceOf(TaskStatusUpdateEvent.class, deserializedAsStreaming, "Should deserialize to TaskStatusUpdateEvent as StreamingEventKind"); + StreamingEventKind deserializedAsStreaming = + JsonUtil.fromJson(json, StreamingEventKind.class); + assertInstanceOf( + TaskStatusUpdateEvent.class, + deserializedAsStreaming, + "Should deserialize to TaskStatusUpdateEvent as StreamingEventKind"); } @Test public void testTaskArtifactUpdateEventSerialization() throws JsonProcessingException { // Create a TaskArtifactUpdateEvent List> parts = List.of(new TextPart("Test artifact content")); - Artifact artifact = new Artifact("test-artifact-123", "Test Artifact", "Test description", parts, null, null); - TaskArtifactUpdateEvent originalEvent = TaskArtifactUpdateEvent.builder() - .taskId("test-task-xyz") - .contextId("test-context-uvw") - .artifact(artifact) - .build(); + Artifact artifact = + new Artifact( + "test-artifact-123", + "Test Artifact", + "Test description", + parts, + null, + null); + TaskArtifactUpdateEvent originalEvent = + TaskArtifactUpdateEvent.builder() + .taskId("test-task-xyz") + .contextId("test-context-uvw") + .artifact(artifact) + .build(); // Test streaming-event serialization String json = JsonUtil.toJsonStreamingEvent(originalEvent); - assertTrue(json.contains("\"artifactUpdate\""), "JSON should contain artifactUpdate wrapper"); + assertTrue( + json.contains("\"artifactUpdate\""), "JSON should contain artifactUpdate wrapper"); assertTrue(json.contains("\"taskId\":\"test-task-xyz\""), "JSON should contain task ID"); assertTrue(json.contains("\"test-artifact-123\""), "JSON should contain artifact ID"); // Test deserialization back to StreamingEventKind StreamingEventKind deserializedEvent = JsonUtil.fromJson(json, StreamingEventKind.class); - assertInstanceOf(TaskArtifactUpdateEvent.class, deserializedEvent, "Should deserialize to TaskArtifactUpdateEvent"); + assertInstanceOf( + TaskArtifactUpdateEvent.class, + deserializedEvent, + "Should deserialize to TaskArtifactUpdateEvent"); - TaskArtifactUpdateEvent deserializedArtifactEvent = (TaskArtifactUpdateEvent) deserializedEvent; + TaskArtifactUpdateEvent deserializedArtifactEvent = + (TaskArtifactUpdateEvent) deserializedEvent; assertEquals(originalEvent.taskId(), deserializedArtifactEvent.taskId()); assertEquals(originalEvent.kind(), deserializedArtifactEvent.kind()); assertEquals(originalEvent.contextId(), deserializedArtifactEvent.contextId()); - assertEquals(originalEvent.artifact().artifactId(), deserializedArtifactEvent.artifact().artifactId()); + assertEquals( + originalEvent.artifact().artifactId(), + deserializedArtifactEvent.artifact().artifactId()); assertEquals(originalEvent.artifact().name(), deserializedArtifactEvent.artifact().name()); // Test as StreamingEventKind - StreamingEventKind deserializedAsStreaming = JsonUtil.fromJson(json, StreamingEventKind.class); - assertInstanceOf(TaskArtifactUpdateEvent.class, deserializedAsStreaming, "Should deserialize to TaskArtifactUpdateEvent as StreamingEventKind"); + StreamingEventKind deserializedAsStreaming = + JsonUtil.fromJson(json, StreamingEventKind.class); + assertInstanceOf( + TaskArtifactUpdateEvent.class, + deserializedAsStreaming, + "Should deserialize to TaskArtifactUpdateEvent as StreamingEventKind"); } @Test @@ -180,51 +216,74 @@ public void testA2AErrorSubclassesSerialization() throws JsonProcessingException new TaskNotCancelableError(), new UnsupportedOperationError(), new PushNotificationNotSupportedError() - // Note: ContentTypeNotSupportedError and InvalidAgentResponseError need specific constructor parameters + // Note: ContentTypeNotSupportedError and InvalidAgentResponseError need specific + // constructor parameters }; for (A2AError originalError : errors) { // Test serialization String json = JsonUtil.toJson(originalError); - assertTrue(json.contains("\"message\""), "JSON should contain error message for " + originalError.getClass().getSimpleName()); + assertTrue( + json.contains("\"message\""), + "JSON should contain error message for " + + originalError.getClass().getSimpleName()); // Test deserialization - it's acceptable to deserialize as base A2AError A2AError deserializedError = JsonUtil.fromJson(json, A2AError.class); - assertNotNull(deserializedError, "Should deserialize successfully for " + originalError.getClass().getSimpleName()); - assertEquals(originalError.getMessage(), deserializedError.getMessage(), "Error message should match for " + originalError.getClass().getSimpleName()); - assertEquals(originalError.getCode(), deserializedError.getCode(), "Error code should match for " + originalError.getClass().getSimpleName()); - - // The deserialized error might be the base class, which is acceptable per the requirements + assertNotNull( + deserializedError, + "Should deserialize successfully for " + + originalError.getClass().getSimpleName()); + assertEquals( + originalError.getMessage(), + deserializedError.getMessage(), + "Error message should match for " + originalError.getClass().getSimpleName()); + assertEquals( + originalError.getCode(), + deserializedError.getCode(), + "Error code should match for " + originalError.getClass().getSimpleName()); + + // The deserialized error might be the base class, which is acceptable per the + // requirements } } @Test - public void testReplicatedEventWithStreamingEventSerialization() throws JsonProcessingException { + public void testReplicatedEventWithStreamingEventSerialization() + throws JsonProcessingException { // Test that ReplicatedEventQueueItem can properly handle StreamingEventKind - TaskStatusUpdateEvent statusEvent = TaskStatusUpdateEvent.builder() - .taskId("replicated-test-task") - .contextId("replicated-test-context") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + TaskStatusUpdateEvent statusEvent = + TaskStatusUpdateEvent.builder() + .taskId("replicated-test-task") + .contextId("replicated-test-context") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); // Create ReplicatedEventQueueItem with StreamingEventKind - ReplicatedEventQueueItem originalReplicatedEvent = new ReplicatedEventQueueItem("replicated-test-task", statusEvent); + ReplicatedEventQueueItem originalReplicatedEvent = + new ReplicatedEventQueueItem("replicated-test-task", statusEvent); // Serialize the ReplicatedEventQueueItem String json = JsonUtil.toJson(originalReplicatedEvent); - assertTrue(json.contains("\"taskId\":\"replicated-test-task\""), "JSON should contain task ID"); + assertTrue( + json.contains("\"taskId\":\"replicated-test-task\""), + "JSON should contain task ID"); assertTrue(json.contains("\"event\""), "JSON should contain event field"); assertTrue(json.contains("\"statusUpdate\""), "JSON should contain the event type wrapper"); assertFalse(json.contains("\"error\""), "JSON should not contain error field"); // Deserialize the ReplicatedEventQueueItem - ReplicatedEventQueueItem deserializedReplicatedEvent = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserializedReplicatedEvent = + JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertEquals(originalReplicatedEvent.getTaskId(), deserializedReplicatedEvent.getTaskId()); // Now we should get the proper type back! Event retrievedEventAsEvent = deserializedReplicatedEvent.getEvent(); assertNotNull(retrievedEventAsEvent); - assertInstanceOf(TaskStatusUpdateEvent.class, retrievedEventAsEvent, "Should deserialize to TaskStatusUpdateEvent"); + assertInstanceOf( + TaskStatusUpdateEvent.class, + retrievedEventAsEvent, + "Should deserialize to TaskStatusUpdateEvent"); TaskStatusUpdateEvent retrievedStatusEvent = (TaskStatusUpdateEvent) retrievedEventAsEvent; assertEquals(statusEvent.taskId(), retrievedStatusEvent.taskId()); @@ -245,7 +304,8 @@ public void testReplicatedEventWithErrorSerialization() throws JsonProcessingExc InvalidRequestError error = new InvalidRequestError("Invalid request for testing"); // Create ReplicatedEventQueueItem with A2AError - ReplicatedEventQueueItem originalReplicatedEvent = new ReplicatedEventQueueItem("error-test-task", error); + ReplicatedEventQueueItem originalReplicatedEvent = + new ReplicatedEventQueueItem("error-test-task", error); // Serialize the ReplicatedEventQueueItemQueueItem String json = JsonUtil.toJson(originalReplicatedEvent); @@ -255,7 +315,8 @@ public void testReplicatedEventWithErrorSerialization() throws JsonProcessingExc assertFalse(json.contains("\"event\""), "JSON should not contain event field"); // Deserialize the ReplicatedEventQueueItem - ReplicatedEventQueueItem deserializedReplicatedEvent = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserializedReplicatedEvent = + JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertEquals(originalReplicatedEvent.getTaskId(), deserializedReplicatedEvent.getTaskId()); // Should get the error back @@ -274,14 +335,16 @@ public void testReplicatedEventWithErrorSerialization() throws JsonProcessingExc @Test public void testReplicatedEventBackwardCompatibility() throws JsonProcessingException { // Test backward compatibility with generic Event constructor - TaskStatusUpdateEvent statusEvent = TaskStatusUpdateEvent.builder() - .taskId("backward-compat-task") - .contextId("backward-compat-context") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + TaskStatusUpdateEvent statusEvent = + TaskStatusUpdateEvent.builder() + .taskId("backward-compat-task") + .contextId("backward-compat-context") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); // Use the backward compatibility constructor - ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem("backward-compat-task", (Event) statusEvent); + ReplicatedEventQueueItem replicatedEvent = + new ReplicatedEventQueueItem("backward-compat-task", (Event) statusEvent); // Should work the same as the specific constructor assertTrue(replicatedEvent.hasEvent()); @@ -292,12 +355,14 @@ public void testReplicatedEventBackwardCompatibility() throws JsonProcessingExce @Test public void testQueueClosedEventSerialization() throws JsonProcessingException { - // Test that QueueClosedEvent can be properly serialized and deserialized via ReplicatedEventQueueItem + // Test that QueueClosedEvent can be properly serialized and deserialized via + // ReplicatedEventQueueItem String taskId = "queue-closed-serialization-test"; QueueClosedEvent closedEvent = new QueueClosedEvent(taskId); // Create ReplicatedEventQueueItem with QueueClosedEvent - ReplicatedEventQueueItem originalReplicatedEvent = new ReplicatedEventQueueItem(taskId, closedEvent); + ReplicatedEventQueueItem originalReplicatedEvent = + new ReplicatedEventQueueItem(taskId, closedEvent); // Verify the item is marked as a closed event assertTrue(originalReplicatedEvent.isClosedEvent(), "Should be marked as closed event"); @@ -307,25 +372,36 @@ public void testQueueClosedEventSerialization() throws JsonProcessingException { // Serialize the ReplicatedEventQueueItem String json = JsonUtil.toJson(originalReplicatedEvent); assertTrue(json.contains("\"taskId\":\"" + taskId + "\""), "JSON should contain task ID"); - assertTrue(json.contains("\"closedEvent\":true"), "JSON should contain closedEvent flag set to true"); + assertTrue( + json.contains("\"closedEvent\":true"), + "JSON should contain closedEvent flag set to true"); assertFalse(json.contains("\"event\""), "JSON should not contain event field"); assertFalse(json.contains("\"error\""), "JSON should not contain error field"); // Deserialize the ReplicatedEventQueueItem - ReplicatedEventQueueItem deserializedReplicatedEvent = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserializedReplicatedEvent = + JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertEquals(taskId, deserializedReplicatedEvent.getTaskId()); // Verify the deserialized item is marked as a closed event - assertTrue(deserializedReplicatedEvent.isClosedEvent(), "Deserialized should be marked as closed event"); - assertFalse(deserializedReplicatedEvent.hasEvent(), "Deserialized should not have regular event"); + assertTrue( + deserializedReplicatedEvent.isClosedEvent(), + "Deserialized should be marked as closed event"); + assertFalse( + deserializedReplicatedEvent.hasEvent(), + "Deserialized should not have regular event"); assertFalse(deserializedReplicatedEvent.hasError(), "Deserialized should not have error"); // Verify getEvent() reconstructs the QueueClosedEvent Event retrievedEvent = deserializedReplicatedEvent.getEvent(); assertNotNull(retrievedEvent, "getEvent() should return a reconstructed QueueClosedEvent"); - assertInstanceOf(QueueClosedEvent.class, retrievedEvent, "Should deserialize to QueueClosedEvent"); + assertInstanceOf( + QueueClosedEvent.class, retrievedEvent, "Should deserialize to QueueClosedEvent"); QueueClosedEvent retrievedClosedEvent = (QueueClosedEvent) retrievedEvent; - assertEquals(taskId, retrievedClosedEvent.getTaskId(), "Reconstructed event should have correct task ID"); + assertEquals( + taskId, + retrievedClosedEvent.getTaskId(), + "Reconstructed event should have correct task ID"); } } diff --git a/extras/queue-manager-replicated/core/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java b/extras/queue-manager-replicated/core/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java index 6c837ba4c..dfa8f8b70 100644 --- a/extras/queue-manager-replicated/core/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java +++ b/extras/queue-manager-replicated/core/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/core/ReplicatedQueueManagerTest.java @@ -15,7 +15,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; - import org.a2aproject.sdk.extras.common.events.TaskFinalizedEvent; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.server.events.EventQueue; @@ -43,6 +42,7 @@ class ReplicatedQueueManagerTest { private StreamingEventKind testEvent; private MainEventBus mainEventBus; private MainEventBusProcessor mainEventBusProcessor; + @BeforeEach void setUp() { // Create MainEventBus first @@ -50,21 +50,24 @@ void setUp() { mainEventBus = new MainEventBus(); // Create QueueManager before MainEventBusProcessor (processor needs it as parameter) - queueManager = new ReplicatedQueueManager( - new NoOpReplicationStrategy(), - new MockTaskStateProvider(true), - mainEventBus - ); + queueManager = + new ReplicatedQueueManager( + new NoOpReplicationStrategy(), + new MockTaskStateProvider(true), + mainEventBus); // Create MainEventBusProcessor with QueueManager - mainEventBusProcessor = new MainEventBusProcessor(mainEventBus, taskStore, PushNotificationSender.NOOP, queueManager); + mainEventBusProcessor = + new MainEventBusProcessor( + mainEventBus, taskStore, PushNotificationSender.NOOP, queueManager); EventQueueUtil.start(mainEventBusProcessor); - testEvent = TaskStatusUpdateEvent.builder() - .taskId("test-task") - .contextId("test-context") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + testEvent = + TaskStatusUpdateEvent.builder() + .taskId("test-task") + .contextId("test-context") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); } /** @@ -82,7 +85,7 @@ private TaskStatusUpdateEvent createEventForTask(String taskId) { @AfterEach void tearDown() { if (mainEventBusProcessor != null) { - mainEventBusProcessor.setCallback(null); // Clear any test callbacks + mainEventBusProcessor.setCallback(null); // Clear any test callbacks EventQueueUtil.stop(mainEventBusProcessor); } mainEventBusProcessor = null; @@ -100,21 +103,24 @@ void tearDown() { */ private void waitForEventProcessing(Runnable action) throws InterruptedException { CountDownLatch processingLatch = new CountDownLatch(1); - mainEventBusProcessor.setCallback(new org.a2aproject.sdk.server.events.MainEventBusProcessorCallback() { - @Override - public void onEventProcessed(String taskId, org.a2aproject.sdk.spec.Event event) { - processingLatch.countDown(); - } - - @Override - public void onTaskFinalized(String taskId) { - // Not needed for basic event processing wait - } - }); + mainEventBusProcessor.setCallback( + new org.a2aproject.sdk.server.events.MainEventBusProcessorCallback() { + @Override + public void onEventProcessed( + String taskId, org.a2aproject.sdk.spec.Event event) { + processingLatch.countDown(); + } + + @Override + public void onTaskFinalized(String taskId) { + // Not needed for basic event processing wait + } + }); try { action.run(); - assertTrue(processingLatch.await(5, TimeUnit.SECONDS), + assertTrue( + processingLatch.await(5, TimeUnit.SECONDS), "MainEventBusProcessor should have processed the event within timeout"); } finally { mainEventBusProcessor.setCallback(null); @@ -124,7 +130,8 @@ public void onTaskFinalized(String taskId) { @Test void testReplicationStrategyTriggeredOnNormalEnqueue() throws InterruptedException { CountingReplicationStrategy strategy = new CountingReplicationStrategy(); - queueManager = new ReplicatedQueueManager(strategy, new MockTaskStateProvider(true), mainEventBus); + queueManager = + new ReplicatedQueueManager(strategy, new MockTaskStateProvider(true), mainEventBus); String taskId = "test-task-1"; EventQueue queue = queueManager.createOrTap(taskId); @@ -141,12 +148,14 @@ void testReplicationStrategyTriggeredOnNormalEnqueue() throws InterruptedExcepti @Test void testReplicationStrategyNotTriggeredOnReplicatedEvent() throws InterruptedException { CountingReplicationStrategy strategy = new CountingReplicationStrategy(); - queueManager = new ReplicatedQueueManager(strategy, new MockTaskStateProvider(true), mainEventBus); + queueManager = + new ReplicatedQueueManager(strategy, new MockTaskStateProvider(true), mainEventBus); String taskId = "test-task-2"; EventQueue queue = queueManager.createOrTap(taskId); - ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, getTaskStatusUpdateEventWithNewId(taskId)); + ReplicatedEventQueueItem replicatedEvent = + new ReplicatedEventQueueItem(taskId, getTaskStatusUpdateEventWithNewId(taskId)); queueManager.onReplicatedEvent(replicatedEvent); assertEquals(0, strategy.getCallCount()); @@ -155,7 +164,9 @@ void testReplicationStrategyNotTriggeredOnReplicatedEvent() throws InterruptedEx @Test void testReplicationStrategyWithCountingImplementation() throws InterruptedException { CountingReplicationStrategy countingStrategy = new CountingReplicationStrategy(); - queueManager = new ReplicatedQueueManager(countingStrategy, new MockTaskStateProvider(true), mainEventBus); + queueManager = + new ReplicatedQueueManager( + countingStrategy, new MockTaskStateProvider(true), mainEventBus); String taskId = "test-task-3"; EventQueue queue = queueManager.createOrTap(taskId); @@ -169,7 +180,8 @@ void testReplicationStrategyWithCountingImplementation() throws InterruptedExcep assertEquals(taskId, countingStrategy.getLastTaskId()); assertEquals(event, countingStrategy.getLastEvent()); - ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, getTaskStatusUpdateEventWithNewId(taskId)); + ReplicatedEventQueueItem replicatedEvent = + new ReplicatedEventQueueItem(taskId, getTaskStatusUpdateEventWithNewId(taskId)); queueManager.onReplicatedEvent(replicatedEvent); assertEquals(2, countingStrategy.getCallCount()); @@ -178,13 +190,15 @@ void testReplicationStrategyWithCountingImplementation() throws InterruptedExcep @Test void testReplicatedEventDeliveredToCorrectQueue() throws InterruptedException { String taskId = "test-task-4"; - TaskStatusUpdateEvent eventForTask = createEventForTask(taskId); // Use matching taskId + TaskStatusUpdateEvent eventForTask = createEventForTask(taskId); // Use matching taskId EventQueue queue = queueManager.createOrTap(taskId); - ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, eventForTask); + ReplicatedEventQueueItem replicatedEvent = + new ReplicatedEventQueueItem(taskId, eventForTask); // Use callback to wait for event processing - EventQueueItem item = dequeueEventWithRetry(queue, () -> queueManager.onReplicatedEvent(replicatedEvent)); + EventQueueItem item = + dequeueEventWithRetry(queue, () -> queueManager.onReplicatedEvent(replicatedEvent)); assertNotNull(item, "Event should be available in queue"); Event dequeuedEvent = item.getEvent(); assertEquals(eventForTask, dequeuedEvent); @@ -193,7 +207,7 @@ void testReplicatedEventDeliveredToCorrectQueue() throws InterruptedException { @Test void testReplicatedEventCreatesQueueIfNeeded() throws InterruptedException { String taskId = "non-existent-task"; - TaskStatusUpdateEvent eventForTask = createEventForTask(taskId); // Use matching taskId + TaskStatusUpdateEvent eventForTask = createEventForTask(taskId); // Use matching taskId // Verify no queue exists initially assertNull(queueManager.get(taskId)); @@ -207,16 +221,24 @@ void testReplicatedEventCreatesQueueIfNeeded() throws InterruptedException { EventQueue mainQueue = queueManager.get(taskId); assertNotNull(mainQueue, "MainQueue should exist after createOrTap"); - ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, eventForTask); + ReplicatedEventQueueItem replicatedEvent = + new ReplicatedEventQueueItem(taskId, eventForTask); // Process the replicated event and wait for distribution // Use callback to wait for event processing - EventQueueItem item = dequeueEventWithRetry(childQueue, () -> { - assertDoesNotThrow(() -> queueManager.onReplicatedEvent(replicatedEvent)); - }); + EventQueueItem item = + dequeueEventWithRetry( + childQueue, + () -> { + assertDoesNotThrow( + () -> queueManager.onReplicatedEvent(replicatedEvent)); + }); assertNotNull(item, "Event should be available in queue"); Event dequeuedEvent = item.getEvent(); - assertEquals(eventForTask, dequeuedEvent, "The replicated event should be enqueued in the newly created queue"); + assertEquals( + eventForTask, + dequeuedEvent, + "The replicated event should be enqueued in the newly created queue"); } @Test @@ -247,7 +269,9 @@ void testBasicQueueManagerFunctionality() throws InterruptedException { void testQueueToTaskIdMappingMaintained() throws InterruptedException { String taskId = "test-task-6"; CountingReplicationStrategy countingStrategy = new CountingReplicationStrategy(); - queueManager = new ReplicatedQueueManager(countingStrategy, new MockTaskStateProvider(true), mainEventBus); + queueManager = + new ReplicatedQueueManager( + countingStrategy, new MockTaskStateProvider(true), mainEventBus); TaskStatusUpdateEvent event = createEventForTask(taskId); EventQueue queue = queueManager.createOrTap(taskId); @@ -255,25 +279,29 @@ void testQueueToTaskIdMappingMaintained() throws InterruptedException { assertEquals(taskId, countingStrategy.getLastTaskId()); - queueManager.close(taskId); // Task is active, so NO poison pill is sent + queueManager.close(taskId); // Task is active, so NO poison pill is sent EventQueue newQueue = queueManager.createOrTap(taskId); waitForEventProcessing(() -> newQueue.enqueueEvent(event)); assertEquals(taskId, countingStrategy.getLastTaskId()); - // 2 replication calls: 1 testEvent, 1 testEvent (no QueueClosedEvent because task is active) + // 2 replication calls: 1 testEvent, 1 testEvent (no QueueClosedEvent because task is + // active) assertEquals(2, countingStrategy.getCallCount()); } @Test void testReplicatedEventJsonSerialization() throws Exception { - // Test that ReplicatedEventQueueItem can be properly serialized and deserialized with StreamingEventKind - TaskStatusUpdateEvent originalEvent = TaskStatusUpdateEvent.builder() - .taskId("json-test-task") - .contextId("json-test-context") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); - ReplicatedEventQueueItem original = new ReplicatedEventQueueItem("json-test-task", originalEvent); + // Test that ReplicatedEventQueueItem can be properly serialized and deserialized with + // StreamingEventKind + TaskStatusUpdateEvent originalEvent = + TaskStatusUpdateEvent.builder() + .taskId("json-test-task") + .contextId("json-test-context") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); + ReplicatedEventQueueItem original = + new ReplicatedEventQueueItem("json-test-task", originalEvent); // Serialize to JSON String json = JsonUtil.toJson(original); @@ -283,7 +311,8 @@ void testReplicatedEventJsonSerialization() throws Exception { assertTrue(json.contains("\"statusUpdate\"")); // Deserialize back - ReplicatedEventQueueItem deserialized = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserialized = + JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertNotNull(deserialized); assertEquals("json-test-task", deserialized.getTaskId()); assertNotNull(deserialized.getEvent()); @@ -294,7 +323,8 @@ void testReplicatedEventJsonSerialization() throws Exception { @Test void testParallelReplicationBehavior() throws InterruptedException { CountingReplicationStrategy strategy = new CountingReplicationStrategy(); - queueManager = new ReplicatedQueueManager(strategy, new MockTaskStateProvider(true), mainEventBus); + queueManager = + new ReplicatedQueueManager(strategy, new MockTaskStateProvider(true), mainEventBus); String taskId = "parallel-test-task"; EventQueue queue = queueManager.createOrTap(taskId); @@ -306,105 +336,125 @@ void testParallelReplicationBehavior() throws InterruptedException { ExecutorService executor = Executors.newFixedThreadPool(numThreads); CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch doneLatch = new CountDownLatch(numThreads); - + // Use CyclicBarrier for better thread synchronization // This ensures all threads start their work at approximately the same time - java.util.concurrent.CyclicBarrier barrier = new java.util.concurrent.CyclicBarrier(numThreads); + java.util.concurrent.CyclicBarrier barrier = + new java.util.concurrent.CyclicBarrier(numThreads); // Track processed events for better diagnostics on failure - java.util.concurrent.CopyOnWriteArrayList processedEvents = - new java.util.concurrent.CopyOnWriteArrayList<>(); + java.util.concurrent.CopyOnWriteArrayList processedEvents = + new java.util.concurrent.CopyOnWriteArrayList<>(); // Set up callback to wait for ALL events to be processed by MainEventBusProcessor // Must wait for all 50 events (25 normal + 25 replicated) to ensure all normal events // have triggered replication before we check the count CountDownLatch processingLatch = new CountDownLatch(totalEventCount); - mainEventBusProcessor.setCallback(new org.a2aproject.sdk.server.events.MainEventBusProcessorCallback() { - @Override - public void onEventProcessed(String tid, org.a2aproject.sdk.spec.Event event) { - processedEvents.add(event); - processingLatch.countDown(); - } - - @Override - public void onTaskFinalized(String tid) { - // Not needed for this test - } - }); + mainEventBusProcessor.setCallback( + new org.a2aproject.sdk.server.events.MainEventBusProcessorCallback() { + @Override + public void onEventProcessed(String tid, org.a2aproject.sdk.spec.Event event) { + processedEvents.add(event); + processingLatch.countDown(); + } + + @Override + public void onTaskFinalized(String tid) { + // Not needed for this test + } + }); // Launch threads that will enqueue events normally (should trigger replication) for (int i = 0; i < numThreads / 2; i++) { final int threadId = i; - executor.submit(() -> { - try { - startLatch.await(); - barrier.await(); // Synchronize thread starts for better interleaving - for (int j = 0; j < eventsPerThread; j++) { - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId(taskId) // Use same taskId as queue - .contextId("test-context") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - queue.enqueueEvent(event); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (java.util.concurrent.BrokenBarrierException e) { - throw new RuntimeException("Barrier broken", e); - } finally { - doneLatch.countDown(); - } - }); + executor.submit( + () -> { + try { + startLatch.await(); + barrier.await(); // Synchronize thread starts for better interleaving + for (int j = 0; j < eventsPerThread; j++) { + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId(taskId) // Use same taskId as queue + .contextId("test-context") + .status( + new TaskStatus( + TaskState.TASK_STATE_WORKING)) + .build(); + queue.enqueueEvent(event); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (java.util.concurrent.BrokenBarrierException e) { + throw new RuntimeException("Barrier broken", e); + } finally { + doneLatch.countDown(); + } + }); } // Launch threads that will send replicated events (should NOT trigger replication) for (int i = numThreads / 2; i < numThreads; i++) { final int threadId = i; - executor.submit(() -> { - try { - startLatch.await(); - barrier.await(); // Synchronize thread starts for better interleaving - for (int j = 0; j < eventsPerThread; j++) { - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId(taskId) // Use same taskId as queue - .contextId("test-context") - // Use a non-terminal state: a COMPLETED event processed mid-stream - // finalizes the task and closes the queue, so overlapping normal - // enqueues no longer trigger replication and the count assertion - // becomes timing-dependent (flaky). Replicated events are skipped by - // the replication hook via isReplicated() regardless of state. - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, event); - queueManager.onReplicatedEvent(replicatedEvent); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (java.util.concurrent.BrokenBarrierException e) { - throw new RuntimeException("Barrier broken", e); - } finally { - doneLatch.countDown(); - } - }); + executor.submit( + () -> { + try { + startLatch.await(); + barrier.await(); // Synchronize thread starts for better interleaving + for (int j = 0; j < eventsPerThread; j++) { + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId(taskId) // Use same taskId as queue + .contextId("test-context") + // Use a non-terminal state: a COMPLETED event + // processed mid-stream + // finalizes the task and closes the queue, so + // overlapping normal + // enqueues no longer trigger replication and the + // count assertion + // becomes timing-dependent (flaky). Replicated + // events are skipped by + // the replication hook via isReplicated() + // regardless of state. + .status( + new TaskStatus( + TaskState.TASK_STATE_WORKING)) + .build(); + ReplicatedEventQueueItem replicatedEvent = + new ReplicatedEventQueueItem(taskId, event); + queueManager.onReplicatedEvent(replicatedEvent); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (java.util.concurrent.BrokenBarrierException e) { + throw new RuntimeException("Barrier broken", e); + } finally { + doneLatch.countDown(); + } + }); } // Start all threads simultaneously startLatch.countDown(); // Wait for all threads to complete with explicit timeout - assertTrue(doneLatch.await(10, TimeUnit.SECONDS), + assertTrue( + doneLatch.await(10, TimeUnit.SECONDS), "All " + numThreads + " threads should complete within 10 seconds"); executor.shutdown(); - assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS), + assertTrue( + executor.awaitTermination(5, TimeUnit.SECONDS), "Executor should shutdown within 5 seconds"); // Wait for MainEventBusProcessor to process all events try { boolean allProcessed = processingLatch.await(10, TimeUnit.SECONDS); - assertTrue(allProcessed, - String.format("MainEventBusProcessor should have processed all %d events within timeout. " + - "Processed: %d, Remaining: %d", + assertTrue( + allProcessed, + String.format( + "MainEventBusProcessor should have processed all %d events within" + + " timeout. Processed: %d, Remaining: %d", totalEventCount, processedEvents.size(), processingLatch.getCount())); } finally { mainEventBusProcessor.setCallback(null); @@ -412,22 +462,32 @@ public void onTaskFinalized(String tid) { } // Verify we processed the expected number of events - assertEquals(totalEventCount, processedEvents.size(), - "Should have processed exactly " + totalEventCount + " events (normal + replicated)"); + assertEquals( + totalEventCount, + processedEvents.size(), + "Should have processed exactly " + + totalEventCount + + " events (normal + replicated)"); // Only the normal enqueue operations should have triggered replication // numThreads/2 threads * eventsPerThread events each = total expected replication calls int expectedReplicationCalls = (numThreads / 2) * eventsPerThread; - assertEquals(expectedReplicationCalls, strategy.getCallCount(), - String.format("Only normal enqueue operations should trigger replication, not replicated events. " + - "Expected: %d, Actual: %d", expectedReplicationCalls, strategy.getCallCount())); + assertEquals( + expectedReplicationCalls, + strategy.getCallCount(), + String.format( + "Only normal enqueue operations should trigger replication, not replicated" + + " events. Expected: %d, Actual: %d", + expectedReplicationCalls, strategy.getCallCount())); } @Test void testReplicatedEventSkippedWhenTaskInactive() throws InterruptedException { // Create a task state provider that returns false (task is inactive) MockTaskStateProvider stateProvider = new MockTaskStateProvider(false); - queueManager = new ReplicatedQueueManager(new CountingReplicationStrategy(), stateProvider, mainEventBus); + queueManager = + new ReplicatedQueueManager( + new CountingReplicationStrategy(), stateProvider, mainEventBus); String taskId = "inactive-task"; @@ -446,10 +506,12 @@ void testReplicatedEventSkippedWhenTaskInactive() throws InterruptedException { void testReplicatedEventProcessedWhenTaskActive() throws InterruptedException { // Create a task state provider that returns true (task is active) MockTaskStateProvider stateProvider = new MockTaskStateProvider(true); - queueManager = new ReplicatedQueueManager(new CountingReplicationStrategy(), stateProvider, mainEventBus); + queueManager = + new ReplicatedQueueManager( + new CountingReplicationStrategy(), stateProvider, mainEventBus); String taskId = "active-task"; - TaskStatusUpdateEvent eventForTask = createEventForTask(taskId); // Use matching taskId + TaskStatusUpdateEvent eventForTask = createEventForTask(taskId); // Use matching taskId // Verify no queue exists initially assertNull(queueManager.get(taskId)); @@ -464,22 +526,26 @@ void testReplicatedEventProcessedWhenTaskActive() throws InterruptedException { assertNotNull(mainQueue, "MainQueue should exist after createOrTap"); // Process a replicated event for an active task - ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, eventForTask); + ReplicatedEventQueueItem replicatedEvent = + new ReplicatedEventQueueItem(taskId, eventForTask); // Verify the event was enqueued and distributed to our ChildQueue // Use callback to wait for event processing - EventQueueItem item = dequeueEventWithRetry(childQueue, () -> queueManager.onReplicatedEvent(replicatedEvent)); + EventQueueItem item = + dequeueEventWithRetry( + childQueue, () -> queueManager.onReplicatedEvent(replicatedEvent)); assertNotNull(item, "Event should be available in queue"); Event dequeuedEvent = item.getEvent(); assertEquals(eventForTask, dequeuedEvent, "Event should be enqueued for active task"); } - @Test void testReplicatedEventToExistingQueueWhenTaskBecomesInactive() throws InterruptedException { // Create a task state provider that returns true initially MockTaskStateProvider stateProvider = new MockTaskStateProvider(true); - queueManager = new ReplicatedQueueManager(new CountingReplicationStrategy(), stateProvider, mainEventBus); + queueManager = + new ReplicatedQueueManager( + new CountingReplicationStrategy(), stateProvider, mainEventBus); String taskId = "task-becomes-inactive"; @@ -498,11 +564,12 @@ void testReplicatedEventToExistingQueueWhenTaskBecomesInactive() throws Interrup stateProvider.setActive(false); // Process a replicated event - should be skipped - TaskStatusUpdateEvent newEvent = TaskStatusUpdateEvent.builder() - .taskId(taskId) - .contextId("test-context") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + TaskStatusUpdateEvent newEvent = + TaskStatusUpdateEvent.builder() + .taskId(taskId) + .contextId("test-context") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, newEvent); queueManager.onReplicatedEvent(replicatedEvent); @@ -518,7 +585,8 @@ void testReplicatedEventToExistingQueueWhenTaskBecomesInactive() throws Interrup @Test void testPoisonPillSentViaTransactionAwareEvent() throws InterruptedException { CountingReplicationStrategy strategy = new CountingReplicationStrategy(); - queueManager = new ReplicatedQueueManager(strategy, new MockTaskStateProvider(true), mainEventBus); + queueManager = + new ReplicatedQueueManager(strategy, new MockTaskStateProvider(true), mainEventBus); String taskId = "poison-pill-test"; EventQueue queue = queueManager.createOrTap(taskId); @@ -533,23 +601,30 @@ void testPoisonPillSentViaTransactionAwareEvent() throws InterruptedException { // Simulate the CDI event observer being called (what happens in real execution) // Create a final task for the event - Task finalTask = Task.builder() - .id(taskId) - .contextId("test-context") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + Task finalTask = + Task.builder() + .id(taskId) + .contextId("test-context") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); TaskFinalizedEvent taskFinalizedEvent = new TaskFinalizedEvent(taskId, finalTask); // Call the observer method directly (simulating CDI event delivery) queueManager.onTaskFinalized(taskFinalizedEvent); // Verify that final Task and QueueClosedEvent were replicated - // strategy.getCallCount() should be 3: testEvent, final Task, then QueueClosedEvent (poison pill) - assertEquals(3, strategy.getCallCount(), "Should have replicated testEvent, final Task, and QueueClosedEvent"); + // strategy.getCallCount() should be 3: testEvent, final Task, then QueueClosedEvent (poison + // pill) + assertEquals( + 3, + strategy.getCallCount(), + "Should have replicated testEvent, final Task, and QueueClosedEvent"); // Verify the last event is QueueClosedEvent (poison pill) Event lastEvent = strategy.getLastEvent(); - assertTrue(lastEvent instanceof QueueClosedEvent, "Last replicated event should be QueueClosedEvent (poison pill)"); + assertTrue( + lastEvent instanceof QueueClosedEvent, + "Last replicated event should be QueueClosedEvent (poison pill)"); assertEquals(taskId, ((QueueClosedEvent) lastEvent).getTaskId()); } @@ -572,7 +647,8 @@ void testQueueClosedEventJsonSerialization() throws Exception { assertTrue(json.contains("\"closedEvent\":true"), "JSON should contain closedEvent flag"); // Deserialize back - ReplicatedEventQueueItem deserialized = JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem deserialized = + JsonUtil.fromJson(json, ReplicatedEventQueueItem.class); assertNotNull(deserialized); assertEquals(taskId, deserialized.getTaskId()); assertTrue(deserialized.isClosedEvent(), "Deserialized should be marked as closed event"); @@ -582,7 +658,8 @@ void testQueueClosedEventJsonSerialization() throws Exception { // Verify getEvent() returns QueueClosedEvent Event reconstructedEvent = deserialized.getEvent(); assertNotNull(reconstructedEvent); - assertTrue(reconstructedEvent instanceof QueueClosedEvent, + assertTrue( + reconstructedEvent instanceof QueueClosedEvent, "getEvent() should return QueueClosedEvent"); assertEquals(taskId, ((QueueClosedEvent) reconstructedEvent).getTaskId()); } @@ -590,12 +667,13 @@ void testQueueClosedEventJsonSerialization() throws Exception { @Test void testReplicatedQueueClosedEventTerminatesConsumer() throws InterruptedException { String taskId = "remote-close-test"; - TaskStatusUpdateEvent eventForTask = createEventForTask(taskId); // Use matching taskId + TaskStatusUpdateEvent eventForTask = createEventForTask(taskId); // Use matching taskId EventQueue queue = queueManager.createOrTap(taskId); // Simulate receiving QueueClosedEvent from remote node QueueClosedEvent closedEvent = new QueueClosedEvent(taskId); - ReplicatedEventQueueItem replicatedClosedEvent = new ReplicatedEventQueueItem(taskId, closedEvent); + ReplicatedEventQueueItem replicatedClosedEvent = + new ReplicatedEventQueueItem(taskId, closedEvent); // Dequeue the normal event first (use callback to wait for async processing) EventQueueItem item1 = dequeueEventWithRetry(queue, () -> queue.enqueueEvent(eventForTask)); @@ -603,17 +681,21 @@ void testReplicatedQueueClosedEventTerminatesConsumer() throws InterruptedExcept assertEquals(eventForTask, item1.getEvent()); // Next dequeue should get the QueueClosedEvent (use callback to wait for async processing) - EventQueueItem item2 = dequeueEventWithRetry(queue, () -> queueManager.onReplicatedEvent(replicatedClosedEvent)); + EventQueueItem item2 = + dequeueEventWithRetry( + queue, () -> queueManager.onReplicatedEvent(replicatedClosedEvent)); assertNotNull(item2, "QueueClosedEvent should be available"); - assertTrue(item2.getEvent() instanceof QueueClosedEvent, + assertTrue( + item2.getEvent() instanceof QueueClosedEvent, "Second event should be QueueClosedEvent"); } private TaskStatusUpdateEvent getTaskStatusUpdateEventWithNewId(String taskId) { - return TaskStatusUpdateEvent.builder((TaskStatusUpdateEvent) testEvent).taskId(taskId).build(); + return TaskStatusUpdateEvent.builder((TaskStatusUpdateEvent) testEvent) + .taskId(taskId) + .build(); } - private static class NoOpReplicationStrategy implements ReplicationStrategy { @Override public void send(String taskId, Event event) { @@ -646,8 +728,8 @@ public Event getLastEvent() { } } - - private static class MockTaskStateProvider implements org.a2aproject.sdk.server.tasks.TaskStateProvider { + private static class MockTaskStateProvider + implements org.a2aproject.sdk.server.tasks.TaskStateProvider { private volatile boolean active; public MockTaskStateProvider(boolean active) { @@ -661,7 +743,7 @@ public boolean isTaskActive(String taskId) { @Override public boolean isTaskFinalized(String taskId) { - return !active; // If task is inactive, it's finalized; if active, not finalized + return !active; // If task is inactive, it's finalized; if active, not finalized } public void setActive(boolean active) { @@ -677,7 +759,8 @@ public void setActive(boolean active) { * @param enqueueAction the action that enqueues the event (triggers event processing) * @return the dequeued EventQueueItem, or null if queue is closed */ - private EventQueueItem dequeueEventWithRetry(EventQueue queue, Runnable enqueueAction) throws InterruptedException { + private EventQueueItem dequeueEventWithRetry(EventQueue queue, Runnable enqueueAction) + throws InterruptedException { // Wait for event to be processed and distributed waitForEventProcessing(enqueueAction); @@ -689,4 +772,4 @@ private EventQueueItem dequeueEventWithRetry(EventQueue queue, Runnable enqueueA return null; } } -} \ No newline at end of file +} diff --git a/extras/queue-manager-replicated/pom.xml b/extras/queue-manager-replicated/pom.xml index 1ee2644cf..20cd3bf0f 100644 --- a/extras/queue-manager-replicated/pom.xml +++ b/extras/queue-manager-replicated/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 @@ -22,4 +20,4 @@ tests-multi-instance - \ No newline at end of file + diff --git a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml index b1b7ebd37..739ca09d1 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/pom.xml +++ b/extras/queue-manager-replicated/replication-mp-reactive/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 @@ -56,4 +54,4 @@ - \ No newline at end of file + diff --git a/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java b/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java index e0ffccb7f..8fd1414d2 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java +++ b/extras/queue-manager-replicated/replication-mp-reactive/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategy.java @@ -3,7 +3,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Event; import jakarta.inject.Inject; - import org.a2aproject.sdk.extras.queuemanager.replicated.core.ReplicatedEventQueueItem; import org.a2aproject.sdk.extras.queuemanager.replicated.core.ReplicationStrategy; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; @@ -16,14 +15,14 @@ @ApplicationScoped public class ReactiveMessagingReplicationStrategy implements ReplicationStrategy { - private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveMessagingReplicationStrategy.class); + private static final Logger LOGGER = + LoggerFactory.getLogger(ReactiveMessagingReplicationStrategy.class); @Inject @Channel("replicated-events-out") private Emitter emitter; - @Inject - private Event cdiEvent; + @Inject private Event cdiEvent; @Override public void send(String taskId, org.a2aproject.sdk.spec.Event event) { @@ -35,7 +34,8 @@ public void send(String taskId, org.a2aproject.sdk.spec.Event event) { emitter.send(json); LOGGER.debug("Successfully sent replicated event for task: {}", taskId); } catch (Exception e) { - LOGGER.error("Failed to send replicated event for task: {}, event: {}", taskId, event, e); + LOGGER.error( + "Failed to send replicated event for task: {}, event: {}", taskId, event, e); throw new RuntimeException("Failed to send replicated event", e); } } @@ -45,9 +45,12 @@ public void onReplicatedEvent(String jsonMessage) { LOGGER.debug("Received replicated event JSON: {}", jsonMessage); try { - ReplicatedEventQueueItem replicatedEvent = JsonUtil.fromJson(jsonMessage, ReplicatedEventQueueItem.class); - LOGGER.debug("Deserialized replicated event for task: {}, event: {}", - replicatedEvent.getTaskId(), replicatedEvent.getEvent()); + ReplicatedEventQueueItem replicatedEvent = + JsonUtil.fromJson(jsonMessage, ReplicatedEventQueueItem.class); + LOGGER.debug( + "Deserialized replicated event for task: {}, event: {}", + replicatedEvent.getTaskId(), + replicatedEvent.getEvent()); // Fire the CDI event directly cdiEvent.fire(replicatedEvent); @@ -58,4 +61,4 @@ public void onReplicatedEvent(String jsonMessage) { // This prevents one bad message from stopping the entire message processing } } -} \ No newline at end of file +} diff --git a/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java b/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java index 08d54009f..2034955fe 100644 --- a/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java +++ b/extras/queue-manager-replicated/replication-mp-reactive/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/mp_reactive/ReactiveMessagingReplicationStrategyTest.java @@ -9,7 +9,6 @@ import static org.mockito.Mockito.verify; import jakarta.enterprise.event.Event; - import org.a2aproject.sdk.extras.queuemanager.replicated.core.ReplicatedEventQueueItem; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.spec.StreamingEventKind; @@ -27,33 +26,32 @@ @ExtendWith(MockitoExtension.class) class ReactiveMessagingReplicationStrategyTest { - @Mock - private Emitter emitter; + @Mock private Emitter emitter; - @Mock - private Event cdiEvent; + @Mock private Event cdiEvent; - @InjectMocks - private ReactiveMessagingReplicationStrategy strategy; + @InjectMocks private ReactiveMessagingReplicationStrategy strategy; private StreamingEventKind testEvent; @BeforeEach public void setUp() { - testEvent = TaskStatusUpdateEvent.builder() - .taskId("test-task") - .contextId("test-context") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + testEvent = + TaskStatusUpdateEvent.builder() + .taskId("test-task") + .contextId("test-context") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); } private String createValidJsonMessage(String taskId, String contextId) throws Exception { // Create a proper ReplicatedEventQueueItem JSON with StreamingEventKind - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId(taskId) - .contextId(contextId) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId(taskId) + .contextId(contextId) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, event); return JsonUtil.toJson(replicatedEvent); } @@ -75,9 +73,8 @@ public void testSendPropagatesEmitterExceptions() { doThrow(emitterException).when(emitter).send(any(String.class)); - RuntimeException exception = assertThrows(RuntimeException.class, () -> - strategy.send(taskId, testEvent) - ); + RuntimeException exception = + assertThrows(RuntimeException.class, () -> strategy.send(taskId, testEvent)); // The implementation wraps the original exception, so check the cause assertEquals(emitterException, exception.getCause()); @@ -95,7 +92,6 @@ public void testOnReplicatedEventWithValidJson() throws Exception { // Unit tests here focus on error handling and basic flow } - @Test public void testOnReplicatedEventHandlesInvalidJson() { String invalidJsonMessage = "invalid-json"; @@ -106,6 +102,4 @@ public void testOnReplicatedEventHandlesInvalidJson() { // CDI event should not be fired for invalid JSON verify(cdiEvent, never()).fire(any(ReplicatedEventQueueItem.class)); } - - -} \ No newline at end of file +} diff --git a/extras/queue-manager-replicated/tests-multi-instance/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/pom.xml index af4fb8101..d601b02d2 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 @@ -15,10 +13,6 @@ pom Java A2A Extras: Replicated Queue Manager Tests - Multi Instance Parent - - true - - quarkus-common quarkus-app-1 @@ -26,4 +20,8 @@ tests + + true + + diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml index c40aca2e7..3a5f7800c 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app1/MIRApp1AgentCardProducer.java b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app1/MIRApp1AgentCardProducer.java index 3d82d1401..99972d8ae 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app1/MIRApp1AgentCardProducer.java +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app1/MIRApp1AgentCardProducer.java @@ -2,7 +2,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - import org.a2aproject.sdk.extras.queuemanager.replicated.tests.multiinstance.common.MultiInstanceReplicationAgentCards; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCard; diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app1/MIRApp1AgentExecutorProducer.java b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app1/MIRApp1AgentExecutorProducer.java index ccf2a904d..688fee681 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app1/MIRApp1AgentExecutorProducer.java +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-1/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app1/MIRApp1AgentExecutorProducer.java @@ -2,7 +2,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - import org.a2aproject.sdk.extras.queuemanager.replicated.tests.multiinstance.common.MultiInstanceReplicationAgentExecutor; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml index 00ad36322..3d3a4be57 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app2/MIRApp2AgentCardProducer.java b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app2/MIRApp2AgentCardProducer.java index 704a0652a..bbfb701ee 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app2/MIRApp2AgentCardProducer.java +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app2/MIRApp2AgentCardProducer.java @@ -2,7 +2,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - import org.a2aproject.sdk.extras.queuemanager.replicated.tests.multiinstance.common.MultiInstanceReplicationAgentCards; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCard; diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app2/MIRApp2AgentExecutorProducer.java b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app2/MIRApp2AgentExecutorProducer.java index da0f5731e..676678cfd 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app2/MIRApp2AgentExecutorProducer.java +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-app-2/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/app2/MIRApp2AgentExecutorProducer.java @@ -2,7 +2,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - import org.a2aproject.sdk.extras.queuemanager.replicated.tests.multiinstance.common.MultiInstanceReplicationAgentExecutor; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml index 3e5fc3bb4..be256be5b 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/common/MultiInstanceReplicationAgentCards.java b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/common/MultiInstanceReplicationAgentCards.java index d659bdff9..01b782fe3 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/common/MultiInstanceReplicationAgentCards.java +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/common/MultiInstanceReplicationAgentCards.java @@ -1,9 +1,7 @@ package org.a2aproject.sdk.extras.queuemanager.replicated.tests.multiinstance.common; - import java.util.Collections; import java.util.List; - import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; @@ -15,8 +13,7 @@ */ public final class MultiInstanceReplicationAgentCards { - private MultiInstanceReplicationAgentCards() { - } + private MultiInstanceReplicationAgentCards() {} /** * Creates an AgentCard for a test instance. @@ -30,22 +27,28 @@ public static AgentCard createAgentCard(int instanceNumber, int port) { return AgentCard.builder() .name("Multi-Instance Test Agent " + instanceNumber) - .description("Test agent for multi-instance replication testing - Instance " + instanceNumber) - .supportedInterfaces(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), url))) + .description( + "Test agent for multi-instance replication testing - Instance " + + instanceNumber) + .supportedInterfaces( + List.of(new AgentInterface(TransportProtocol.JSONRPC.asString(), url))) .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) .defaultInputModes(Collections.singletonList("text")) .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(AgentSkill.builder() - .id("replication_test") - .name("Replication Test") - .description("Fire-and-forget agent for testing replication") - .tags(Collections.singletonList("test")) - .build())) + .skills( + Collections.singletonList( + AgentSkill.builder() + .id("replication_test") + .name("Replication Test") + .description( + "Fire-and-forget agent for testing replication") + .tags(Collections.singletonList("test")) + .build())) .build(); } } diff --git a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/common/MultiInstanceReplicationAgentExecutor.java b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/common/MultiInstanceReplicationAgentExecutor.java index 66e50228e..741950a46 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/common/MultiInstanceReplicationAgentExecutor.java +++ b/extras/queue-manager-replicated/tests-multi-instance/quarkus-common/src/main/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/common/MultiInstanceReplicationAgentExecutor.java @@ -21,10 +21,13 @@ public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2 Task task = context.getTask(); // Check if message contains "close" signal - boolean shouldClose = context.getMessage().parts().stream() - .anyMatch(part -> part instanceof TextPart tp && - tp.text() != null && - tp.text().toLowerCase().contains("close")); + boolean shouldClose = + context.getMessage().parts().stream() + .anyMatch( + part -> + part instanceof TextPart tp + && tp.text() != null + && tp.text().toLowerCase().contains("close")); if (shouldClose) { // Close the task diff --git a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml index a35b26054..be1a66e87 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml +++ b/extras/queue-manager-replicated/tests-multi-instance/tests/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/extras/queue-manager-replicated/tests-multi-instance/tests/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/MultiInstanceReplicationTest.java b/extras/queue-manager-replicated/tests-multi-instance/tests/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/MultiInstanceReplicationTest.java index ee2a01868..a7706e7c0 100644 --- a/extras/queue-manager-replicated/tests-multi-instance/tests/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/MultiInstanceReplicationTest.java +++ b/extras/queue-manager-replicated/tests-multi-instance/tests/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/multiinstance/MultiInstanceReplicationTest.java @@ -9,15 +9,14 @@ import java.time.Duration; import java.util.Collections; import java.util.List; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Consumer; - import org.a2aproject.sdk.A2A; import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientEvent; @@ -84,19 +83,22 @@ public static void setup() { network = Network.newNetwork(); // Start Kafka container - kafka = new KafkaContainer(DockerImageName.parse(KAFKA_IMAGE) - .asCompatibleSubstituteFor("apache/kafka")) - .withNetwork(network) - .withListener("kafka:19092"); + kafka = + new KafkaContainer( + DockerImageName.parse(KAFKA_IMAGE) + .asCompatibleSubstituteFor("apache/kafka")) + .withNetwork(network) + .withListener("kafka:19092"); kafka.start(); // Start PostgreSQL container - postgres = new PostgreSQLContainer<>(DockerImageName.parse(POSTGRES_IMAGE)) - .withNetwork(network) - .withNetworkAliases("postgres") - .withDatabaseName("a2adb") - .withUsername("a2auser") - .withPassword("a2apass"); + postgres = + new PostgreSQLContainer<>(DockerImageName.parse(POSTGRES_IMAGE)) + .withNetwork(network) + .withNetworkAliases("postgres") + .withDatabaseName("a2adb") + .withUsername("a2auser") + .withPassword("a2apass"); postgres.start(); // Get Kafka bootstrap servers for app configuration @@ -109,36 +111,38 @@ public static void setup() { // Build and start Quarkus app1 container File app1Dir = new File("../quarkus-app-1").getAbsoluteFile(); - app1 = new GenericContainer<>( - new ImageFromDockerfile() - .withFileFromPath(".", app1Dir.toPath())) - .withNetwork(network) - .withNetworkAliases("app1") - .withEnv("KAFKA_BOOTSTRAP_SERVERS", kafkaBootstrapServers) - .withEnv("DATABASE_URL", dbUrl) - .withEnv("DATABASE_USER", dbUser) - .withEnv("DATABASE_PASSWORD", dbPassword) - .withExposedPorts(8081) - .waitingFor(Wait.forHttp("/q/health/ready") - .forPort(8081) - .withStartupTimeout(Duration.ofMinutes(2))); + app1 = + new GenericContainer<>( + new ImageFromDockerfile().withFileFromPath(".", app1Dir.toPath())) + .withNetwork(network) + .withNetworkAliases("app1") + .withEnv("KAFKA_BOOTSTRAP_SERVERS", kafkaBootstrapServers) + .withEnv("DATABASE_URL", dbUrl) + .withEnv("DATABASE_USER", dbUser) + .withEnv("DATABASE_PASSWORD", dbPassword) + .withExposedPorts(8081) + .waitingFor( + Wait.forHttp("/q/health/ready") + .forPort(8081) + .withStartupTimeout(Duration.ofMinutes(2))); app1.start(); // Build and start Quarkus app2 container File app2Dir = new File("../quarkus-app-2").getAbsoluteFile(); - app2 = new GenericContainer<>( - new ImageFromDockerfile() - .withFileFromPath(".", app2Dir.toPath())) - .withNetwork(network) - .withNetworkAliases("app2") - .withEnv("KAFKA_BOOTSTRAP_SERVERS", kafkaBootstrapServers) - .withEnv("DATABASE_URL", dbUrl) - .withEnv("DATABASE_USER", dbUser) - .withEnv("DATABASE_PASSWORD", dbPassword) - .withExposedPorts(8082) - .waitingFor(Wait.forHttp("/q/health/ready") - .forPort(8082) - .withStartupTimeout(Duration.ofMinutes(2))); + app2 = + new GenericContainer<>( + new ImageFromDockerfile().withFileFromPath(".", app2Dir.toPath())) + .withNetwork(network) + .withNetworkAliases("app2") + .withEnv("KAFKA_BOOTSTRAP_SERVERS", kafkaBootstrapServers) + .withEnv("DATABASE_URL", dbUrl) + .withEnv("DATABASE_USER", dbUser) + .withEnv("DATABASE_PASSWORD", dbPassword) + .withExposedPorts(8082) + .waitingFor( + Wait.forHttp("/q/health/ready") + .forPort(8082) + .withStartupTimeout(Duration.ofMinutes(2))); app2.start(); // Store URLs for creating clients @@ -151,17 +155,21 @@ public static void setup() { AgentCard originalApp2Card = A2A.getAgentCard(app2Url); // Rebuild AgentCards with correct URLs (mapped ports, not container ports) - app1Card = AgentCard.builder(originalApp1Card) - .supportedInterfaces(Collections.singletonList( - new AgentInterface(TransportProtocol.JSONRPC.asString(), app1Url) - )) - .build(); - - app2Card = AgentCard.builder(originalApp2Card) - .supportedInterfaces(Collections.singletonList( - new AgentInterface(TransportProtocol.JSONRPC.asString(), app2Url) - )) - .build(); + app1Card = + AgentCard.builder(originalApp1Card) + .supportedInterfaces( + Collections.singletonList( + new AgentInterface( + TransportProtocol.JSONRPC.asString(), app1Url))) + .build(); + + app2Card = + AgentCard.builder(originalApp2Card) + .supportedInterfaces( + Collections.singletonList( + new AgentInterface( + TransportProtocol.JSONRPC.asString(), app2Url))) + .build(); } catch (Exception e) { System.err.println("=== Failed to get AgentCards ==="); System.err.println("App1 logs:"); @@ -206,9 +214,7 @@ public void cleanupClients() throws Exception { // Helper methods for creating A2A clients private Client createClient(AgentCard agentCard) throws A2AClientException { - ClientConfig config = ClientConfig.builder() - .setStreaming(true) - .build(); + ClientConfig config = ClientConfig.builder().setStreaming(true).build(); return Client.builder(agentCard) .clientConfig(config) @@ -261,215 +267,267 @@ public void testMultiInstanceEventReplication() throws Exception { try { // Step 1: Send initial message NON-streaming to create task (no client-provided taskId) - Message initialMessage = Message.builder(A2A.toUserMessage("Initial test message")) - .build(); - - // Use NON-streaming client to create the task - // Note: app1Card has the correct URL from Testcontainers setup - Client nonStreamingClient = Client.builder(app1Card) - .clientConfig(ClientConfig.builder().setStreaming(false).build()) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) - .build(); - - try { - CountDownLatch createLatch = new CountDownLatch(1); - AtomicReference taskRef = new AtomicReference<>(); - nonStreamingClient.sendMessage(initialMessage, List.of((ClientEvent event, AgentCard card) -> { - if (event instanceof TaskEvent te) { - taskRef.set(te.getTask()); - } - createLatch.countDown(); - }), (Throwable err) -> createLatch.countDown()); - assertTrue(createLatch.await(15, TimeUnit.SECONDS), "Task creation timed out"); - - Task createdTask = taskRef.get(); - assertNotNull(createdTask, "Task should be created"); - taskIdHolder[0] = createdTask.id(); - contextIdHolder[0] = createdTask.contextId(); - assertNotNull(taskIdHolder[0], "Server-generated task ID should not be null"); - - // Task should be in a non-final state (SUBMITTED or WORKING are both valid) - TaskState state = createdTask.status().state(); - assertTrue(state == TaskState.TASK_STATE_SUBMITTED || state == TaskState.TASK_STATE_WORKING, - "Task should be in SUBMITTED or WORKING state, but was: " + state); - nonStreamingClient.close(); - } catch (Exception e) { - System.err.println("\n=== FAILED TO CREATE TASK ==="); - System.err.println("Error: " + e.getMessage()); - System.err.println("\n=== APP1 CONTAINER LOGS ==="); - System.err.println(app1.getLogs()); - - System.err.println("\n=== APP2 CONTAINER LOGS ==="); - System.err.println(app2.getLogs()); - - throw e; - } - - final String taskId = taskIdHolder[0]; - final String contextId = contextIdHolder[0]; - - // Step 2: Subscribe from both app1 and app2 with proper latches - - // We need to wait for at least 3 new events after resubscription: - // 1. TaskArtifactUpdateEvent (message from app1) - // 2. TaskArtifactUpdateEvent (message from app2) - // 3. TaskStatusUpdateEvent(COMPLETED) (close message) - // Note: may also receive initial TaskEvent/TaskUpdateEvent when resubscribing - AtomicInteger app1EventCount = new AtomicInteger(0); - AtomicInteger app2EventCount = new AtomicInteger(0); - - // Track events received - CopyOnWriteArrayList app1Events = new CopyOnWriteArrayList<>(); - CopyOnWriteArrayList app2Events = new CopyOnWriteArrayList<>(); - - AtomicReference app1Error = new AtomicReference<>(); - AtomicReference app2Error = new AtomicReference<>(); - AtomicBoolean app1ReceivedInitialTask = new AtomicBoolean(false); - AtomicBoolean app2ReceivedInitialTask = new AtomicBoolean(false); - - // App1 subscriber - BiConsumer app1Subscriber = (event, card) -> { - String eventDetail = event.getClass().getSimpleName(); - if (event instanceof org.a2aproject.sdk.client.TaskUpdateEvent tue) { - eventDetail += " [" + tue.getUpdateEvent().getClass().getSimpleName(); - if (tue.getUpdateEvent() instanceof org.a2aproject.sdk.spec.TaskStatusUpdateEvent statusEvent) { - eventDetail += ", state=" + (statusEvent.status() != null ? statusEvent.status().state() : "null"); - } - eventDetail += "]"; - } else if (event instanceof org.a2aproject.sdk.client.TaskEvent te) { - eventDetail += " [state=" + (te.getTask().status() != null ? te.getTask().status().state() : "null") + "]"; + Message initialMessage = + Message.builder(A2A.toUserMessage("Initial test message")).build(); + + // Use NON-streaming client to create the task + // Note: app1Card has the correct URL from Testcontainers setup + Client nonStreamingClient = + Client.builder(app1Card) + .clientConfig(ClientConfig.builder().setStreaming(false).build()) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig()) + .build(); + + try { + CountDownLatch createLatch = new CountDownLatch(1); + AtomicReference taskRef = new AtomicReference<>(); + nonStreamingClient.sendMessage( + initialMessage, + List.of( + (ClientEvent event, AgentCard card) -> { + if (event instanceof TaskEvent te) { + taskRef.set(te.getTask()); + } + createLatch.countDown(); + }), + (Throwable err) -> createLatch.countDown()); + assertTrue(createLatch.await(15, TimeUnit.SECONDS), "Task creation timed out"); + + Task createdTask = taskRef.get(); + assertNotNull(createdTask, "Task should be created"); + taskIdHolder[0] = createdTask.id(); + contextIdHolder[0] = createdTask.contextId(); + assertNotNull(taskIdHolder[0], "Server-generated task ID should not be null"); + + // Task should be in a non-final state (SUBMITTED or WORKING are both valid) + TaskState state = createdTask.status().state(); + assertTrue( + state == TaskState.TASK_STATE_SUBMITTED + || state == TaskState.TASK_STATE_WORKING, + "Task should be in SUBMITTED or WORKING state, but was: " + state); + nonStreamingClient.close(); + } catch (Exception e) { + System.err.println("\n=== FAILED TO CREATE TASK ==="); + System.err.println("Error: " + e.getMessage()); + System.err.println("\n=== APP1 CONTAINER LOGS ==="); + System.err.println(app1.getLogs()); + + System.err.println("\n=== APP2 CONTAINER LOGS ==="); + System.err.println(app2.getLogs()); + + throw e; } - System.out.println("APP1 received event: " + eventDetail); - - // Per A2A spec 3.1.6: Handle initial TaskEvent on subscribe - if (!app1ReceivedInitialTask.get() && event instanceof org.a2aproject.sdk.client.TaskEvent) { - app1ReceivedInitialTask.set(true); - System.out.println("APP1 filtered initial TaskEvent"); - // Don't count initial TaskEvent toward expected artifact/status events - return; - } - app1Events.add(event); - int count = app1EventCount.incrementAndGet(); - System.out.println("APP1 event count now: " + count + ", event: " + eventDetail); - }; - - Consumer app1ErrorHandler = error -> { - if (!isStreamClosedError(error)) { - app1Error.set(error); - } - }; - - // App2 subscriber - BiConsumer app2Subscriber = (event, card) -> { - String eventDetail = event.getClass().getSimpleName(); - if (event instanceof org.a2aproject.sdk.client.TaskUpdateEvent tue) { - eventDetail += " [" + tue.getUpdateEvent().getClass().getSimpleName(); - if (tue.getUpdateEvent() instanceof org.a2aproject.sdk.spec.TaskStatusUpdateEvent statusEvent) { - eventDetail += ", state=" + (statusEvent.status() != null ? statusEvent.status().state() : "null"); - } - eventDetail += "]"; - } else if (event instanceof org.a2aproject.sdk.client.TaskEvent te) { - eventDetail += " [state=" + (te.getTask().status() != null ? te.getTask().status().state() : "null") + "]"; - } - System.out.println("APP2 received event: " + eventDetail); - - // Per A2A spec 3.1.6: Handle initial TaskEvent on subscribe - if (!app2ReceivedInitialTask.get() && event instanceof org.a2aproject.sdk.client.TaskEvent) { - app2ReceivedInitialTask.set(true); - System.out.println("APP2 filtered initial TaskEvent"); - // Don't count initial TaskEvent toward expected artifact/status events - return; + + final String taskId = taskIdHolder[0]; + final String contextId = contextIdHolder[0]; + + // Step 2: Subscribe from both app1 and app2 with proper latches + + // We need to wait for at least 3 new events after resubscription: + // 1. TaskArtifactUpdateEvent (message from app1) + // 2. TaskArtifactUpdateEvent (message from app2) + // 3. TaskStatusUpdateEvent(COMPLETED) (close message) + // Note: may also receive initial TaskEvent/TaskUpdateEvent when resubscribing + AtomicInteger app1EventCount = new AtomicInteger(0); + AtomicInteger app2EventCount = new AtomicInteger(0); + + // Track events received + CopyOnWriteArrayList app1Events = new CopyOnWriteArrayList<>(); + CopyOnWriteArrayList app2Events = new CopyOnWriteArrayList<>(); + + AtomicReference app1Error = new AtomicReference<>(); + AtomicReference app2Error = new AtomicReference<>(); + AtomicBoolean app1ReceivedInitialTask = new AtomicBoolean(false); + AtomicBoolean app2ReceivedInitialTask = new AtomicBoolean(false); + + // App1 subscriber + BiConsumer app1Subscriber = + (event, card) -> { + String eventDetail = event.getClass().getSimpleName(); + if (event instanceof org.a2aproject.sdk.client.TaskUpdateEvent tue) { + eventDetail += " [" + tue.getUpdateEvent().getClass().getSimpleName(); + if (tue.getUpdateEvent() + instanceof + org.a2aproject.sdk.spec.TaskStatusUpdateEvent statusEvent) { + eventDetail += + ", state=" + + (statusEvent.status() != null + ? statusEvent.status().state() + : "null"); + } + eventDetail += "]"; + } else if (event instanceof org.a2aproject.sdk.client.TaskEvent te) { + eventDetail += + " [state=" + + (te.getTask().status() != null + ? te.getTask().status().state() + : "null") + + "]"; + } + System.out.println("APP1 received event: " + eventDetail); + + // Per A2A spec 3.1.6: Handle initial TaskEvent on subscribe + if (!app1ReceivedInitialTask.get() + && event instanceof org.a2aproject.sdk.client.TaskEvent) { + app1ReceivedInitialTask.set(true); + System.out.println("APP1 filtered initial TaskEvent"); + // Don't count initial TaskEvent toward expected artifact/status events + return; + } + app1Events.add(event); + int count = app1EventCount.incrementAndGet(); + System.out.println( + "APP1 event count now: " + count + ", event: " + eventDetail); + }; + + Consumer app1ErrorHandler = + error -> { + if (!isStreamClosedError(error)) { + app1Error.set(error); + } + }; + + // App2 subscriber + BiConsumer app2Subscriber = + (event, card) -> { + String eventDetail = event.getClass().getSimpleName(); + if (event instanceof org.a2aproject.sdk.client.TaskUpdateEvent tue) { + eventDetail += " [" + tue.getUpdateEvent().getClass().getSimpleName(); + if (tue.getUpdateEvent() + instanceof + org.a2aproject.sdk.spec.TaskStatusUpdateEvent statusEvent) { + eventDetail += + ", state=" + + (statusEvent.status() != null + ? statusEvent.status().state() + : "null"); + } + eventDetail += "]"; + } else if (event instanceof org.a2aproject.sdk.client.TaskEvent te) { + eventDetail += + " [state=" + + (te.getTask().status() != null + ? te.getTask().status().state() + : "null") + + "]"; + } + System.out.println("APP2 received event: " + eventDetail); + + // Per A2A spec 3.1.6: Handle initial TaskEvent on subscribe + if (!app2ReceivedInitialTask.get() + && event instanceof org.a2aproject.sdk.client.TaskEvent) { + app2ReceivedInitialTask.set(true); + System.out.println("APP2 filtered initial TaskEvent"); + // Don't count initial TaskEvent toward expected artifact/status events + return; + } + app2Events.add(event); + int count = app2EventCount.incrementAndGet(); + System.out.println( + "APP2 event count now: " + count + ", event: " + eventDetail); + }; + + Consumer app2ErrorHandler = + error -> { + if (!isStreamClosedError(error)) { + app2Error.set(error); + } + }; + + // Start subscriptions (subscribe returns void) + getClient1() + .subscribeToTask( + new TaskIdParams(taskId), List.of(app1Subscriber), app1ErrorHandler); + getClient2() + .subscribeToTask( + new TaskIdParams(taskId), List.of(app2Subscriber), app2ErrorHandler); + + // Wait for subscriptions to be established - initial TaskEvent should arrive on each + await().atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(500)) + .until(() -> app1ReceivedInitialTask.get() && app2ReceivedInitialTask.get()); + + // Step 3: Send message on app1 (should generate TaskArtifactUpdateEvent) + int app1BeforeMsg1 = app1EventCount.get(); + int app2BeforeMsg1 = app2EventCount.get(); + + Message messageFromApp1 = + Message.builder(A2A.toUserMessage("Message from app1")) + .taskId(taskId) + .contextId(contextId) + .build(); + getClient1().sendMessage(messageFromApp1, List.of(), null); + + // Wait for both subscribers to receive the replicated event + await().atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(500)) + .until( + () -> + app1EventCount.get() > app1BeforeMsg1 + && app2EventCount.get() > app2BeforeMsg1); + + // Step 4: Send message on app2 (should generate TaskArtifactUpdateEvent) + int app1BeforeMsg2 = app1EventCount.get(); + int app2BeforeMsg2 = app2EventCount.get(); + + Message messageFromApp2 = + Message.builder(A2A.toUserMessage("Message from app2")) + .taskId(taskId) + .contextId(contextId) + .build(); + getClient2().sendMessage(messageFromApp2, List.of(), null); + + // Wait for both subscribers to receive the replicated event + await().atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(500)) + .until( + () -> + app1EventCount.get() > app1BeforeMsg2 + && app2EventCount.get() > app2BeforeMsg2); + + // Step 5: Send close message (should generate TaskStatusUpdateEvent with COMPLETED) + int app1BeforeClose = app1EventCount.get(); + int app2BeforeClose = app2EventCount.get(); + + Message closeMessage = + Message.builder(A2A.toUserMessage("close")) + .taskId(taskId) + .contextId(contextId) + .build(); + getClient1().sendMessage(closeMessage, List.of(), null); + + // Wait for both subscribers to receive the completion event + await().atMost(Duration.ofSeconds(10)) + .pollInterval(Duration.ofMillis(500)) + .until( + () -> + app1EventCount.get() > app1BeforeClose + && app2EventCount.get() > app2BeforeClose); + + // Verify we got at least 3 new events after initial subscription (artifact1, artifact2, + // completed) + assertTrue( + app1Events.size() >= 3, + "App1 should receive at least 3 events (got " + app1Events.size() + ")"); + assertTrue( + app2Events.size() >= 3, + "App2 should receive at least 3 events (got " + app2Events.size() + ")"); + + // Verify no errors + if (app1Error.get() != null) { + throw new AssertionError("App1 subscriber error", app1Error.get()); } - app2Events.add(event); - int count = app2EventCount.incrementAndGet(); - System.out.println("APP2 event count now: " + count + ", event: " + eventDetail); - }; - - Consumer app2ErrorHandler = error -> { - if (!isStreamClosedError(error)) { - app2Error.set(error); + if (app2Error.get() != null) { + throw new AssertionError("App2 subscriber error", app2Error.get()); } - }; - - // Start subscriptions (subscribe returns void) - getClient1().subscribeToTask(new TaskIdParams(taskId), List.of(app1Subscriber), app1ErrorHandler); - getClient2().subscribeToTask(new TaskIdParams(taskId), List.of(app2Subscriber), app2ErrorHandler); - - // Wait for subscriptions to be established - initial TaskEvent should arrive on each - await() - .atMost(Duration.ofSeconds(10)) - .pollInterval(Duration.ofMillis(500)) - .until(() -> app1ReceivedInitialTask.get() && app2ReceivedInitialTask.get()); - - // Step 3: Send message on app1 (should generate TaskArtifactUpdateEvent) - int app1BeforeMsg1 = app1EventCount.get(); - int app2BeforeMsg1 = app2EventCount.get(); - - Message messageFromApp1 = Message.builder(A2A.toUserMessage("Message from app1")) - .taskId(taskId) - .contextId(contextId) - .build(); - getClient1().sendMessage(messageFromApp1, List.of(), null); - - // Wait for both subscribers to receive the replicated event - await() - .atMost(Duration.ofSeconds(10)) - .pollInterval(Duration.ofMillis(500)) - .until(() -> app1EventCount.get() > app1BeforeMsg1 && - app2EventCount.get() > app2BeforeMsg1); - - // Step 4: Send message on app2 (should generate TaskArtifactUpdateEvent) - int app1BeforeMsg2 = app1EventCount.get(); - int app2BeforeMsg2 = app2EventCount.get(); - - Message messageFromApp2 = Message.builder(A2A.toUserMessage("Message from app2")) - .taskId(taskId) - .contextId(contextId) - .build(); - getClient2().sendMessage(messageFromApp2, List.of(), null); - - // Wait for both subscribers to receive the replicated event - await() - .atMost(Duration.ofSeconds(10)) - .pollInterval(Duration.ofMillis(500)) - .until(() -> app1EventCount.get() > app1BeforeMsg2 && - app2EventCount.get() > app2BeforeMsg2); - - // Step 5: Send close message (should generate TaskStatusUpdateEvent with COMPLETED) - int app1BeforeClose = app1EventCount.get(); - int app2BeforeClose = app2EventCount.get(); - - Message closeMessage = Message.builder(A2A.toUserMessage("close")) - .taskId(taskId) - .contextId(contextId) - .build(); - getClient1().sendMessage(closeMessage, List.of(), null); - - // Wait for both subscribers to receive the completion event - await() - .atMost(Duration.ofSeconds(10)) - .pollInterval(Duration.ofMillis(500)) - .until(() -> app1EventCount.get() > app1BeforeClose && - app2EventCount.get() > app2BeforeClose); - - - // Verify we got at least 3 new events after initial subscription (artifact1, artifact2, completed) - assertTrue(app1Events.size() >= 3, - "App1 should receive at least 3 events (got " + app1Events.size() + ")"); - assertTrue(app2Events.size() >= 3, - "App2 should receive at least 3 events (got " + app2Events.size() + ")"); - - // Verify no errors - if (app1Error.get() != null) { - throw new AssertionError("App1 subscriber error", app1Error.get()); - } - if (app2Error.get() != null) { - throw new AssertionError("App2 subscriber error", app2Error.get()); - } // Verify both received at least 3 events (could be more due to initial state events) - assertTrue(app1Events.size() >= 3, "App1 should receive at least 3 events, got: " + app1Events.size()); - assertTrue(app2Events.size() >= 3, "App2 should receive at least 3 events, got: " + app2Events.size()); + assertTrue( + app1Events.size() >= 3, + "App1 should receive at least 3 events, got: " + app1Events.size()); + assertTrue( + app2Events.size() >= 3, + "App2 should receive at least 3 events, got: " + app2Events.size()); } catch (Throwable t) { testFailure = t; throw t; @@ -494,7 +552,10 @@ public void testMultiInstanceEventReplication() throws Exception { /** * Dumps the last N lines of logs from a container to stderr. */ - private void dumpContainerLogs(String containerName, org.testcontainers.containers.ContainerState container, int lastLines) { + private void dumpContainerLogs( + String containerName, + org.testcontainers.containers.ContainerState container, + int lastLines) { System.err.println("\n--- " + containerName + " LOGS (last " + lastLines + " lines) ---"); try { String logs = container.getLogs(); @@ -522,10 +583,10 @@ private boolean isStreamClosedError(Throwable error) { String message = error.getMessage(); if (message != null) { // Filter out normal stream closure/cancellation errors - if (message.contains("Stream closed") || - message.contains("Stream") && message.contains("cancelled") || - message.contains("EOF reached") || - message.contains("CANCEL")) { + if (message.contains("Stream closed") + || message.contains("Stream") && message.contains("cancelled") + || message.contains("EOF reached") + || message.contains("CANCEL")) { return true; } } diff --git a/extras/queue-manager-replicated/tests-single-instance/pom.xml b/extras/queue-manager-replicated/tests-single-instance/pom.xml index 588c8ebe9..6554659b0 100644 --- a/extras/queue-manager-replicated/tests-single-instance/pom.xml +++ b/extras/queue-manager-replicated/tests-single-instance/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 org.a2aproject.sdk @@ -97,6 +95,22 @@ + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + java24-exclude-vertx-incompatible-tests @@ -121,20 +135,4 @@ - - - - - io.quarkus - quarkus-maven-plugin - - - - build - - - - - - - \ No newline at end of file + diff --git a/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java b/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java index c5b74c0b6..0c07746a7 100644 --- a/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java +++ b/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/KafkaReplicationIntegrationTest.java @@ -7,6 +7,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; import java.io.IOException; import java.util.HashSet; import java.util.List; @@ -17,9 +19,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; import java.util.function.Consumer; - -import jakarta.inject.Inject; - import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientEvent; import org.a2aproject.sdk.client.TaskEvent; @@ -41,7 +40,6 @@ import org.a2aproject.sdk.spec.TaskStatusUpdateEvent; import org.a2aproject.sdk.spec.TextPart; import org.a2aproject.sdk.testutils.docker.RequiresDocker; -import io.quarkus.test.junit.QuarkusTest; import org.eclipse.microprofile.reactive.messaging.Channel; import org.eclipse.microprofile.reactive.messaging.Emitter; import org.junit.jupiter.api.AfterEach; @@ -58,14 +56,12 @@ @RequiresDocker public class KafkaReplicationIntegrationTest { - private static final Logger LOGGER = LoggerFactory.getLogger(KafkaReplicationIntegrationTest.class); + private static final Logger LOGGER = + LoggerFactory.getLogger(KafkaReplicationIntegrationTest.class); - @Inject - @PublicAgentCard - AgentCard agentCard; + @Inject @PublicAgentCard AgentCard agentCard; - @Inject - TestKafkaEventConsumer testConsumer; + @Inject TestKafkaEventConsumer testConsumer; @Inject @Channel("replicated-events-out") @@ -78,35 +74,32 @@ public class KafkaReplicationIntegrationTest { @BeforeEach public void setup() throws A2AClientException { // Create non-streaming client for initial task creation - ClientConfig nonStreamingConfig = new ClientConfig.Builder() - .setStreaming(false) - .build(); + ClientConfig nonStreamingConfig = new ClientConfig.Builder().setStreaming(false).build(); - nonStreamingClient = Client.builder(agentCard) - .clientConfig(nonStreamingConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); + nonStreamingClient = + Client.builder(agentCard) + .clientConfig(nonStreamingConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); // Create streaming client for resubscription - ClientConfig streamingConfig = new ClientConfig.Builder() - .setStreaming(true) - .build(); + ClientConfig streamingConfig = new ClientConfig.Builder().setStreaming(true).build(); - streamingClient = Client.builder(agentCard) - .clientConfig(streamingConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); + streamingClient = + Client.builder(agentCard) + .clientConfig(streamingConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); // Create polling client for non-blocking operations - ClientConfig pollingConfig = new ClientConfig.Builder() - .setStreaming(false) - .setPolling(true) - .build(); - - pollingClient = Client.builder(agentCard) - .clientConfig(pollingConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); + ClientConfig pollingConfig = + new ClientConfig.Builder().setStreaming(false).setPolling(true).build(); + + pollingClient = + Client.builder(agentCard) + .clientConfig(pollingConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); } @AfterEach @@ -129,26 +122,31 @@ public void testA2AMessageReplicatedToKafka() throws Exception { testConsumer.clear(); // Send A2A message that should trigger events and replication (no client-provided taskId) - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("create"))) - .messageId("test-msg-" + System.currentTimeMillis()) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("create"))) + .messageId("test-msg-" + System.currentTimeMillis()) + .build(); CountDownLatch a2aLatch = new CountDownLatch(1); AtomicReference createdTask = new AtomicReference<>(); AtomicBoolean a2aError = new AtomicBoolean(false); // Send message and verify A2A processing works - nonStreamingClient.sendMessage(message, List.of((ClientEvent event, AgentCard card) -> { - if (event instanceof TaskEvent taskEvent) { - createdTask.set(taskEvent.getTask()); - a2aLatch.countDown(); - } - }), (Throwable error) -> { - a2aError.set(true); - a2aLatch.countDown(); - }); + nonStreamingClient.sendMessage( + message, + List.of( + (ClientEvent event, AgentCard card) -> { + if (event instanceof TaskEvent taskEvent) { + createdTask.set(taskEvent.getTask()); + a2aLatch.countDown(); + } + }), + (Throwable error) -> { + a2aError.set(true); + a2aLatch.countDown(); + }); // Wait for A2A processing to complete assertTrue(a2aLatch.await(15, TimeUnit.SECONDS), "A2A message processing timed out"); @@ -172,15 +170,28 @@ public void testA2AMessageReplicatedToKafka() throws Exception { // The event should now maintain its proper type through Kafka serialization // This verifies that our polymorphic serialization is working correctly - assertInstanceOf(TaskStatusUpdateEvent.class, receivedEvent, "Event should maintain TaskStatusUpdateEvent type after Kafka round-trip"); + assertInstanceOf( + TaskStatusUpdateEvent.class, + receivedEvent, + "Event should maintain TaskStatusUpdateEvent type after Kafka round-trip"); TaskStatusUpdateEvent statusUpdateEvent = (TaskStatusUpdateEvent) receivedEvent; // Verify the event data is consistent with the task returned from the client - assertEquals(taskId, statusUpdateEvent.taskId(), "Event task ID should match original task ID"); - assertEquals(contextId, statusUpdateEvent.contextId(), "Event context ID should match original context ID"); - assertEquals(TaskState.TASK_STATE_SUBMITTED, statusUpdateEvent.status().state(), "Event should show SUBMITTED state"); + assertEquals( + taskId, statusUpdateEvent.taskId(), "Event task ID should match original task ID"); + assertEquals( + contextId, + statusUpdateEvent.contextId(), + "Event context ID should match original context ID"); + assertEquals( + TaskState.TASK_STATE_SUBMITTED, + statusUpdateEvent.status().state(), + "Event should show SUBMITTED state"); assertFalse(statusUpdateEvent.isFinal(), "Event should show final:false"); - assertEquals(TaskStatusUpdateEvent.STREAMING_EVENT_ID, statusUpdateEvent.kind(), "Event should indicate status-update type"); + assertEquals( + TaskStatusUpdateEvent.STREAMING_EVENT_ID, + statusUpdateEvent.kind(), + "Event should indicate status-update type"); } @Test @@ -188,24 +199,30 @@ public void testKafkaEventReceivedByA2AServer() throws Exception { // Clear any previous events testConsumer.clear(); - // First create a task in the A2A system using non-streaming client (no client-provided taskId) - Message createMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("create"))) - .messageId("create-msg-" + System.currentTimeMillis()) - .build(); + // First create a task in the A2A system using non-streaming client (no client-provided + // taskId) + Message createMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("create"))) + .messageId("create-msg-" + System.currentTimeMillis()) + .build(); CountDownLatch createLatch = new CountDownLatch(1); AtomicReference createdTask = new AtomicReference<>(); - nonStreamingClient.sendMessage(createMessage, List.of((ClientEvent event, AgentCard card) -> { - if (event instanceof TaskEvent taskEvent) { - createdTask.set(taskEvent.getTask()); - createLatch.countDown(); - } - }), (Throwable error) -> { - createLatch.countDown(); - }); + nonStreamingClient.sendMessage( + createMessage, + List.of( + (ClientEvent event, AgentCard card) -> { + if (event instanceof TaskEvent taskEvent) { + createdTask.set(taskEvent.getTask()); + createLatch.countDown(); + } + }), + (Throwable error) -> { + createLatch.countDown(); + }); assertTrue(createLatch.await(15, TimeUnit.SECONDS), "Task creation timed out"); Task initialTask = createdTask.get(); @@ -213,7 +230,10 @@ public void testKafkaEventReceivedByA2AServer() throws Exception { String taskId = initialTask.id(); String contextId = initialTask.contextId(); assertNotNull(taskId, "Server-generated task ID should not be null"); - assertEquals(TaskState.TASK_STATE_SUBMITTED, initialTask.status().state(), "Initial task should be in SUBMITTED state"); + assertEquals( + TaskState.TASK_STATE_SUBMITTED, + initialTask.status().state(), + "Initial task should be in SUBMITTED state"); // Add a small delay to ensure the task is fully processed before resubscription Thread.sleep(1000); @@ -226,53 +246,60 @@ public void testKafkaEventReceivedByA2AServer() throws Exception { AtomicBoolean receivedInitialTask = new AtomicBoolean(false); // Create consumer to handle subscribed events - BiConsumer consumer = (event, agentCard) -> { - // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent - if (!receivedInitialTask.get()) { - if (event instanceof TaskEvent) { - receivedInitialTask.set(true); - return; - } else { - throw new AssertionError("First event on subscribe MUST be TaskEvent, but was: " + event.getClass().getSimpleName()); - } - } + BiConsumer consumer = + (event, agentCard) -> { + // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent + if (!receivedInitialTask.get()) { + if (event instanceof TaskEvent) { + receivedInitialTask.set(true); + return; + } else { + throw new AssertionError( + "First event on subscribe MUST be TaskEvent, but was: " + + event.getClass().getSimpleName()); + } + } - // Process subsequent events - if (event instanceof TaskUpdateEvent taskUpdateEvent) { - if (taskUpdateEvent.getUpdateEvent() instanceof TaskStatusUpdateEvent statusEvent) { - if (statusEvent.status().state() == TaskState.TASK_STATE_COMPLETED) { - receivedCompletedEvent.set(statusEvent); - subscribeLatch.countDown(); + // Process subsequent events + if (event instanceof TaskUpdateEvent taskUpdateEvent) { + if (taskUpdateEvent.getUpdateEvent() + instanceof TaskStatusUpdateEvent statusEvent) { + if (statusEvent.status().state() == TaskState.TASK_STATE_COMPLETED) { + receivedCompletedEvent.set(statusEvent); + subscribeLatch.countDown(); + } + } else { + wasUnexpectedEvent.set(true); + } + } else { + wasUnexpectedEvent.set(true); } - } else { - wasUnexpectedEvent.set(true); - } - } else { - wasUnexpectedEvent.set(true); - } - }; + }; // Create error handler - filter out benign stream closed errors. // HTTP/2 streams are cancelled during normal cleanup when subscriptions end, // which is expected behavior and not an actual error condition. - Consumer errorHandler = error -> { - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - subscribeLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + subscribeLatch.countDown(); + }; // Subscribe to the task to listen for updates streamingClient.subscribeToTask(new TaskIdParams(taskId), List.of(consumer), errorHandler); // Now manually send a TaskStatusUpdateEvent to Kafka using reactive messaging - TaskStatusUpdateEvent statusEvent = TaskStatusUpdateEvent.builder() - .taskId(taskId) - .contextId(contextId) - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); - - ReplicatedEventQueueItem replicatedEvent = new ReplicatedEventQueueItem(taskId, statusEvent); + TaskStatusUpdateEvent statusEvent = + TaskStatusUpdateEvent.builder() + .taskId(taskId) + .contextId(contextId) + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); + + ReplicatedEventQueueItem replicatedEvent = + new ReplicatedEventQueueItem(taskId, statusEvent); String eventJson = JsonUtil.toJson(replicatedEvent); // Send to Kafka using reactive messaging @@ -280,7 +307,9 @@ public void testKafkaEventReceivedByA2AServer() throws Exception { // Wait for the replicated event to be received via streaming resubscription // This tests the full round-trip: Manual Kafka Event -> A2A System -> Streaming Client - assertTrue(subscribeLatch.await(15, TimeUnit.SECONDS), "Should receive COMPLETED event via resubscription"); + assertTrue( + subscribeLatch.await(15, TimeUnit.SECONDS), + "Should receive COMPLETED event via resubscription"); // Verify no unexpected events or errors assertFalse(wasUnexpectedEvent.get(), "Should not receive unexpected events"); @@ -289,7 +318,10 @@ public void testKafkaEventReceivedByA2AServer() throws Exception { // Verify the received event TaskStatusUpdateEvent completedEvent = receivedCompletedEvent.get(); assertNotNull(completedEvent, "Should have received a TaskStatusUpdateEvent"); - assertEquals(TaskState.TASK_STATE_COMPLETED, completedEvent.status().state(), "Event should show COMPLETED state"); + assertEquals( + TaskState.TASK_STATE_COMPLETED, + completedEvent.status().state(), + "Event should show COMPLETED state"); assertTrue(completedEvent.isFinal(), "Event should be marked as final"); assertEquals(taskId, completedEvent.taskId(), "Event should have correct task ID"); assertEquals(contextId, completedEvent.contextId(), "Event should have correct context ID"); @@ -307,28 +339,35 @@ public void testQueueClosedEventTerminatesRemoteSubscribers() throws Exception { // Use polling (non-blocking) client with "working" command (no client-provided taskId) // This creates task in WORKING state (non-final) and keeps queue alive - Message workingMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("working"))) - .messageId("working-msg-" + System.currentTimeMillis()) - .build(); + Message workingMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("working"))) + .messageId("working-msg-" + System.currentTimeMillis()) + .build(); CountDownLatch workingLatch = new CountDownLatch(1); AtomicReference taskIdRef = new AtomicReference<>(); - pollingClient.sendMessage(workingMessage, List.of((ClientEvent event, AgentCard card) -> { - if (event instanceof TaskEvent taskEvent) { - taskIdRef.set(taskEvent.getTask().id()); - workingLatch.countDown(); - } else if (event instanceof TaskUpdateEvent tue && tue.getUpdateEvent() instanceof TaskStatusUpdateEvent status) { - if (status.status().state() == TaskState.TASK_STATE_WORKING) { - taskIdRef.set(status.taskId()); + pollingClient.sendMessage( + workingMessage, + List.of( + (ClientEvent event, AgentCard card) -> { + if (event instanceof TaskEvent taskEvent) { + taskIdRef.set(taskEvent.getTask().id()); + workingLatch.countDown(); + } else if (event instanceof TaskUpdateEvent tue + && tue.getUpdateEvent() + instanceof TaskStatusUpdateEvent status) { + if (status.status().state() == TaskState.TASK_STATE_WORKING) { + taskIdRef.set(status.taskId()); + workingLatch.countDown(); + } + } + }), + (Throwable error) -> { workingLatch.countDown(); - } - } - }), (Throwable error) -> { - workingLatch.countDown(); - }); + }); assertTrue(workingLatch.await(15, TimeUnit.SECONDS), "Task creation timed out"); String createdTaskId = taskIdRef.get(); @@ -344,24 +383,29 @@ public void testQueueClosedEventTerminatesRemoteSubscribers() throws Exception { // Create consumer - signal when the first event arrives (proves HTTP connection is live), // then ignore subsequent events while waiting for the stream to complete - BiConsumer consumer = (event, agentCard) -> { - if (firstEventLatch.getCount() > 0) { - assertInstanceOf(TaskEvent.class, event, "First event on subscribe MUST be TaskEvent (A2A spec 3.1.6)"); - } - firstEventLatch.countDown(); - }; + BiConsumer consumer = + (event, agentCard) -> { + if (firstEventLatch.getCount() > 0) { + assertInstanceOf( + TaskEvent.class, + event, + "First event on subscribe MUST be TaskEvent (A2A spec 3.1.6)"); + } + firstEventLatch.countDown(); + }; // Create error handler that captures completion - Consumer errorHandler = error -> { - if (error == null || isStreamClosedError(error)) { - // null error means stream completed normally - streamCompleted.set(true); - } else { - streamErrored.set(true); - errorRef.set(error); - } - streamCompletedLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (error == null || isStreamClosedError(error)) { + // null error means stream completed normally + streamCompleted.set(true); + } else { + streamErrored.set(true); + errorRef.set(error); + } + streamCompletedLatch.countDown(); + }; // Subscribe to the task - this creates a streaming subscription streamingClient.subscribeToTask(new TaskIdParams(taskId), List.of(consumer), errorHandler); @@ -371,12 +415,14 @@ public void testQueueClosedEventTerminatesRemoteSubscribers() throws Exception { // before we send the QueueClosedEvent. Using awaitQueuePollerStart(mainQueue) is not // reliable here because the MainQueue's one-time latch was already fired by the first // subscription (from pollingClient.sendMessage), so it returns immediately. - assertTrue(firstEventLatch.await(15, TimeUnit.SECONDS), + assertTrue( + firstEventLatch.await(15, TimeUnit.SECONDS), "Should receive initial task snapshot from subscription"); // Now manually send a QueueClosedEvent to Kafka to simulate queue closure on another node QueueClosedEvent closedEvent = new QueueClosedEvent(taskId); - ReplicatedEventQueueItem replicatedClosedEvent = new ReplicatedEventQueueItem(taskId, closedEvent); + ReplicatedEventQueueItem replicatedClosedEvent = + new ReplicatedEventQueueItem(taskId, closedEvent); String eventJson = JsonUtil.toJson(replicatedClosedEvent); // Send to Kafka using reactive messaging @@ -384,15 +430,21 @@ public void testQueueClosedEventTerminatesRemoteSubscribers() throws Exception { // Wait for the stream to complete - should happen when QueueClosedEvent is received // Allow extra time for Kafka message processing and CDI event propagation - assertTrue(streamCompletedLatch.await(30, TimeUnit.SECONDS), + assertTrue( + streamCompletedLatch.await(30, TimeUnit.SECONDS), "Streaming subscription should complete when QueueClosedEvent is received"); // Verify the stream completed normally (not with an error) if (!streamCompleted.get()) { - LOGGER.error("Stream did not complete normally! streamErrored={}, errorRef={}", - streamErrored.get(), errorRef.get(), errorRef.get()); + LOGGER.error( + "Stream did not complete normally! streamErrored={}, errorRef={}", + streamErrored.get(), + errorRef.get(), + errorRef.get()); } - assertTrue(streamCompleted.get(), "Stream should complete normally when QueueClosedEvent is received"); + assertTrue( + streamCompleted.get(), + "Stream should complete normally when QueueClosedEvent is received"); assertFalse(streamErrored.get(), "Stream should not error on QueueClosedEvent"); assertNull(errorRef.get(), "Should not receive error when stream completes gracefully"); } @@ -403,24 +455,30 @@ public void testPoisonPillGenerationOnTaskFinalization() throws Exception { testConsumer.clear(); // Create a task that will be completed (finalized) - no client-provided taskId - Message completeMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("complete"))) - .messageId("complete-msg-" + System.currentTimeMillis()) - .build(); + Message completeMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("complete"))) + .messageId("complete-msg-" + System.currentTimeMillis()) + .build(); CountDownLatch completeLatch = new CountDownLatch(1); AtomicReference finalTask = new AtomicReference<>(); - nonStreamingClient.sendMessage(completeMessage, List.of((ClientEvent event, AgentCard card) -> { - if (event instanceof TaskEvent taskEvent) { - finalTask.set(taskEvent.getTask()); - // Count down for any task event - we just need the task to be created - completeLatch.countDown(); - } - }), (Throwable error) -> { - completeLatch.countDown(); - }); + nonStreamingClient.sendMessage( + completeMessage, + List.of( + (ClientEvent event, AgentCard card) -> { + if (event instanceof TaskEvent taskEvent) { + finalTask.set(taskEvent.getTask()); + // Count down for any task event - we just need the task to be + // created + completeLatch.countDown(); + } + }), + (Throwable error) -> { + completeLatch.countDown(); + }); assertTrue(completeLatch.await(15, TimeUnit.SECONDS), "Task creation timed out"); Task createdTask = finalTask.get(); @@ -435,15 +493,18 @@ public void testPoisonPillGenerationOnTaskFinalization() throws Exception { // Wait for the QueueClosedEvent to be published to Kafka // The event should be published automatically because isTaskFinalized() returns true ReplicatedEventQueueItem poisonPill = testConsumer.waitForClosedEvent(taskId, 30); - assertNotNull(poisonPill, "QueueClosedEvent should be published to Kafka when task is finalized"); + assertNotNull( + poisonPill, "QueueClosedEvent should be published to Kafka when task is finalized"); assertTrue(poisonPill.isClosedEvent(), "Event should be marked as closedEvent"); - assertEquals(taskId, poisonPill.getTaskId(), "QueueClosedEvent should have correct task ID"); + assertEquals( + taskId, poisonPill.getTaskId(), "QueueClosedEvent should have correct task ID"); // Verify the event is actually a QueueClosedEvent - assertInstanceOf(QueueClosedEvent.class, poisonPill.getEvent(), + assertInstanceOf( + QueueClosedEvent.class, + poisonPill.getEvent(), "Event should be a QueueClosedEvent"); - QueueClosedEvent closedEvent = - (QueueClosedEvent) poisonPill.getEvent(); + QueueClosedEvent closedEvent = (QueueClosedEvent) poisonPill.getEvent(); assertEquals(taskId, closedEvent.getTaskId(), "QueueClosedEvent task ID should match"); } @@ -476,10 +537,10 @@ private boolean isStreamClosedError(Throwable error, Set visited) { String message = error.getMessage(); if (message != null) { // Filter out normal stream closure/cancellation errors - if (message.contains("Stream closed") || - message.contains("Stream") && message.contains("cancelled") || - message.contains("EOF reached") || - message.contains("CANCEL")) { + if (message.contains("Stream closed") + || message.contains("Stream") && message.contains("cancelled") + || message.contains("EOF reached") + || message.contains("CANCEL")) { return true; } } @@ -493,5 +554,4 @@ private boolean isStreamClosedError(Throwable error, Set visited) { return false; } - -} \ No newline at end of file +} diff --git a/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicatedQueueManagerTest.java b/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicatedQueueManagerTest.java index a3d2a4f23..b0b757179 100644 --- a/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicatedQueueManagerTest.java +++ b/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicatedQueueManagerTest.java @@ -2,12 +2,11 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; - import org.a2aproject.sdk.extras.queuemanager.replicated.core.ReplicatedQueueManager; -import org.a2aproject.sdk.testutils.docker.RequiresDocker; import org.a2aproject.sdk.server.events.QueueManager; -import io.quarkus.test.junit.QuarkusTest; +import org.a2aproject.sdk.testutils.docker.RequiresDocker; import org.junit.jupiter.api.Test; /** @@ -18,12 +17,11 @@ @RequiresDocker public class ReplicatedQueueManagerTest { - @Inject - QueueManager queueManager; + @Inject QueueManager queueManager; @Test public void testReplicationSystemIsConfigured() { // Verify that we're using the ReplicatedQueueManager assertInstanceOf(ReplicatedQueueManager.class, queueManager); } -} \ No newline at end of file +} diff --git a/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicationTestAgentCardProducer.java b/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicationTestAgentCardProducer.java index 14e2f3e58..015089b51 100644 --- a/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicationTestAgentCardProducer.java +++ b/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicationTestAgentCardProducer.java @@ -1,17 +1,14 @@ package org.a2aproject.sdk.extras.queuemanager.replicated.tests; - -import java.util.List; - +import io.quarkus.arc.profile.IfBuildProfile; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - +import java.util.List; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.TransportProtocol; -import io.quarkus.arc.profile.IfBuildProfile; /** * Produces the AgentCard for replicated queue manager integration tests. @@ -28,15 +25,16 @@ public AgentCard agentCard() { .description("Test agent for replicated queue manager integration testing") .version("1.0.0") .documentationUrl("http://localhost:8081/docs") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .build()) + .capabilities( + AgentCapabilities.builder().streaming(true).pushNotifications(true).build()) .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) .skills(List.of()) - .supportedInterfaces(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:8081"))) + .supportedInterfaces( + List.of( + new AgentInterface( + TransportProtocol.JSONRPC.asString(), + "http://localhost:8081"))) .build(); } -} \ No newline at end of file +} diff --git a/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicationTestAgentExecutor.java b/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicationTestAgentExecutor.java index f65352e8d..fd25d3e4c 100644 --- a/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicationTestAgentExecutor.java +++ b/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/ReplicationTestAgentExecutor.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.extras.queuemanager.replicated.tests; -import java.util.List; - +import io.quarkus.arc.profile.IfBuildProfile; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - +import java.util.List; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; @@ -13,7 +12,6 @@ import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.Part; import org.a2aproject.sdk.spec.TextPart; -import io.quarkus.arc.profile.IfBuildProfile; /** * Test AgentExecutor for replicated queue manager integration testing. @@ -69,4 +67,4 @@ private String getLastTextPart(Message message) throws A2AError { } throw new InvalidRequestError("Last part is not text"); } -} \ No newline at end of file +} diff --git a/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java b/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java index 7c9c501dc..797c3ea64 100644 --- a/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java +++ b/extras/queue-manager-replicated/tests-single-instance/src/test/java/org/a2aproject/sdk/extras/queuemanager/replicated/tests/TestKafkaEventConsumer.java @@ -1,14 +1,12 @@ package org.a2aproject.sdk.extras.queuemanager.replicated.tests; +import io.quarkus.arc.profile.IfBuildProfile; +import jakarta.enterprise.context.ApplicationScoped; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; - -import jakarta.enterprise.context.ApplicationScoped; - import org.a2aproject.sdk.extras.queuemanager.replicated.core.ReplicatedEventQueueItem; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; -import io.quarkus.arc.profile.IfBuildProfile; import org.eclipse.microprofile.reactive.messaging.Incoming; /** @@ -19,13 +17,15 @@ @ApplicationScoped public class TestKafkaEventConsumer { - private final ConcurrentLinkedQueue receivedEvents = new ConcurrentLinkedQueue<>(); + private final ConcurrentLinkedQueue receivedEvents = + new ConcurrentLinkedQueue<>(); private volatile CountDownLatch eventLatch; @Incoming("test-replicated-events-in") public void onTestReplicatedEvent(String jsonMessage) { try { - ReplicatedEventQueueItem event = JsonUtil.fromJson(jsonMessage, ReplicatedEventQueueItem.class); + ReplicatedEventQueueItem event = + JsonUtil.fromJson(jsonMessage, ReplicatedEventQueueItem.class); receivedEvents.offer(event); // Signal any waiting threads @@ -45,7 +45,8 @@ public void onTestReplicatedEvent(String jsonMessage) { * @param timeoutSeconds maximum time to wait * @return the matching ReplicatedEventQueueItem, or null if timeout */ - public ReplicatedEventQueueItem waitForEvent(String taskId, int timeoutSeconds) throws InterruptedException { + public ReplicatedEventQueueItem waitForEvent(String taskId, int timeoutSeconds) + throws InterruptedException { // Check if we already have the event ReplicatedEventQueueItem existing = findEventByTaskId(taskId); if (existing != null) { @@ -75,7 +76,8 @@ public ReplicatedEventQueueItem waitForEvent(String taskId, int timeoutSeconds) * @param timeoutSeconds maximum time to wait * @return the matching ReplicatedEventQueueItem, or null if timeout */ - public ReplicatedEventQueueItem waitForEventWithContent(String taskId, String contentMatch, int timeoutSeconds) throws InterruptedException { + public ReplicatedEventQueueItem waitForEventWithContent( + String taskId, String contentMatch, int timeoutSeconds) throws InterruptedException { // Check if we already have the event ReplicatedEventQueueItem existing = findEventByTaskIdWithContent(taskId, contentMatch); if (existing != null) { @@ -104,7 +106,8 @@ public ReplicatedEventQueueItem waitForEventWithContent(String taskId, String co * @param timeoutSeconds maximum time to wait * @return the matching ReplicatedEventQueueItem with QueueClosedEvent, or null if timeout */ - public ReplicatedEventQueueItem waitForClosedEvent(String taskId, int timeoutSeconds) throws InterruptedException { + public ReplicatedEventQueueItem waitForClosedEvent(String taskId, int timeoutSeconds) + throws InterruptedException { // Check if we already have the event ReplicatedEventQueueItem existing = findClosedEventByTaskId(taskId); if (existing != null) { @@ -150,10 +153,13 @@ private ReplicatedEventQueueItem findClosedEventByTaskId(String taskId) { /** * Find an event by task ID and content match in the received events. */ - private ReplicatedEventQueueItem findEventByTaskIdWithContent(String taskId, String contentMatch) { + private ReplicatedEventQueueItem findEventByTaskIdWithContent( + String taskId, String contentMatch) { return receivedEvents.stream() - .filter(event -> taskId.equals(event.getTaskId()) && - event.getEvent().toString().contains(contentMatch)) + .filter( + event -> + taskId.equals(event.getTaskId()) + && event.getEvent().toString().contains(contentMatch)) .findFirst() .orElse(null); } @@ -171,4 +177,4 @@ public void clear() { public int getEventCount() { return receivedEvents.size(); } -} \ No newline at end of file +} diff --git a/extras/task-store-database-jpa/pom.xml b/extras/task-store-database-jpa/pom.xml index a60a1d6f6..5c249bc69 100644 --- a/extras/task-store-database-jpa/pom.xml +++ b/extras/task-store-database-jpa/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -128,4 +126,4 @@ test - \ No newline at end of file + diff --git a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java index b63321240..ee8586416 100644 --- a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java +++ b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStore.java @@ -1,11 +1,5 @@ package org.a2aproject.sdk.extras.taskstore.database.jpa; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - import jakarta.annotation.PostConstruct; import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; @@ -19,7 +13,11 @@ import jakarta.persistence.PersistenceException; import jakarta.persistence.TypedQuery; import jakarta.transaction.Transactional; - +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; import org.a2aproject.sdk.extras.common.events.TaskFinalizedEvent; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; @@ -59,19 +57,19 @@ public class JpaDatabaseTaskStore implements TaskStore, TaskStateProvider { private static final Logger LOGGER = LoggerFactory.getLogger(JpaDatabaseTaskStore.class); - private static final String A2A_REPLICATION_GRACE_PERIOD_SECONDS = "a2a.replication.grace-period-seconds"; + private static final String A2A_REPLICATION_GRACE_PERIOD_SECONDS = + "a2a.replication.grace-period-seconds"; private static final String A2A_REPLICATION_MAX_SCANNED_TASKS = "a2a.max-scanned-tasks"; - private static final String A2A_AUTHORIZATION_SCAN_BATCH_SIZE = "a2a.authorization-scan-batch-size"; + private static final String A2A_AUTHORIZATION_SCAN_BATCH_SIZE = + "a2a.authorization-scan-batch-size"; private static final int DEFAULT_AUTHORIZATION_SCAN_BATCH_SIZE = 100; @PersistenceContext(unitName = "a2a-java") private EntityManager em; - @Inject - private Event taskFinalizedEvent; + @Inject private Event taskFinalizedEvent; - @Inject - private A2AConfigProvider configProvider; + @Inject private A2AConfigProvider configProvider; private final @Nullable TaskAuthorizationProvider authorizationProvider; @@ -80,7 +78,8 @@ public JpaDatabaseTaskStore() { } @Inject - public JpaDatabaseTaskStore(@Any Instance authorizationProviderInstance) { + public JpaDatabaseTaskStore( + @Any Instance authorizationProviderInstance) { this.authorizationProvider = CdiUtils.getIfResolvable(authorizationProviderInstance); } @@ -94,6 +93,7 @@ public JpaDatabaseTaskStore(@Any Instance authorizati * Note: Property override requires a configurable {@link A2AConfigProvider} on the classpath. */ private long gracePeriodSeconds; + /** * Maximum number of candidate rows inspected by the iterative authorization fallback. * Capability-based database queries do not use this limit. @@ -101,6 +101,7 @@ public JpaDatabaseTaskStore(@Any Instance authorizati * fails instead of returning a partial count. */ private long maxScanned; + /** * Number of candidate rows per iterative authorization fallback query only. */ @@ -108,18 +109,20 @@ public JpaDatabaseTaskStore(@Any Instance authorizati @PostConstruct void initConfig() { - gracePeriodSeconds = Long.parseLong(configProvider.getValue(A2A_REPLICATION_GRACE_PERIOD_SECONDS)); + gracePeriodSeconds = + Long.parseLong(configProvider.getValue(A2A_REPLICATION_GRACE_PERIOD_SECONDS)); maxScanned = Long.parseLong(configProvider.getValue(A2A_REPLICATION_MAX_SCANNED_TASKS)); - authorizationScanBatchSize = Integer.parseInt(configProvider.getValue(A2A_AUTHORIZATION_SCAN_BATCH_SIZE)); + authorizationScanBatchSize = + Integer.parseInt(configProvider.getValue(A2A_AUTHORIZATION_SCAN_BATCH_SIZE)); if (maxScanned <= 0) { throw new TaskStoreException("a2a.max-scanned-tasks must be greater than zero"); } if (authorizationScanBatchSize <= 0) { - throw new TaskStoreException("a2a.authorization-scan-batch-size must be greater than zero"); + throw new TaskStoreException( + "a2a.authorization-scan-batch-size must be greater than zero"); } } - @Transactional @Override public void save(Task task, boolean isReplicated) { @@ -129,25 +132,39 @@ public void save(Task task, boolean isReplicated) { em.merge(jpaTask); LOGGER.debug("Persisted/updated task with ID: {}", task.id()); - // Only fire TaskFinalizedEvent for locally-generated final states, NOT for replicated events - // This prevents feedback loops where receiving a replicated final task triggers another replication - if (!isReplicated && task.status() != null && task.status().state() != null && task.status().state().isFinal()) { + // Only fire TaskFinalizedEvent for locally-generated final states, NOT for replicated + // events + // This prevents feedback loops where receiving a replicated final task triggers another + // replication + if (!isReplicated + && task.status() != null + && task.status().state() != null + && task.status().state().isFinal()) { // Fire CDI event if task reached final state - // IMPORTANT: The event will be delivered AFTER transaction commits (AFTER_SUCCESS observers) - // This ensures the task's final state is durably stored before the final task and poison pill are sent - LOGGER.debug("Task {} is in final state, firing TaskFinalizedEvent with full Task", task.id()); + // IMPORTANT: The event will be delivered AFTER transaction commits (AFTER_SUCCESS + // observers) + // This ensures the task's final state is durably stored before the final task and + // poison pill are sent + LOGGER.debug( + "Task {} is in final state, firing TaskFinalizedEvent with full Task", + task.id()); taskFinalizedEvent.fire(new TaskFinalizedEvent(task.id(), task)); - } else if (isReplicated && task.status() != null && task.status().state() != null && task.status().state().isFinal()) { - LOGGER.debug("Task {} is in final state but from replication - NOT firing TaskFinalizedEvent (prevents feedback loop)", task.id()); + } else if (isReplicated + && task.status() != null + && task.status().state() != null + && task.status().state().isFinal()) { + LOGGER.debug( + "Task {} is in final state but from replication - NOT firing" + + " TaskFinalizedEvent (prevents feedback loop)", + task.id()); } } catch (JsonProcessingException e) { LOGGER.error("Failed to serialize task with ID: {}", task.id(), e); - throw new TaskSerializationException(task.id(), - "Failed to serialize task for persistence", e); + throw new TaskSerializationException( + task.id(), "Failed to serialize task for persistence", e); } catch (PersistenceException e) { LOGGER.error("Database save failed for task with ID: {}", task.id(), e); - throw new TaskPersistenceException(task.id(), - "Database save failed for task", e); + throw new TaskPersistenceException(task.id(), "Database save failed for task", e); } } @@ -168,14 +185,13 @@ public Task get(String taskId) { return task; } catch (JsonProcessingException e) { LOGGER.error("Failed to deserialize task with ID: {}", taskId, e); - throw new TaskSerializationException(taskId, - "Failed to deserialize task from database", e); + throw new TaskSerializationException( + taskId, "Failed to deserialize task from database", e); } } catch (PersistenceException e) { LOGGER.error("Database retrieval failed for task with ID: {}", taskId, e); - throw new TaskPersistenceException(taskId, - "Database retrieval failed for task", e); + throw new TaskPersistenceException(taskId, "Database retrieval failed for task", e); } } @@ -185,7 +201,8 @@ public void delete(String taskId) { LOGGER.debug("Deleting task with ID: {}", taskId); try { em.createQuery("DELETE FROM JpaTaskOwner o WHERE o.taskId = :taskId") - .setParameter("taskId", taskId).executeUpdate(); + .setParameter("taskId", taskId) + .executeUpdate(); JpaTask jpaTask = em.find(JpaTask.class, taskId); if (jpaTask != null) { em.remove(jpaTask); @@ -195,8 +212,7 @@ public void delete(String taskId) { } } catch (PersistenceException e) { LOGGER.error("Database deletion failed for task with ID: {}", taskId, e); - throw new TaskPersistenceException(taskId, - "Database deletion failed for task", e); + throw new TaskPersistenceException(taskId, "Database deletion failed for task", e); } } @@ -230,7 +246,9 @@ public boolean isTaskActive(String taskId) { Task task = jpaTask.getTask(); // Task is active if not in final state - if (task.status() == null || task.status().state() == null || !task.status().state().isFinal()) { + if (task.status() == null + || task.status().state() == null + || !task.status().state().isFinal()) { LOGGER.debug("Task is not in final state, considering active: {}", taskId); return true; } @@ -238,8 +256,12 @@ public boolean isTaskActive(String taskId) { // Task is in final state - check grace period Instant finalizedAt = jpaTask.getFinalizedAt(); if (finalizedAt == null) { - // Should not happen, but defensive: if final state but no timestamp, consider inactive - LOGGER.warn("Task {} is in final state but has no finalizedAt timestamp, considering inactive", taskId); + // Should not happen, but defensive: if final state but no timestamp, consider + // inactive + LOGGER.warn( + "Task {} is in final state but has no finalizedAt timestamp, considering" + + " inactive", + taskId); return false; } @@ -247,8 +269,13 @@ public boolean isTaskActive(String taskId) { Instant now = Instant.now(); boolean withinGracePeriod = now.isBefore(gracePeriodEnd); - LOGGER.debug("Task {} is final. FinalizedAt: {}, GracePeriodEnd: {}, Now: {}, Active: {}", - taskId, finalizedAt, gracePeriodEnd, now, withinGracePeriod); + LOGGER.debug( + "Task {} is final. FinalizedAt: {}, GracePeriodEnd: {}, Now: {}, Active: {}", + taskId, + finalizedAt, + gracePeriodEnd, + now, + withinGracePeriod); return withinGracePeriod; @@ -290,15 +317,17 @@ public boolean isTaskFinalized(String taskId) { Task task = jpaTask.getTask(); // Task is finalized if in final state (ignore grace period) - boolean isFinalized = task.status() != null - && task.status().state() != null - && task.status().state().isFinal(); + boolean isFinalized = + task.status() != null + && task.status().state() != null + && task.status().state().isFinal(); LOGGER.debug("Task {} finalization check: {}", taskId, isFinalized); return isFinalized; } catch (JsonProcessingException e) { - LOGGER.error("Failed to deserialize task with ID: {}, considering not finalized", taskId, e); + LOGGER.error( + "Failed to deserialize task with ID: {}, considering not finalized", taskId, e); return false; } } @@ -311,38 +340,53 @@ public ListTasksResult list(ListTasksParams params, @Nullable ServerCallContext @Transactional @Override - public ListTasksResult listAuthorized(ListTasksParams params, ServerCallContext context, - TaskAuthorizationProvider provider) { + public ListTasksResult listAuthorized( + ListTasksParams params, ServerCallContext context, TaskAuthorizationProvider provider) { return listWithAuthorization(params, context, provider); } - private ListTasksResult listWithAuthorization(ListTasksParams params, @Nullable ServerCallContext context, + private ListTasksResult listWithAuthorization( + ListTasksParams params, + @Nullable ServerCallContext context, @Nullable TaskAuthorizationProvider provider) { - LOGGER.debug("Listing tasks with params: contextId={}, status={}, pageSize={}, pageToken={}", - params.contextId(), params.status(), params.pageSize(), params.pageToken()); + LOGGER.debug( + "Listing tasks with params: contextId={}, status={}, pageSize={}, pageToken={}", + params.contextId(), + params.status(), + params.pageSize(), + params.pageToken()); try { if (provider != null && context == null) { - LOGGER.warn("Authorization provider is configured but no ServerCallContext available — " - + "returning empty result (fail-closed)"); + LOGGER.warn( + "Authorization provider is configured but no ServerCallContext available — " + + "returning empty result (fail-closed)"); return new ListTasksResult(List.of(), 0, 0, null); } - Optional capability = provider == null ? Optional.empty() - : provider.getCapability(JpaTaskAuthorizationCapability.class); - JpaTaskReadScope scope = capability.isPresent() - ? java.util.Objects.requireNonNull(capability.get().readScope(context, TaskOperation.LIST_TASKS)) : null; + Optional capability = + provider == null + ? Optional.empty() + : provider.getCapability(JpaTaskAuthorizationCapability.class); + JpaTaskReadScope scope = + capability.isPresent() + ? java.util.Objects.requireNonNull( + capability.get().readScope(context, TaskOperation.LIST_TASKS)) + : null; if (scope instanceof JpaTaskReadScope.None) { return new ListTasksResult(List.of(), 0, 0, null); } - byte[] subjectKey = scope instanceof JpaTaskReadScope.OwnedBy owned - ? JpaSubjectKey.encode(owned.subjectKey()) : null; + byte[] subjectKey = + scope instanceof JpaTaskReadScope.OwnedBy owned + ? JpaSubjectKey.encode(owned.subjectKey()) + : null; int pageSize = params.getEffectivePageSize(); // Build base WHERE clause (without cursor — shared across iterations) String baseWhereClause = buildBaseWhereClause(params); if (subjectKey != null) { - baseWhereClause += " AND EXISTS (SELECT 1 FROM JpaTaskOwner o" - + " WHERE o.taskId = t.id AND o.subjectKey = :subjectKey)"; + baseWhereClause += + " AND EXISTS (SELECT 1 FROM JpaTaskOwner o" + + " WHERE o.taskId = t.id AND o.subjectKey = :subjectKey)"; } List tasks; @@ -352,8 +396,8 @@ private ListTasksResult listWithAuthorization(ListTasksParams params, @Nullable PageToken scanCursor = cursor; if (provider != null && capability.isEmpty()) { - AuthorizedTaskCount authorizedTaskCount = countAuthorizedTasks( - baseWhereClause, params, cursor, context, provider); + AuthorizedTaskCount authorizedTaskCount = + countAuthorizedTasks(baseWhereClause, params, cursor, context, provider); tasks = new ArrayList<>(pageSize); boolean dbExhausted = false; long totalScanned = 0; @@ -361,8 +405,8 @@ private ListTasksResult listWithAuthorization(ListTasksParams params, @Nullable while (tasks.size() < pageSize && !dbExhausted && totalScanned < maxScanned) { long remaining = maxScanned - totalScanned; int limit = (int) Math.min(remaining, authorizationScanBatchSize); - TypedQuery query = createPageQuery( - baseWhereClause, params, scanCursor, limit); + TypedQuery query = + createPageQuery(baseWhereClause, params, scanCursor, limit); List batch = query.getResultList(); dbExhausted = batch.size() < limit; @@ -386,13 +430,15 @@ private ListTasksResult listWithAuthorization(ListTasksParams params, @Nullable } } - hasMore = authorizedTaskCount.totalSize() > authorizedTaskCount.beforePage() + tasks.size(); + hasMore = + authorizedTaskCount.totalSize() + > authorizedTaskCount.beforePage() + tasks.size(); totalSize = authorizedTaskCount.totalSize(); } else { // Supported authorization scopes are enforced by the database. totalSize = executeCountQuery(baseWhereClause, params, subjectKey); - TypedQuery query = createPageQuery( - baseWhereClause, params, cursor, pageSize + 1, subjectKey); + TypedQuery query = + createPageQuery(baseWhereClause, params, cursor, pageSize + 1, subjectKey); List jpaTasksPage = query.getResultList(); hasMore = jpaTasksPage.size() > pageSize; @@ -418,17 +464,19 @@ private ListTasksResult listWithAuthorization(ListTasksParams params, @Nullable int historyLength = params.getEffectiveHistoryLength(); boolean includeArtifacts = params.shouldIncludeArtifacts(); - List transformedTasks = tasks.stream() - .map(task -> transformTask(task, historyLength, includeArtifacts)) - .toList(); + List transformedTasks = + tasks.stream() + .map(task -> transformTask(task, historyLength, includeArtifacts)) + .toList(); LOGGER.debug("Returning {} tasks out of {} total", transformedTasks.size(), totalSize); - return new ListTasksResult(transformedTasks, totalSize, transformedTasks.size(), nextPageToken); + return new ListTasksResult( + transformedTasks, totalSize, transformedTasks.size(), nextPageToken); } catch (PersistenceException e) { LOGGER.error("Database query failed during list operation", e); - throw new TaskPersistenceException(null, - "Database query failed during list operation", e); + throw new TaskPersistenceException( + null, "Database query failed during list operation", e); } } @@ -446,23 +494,33 @@ private String buildBaseWhereClause(ListTasksParams params) { return sb.toString(); } - private TypedQuery createPageQuery(String baseWhereClause, - ListTasksParams params, @org.jspecify.annotations.Nullable PageToken cursor, int maxResults) { + private TypedQuery createPageQuery( + String baseWhereClause, + ListTasksParams params, + @org.jspecify.annotations.Nullable PageToken cursor, + int maxResults) { return createPageQuery(baseWhereClause, params, cursor, maxResults, null); } - private TypedQuery createPageQuery(String baseWhereClause, - ListTasksParams params, @Nullable PageToken cursor, int maxResults, byte @Nullable [] subjectKey) { + private TypedQuery createPageQuery( + String baseWhereClause, + ListTasksParams params, + @Nullable PageToken cursor, + int maxResults, + byte @Nullable [] subjectKey) { StringBuilder jpql = new StringBuilder("SELECT t FROM JpaTask t").append(baseWhereClause); if (cursor != null) { - jpql.append(" AND (t.statusTimestamp < :tokenTimestamp" - + " OR (t.statusTimestamp = :tokenTimestamp AND t.id > :tokenId))"); + jpql.append( + " AND (t.statusTimestamp < :tokenTimestamp" + + " OR (t.statusTimestamp = :tokenTimestamp AND t.id > :tokenId))"); } jpql.append(" ORDER BY t.statusTimestamp DESC, t.id ASC"); TypedQuery query = em.createQuery(jpql.toString(), JpaTask.class); setFilterParameters(query, params); - if (subjectKey != null) { query.setParameter("subjectKey", subjectKey); } + if (subjectKey != null) { + query.setParameter("subjectKey", subjectKey); + } if (cursor != null) { query.setParameter("tokenTimestamp", cursor.timestamp()); query.setParameter("tokenId", cursor.id()); @@ -471,11 +529,14 @@ private TypedQuery createPageQuery(String baseWhereClause, return query; } - private int executeCountQuery(String baseWhereClause, ListTasksParams params, byte @Nullable [] subjectKey) { + private int executeCountQuery( + String baseWhereClause, ListTasksParams params, byte @Nullable [] subjectKey) { String jpql = "SELECT COUNT(t) FROM JpaTask t" + baseWhereClause; TypedQuery countQuery = em.createQuery(jpql, Long.class); setFilterParameters(countQuery, params); - if (subjectKey != null) { countQuery.setParameter("subjectKey", subjectKey); } + if (subjectKey != null) { + countQuery.setParameter("subjectKey", subjectKey); + } return countQuery.getSingleResult().intValue(); } @@ -484,8 +545,12 @@ private int executeCountQuery(String baseWhereClause, ListTasksParams params, by * cursor. The latter lets the page scan determine whether another authorized task exists * without advertising a token that can only lead to an empty page. */ - private AuthorizedTaskCount countAuthorizedTasks(String baseWhereClause, ListTasksParams params, - @Nullable PageToken pageCursor, ServerCallContext context, TaskAuthorizationProvider provider) { + private AuthorizedTaskCount countAuthorizedTasks( + String baseWhereClause, + ListTasksParams params, + @Nullable PageToken pageCursor, + ServerCallContext context, + TaskAuthorizationProvider provider) { PageToken countCursor = null; long scanned = 0; int totalSize = 0; @@ -499,8 +564,9 @@ private AuthorizedTaskCount countAuthorizedTasks(String baseWhereClause, ListTas int batchSize = (int) Math.min(remaining, authorizationScanBatchSize); // Fetch a look-ahead row. It distinguishes an exhausted database from a full final // batch without inspecting an additional candidate for authorization. - List batch = createPageQuery(baseWhereClause, params, countCursor, batchSize + 1) - .getResultList(); + List batch = + createPageQuery(baseWhereClause, params, countCursor, batchSize + 1) + .getResultList(); int processedCount = Math.min(batch.size(), batchSize); for (int i = 0; i < processedCount; i++) { JpaTask jpaTask = batch.get(i); @@ -529,8 +595,7 @@ private static boolean isAtOrBeforeCursor(JpaTask task, @Nullable PageToken curs || (timestampComparison == 0 && task.getId().compareTo(cursor.id()) <= 0); } - private record AuthorizedTaskCount(int totalSize, int beforePage) { - } + private record AuthorizedTaskCount(int totalSize, int beforePage) {} private void setFilterParameters(TypedQuery query, ListTasksParams params) { if (params.contextId() != null) { @@ -549,8 +614,8 @@ private Task deserializeTask(JpaTask jpaTask) { return jpaTask.getTask(); } catch (JsonProcessingException e) { LOGGER.error("Failed to deserialize task with ID: {}", jpaTask.getId(), e); - throw new TaskSerializationException(jpaTask.getId(), - "Failed to deserialize task during list operation", e); + throw new TaskSerializationException( + jpaTask.getId(), "Failed to deserialize task during list operation", e); } } @@ -573,9 +638,6 @@ private Task transformTask(Task task, int historyLength, boolean includeArtifact } // Build new task with transformed data - return Task.builder(task) - .artifacts(artifacts) - .history(history) - .build(); + return Task.builder(task).artifacts(artifacts).history(history).build(); } } diff --git a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaSubjectKey.java b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaSubjectKey.java index 3f69ca07d..b5d329354 100644 --- a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaSubjectKey.java +++ b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaSubjectKey.java @@ -8,14 +8,16 @@ /** Exact UTF-8 subject encoding; never normalizes or truncates identity. */ public final class JpaSubjectKey { - private JpaSubjectKey() { - } + private JpaSubjectKey() {} public static byte[] encode(String subject) { try { - ByteBuffer encoded = StandardCharsets.UTF_8.newEncoder() - .onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT).encode(CharBuffer.wrap(subject)); + ByteBuffer encoded = + StandardCharsets.UTF_8 + .newEncoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .encode(CharBuffer.wrap(subject)); if (encoded.remaining() > 1024) { throw new IllegalArgumentException("Subject identity exceeds 1024 UTF-8 bytes"); } diff --git a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTask.java b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTask.java index 25091226c..74a138078 100644 --- a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTask.java +++ b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTask.java @@ -1,13 +1,11 @@ package org.a2aproject.sdk.extras.taskstore.database.jpa; -import java.time.Instant; - import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.persistence.Transient; - +import java.time.Instant; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.spec.Task; @@ -34,12 +32,10 @@ public class JpaTask { @Column(name = "finalized_at") private Instant finalizedAt; - @Transient - private Task task; + @Transient private Task task; // Default constructor required by JPA - public JpaTask() { - } + public JpaTask() {} public JpaTask(String id, String taskJson) { this.id = id; @@ -145,7 +141,11 @@ private void updateDenormalizedFields(Task task) { this.state = taskState.name(); // Extract status timestamp for efficient querying and sorting // Truncate to milliseconds for keyset pagination consistency (pageToken uses millis) - this.statusTimestamp = task.status().timestamp().toInstant().truncatedTo(java.time.temporal.ChronoUnit.MILLIS); + this.statusTimestamp = + task.status() + .timestamp() + .toInstant() + .truncatedTo(java.time.temporal.ChronoUnit.MILLIS); } /** diff --git a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationProvider.java b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationProvider.java index 6a844fa3a..9a356c724 100644 --- a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationProvider.java +++ b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationProvider.java @@ -1,12 +1,10 @@ package org.a2aproject.sdk.extras.taskstore.database.jpa; -import java.util.Arrays; -import java.util.Optional; - import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Alternative; import jakarta.inject.Inject; - +import java.util.Arrays; +import java.util.Optional; import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.auth.TaskAuthorizationProvider; import org.a2aproject.sdk.server.auth.TaskOperation; @@ -20,7 +18,8 @@ */ @Dependent @Alternative -public final class JpaTaskAuthorizationProvider implements TaskAuthorizationProvider, JpaTaskAuthorizationCapability { +public final class JpaTaskAuthorizationProvider + implements TaskAuthorizationProvider, JpaTaskAuthorizationCapability { private final JpaTaskOwnershipRepository repository; @Inject @@ -28,16 +27,18 @@ public JpaTaskAuthorizationProvider(JpaTaskOwnershipRepository repository) { this.repository = repository; } - @Override public Optional getCapability(Class type) { - return type == JpaTaskAuthorizationCapability.class ? Optional.of(type.cast(this)) : Optional.empty(); + return type == JpaTaskAuthorizationCapability.class + ? Optional.of(type.cast(this)) + : Optional.empty(); } @Override public JpaTaskReadScope readScope(ServerCallContext context, TaskOperation operation) { return context.getUser() != null && context.getUser().isAuthenticated() - ? new JpaTaskReadScope.OwnedBy(context.getUser().getUsername()) : new JpaTaskReadScope.None(); + ? new JpaTaskReadScope.OwnedBy(context.getUser().getUsername()) + : new JpaTaskReadScope.None(); } @Override @@ -50,13 +51,15 @@ public boolean checkRead(ServerCallContext context, String taskId, TaskOperation return true; } return scope instanceof JpaTaskReadScope.OwnedBy owner - && Arrays.equals(JpaSubjectKey.encode(owner.subjectKey()), repository.owner(taskId)); + && Arrays.equals( + JpaSubjectKey.encode(owner.subjectKey()), repository.owner(taskId)); } @Override public boolean checkWrite(ServerCallContext context, String taskId, TaskOperation operation) { return readScope(context, operation) instanceof JpaTaskReadScope.OwnedBy owner - && Arrays.equals(JpaSubjectKey.encode(owner.subjectKey()), repository.owner(taskId)); + && Arrays.equals( + JpaSubjectKey.encode(owner.subjectKey()), repository.owner(taskId)); } @Override diff --git a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwner.java b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwner.java index 8afb409b6..de2e1bbc1 100644 --- a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwner.java +++ b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwner.java @@ -8,7 +8,9 @@ /** Ownership may precede task persistence, so this entity deliberately has no task foreign key. */ @Entity -@Table(name = "a2a_task_owners", indexes = @Index(name = "a2a_task_owner_subject", columnList = "subject_key, task_id")) +@Table( + name = "a2a_task_owners", + indexes = @Index(name = "a2a_task_owner_subject", columnList = "subject_key, task_id")) public class JpaTaskOwner { @Id @Column(name = "task_id") @@ -17,8 +19,7 @@ public class JpaTaskOwner { @Column(name = "subject_key", nullable = false, length = 1024) private byte[] subjectKey; - protected JpaTaskOwner() { - } + protected JpaTaskOwner() {} public JpaTaskOwner(String taskId, byte[] subjectKey) { this.taskId = taskId; diff --git a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipRepository.java b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipRepository.java index 8873d32a8..6f37c9137 100644 --- a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipRepository.java +++ b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipRepository.java @@ -1,15 +1,13 @@ package org.a2aproject.sdk.extras.taskstore.database.jpa; -import java.util.Objects; -import java.util.function.Function; - import jakarta.enterprise.context.Dependent; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.transaction.Transaction; import jakarta.transaction.TransactionManager; - +import java.util.Objects; +import java.util.function.Function; import org.jspecify.annotations.Nullable; /** Persistent first-writer-wins ownership with independent insert and confirmation transactions. */ @@ -28,23 +26,33 @@ public class JpaTaskOwnershipRepository { * database as the task store; the transaction manager must own their transactions. * Each operation suspends the caller and creates/closes a separate persistence context. */ - public static JpaTaskOwnershipRepository create(EntityManagerFactory factory, TransactionManager manager) { - return new JpaTaskOwnershipRepository(new JpaTaskOwnershipTransactions() { - @Override - public void insert(String id, byte[] subject) { - isolated(factory, manager, em -> { - JpaTaskOwnershipTransactions.insert(em, id, subject); - return null; + public static JpaTaskOwnershipRepository create( + EntityManagerFactory factory, TransactionManager manager) { + return new JpaTaskOwnershipRepository( + new JpaTaskOwnershipTransactions() { + @Override + public void insert(String id, byte[] subject) { + isolated( + factory, + manager, + em -> { + JpaTaskOwnershipTransactions.insert(em, id, subject); + return null; + }); + } + + @Override + public byte @Nullable [] owner(String id) { + return isolated( + factory, manager, em -> JpaTaskOwnershipTransactions.owner(em, id)); + } }); - } - @Override - public byte @Nullable [] owner(String id) { - return isolated(factory, manager, em -> JpaTaskOwnershipTransactions.owner(em, id)); - } - }); } - private static T isolated(EntityManagerFactory factory, TransactionManager manager, Function action) { + private static T isolated( + EntityManagerFactory factory, + TransactionManager manager, + Function action) { Transaction suspended = null; try { suspended = manager.suspend(); diff --git a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipTransactions.java b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipTransactions.java index 8899c4e9e..15e11b018 100644 --- a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipTransactions.java +++ b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipTransactions.java @@ -5,7 +5,6 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.transaction.Transactional; - import org.jspecify.annotations.Nullable; /** @@ -33,9 +32,14 @@ static void insert(EntityManager em, String taskId, byte[] subjectKey) { } static byte @Nullable [] owner(EntityManager em, String taskId) { - return em.createQuery("SELECT o.subjectKey FROM JpaTaskOwner o WHERE o.taskId = :taskId", byte[].class) + return em.createQuery( + "SELECT o.subjectKey FROM JpaTaskOwner o WHERE o.taskId = :taskId", + byte[].class) .setParameter("taskId", taskId) .setHint("jakarta.persistence.cache.retrieveMode", CacheRetrieveMode.BYPASS) - .setHint("org.hibernate.cacheable", false).getResultStream().findFirst().orElse(null); + .setHint("org.hibernate.cacheable", false) + .getResultStream() + .findFirst() + .orElse(null); } } diff --git a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskReadScope.java b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskReadScope.java index 80af84883..964cb16c0 100644 --- a/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskReadScope.java +++ b/extras/task-store-database-jpa/src/main/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskReadScope.java @@ -11,9 +11,7 @@ record OwnedBy(String subjectKey) implements JpaTaskReadScope { } } - record All() implements JpaTaskReadScope { - } + record All() implements JpaTaskReadScope {} - record None() implements JpaTaskReadScope { - } + record None() implements JpaTaskReadScope {} } diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreAuthorizationTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreAuthorizationTest.java index ce6b7dcdc..5685b2f11 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreAuthorizationTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreAuthorizationTest.java @@ -9,17 +9,15 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import jakarta.enterprise.inject.Instance; +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; import java.lang.reflect.Field; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; - -import jakarta.enterprise.inject.Instance; -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; - import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.auth.TaskAuthorizationProvider; @@ -55,10 +53,12 @@ void listWithProviderPreservesAuthorizedTasksBeyondFirstPage() throws Exception ServerCallContext context = mock(ServerCallContext.class); Task first = task("task-1", OffsetDateTime.of(2025, 1, 2, 0, 0, 0, 0, ZoneOffset.UTC)); Task second = task("task-2", OffsetDateTime.of(2025, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)); - when(authProvider.checkRead(context, first.id(), TaskOperation.LIST_TASKS)).thenReturn(true); - when(authProvider.checkRead(context, second.id(), TaskOperation.LIST_TASKS)).thenReturn(true); - JpaDatabaseTaskStore store = storeWithProvider(authProvider, - entityManagerReturning(first, second), 10); + when(authProvider.checkRead(context, first.id(), TaskOperation.LIST_TASKS)) + .thenReturn(true); + when(authProvider.checkRead(context, second.id(), TaskOperation.LIST_TASKS)) + .thenReturn(true); + JpaDatabaseTaskStore store = + storeWithProvider(authProvider, entityManagerReturning(first, second), 10); ListTasksResult firstPage = store.list(new ListTasksParams(1, null), context); @@ -67,7 +67,8 @@ void listWithProviderPreservesAuthorizedTasksBeyondFirstPage() throws Exception assertEquals(1, firstPage.pageSize()); assertNotNull(firstPage.nextPageToken()); - ListTasksResult secondPage = store.list(new ListTasksParams(1, firstPage.nextPageToken()), context); + ListTasksResult secondPage = + store.list(new ListTasksParams(1, firstPage.nextPageToken()), context); assertEquals(List.of(second), secondPage.tasks()); assertEquals(2, secondPage.totalSize()); @@ -78,14 +79,22 @@ void listWithProviderPreservesAuthorizedTasksBeyondFirstPage() throws Exception void listWithProviderSkipsDeniedTasksWithoutReturningAnEmptyPage() throws Exception { TaskAuthorizationProvider authProvider = mock(TaskAuthorizationProvider.class); ServerCallContext context = mock(ServerCallContext.class); - Task firstDenied = task("task-1", OffsetDateTime.of(2025, 1, 3, 0, 0, 0, 0, ZoneOffset.UTC)); - Task secondDenied = task("task-2", OffsetDateTime.of(2025, 1, 2, 0, 0, 0, 0, ZoneOffset.UTC)); + Task firstDenied = + task("task-1", OffsetDateTime.of(2025, 1, 3, 0, 0, 0, 0, ZoneOffset.UTC)); + Task secondDenied = + task("task-2", OffsetDateTime.of(2025, 1, 2, 0, 0, 0, 0, ZoneOffset.UTC)); Task authorized = task("task-3", OffsetDateTime.of(2025, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)); - when(authProvider.checkRead(context, firstDenied.id(), TaskOperation.LIST_TASKS)).thenReturn(false); - when(authProvider.checkRead(context, secondDenied.id(), TaskOperation.LIST_TASKS)).thenReturn(false); - when(authProvider.checkRead(context, authorized.id(), TaskOperation.LIST_TASKS)).thenReturn(true); - JpaDatabaseTaskStore store = storeWithProvider(authProvider, - entityManagerReturning(firstDenied, secondDenied, authorized), 10); + when(authProvider.checkRead(context, firstDenied.id(), TaskOperation.LIST_TASKS)) + .thenReturn(false); + when(authProvider.checkRead(context, secondDenied.id(), TaskOperation.LIST_TASKS)) + .thenReturn(false); + when(authProvider.checkRead(context, authorized.id(), TaskOperation.LIST_TASKS)) + .thenReturn(true); + JpaDatabaseTaskStore store = + storeWithProvider( + authProvider, + entityManagerReturning(firstDenied, secondDenied, authorized), + 10); ListTasksResult firstPage = store.list(new ListTasksParams(1, null), context); @@ -100,13 +109,16 @@ void listAuthorizedReturnsPageForBuilderConstructedStore() throws Exception { ServerCallContext context = mock(ServerCallContext.class); Task first = task("task-1", OffsetDateTime.of(2025, 1, 2, 0, 0, 0, 0, ZoneOffset.UTC)); Task second = task("task-2", OffsetDateTime.of(2025, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)); - when(authProvider.checkRead(context, first.id(), TaskOperation.LIST_TASKS)).thenReturn(true); - when(authProvider.checkRead(context, second.id(), TaskOperation.LIST_TASKS)).thenReturn(true); + when(authProvider.checkRead(context, first.id(), TaskOperation.LIST_TASKS)) + .thenReturn(true); + when(authProvider.checkRead(context, second.id(), TaskOperation.LIST_TASKS)) + .thenReturn(true); JpaDatabaseTaskStore store = new JpaDatabaseTaskStore(); setField(store, "em", entityManagerReturning(first, second)); setField(store, "maxScanned", 10L); - ListTasksResult result = store.listAuthorized(new ListTasksParams(1, null), context, authProvider); + ListTasksResult result = + store.listAuthorized(new ListTasksParams(1, null), context, authProvider); assertEquals(List.of(first), result.tasks()); assertEquals(2, result.totalSize()); @@ -119,11 +131,15 @@ void listAuthorizedFailsWhenExactCountExceedsScanLimit() throws Exception { ServerCallContext context = mock(ServerCallContext.class); Task first = task("task-1", OffsetDateTime.of(2025, 1, 2, 0, 0, 0, 0, ZoneOffset.UTC)); Task second = task("task-2", OffsetDateTime.of(2025, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)); - when(authProvider.checkRead(context, first.id(), TaskOperation.LIST_TASKS)).thenReturn(true); - when(authProvider.checkRead(context, second.id(), TaskOperation.LIST_TASKS)).thenReturn(false); - JpaDatabaseTaskStore store = storeWithProvider(authProvider, entityManagerReturning(first, second), 1); + when(authProvider.checkRead(context, first.id(), TaskOperation.LIST_TASKS)) + .thenReturn(true); + when(authProvider.checkRead(context, second.id(), TaskOperation.LIST_TASKS)) + .thenReturn(false); + JpaDatabaseTaskStore store = + storeWithProvider(authProvider, entityManagerReturning(first, second), 1); - assertThrows(TaskStoreException.class, + assertThrows( + TaskStoreException.class, () -> store.listAuthorized(new ListTasksParams(1, null), context, authProvider)); } @@ -133,7 +149,8 @@ void listWithProviderDoesNotAdvertiseTokenForFullTerminalBatch() throws Exceptio ServerCallContext context = mock(ServerCallContext.class); Task task = task("task-1", OffsetDateTime.of(2025, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)); when(authProvider.checkRead(context, task.id(), TaskOperation.LIST_TASKS)).thenReturn(true); - JpaDatabaseTaskStore store = storeWithProvider(authProvider, entityManagerReturning(task), 10); + JpaDatabaseTaskStore store = + storeWithProvider(authProvider, entityManagerReturning(task), 10); setField(store, "authorizationScanBatchSize", 1); ListTasksResult result = store.list(new ListTasksParams(1, null), context); @@ -144,8 +161,9 @@ void listWithProviderDoesNotAdvertiseTokenForFullTerminalBatch() throws Exceptio } @SuppressWarnings("unchecked") - private static JpaDatabaseTaskStore storeWithProvider(TaskAuthorizationProvider provider, - EntityManager entityManager, long maxScanned) throws Exception { + private static JpaDatabaseTaskStore storeWithProvider( + TaskAuthorizationProvider provider, EntityManager entityManager, long maxScanned) + throws Exception { Instance instance = mock(Instance.class); when(instance.isResolvable()).thenReturn(true); when(instance.get()).thenReturn(provider); @@ -162,43 +180,83 @@ private static EntityManager entityManagerReturning(Task... tasks) throws Except jpaTasks.add(JpaTask.createFromTask(task)); } EntityManager entityManager = mock(EntityManager.class); - when(entityManager.createQuery(org.mockito.ArgumentMatchers.anyString(), eq(JpaTask.class))).thenAnswer(invocation -> { - AtomicInteger maxResults = new AtomicInteger(); - AtomicReference timestamp = new AtomicReference<>(); - AtomicReference id = new AtomicReference<>(); - TypedQuery query = mock(TypedQuery.class); - when(query.setMaxResults(anyInt())).thenAnswer(call -> { - maxResults.set(call.getArgument(0)); - return query; - }); - when(query.setParameter(eq("tokenTimestamp"), org.mockito.ArgumentMatchers.any(java.time.Instant.class))) - .thenAnswer(call -> { - timestamp.set(call.getArgument(1)); - return query; - }); - when(query.setParameter(eq("tokenId"), org.mockito.ArgumentMatchers.anyString())).thenAnswer(call -> { - id.set(call.getArgument(1)); - return query; - }); - when(query.getResultList()).thenAnswer(call -> jpaTasks.stream() - .filter(task -> timestamp.get() == null - || task.getStatusTimestamp().isBefore(timestamp.get()) - || (task.getStatusTimestamp().equals(timestamp.get()) - && task.getId().compareTo(id.get()) > 0)) - .sorted((left, right) -> { - int timestampOrder = right.getStatusTimestamp().compareTo(left.getStatusTimestamp()); - return timestampOrder != 0 ? timestampOrder : left.getId().compareTo(right.getId()); - }) - .limit(maxResults.get()) - .toList()); - return query; - }); + when(entityManager.createQuery(org.mockito.ArgumentMatchers.anyString(), eq(JpaTask.class))) + .thenAnswer( + invocation -> { + AtomicInteger maxResults = new AtomicInteger(); + AtomicReference timestamp = new AtomicReference<>(); + AtomicReference id = new AtomicReference<>(); + TypedQuery query = mock(TypedQuery.class); + when(query.setMaxResults(anyInt())) + .thenAnswer( + call -> { + maxResults.set(call.getArgument(0)); + return query; + }); + when(query.setParameter( + eq("tokenTimestamp"), + org.mockito.ArgumentMatchers.any( + java.time.Instant.class))) + .thenAnswer( + call -> { + timestamp.set(call.getArgument(1)); + return query; + }); + when(query.setParameter( + eq("tokenId"), + org.mockito.ArgumentMatchers.anyString())) + .thenAnswer( + call -> { + id.set(call.getArgument(1)); + return query; + }); + when(query.getResultList()) + .thenAnswer( + call -> + jpaTasks.stream() + .filter( + task -> + timestamp.get() == null + || task.getStatusTimestamp() + .isBefore( + timestamp + .get()) + || (task.getStatusTimestamp() + .equals( + timestamp + .get()) + && task.getId() + .compareTo( + id + .get()) + > 0)) + .sorted( + (left, right) -> { + int timestampOrder = + right.getStatusTimestamp() + .compareTo( + left + .getStatusTimestamp()); + return timestampOrder != 0 + ? timestampOrder + : left.getId() + .compareTo( + right + .getId()); + }) + .limit(maxResults.get()) + .toList()); + return query; + }); return entityManager; } private static Task task(String id, OffsetDateTime timestamp) { - return Task.builder().id(id).contextId("context") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, null, timestamp)).build(); + return Task.builder() + .id(id) + .contextId("context") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, null, timestamp)) + .build(); } private static void setField(Object target, String name, Object value) throws Exception { diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreConfigTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreConfigTest.java index 9fb7132a6..3501d6667 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreConfigTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreConfigTest.java @@ -7,7 +7,6 @@ import static org.mockito.Mockito.when; import java.lang.reflect.Field; - import org.a2aproject.sdk.server.config.A2AConfigProvider; import org.a2aproject.sdk.server.tasks.TaskStoreException; import org.junit.jupiter.api.Test; @@ -19,7 +18,8 @@ void initConfigRejectsNonpositiveScanLimit() throws Exception { for (long maxScanned : new long[] {0, -1}) { JpaDatabaseTaskStore store = configuredStore(maxScanned, 100); - TaskStoreException exception = assertThrows(TaskStoreException.class, store::initConfig); + TaskStoreException exception = + assertThrows(TaskStoreException.class, store::initConfig); assertEquals("a2a.max-scanned-tasks must be greater than zero", exception.getMessage()); } @@ -30,9 +30,12 @@ void initConfigRejectsNonpositiveAuthorizationScanBatchSize() throws Exception { for (int batchSize : new int[] {0, -1}) { JpaDatabaseTaskStore store = configuredStore(10_000, batchSize); - TaskStoreException exception = assertThrows(TaskStoreException.class, store::initConfig); + TaskStoreException exception = + assertThrows(TaskStoreException.class, store::initConfig); - assertEquals("a2a.authorization-scan-batch-size must be greater than zero", exception.getMessage()); + assertEquals( + "a2a.authorization-scan-batch-size must be greater than zero", + exception.getMessage()); } } @@ -43,11 +46,14 @@ void initConfigAcceptsPositiveScanLimits() throws Exception { assertDoesNotThrow(store::initConfig); } - private static JpaDatabaseTaskStore configuredStore(long maxScanned, int batchSize) throws Exception { + private static JpaDatabaseTaskStore configuredStore(long maxScanned, int batchSize) + throws Exception { A2AConfigProvider configProvider = mock(A2AConfigProvider.class); when(configProvider.getValue("a2a.replication.grace-period-seconds")).thenReturn("15"); - when(configProvider.getValue("a2a.max-scanned-tasks")).thenReturn(Long.toString(maxScanned)); - when(configProvider.getValue("a2a.authorization-scan-batch-size")).thenReturn(Integer.toString(batchSize)); + when(configProvider.getValue("a2a.max-scanned-tasks")) + .thenReturn(Long.toString(maxScanned)); + when(configProvider.getValue("a2a.authorization-scan-batch-size")) + .thenReturn(Integer.toString(batchSize)); JpaDatabaseTaskStore store = new JpaDatabaseTaskStore(); Field field = JpaDatabaseTaskStore.class.getDeclaredField("configProvider"); field.setAccessible(true); diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreIntegrationTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreIntegrationTest.java index 7bc227553..5f645828d 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreIntegrationTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreIntegrationTest.java @@ -5,14 +5,13 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; - -import jakarta.inject.Inject; - import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientEvent; import org.a2aproject.sdk.client.TaskEvent; @@ -29,7 +28,6 @@ import org.a2aproject.sdk.spec.TaskQueryParams; import org.a2aproject.sdk.spec.TaskState; import org.a2aproject.sdk.spec.TextPart; -import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,25 +38,21 @@ @QuarkusTest public class JpaDatabaseTaskStoreIntegrationTest { - @Inject - TaskStore taskStore; + @Inject TaskStore taskStore; - @Inject - @PublicAgentCard - AgentCard agentCard; + @Inject @PublicAgentCard AgentCard agentCard; private Client client; @BeforeEach public void setup() throws A2AClientException { - ClientConfig clientConfig = new ClientConfig.Builder() - .setStreaming(false) - .build(); - - client = Client.builder(agentCard) - .clientConfig(clientConfig) - .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) - .build(); + ClientConfig clientConfig = new ClientConfig.Builder().setStreaming(false).build(); + + client = + Client.builder(agentCard) + .clientConfig(clientConfig) + .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()) + .build(); } @Test @@ -69,25 +63,30 @@ public void testIsJpaDatabaseTaskStore() { @Test public void testJpaDatabaseTaskStore() throws Exception { // Send a message creating the Task (no client-provided taskId — server generates it) - Message userMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("create"))) - .messageId("test-msg-1") - .build(); + Message userMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("create"))) + .messageId("test-msg-1") + .build(); CountDownLatch latch = new CountDownLatch(1); AtomicReference taskRef = new AtomicReference<>(); - java.util.function.BiConsumer consumer = (event, card) -> { - if (event instanceof TaskEvent taskEvent) { - taskRef.set(taskEvent.getTask()); - latch.countDown(); - } - }; - - client.sendMessage(userMessage, List.of(consumer), (Throwable e) -> { - latch.countDown(); - }); + java.util.function.BiConsumer consumer = + (event, card) -> { + if (event instanceof TaskEvent taskEvent) { + taskRef.set(taskEvent.getTask()); + latch.countDown(); + } + }; + + client.sendMessage( + userMessage, + List.of(consumer), + (Throwable e) -> { + latch.countDown(); + }); assertTrue(latch.await(10, TimeUnit.SECONDS), "Timeout waiting for task creation"); Task createdTask = taskRef.get(); @@ -99,27 +98,32 @@ public void testJpaDatabaseTaskStore() throws Exception { final String contextId = createdTask.contextId(); // Send a message updating the Task - userMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("add-artifact"))) - .taskId(taskId) - .messageId("test-msg-2") - .contextId(contextId) - .build(); + userMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("add-artifact"))) + .taskId(taskId) + .messageId("test-msg-2") + .contextId(contextId) + .build(); CountDownLatch latch2 = new CountDownLatch(1); AtomicReference taskRef2 = new AtomicReference<>(); - consumer = (event, card) -> { - if (event instanceof TaskEvent taskEvent) { - taskRef2.set(taskEvent.getTask()); - latch2.countDown(); - } - }; - - client.sendMessage(userMessage, List.of(consumer), (Throwable e) -> { - latch2.countDown(); - }); + consumer = + (event, card) -> { + if (event instanceof TaskEvent taskEvent) { + taskRef2.set(taskEvent.getTask()); + latch2.countDown(); + } + }; + + client.sendMessage( + userMessage, + List.of(consumer), + (Throwable e) -> { + latch2.countDown(); + }); assertTrue(latch2.await(10, TimeUnit.SECONDS), "Timeout waiting for task creation"); Task updatedTask = taskRef2.get(); diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java index f95a11fa1..a81db929f 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTest.java @@ -6,15 +6,14 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; - -import jakarta.inject.Inject; -import jakarta.transaction.Transactional; - import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; import org.a2aproject.sdk.server.tasks.TaskStore; import org.a2aproject.sdk.spec.Artifact; @@ -24,18 +23,14 @@ import org.a2aproject.sdk.spec.TaskState; import org.a2aproject.sdk.spec.TaskStatus; import org.a2aproject.sdk.spec.TextPart; -import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; @QuarkusTest public class JpaDatabaseTaskStoreTest { - @Inject - TaskStore taskStore; - + @Inject TaskStore taskStore; - @Inject - jakarta.persistence.EntityManager entityManager; + @Inject jakarta.persistence.EntityManager entityManager; @Test public void testIsJpaDatabaseTaskStore() { @@ -46,19 +41,20 @@ public void testIsJpaDatabaseTaskStore() { @Transactional public void testSaveAndRetrieveTask() { // Create a test task - Task task = Task.builder() - .id("test-task-1") - .contextId("test-context-1") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .metadata(new HashMap<>()) - .build(); + Task task = + Task.builder() + .id("test-task-1") + .contextId("test-context-1") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .metadata(new HashMap<>()) + .build(); // Save the task taskStore.save(task, false); // Retrieve the task Task retrieved = taskStore.get("test-task-1"); - + assertNotNull(retrieved); assertEquals("test-task-1", retrieved.id()); assertEquals("test-context-1", retrieved.contextId()); @@ -69,19 +65,21 @@ public void testSaveAndRetrieveTask() { @Transactional public void testSaveAndRetrieveTaskWithHistory() { // Create a message for the task history - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("Hello, agent!"))) - .messageId("msg-1") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("Hello, agent!"))) + .messageId("msg-1") + .build(); // Create a task with history - Task task = Task.builder() - .id("test-task-2") - .contextId("test-context-2") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .history(Collections.singletonList(message)) - .build(); + Task task = + Task.builder() + .id("test-task-2") + .contextId("test-context-2") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .history(Collections.singletonList(message)) + .build(); // Save the task taskStore.save(task, false); @@ -95,33 +93,36 @@ public void testSaveAndRetrieveTaskWithHistory() { assertEquals(TaskState.TASK_STATE_WORKING, retrieved.status().state()); assertEquals(1, retrieved.history().size()); assertEquals("msg-1", retrieved.history().get(0).messageId()); - assertEquals("Hello, agent!", ((TextPart) retrieved.history().get(0).parts().get(0)).text()); + assertEquals( + "Hello, agent!", ((TextPart) retrieved.history().get(0).parts().get(0)).text()); } @Test @Transactional public void testUpdateExistingTask() { // Create and save initial task - Task initialTask = Task.builder() - .id("test-task-3") - .contextId("test-context-3") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - + Task initialTask = + Task.builder() + .id("test-task-3") + .contextId("test-context-3") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + taskStore.save(initialTask, false); // Update the task - Task updatedTask = Task.builder() - .id("test-task-3") - .contextId("test-context-3") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); - + Task updatedTask = + Task.builder() + .id("test-task-3") + .contextId("test-context-3") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); + taskStore.save(updatedTask, false); // Retrieve and verify the update Task retrieved = taskStore.get("test-task-3"); - + assertNotNull(retrieved); assertEquals("test-task-3", retrieved.id()); assertEquals(TaskState.TASK_STATE_COMPLETED, retrieved.status().state()); @@ -138,12 +139,13 @@ public void testGetNonExistentTask() { @Transactional public void testDeleteTask() { // Create and save a task - Task task = Task.builder() - .id("test-task-4") - .contextId("test-context-4") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - + Task task = + Task.builder() + .id("test-task-4") + .contextId("test-context-4") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + taskStore.save(task, false); // Verify it exists @@ -171,23 +173,24 @@ public void testTaskWithComplexMetadata() { metadata.put("key1", "value1"); metadata.put("key2", 42); metadata.put("key3", true); - - Task task = Task.builder() - .id("test-task-5") - .contextId("test-context-5") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .metadata(metadata) - .build(); + + Task task = + Task.builder() + .id("test-task-5") + .contextId("test-context-5") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .metadata(metadata) + .build(); // Save and retrieve taskStore.save(task, false); Task retrieved = taskStore.get("test-task-5"); - + assertNotNull(retrieved); assertEquals("test-task-5", retrieved.id()); assertNotNull(retrieved.metadata()); assertEquals("value1", retrieved.metadata().get("key1")); - assertEquals(42, ((Number)retrieved.metadata().get("key2")).intValue()); + assertEquals(42, ((Number) retrieved.metadata().get("key2")).intValue()); assertEquals(true, retrieved.metadata().get("key3")); } @@ -195,18 +198,19 @@ public void testTaskWithComplexMetadata() { @Transactional public void testIsTaskActiveForNonFinalTask() { // Create a task in non-final state - Task task = Task.builder() - .id("test-task-active-1") - .contextId("test-context") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - + Task task = + Task.builder() + .id("test-task-active-1") + .contextId("test-context") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + taskStore.save(task, false); - + // Task should be active (not in final state) JpaDatabaseTaskStore jpaDatabaseTaskStore = (JpaDatabaseTaskStore) taskStore; boolean isActive = jpaDatabaseTaskStore.isTaskActive("test-task-active-1"); - + assertEquals(true, isActive, "Non-final task should be active"); } @@ -214,27 +218,29 @@ public void testIsTaskActiveForNonFinalTask() { @Transactional public void testIsTaskActiveForFinalTaskWithinGracePeriod() { // Create a task and update it to final state - Task task = Task.builder() - .id("test-task-active-2") - .contextId("test-context") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - + Task task = + Task.builder() + .id("test-task-active-2") + .contextId("test-context") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + taskStore.save(task, false); - + // Update to final state - Task finalTask = Task.builder() - .id("test-task-active-2") - .contextId("test-context") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); - + Task finalTask = + Task.builder() + .id("test-task-active-2") + .contextId("test-context") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); + taskStore.save(finalTask, false); - + // Task should be active (within grace period - default 15 seconds) JpaDatabaseTaskStore jpaDatabaseTaskStore = (JpaDatabaseTaskStore) taskStore; boolean isActive = jpaDatabaseTaskStore.isTaskActive("test-task-active-2"); - + assertEquals(true, isActive, "Final task within grace period should be active"); } @@ -242,33 +248,35 @@ public void testIsTaskActiveForFinalTaskWithinGracePeriod() { @Transactional public void testIsTaskActiveForFinalTaskBeyondGracePeriod() { // Create and save a task in final state - Task task = Task.builder() - .id("test-task-active-3") - .contextId("test-context") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); - + Task task = + Task.builder() + .id("test-task-active-3") + .contextId("test-context") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); + taskStore.save(task, false); - + // Directly update the finalizedAt timestamp to 20 seconds in the past // (beyond the default 15-second grace period) JpaTask jpaTask = entityManager.find(JpaTask.class, "test-task-active-3"); assertNotNull(jpaTask); - + // Manually set finalizedAt to 20 seconds in the past java.time.Instant pastTime = java.time.Instant.now().minusSeconds(20); - entityManager.createQuery("UPDATE JpaTask j SET j.finalizedAt = :finalizedAt WHERE j.id = :id") + entityManager + .createQuery("UPDATE JpaTask j SET j.finalizedAt = :finalizedAt WHERE j.id = :id") .setParameter("finalizedAt", pastTime) .setParameter("id", "test-task-active-3") .executeUpdate(); - + entityManager.flush(); entityManager.clear(); // Clear persistence context to force fresh read - + // Task should be inactive (beyond grace period) JpaDatabaseTaskStore jpaDatabaseTaskStore = (JpaDatabaseTaskStore) taskStore; boolean isActive = jpaDatabaseTaskStore.isTaskActive("test-task-active-3"); - + assertEquals(false, isActive, "Final task beyond grace period should be inactive"); } @@ -287,10 +295,11 @@ public void testIsTaskActiveForNonExistentTask() { @Transactional public void testListTasksEmpty() { // List with specific context that has no tasks - ListTasksParams params = ListTasksParams.builder() - .contextId("non-existent-context-12345") - .tenant("tenant") - .build(); + ListTasksParams params = + ListTasksParams.builder() + .contextId("non-existent-context-12345") + .tenant("tenant") + .build(); ListTasksResult result = taskStore.list(params, null); assertNotNull(result); @@ -304,33 +313,34 @@ public void testListTasksEmpty() { @Transactional public void testListTasksFilterByContextId() { // Create tasks with different context IDs - Task task1 = Task.builder() - .id("task-context-1") - .contextId("context-A") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - - Task task2 = Task.builder() - .id("task-context-2") - .contextId("context-A") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - - Task task3 = Task.builder() - .id("task-context-3") - .contextId("context-B") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + Task task1 = + Task.builder() + .id("task-context-1") + .contextId("context-A") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + + Task task2 = + Task.builder() + .id("task-context-2") + .contextId("context-A") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + + Task task3 = + Task.builder() + .id("task-context-3") + .contextId("context-B") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); taskStore.save(task1, false); taskStore.save(task2, false); taskStore.save(task3, false); // List tasks for context-A - ListTasksParams params = ListTasksParams.builder() - .contextId("context-A") - .tenant("tenant") - .build(); + ListTasksParams params = + ListTasksParams.builder().contextId("context-A").tenant("tenant").build(); ListTasksResult result = taskStore.list(params, null); assertEquals(2, result.totalSize()); @@ -343,34 +353,38 @@ public void testListTasksFilterByContextId() { @Transactional public void testListTasksFilterByStatus() { // Create tasks with different statuses - use unique context - Task task1 = Task.builder() - .id("task-status-filter-1") - .contextId("context-status-filter-test") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - - Task task2 = Task.builder() - .id("task-status-filter-2") - .contextId("context-status-filter-test") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - - Task task3 = Task.builder() - .id("task-status-filter-3") - .contextId("context-status-filter-test") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + Task task1 = + Task.builder() + .id("task-status-filter-1") + .contextId("context-status-filter-test") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + + Task task2 = + Task.builder() + .id("task-status-filter-2") + .contextId("context-status-filter-test") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + + Task task3 = + Task.builder() + .id("task-status-filter-3") + .contextId("context-status-filter-test") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); taskStore.save(task1, false); taskStore.save(task2, false); taskStore.save(task3, false); // List only WORKING tasks in this context - ListTasksParams params = ListTasksParams.builder() - .contextId("context-status-filter-test") - .tenant("tenant") - .status(TaskState.TASK_STATE_WORKING) - .build(); + ListTasksParams params = + ListTasksParams.builder() + .contextId("context-status-filter-test") + .tenant("tenant") + .status(TaskState.TASK_STATE_WORKING) + .build(); ListTasksResult result = taskStore.list(params, null); assertEquals(1, result.totalSize()); @@ -383,34 +397,38 @@ public void testListTasksFilterByStatus() { @Transactional public void testListTasksCombinedFilters() { // Create tasks with various context IDs and statuses - Task task1 = Task.builder() - .id("task-combined-1") - .contextId("context-X") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - - Task task2 = Task.builder() - .id("task-combined-2") - .contextId("context-X") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - - Task task3 = Task.builder() - .id("task-combined-3") - .contextId("context-Y") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + Task task1 = + Task.builder() + .id("task-combined-1") + .contextId("context-X") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + + Task task2 = + Task.builder() + .id("task-combined-2") + .contextId("context-X") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + + Task task3 = + Task.builder() + .id("task-combined-3") + .contextId("context-Y") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); taskStore.save(task1, false); taskStore.save(task2, false); taskStore.save(task3, false); // List WORKING tasks in context-X - ListTasksParams params = ListTasksParams.builder() - .contextId("context-X") - .tenant("tenant") - .status(TaskState.TASK_STATE_WORKING) - .build(); + ListTasksParams params = + ListTasksParams.builder() + .contextId("context-X") + .tenant("tenant") + .status(TaskState.TASK_STATE_WORKING) + .build(); ListTasksResult result = taskStore.list(params, null); assertEquals(1, result.totalSize()); @@ -427,20 +445,24 @@ public void testListTasksPagination() { // (With timestamp DESC sorting, same timestamps allow ID ASC tie-breaking) OffsetDateTime sameTimestamp = OffsetDateTime.now(java.time.ZoneOffset.UTC); for (int i = 1; i <= 5; i++) { - Task task = Task.builder() - .id("task-page-" + i) - .contextId("context-pagination") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED, null, sameTimestamp)) - .build(); + Task task = + Task.builder() + .id("task-page-" + i) + .contextId("context-pagination") + .status( + new TaskStatus( + TaskState.TASK_STATE_SUBMITTED, null, sameTimestamp)) + .build(); taskStore.save(task, false); } // First page: pageSize=2 - ListTasksParams params1 = ListTasksParams.builder() - .contextId("context-pagination") - .tenant("tenant") - .pageSize(2) - .build(); + ListTasksParams params1 = + ListTasksParams.builder() + .contextId("context-pagination") + .tenant("tenant") + .pageSize(2) + .build(); ListTasksResult result1 = taskStore.list(params1, null); assertEquals(5, result1.totalSize()); @@ -449,12 +471,13 @@ public void testListTasksPagination() { assertNotNull(result1.nextPageToken(), "Should have next page token"); // Second page: use pageToken from first page - ListTasksParams params2 = ListTasksParams.builder() - .contextId("context-pagination") - .tenant("tenant") - .pageSize(2) - .pageToken(result1.nextPageToken()) - .build(); + ListTasksParams params2 = + ListTasksParams.builder() + .contextId("context-pagination") + .tenant("tenant") + .pageSize(2) + .pageToken(result1.nextPageToken()) + .build(); ListTasksResult result2 = taskStore.list(params2, null); assertEquals(5, result2.totalSize()); @@ -462,12 +485,13 @@ public void testListTasksPagination() { assertNotNull(result2.nextPageToken(), "Should have next page token"); // Third page: last page - ListTasksParams params3 = ListTasksParams.builder() - .contextId("context-pagination") - .tenant("tenant") - .pageSize(2) - .pageToken(result2.nextPageToken()) - .build(); + ListTasksParams params3 = + ListTasksParams.builder() + .contextId("context-pagination") + .tenant("tenant") + .pageSize(2) + .pageToken(result2.nextPageToken()) + .build(); ListTasksResult result3 = taskStore.list(params3, null); assertEquals(5, result3.totalSize()); @@ -483,43 +507,56 @@ public void testListTasksPaginationWithDifferentTimestamps() { OffsetDateTime now = OffsetDateTime.now(java.time.ZoneOffset.UTC); // Task 1: 10 minutes ago, ID="task-diff-a" - Task task1 = Task.builder() - .id("task-diff-a") - .contextId("context-diff-timestamps") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, now.minusMinutes(10))) - .build(); + Task task1 = + Task.builder() + .id("task-diff-a") + .contextId("context-diff-timestamps") + .status( + new TaskStatus( + TaskState.TASK_STATE_WORKING, null, now.minusMinutes(10))) + .build(); taskStore.save(task1, false); // Task 2: 5 minutes ago, ID="task-diff-b" - Task task2 = Task.builder() - .id("task-diff-b") - .contextId("context-diff-timestamps") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, now.minusMinutes(5))) - .build(); + Task task2 = + Task.builder() + .id("task-diff-b") + .contextId("context-diff-timestamps") + .status( + new TaskStatus( + TaskState.TASK_STATE_WORKING, null, now.minusMinutes(5))) + .build(); taskStore.save(task2, false); // Task 3: 5 minutes ago, ID="task-diff-c" (same timestamp as task2, tests ID tie-breaker) - Task task3 = Task.builder() - .id("task-diff-c") - .contextId("context-diff-timestamps") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, now.minusMinutes(5))) - .build(); + Task task3 = + Task.builder() + .id("task-diff-c") + .contextId("context-diff-timestamps") + .status( + new TaskStatus( + TaskState.TASK_STATE_WORKING, null, now.minusMinutes(5))) + .build(); taskStore.save(task3, false); // Task 4: Now, ID="task-diff-d" - Task task4 = Task.builder() - .id("task-diff-d") - .contextId("context-diff-timestamps") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, now)) - .build(); + Task task4 = + Task.builder() + .id("task-diff-d") + .contextId("context-diff-timestamps") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, now)) + .build(); taskStore.save(task4, false); // Task 5: 1 minute ago, ID="task-diff-e" - Task task5 = Task.builder() - .id("task-diff-e") - .contextId("context-diff-timestamps") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, now.minusMinutes(1))) - .build(); + Task task5 = + Task.builder() + .id("task-diff-e") + .contextId("context-diff-timestamps") + .status( + new TaskStatus( + TaskState.TASK_STATE_WORKING, null, now.minusMinutes(1))) + .build(); taskStore.save(task5, false); // Expected order (timestamp DESC, id ASC): @@ -530,11 +567,12 @@ public void testListTasksPaginationWithDifferentTimestamps() { // 5. task-diff-a (10 min ago) // Page 1: Get first 2 tasks - ListTasksParams params1 = ListTasksParams.builder() - .contextId("context-diff-timestamps") - .tenant("tenant") - .pageSize(2) - .build(); + ListTasksParams params1 = + ListTasksParams.builder() + .contextId("context-diff-timestamps") + .tenant("tenant") + .pageSize(2) + .build(); ListTasksResult result1 = taskStore.list(params1, null); assertEquals(5, result1.totalSize()); @@ -542,22 +580,25 @@ public void testListTasksPaginationWithDifferentTimestamps() { assertNotNull(result1.nextPageToken(), "Should have next page token"); // Verify first page order - assertEquals("task-diff-d", result1.tasks().get(0).id(), "First task should be most recent"); + assertEquals( + "task-diff-d", result1.tasks().get(0).id(), "First task should be most recent"); assertEquals("task-diff-e", result1.tasks().get(1).id(), "Second task should be 1 min ago"); // Verify pageToken format: "timestamp_millis:taskId" - assertTrue(result1.nextPageToken().contains(":"), "PageToken should have format timestamp:id"); + assertTrue( + result1.nextPageToken().contains(":"), "PageToken should have format timestamp:id"); String[] tokenParts = result1.nextPageToken().split(":", 2); assertEquals(2, tokenParts.length, "PageToken should have exactly 2 parts"); assertEquals("task-diff-e", tokenParts[1], "PageToken should contain last task ID"); // Page 2: Get next 2 tasks - ListTasksParams params2 = ListTasksParams.builder() - .contextId("context-diff-timestamps") - .tenant("tenant") - .pageSize(2) - .pageToken(result1.nextPageToken()) - .build(); + ListTasksParams params2 = + ListTasksParams.builder() + .contextId("context-diff-timestamps") + .tenant("tenant") + .pageSize(2) + .pageToken(result1.nextPageToken()) + .build(); ListTasksResult result2 = taskStore.list(params2, null); assertEquals(5, result2.totalSize()); @@ -565,16 +606,23 @@ public void testListTasksPaginationWithDifferentTimestamps() { assertNotNull(result2.nextPageToken(), "Should have next page token"); // Verify second page order (tasks with same timestamp, sorted by ID) - assertEquals("task-diff-b", result2.tasks().get(0).id(), "Third task should be 5 min ago, ID 'b'"); - assertEquals("task-diff-c", result2.tasks().get(1).id(), "Fourth task should be 5 min ago, ID 'c'"); + assertEquals( + "task-diff-b", + result2.tasks().get(0).id(), + "Third task should be 5 min ago, ID 'b'"); + assertEquals( + "task-diff-c", + result2.tasks().get(1).id(), + "Fourth task should be 5 min ago, ID 'c'"); // Page 3: Get last task - ListTasksParams params3 = ListTasksParams.builder() - .contextId("context-diff-timestamps") - .tenant("tenant") - .pageSize(2) - .pageToken(result2.nextPageToken()) - .build(); + ListTasksParams params3 = + ListTasksParams.builder() + .contextId("context-diff-timestamps") + .tenant("tenant") + .pageSize(2) + .pageToken(result2.nextPageToken()) + .build(); ListTasksResult result3 = taskStore.list(params3, null); assertEquals(5, result3.totalSize()); @@ -600,30 +648,33 @@ public void testListTasksHistoryLimiting() { // Create messages for history List longHistory = new ArrayList<>(); for (int i = 1; i <= 10; i++) { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("Message " + i))) - .messageId("msg-history-limit-" + i) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("Message " + i))) + .messageId("msg-history-limit-" + i) + .build(); longHistory.add(message); } // Create task with long history - use unique context - Task task = Task.builder() - .id("task-history-limit-unique-1") - .contextId("context-history-limit-unique") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .history(longHistory) - .build(); + Task task = + Task.builder() + .id("task-history-limit-unique-1") + .contextId("context-history-limit-unique") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .history(longHistory) + .build(); taskStore.save(task, false); // List with historyLength=3 (should keep only last 3 messages) - filter by unique context - ListTasksParams params = ListTasksParams.builder() - .contextId("context-history-limit-unique") - .tenant("tenant") - .historyLength(3) - .build(); + ListTasksParams params = + ListTasksParams.builder() + .contextId("context-history-limit-unique") + .tenant("tenant") + .historyLength(3) + .build(); ListTasksResult result = taskStore.list(params, null); assertEquals(1, result.tasks().size()); @@ -640,45 +691,53 @@ public void testListTasksHistoryLimiting() { public void testListTasksArtifactInclusion() { // Create task with artifacts - use unique context List artifacts = new ArrayList<>(); - Artifact artifact = Artifact.builder() - .artifactId("artifact-unique-1") - .name("test-artifact") - .parts(Collections.singletonList(new TextPart("Artifact content"))) - .build(); + Artifact artifact = + Artifact.builder() + .artifactId("artifact-unique-1") + .name("test-artifact") + .parts(Collections.singletonList(new TextPart("Artifact content"))) + .build(); artifacts.add(artifact); - Task task = Task.builder() - .id("task-artifact-unique-1") - .contextId("context-artifact-unique") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .artifacts(artifacts) - .build(); + Task task = + Task.builder() + .id("task-artifact-unique-1") + .contextId("context-artifact-unique") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .artifacts(artifacts) + .build(); taskStore.save(task, false); // List without artifacts (default) - filter by unique context - ListTasksParams paramsWithoutArtifacts = ListTasksParams.builder() - .contextId("context-artifact-unique") - .tenant("tenant") - .build(); + ListTasksParams paramsWithoutArtifacts = + ListTasksParams.builder() + .contextId("context-artifact-unique") + .tenant("tenant") + .build(); ListTasksResult resultWithout = taskStore.list(paramsWithoutArtifacts, null); assertEquals(1, resultWithout.tasks().size()); - assertTrue(resultWithout.tasks().get(0).artifacts().isEmpty(), + assertTrue( + resultWithout.tasks().get(0).artifacts().isEmpty(), "By default, artifacts should be excluded"); // List with artifacts - filter by unique context - ListTasksParams paramsWithArtifacts = ListTasksParams.builder() - .contextId("context-artifact-unique") - .tenant("tenant") - .includeArtifacts(true) - .build(); + ListTasksParams paramsWithArtifacts = + ListTasksParams.builder() + .contextId("context-artifact-unique") + .tenant("tenant") + .includeArtifacts(true) + .build(); ListTasksResult resultWith = taskStore.list(paramsWithArtifacts, null); assertEquals(1, resultWith.tasks().size()); - assertEquals(1, resultWith.tasks().get(0).artifacts().size(), + assertEquals( + 1, + resultWith.tasks().get(0).artifacts().size(), "When includeArtifacts=true, artifacts should be included"); - assertEquals("artifact-unique-1", resultWith.tasks().get(0).artifacts().get(0).artifactId()); + assertEquals( + "artifact-unique-1", resultWith.tasks().get(0).artifacts().get(0).artifactId()); } @Test @@ -686,19 +745,21 @@ public void testListTasksArtifactInclusion() { public void testListTasksDefaultPageSize() { // Create 100 tasks (more than default page size of 50) for (int i = 1; i <= 100; i++) { - Task task = Task.builder() - .id("task-default-pagesize-" + String.format("%03d", i)) - .contextId("context-default-pagesize") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + Task task = + Task.builder() + .id("task-default-pagesize-" + String.format("%03d", i)) + .contextId("context-default-pagesize") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); taskStore.save(task, false); } // List without specifying pageSize (should use default of 50) - ListTasksParams params = ListTasksParams.builder() - .contextId("context-default-pagesize") - .tenant("tenant") - .build(); + ListTasksParams params = + ListTasksParams.builder() + .contextId("context-default-pagesize") + .tenant("tenant") + .build(); ListTasksResult result = taskStore.list(params, null); assertEquals(100, result.totalSize()); @@ -710,47 +771,51 @@ public void testListTasksDefaultPageSize() { @Transactional public void testListTasksInvalidPageTokenFormat() { // Create a task - Task task = Task.builder() - .id("task-invalid-token") - .contextId("context-invalid-token") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + Task task = + Task.builder() + .id("task-invalid-token") + .contextId("context-invalid-token") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); taskStore.save(task, false); // Test 1: Legacy ID-only pageToken should throw InvalidParamsError - ListTasksParams params1 = ListTasksParams.builder() - .contextId("context-invalid-token") - .tenant("tenant") - .pageToken("task-invalid-token") // ID-only format (legacy) - .build(); + ListTasksParams params1 = + ListTasksParams.builder() + .contextId("context-invalid-token") + .tenant("tenant") + .pageToken("task-invalid-token") // ID-only format (legacy) + .build(); try { taskStore.list(params1, null); throw new AssertionError("Expected InvalidParamsError for legacy ID-only pageToken"); } catch (org.a2aproject.sdk.spec.InvalidParamsError e) { // Expected - legacy format not supported - assertTrue(e.getMessage().contains("Invalid pageToken format"), + assertTrue( + e.getMessage().contains("Invalid pageToken format"), "Error message should mention invalid format"); } // Test 2: Malformed timestamp in pageToken should throw InvalidParamsError - ListTasksParams params2 = ListTasksParams.builder() - .contextId("context-invalid-token") - .tenant("tenant") - .pageToken("not-a-number:task-id") // Invalid timestamp - .build(); + ListTasksParams params2 = + ListTasksParams.builder() + .contextId("context-invalid-token") + .tenant("tenant") + .pageToken("not-a-number:task-id") // Invalid timestamp + .build(); try { taskStore.list(params2, null); throw new AssertionError("Expected InvalidParamsError for malformed timestamp"); } catch (org.a2aproject.sdk.spec.InvalidParamsError e) { // Expected - malformed timestamp - assertTrue(e.getMessage().contains("timestamp must be numeric"), + assertTrue( + e.getMessage().contains("timestamp must be numeric"), "Error message should mention numeric timestamp requirement"); } } - @Test @Transactional public void testListTasksOrderingById() { @@ -758,23 +823,26 @@ public void testListTasksOrderingById() { // (spec requires sorting by timestamp DESC, then ID ASC) OffsetDateTime sameTimestamp = OffsetDateTime.now(java.time.ZoneOffset.UTC); - Task task1 = Task.builder() - .id("task-order-a") - .contextId("context-order") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED, null, sameTimestamp)) - .build(); - - Task task2 = Task.builder() - .id("task-order-b") - .contextId("context-order") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED, null, sameTimestamp)) - .build(); - - Task task3 = Task.builder() - .id("task-order-c") - .contextId("context-order") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED, null, sameTimestamp)) - .build(); + Task task1 = + Task.builder() + .id("task-order-a") + .contextId("context-order") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED, null, sameTimestamp)) + .build(); + + Task task2 = + Task.builder() + .id("task-order-b") + .contextId("context-order") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED, null, sameTimestamp)) + .build(); + + Task task3 = + Task.builder() + .id("task-order-c") + .contextId("context-order") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED, null, sameTimestamp)) + .build(); // Save in reverse order taskStore.save(task3, false); @@ -782,10 +850,8 @@ public void testListTasksOrderingById() { taskStore.save(task2, false); // List should return sorted by timestamp DESC (all same), then by ID ASC - ListTasksParams params = ListTasksParams.builder() - .contextId("context-order") - .tenant("tenant") - .build(); + ListTasksParams params = + ListTasksParams.builder().contextId("context-order").tenant("tenant").build(); ListTasksResult result = taskStore.list(params, null); assertEquals(3, result.tasks().size()); diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTestAgentCardProducer.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTestAgentCardProducer.java index 32ce758c3..2530e195b 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTestAgentCardProducer.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTestAgentCardProducer.java @@ -1,17 +1,15 @@ package org.a2aproject.sdk.extras.taskstore.database.jpa; -import java.util.Collections; -import java.util.List; - +import io.quarkus.arc.profile.IfBuildProfile; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - +import java.util.Collections; +import java.util.List; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.TransportProtocol; -import io.quarkus.arc.profile.IfBuildProfile; /** * Simple test AgentCard producer for our integration test. @@ -31,9 +29,11 @@ public AgentCard agentCard() { .defaultOutputModes(List.of("text")) .capabilities(AgentCapabilities.builder().build()) .skills(List.of()) - .supportedInterfaces(Collections.singletonList( - new AgentInterface(TransportProtocol.JSONRPC.asString(), "http://localhost:8081") - )) + .supportedInterfaces( + Collections.singletonList( + new AgentInterface( + TransportProtocol.JSONRPC.asString(), + "http://localhost:8081"))) .build(); } } diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTestAgentExecutor.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTestAgentExecutor.java index 30bd21195..aea659528 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTestAgentExecutor.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaDatabaseTaskStoreTestAgentExecutor.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.extras.taskstore.database.jpa; -import java.util.List; - +import io.quarkus.arc.profile.IfBuildProfile; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - +import java.util.List; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; @@ -13,7 +12,6 @@ import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.Part; import org.a2aproject.sdk.spec.TextPart; -import io.quarkus.arc.profile.IfBuildProfile; /** * Simple test AgentExecutor that responds to messages and uses AgentEmitter.addArtifact() @@ -28,7 +26,8 @@ public AgentExecutor agentExecutor() { return new AgentExecutor() { @Override public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2AError { - System.out.println("TestAgentExecutor.execute() called for task: " + context.getTaskId()); + System.out.println( + "TestAgentExecutor.execute() called for task: " + context.getTaskId()); System.out.println("Message " + context.getMessage()); String lastText = getLastTextPart(context.getMessage()); switch (lastText) { @@ -36,7 +35,8 @@ public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2 agentEmitter.submit(); break; case "add-artifact": - agentEmitter.addArtifact(List.of(new TextPart(lastText)), "art-1", "test", null); + agentEmitter.addArtifact( + List.of(new TextPart(lastText)), "art-1", "test", null); break; default: throw new InvalidRequestError(lastText + " is unknown"); diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaSubjectKeyTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaSubjectKeyTest.java index d2022b8a2..6f85e71d1 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaSubjectKeyTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaSubjectKeyTest.java @@ -7,18 +7,24 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; - import org.junit.jupiter.api.Test; class JpaSubjectKeyTest { - @Test void exactEncoding() { + @Test + void exactEncoding() { assertArrayEquals(new byte[] {0x41, 0x6c, 0x69, 0x63, 0x65}, JpaSubjectKey.encode("Alice")); - for (String[] pair : new String[][] {{"Alice", "alice"}, {"Jose", "José"}, {"é", "e\u0301"}, {"alice", "alice "}}) { - assertFalse(Arrays.equals(JpaSubjectKey.encode(pair[0]), JpaSubjectKey.encode(pair[1]))); + for (String[] pair : + new String[][] { + {"Alice", "alice"}, {"Jose", "José"}, {"é", "e\u0301"}, {"alice", "alice "} + }) { + assertFalse( + Arrays.equals(JpaSubjectKey.encode(pair[0]), JpaSubjectKey.encode(pair[1]))); } assertArrayEquals("😀".getBytes(StandardCharsets.UTF_8), JpaSubjectKey.encode("😀")); } - @Test void rejectsMalformedAndOversizedWithoutTruncation() { + + @Test + void rejectsMalformedAndOversizedWithoutTruncation() { assertThrows(IllegalArgumentException.class, () -> JpaSubjectKey.encode("\ud800")); assertThrows(IllegalArgumentException.class, () -> JpaSubjectKey.encode("\udc00")); assertEquals(1024, JpaSubjectKey.encode("é".repeat(512)).length); diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationCdiTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationCdiTest.java index 82fffc182..1079b930f 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationCdiTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationCdiTest.java @@ -3,12 +3,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import jakarta.inject.Inject; import java.time.OffsetDateTime; import java.util.Set; import java.util.UUID; - -import jakarta.inject.Inject; - import org.a2aproject.sdk.server.auth.TaskAuthorizationProvider; import org.a2aproject.sdk.server.auth.TaskOperation; import org.a2aproject.sdk.spec.ListTasksParams; @@ -17,29 +18,40 @@ import org.a2aproject.sdk.spec.TaskStatus; import org.junit.jupiter.api.Test; -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.QuarkusTestProfile; -import io.quarkus.test.junit.TestProfile; - @QuarkusTest @TestProfile(JpaTaskAuthorizationCdiTest.Selected.class) class JpaTaskAuthorizationCdiTest { public static class Selected implements QuarkusTestProfile { - @Override public Set> getEnabledAlternatives() { return Set.of(JpaTaskAuthorizationProvider.class); } + @Override + public Set> getEnabledAlternatives() { + return Set.of(JpaTaskAuthorizationProvider.class); + } } + @Inject TaskAuthorizationProvider provider; @Inject JpaDatabaseTaskStore store; - @Test void explicitlySelectedProviderIsUsedByContextAwareStore() { + @Test + void explicitlySelectedProviderIsUsedByContextAwareStore() { String id = UUID.randomUUID().toString(); var context = JpaTaskOwnershipTest.context("cdi-owner"); assertTrue(provider.getCapability(JpaTaskAuthorizationCapability.class).isPresent()); provider.recordOwnership(context, id, TaskOperation.LIST_TASKS); - store.save(Task.builder().id(id).contextId(id).status(new TaskStatus(TaskState.TASK_STATE_WORKING, - null, OffsetDateTime.parse("2026-01-01T00:00:00Z"))).build(), false); + store.save( + Task.builder() + .id(id) + .contextId(id) + .status( + new TaskStatus( + TaskState.TASK_STATE_WORKING, + null, + OffsetDateTime.parse("2026-01-01T00:00:00Z"))) + .build(), + false); var params = new ListTasksParams(id, null, 1, null, null, null, null, null); assertEquals(1, store.list(params, context).totalSize()); assertEquals(id, store.list(params, context).tasks().get(0).id()); - assertEquals(0, store.list(params, JpaTaskOwnershipTest.context("different-owner")).totalSize()); + assertEquals( + 0, store.list(params, JpaTaskOwnershipTest.context("different-owner")).totalSize()); } } diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseProfile.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseProfile.java index 1c21724ca..249dc9168 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseProfile.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseProfile.java @@ -1,12 +1,11 @@ package org.a2aproject.sdk.extras.taskstore.database.jpa; +import io.quarkus.test.junit.QuarkusTestProfile; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import io.quarkus.test.junit.QuarkusTestProfile; - public abstract class JpaTaskAuthorizationDatabaseProfile implements QuarkusTestProfile { protected abstract String kind(); @@ -18,7 +17,8 @@ public Map getConfigOverrides() { properties.put("quarkus.datasource.\"a2a-java\".devservices.enabled", "false"); properties.put("quarkus.datasource.\"a2a-java\".jdbc.max-size", "12"); properties.put("quarkus.hibernate-orm.\"a2a-java\".datasource", "a2a-java"); - properties.put("quarkus.hibernate-orm.\"a2a-java\".packages", JpaTask.class.getPackageName()); + properties.put( + "quarkus.hibernate-orm.\"a2a-java\".packages", JpaTask.class.getPackageName()); properties.put("quarkus.hibernate-orm.\"a2a-java\".database.generation", "drop-and-create"); properties.put("quarkus.hibernate-orm.log.sql", "false"); properties.put("quarkus.hibernate-orm.\"a2a-java\".log.sql", "false"); @@ -32,16 +32,23 @@ public Set> getEnabledAlternatives() { } public static final class Postgres extends JpaTaskAuthorizationDatabaseProfile { - @Override protected String kind() { return "postgresql"; } + @Override + protected String kind() { + return "postgresql"; + } @Override public List testResources() { - return List.of(new TestResourceEntry(JpaTaskAuthorizationDatabaseResource.Postgres.class)); + return List.of( + new TestResourceEntry(JpaTaskAuthorizationDatabaseResource.Postgres.class)); } } public static final class MySql extends JpaTaskAuthorizationDatabaseProfile { - @Override protected String kind() { return "mysql"; } + @Override + protected String kind() { + return "mysql"; + } @Override public List testResources() { diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseResource.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseResource.java index 6ceff6bce..052bfab3f 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseResource.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseResource.java @@ -1,15 +1,15 @@ package org.a2aproject.sdk.extras.taskstore.database.jpa; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; import java.util.HashMap; import java.util.Map; - -import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.mysql.MySQLContainer; import org.testcontainers.postgresql.PostgreSQLContainer; /** Real database startup deliberately fails when the engine is unavailable. */ -public abstract class JpaTaskAuthorizationDatabaseResource implements QuarkusTestResourceLifecycleManager { +public abstract class JpaTaskAuthorizationDatabaseResource + implements QuarkusTestResourceLifecycleManager { private JdbcDatabaseContainer database; protected abstract JdbcDatabaseContainer createDatabase(); @@ -43,7 +43,9 @@ public static final class MySql extends JpaTaskAuthorizationDatabaseResource { @Override protected JdbcDatabaseContainer createDatabase() { return new MySQLContainer("mysql:8.0") - .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_0900_ai_ci"); + .withCommand( + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_0900_ai_ci"); } } } diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseTest.java index e240a914d..1fb6d0ce5 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationDatabaseTest.java @@ -8,6 +8,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; +import io.agroal.api.AgroalDataSource; +import io.quarkus.agroal.DataSource; +import io.quarkus.hibernate.orm.PersistenceUnit; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.transaction.Status; +import jakarta.transaction.TransactionManager; import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.ResultSet; @@ -23,52 +31,50 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; - -import jakarta.inject.Inject; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.transaction.Status; -import jakarta.transaction.TransactionManager; - import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; -import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.ServerCallContext; +import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.auth.TaskAuthorizationProvider; import org.a2aproject.sdk.server.auth.TaskOperation; import org.a2aproject.sdk.server.auth.User; import org.a2aproject.sdk.server.events.MainEventBusProcessor; -import org.a2aproject.sdk.server.requesthandlers.DefaultRequestHandler; import org.a2aproject.sdk.server.events.QueueManager; +import org.a2aproject.sdk.server.requesthandlers.DefaultRequestHandler; import org.a2aproject.sdk.spec.ListTasksParams; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskState; import org.a2aproject.sdk.spec.TaskStatus; import org.hibernate.Session; -import io.agroal.api.AgroalDataSource; -import io.quarkus.agroal.DataSource; -import io.quarkus.hibernate.orm.PersistenceUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** Identical security assertions run against each production database dialect. */ abstract class JpaTaskAuthorizationDatabaseTest { - private static final List SUBJECTS = List.of("Alice", "alice", "Jose", "José", "é", "e\u0301", "alice "); + private static final List SUBJECTS = + List.of("Alice", "alice", "Jose", "José", "é", "e\u0301", "alice "); private static final OffsetDateTime TIMESTAMP = OffsetDateTime.parse("2025-01-01T00:00:00Z"); @Inject JpaTaskAuthorizationProvider provider; @Inject JpaDatabaseTaskStore store; @Inject TransactionManager transactionManager; - @Inject @PersistenceUnit("a2a-java") EntityManagerFactory factory; - @Inject @DataSource("a2a-java") AgroalDataSource dataSource; + + @Inject + @PersistenceUnit("a2a-java") + EntityManagerFactory factory; + + @Inject + @DataSource("a2a-java") + AgroalDataSource dataSource; protected abstract boolean mysql(); @BeforeEach void clearDatabase() throws Exception { - inTransaction(em -> { - em.createQuery("DELETE FROM JpaTaskOwner").executeUpdate(); - em.createQuery("DELETE FROM JpaTask").executeUpdate(); - }); + inTransaction( + em -> { + em.createQuery("DELETE FROM JpaTaskOwner").executeUpdate(); + em.createQuery("DELETE FROM JpaTask").executeUpdate(); + }); } @Test @@ -77,52 +83,76 @@ void identitiesStaySeparateForChecksOptimizedQueriesHandlerAndFallback() throws for (int page = 0; page < 3; page++) { String id = taskId(subject, page); store.save(task(id), false); - provider.recordOwnership(context(SUBJECTS.get(subject)), id, TaskOperation.MESSAGE_SEND); + provider.recordOwnership( + context(SUBJECTS.get(subject)), id, TaskOperation.MESSAGE_SEND); } } - // Separate transactions and contexts force every subsequent check to reload persisted bytes. + // Separate transactions and contexts force every subsequent check to reload persisted + // bytes. inTransaction(EntityManager::clear); TaskAuthorizationProvider fallback = new UnsupportedCapabilityProvider(provider); for (int subject = 0; subject < SUBJECTS.size(); subject++) { ServerCallContext context = context(SUBJECTS.get(subject)); - List expected = List.of(taskId(subject, 0), taskId(subject, 1), taskId(subject, 2)); + List expected = + List.of(taskId(subject, 0), taskId(subject, 1), taskId(subject, 2)); for (int other = 0; other < SUBJECTS.size(); other++) { String id = taskId(other, 0); - assertEquals(subject == other, provider.checkRead(context, id, TaskOperation.GET_TASK)); - assertEquals(subject == other, provider.checkWrite(context, id, TaskOperation.CANCEL_TASK)); + assertEquals( + subject == other, provider.checkRead(context, id, TaskOperation.GET_TASK)); + assertEquals( + subject == other, + provider.checkWrite(context, id, TaskOperation.CANCEL_TASK)); provider.recordOwnership(context, id, TaskOperation.MESSAGE_SEND); } - assertPages(expected, context, (params, call) -> store.listAuthorized(params, call, provider)); + assertPages( + expected, + context, + (params, call) -> store.listAuthorized(params, call, provider)); assertPages(expected, context, store::list); assertPages(expected, context, handler(provider)::onListTasks); - assertPages(expected, context, (params, call) -> store.listAuthorized(params, call, fallback)); + assertPages( + expected, + context, + (params, call) -> store.listAuthorized(params, call, fallback)); assertPages(expected, context, handler(fallback)::onListTasks); } // Attempts to re-record another subject's task cannot transfer ownership. for (int subject = 0; subject < SUBJECTS.size(); subject++) { - assertArrayEquals(SUBJECTS.get(subject).getBytes(StandardCharsets.UTF_8), owner(taskId(subject, 0))); + assertArrayEquals( + SUBJECTS.get(subject).getBytes(StandardCharsets.UTF_8), + owner(taskId(subject, 0))); } } @Test void generatedSchemaUsesCompleteIndexedVariableLengthBinaryIdentity() throws Exception { - try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { - String columnSql = mysql() - ? "SELECT DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE FROM information_schema.columns " - + "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='a2a_task_owners' AND COLUMN_NAME='subject_key'" - : "SELECT data_type, character_maximum_length, is_nullable FROM information_schema.columns " - + "WHERE table_schema='public' AND table_name='a2a_task_owners' AND column_name='subject_key'"; + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + String columnSql = + mysql() + ? "SELECT DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE FROM" + + " information_schema.columns WHERE TABLE_SCHEMA=DATABASE() AND" + + " TABLE_NAME='a2a_task_owners' AND COLUMN_NAME='subject_key'" + : "SELECT data_type, character_maximum_length, is_nullable FROM" + + " information_schema.columns WHERE table_schema='public' AND" + + " table_name='a2a_task_owners' AND column_name='subject_key'"; try (ResultSet result = statement.executeQuery(columnSql)) { assertTrue(result.next()); assertEquals(mysql() ? "varbinary" : "bytea", result.getString(1)); - if (mysql()) { assertEquals(1024, result.getInt(2)); } + if (mysql()) { + assertEquals(1024, result.getInt(2)); + } assertEquals("NO", result.getString(3)); assertFalse(result.next()); } if (mysql()) { - try (ResultSet result = statement.executeQuery("SELECT COLUMN_NAME, SUB_PART FROM information_schema.statistics " - + "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='a2a_task_owners' " - + "AND INDEX_NAME='a2a_task_owner_subject' ORDER BY SEQ_IN_INDEX")) { + try (ResultSet result = + statement.executeQuery( + "SELECT COLUMN_NAME, SUB_PART FROM information_schema.statistics" + + " WHERE TABLE_SCHEMA=DATABASE() AND" + + " TABLE_NAME='a2a_task_owners' AND" + + " INDEX_NAME='a2a_task_owner_subject' ORDER BY" + + " SEQ_IN_INDEX")) { assertTrue(result.next()); assertEquals("subject_key", result.getString(1)); assertNull(result.getObject(2), "Subject identity must not use a prefix index"); @@ -132,15 +162,23 @@ void generatedSchemaUsesCompleteIndexedVariableLengthBinaryIdentity() throws Exc assertFalse(result.next()); } } else { - try (ResultSet result = statement.executeQuery("SELECT indexdef FROM pg_indexes WHERE schemaname='public' " - + "AND tablename='a2a_task_owners' AND indexname='a2a_task_owner_subject'")) { + try (ResultSet result = + statement.executeQuery( + "SELECT indexdef FROM pg_indexes WHERE schemaname='public' AND" + + " tablename='a2a_task_owners' AND" + + " indexname='a2a_task_owner_subject'")) { assertTrue(result.next()); - assertTrue(result.getString(1).contains("(subject_key, task_id)"), result.getString(1)); + assertTrue( + result.getString(1).contains("(subject_key, task_id)"), + result.getString(1)); assertFalse(result.next()); } } - System.out.println("Authorization database: " + connection.getMetaData().getDatabaseProductName() - + " " + connection.getMetaData().getDatabaseProductVersion()); + System.out.println( + "Authorization database: " + + connection.getMetaData().getDatabaseProductName() + + " " + + connection.getMetaData().getDatabaseProductVersion()); } } @@ -150,30 +188,41 @@ void cdiRecoveryUsesNewSnapshotAndLeavesRepeatableReadCallerCommittable() throws } @Test - void programmaticRecoveryUsesNewSnapshotAndLeavesRepeatableReadCallerCommittable() throws Exception { - JpaTaskAuthorizationProvider programmatic = new JpaTaskAuthorizationProvider( - JpaTaskOwnershipRepository.create(factory, transactionManager)); + void programmaticRecoveryUsesNewSnapshotAndLeavesRepeatableReadCallerCommittable() + throws Exception { + JpaTaskAuthorizationProvider programmatic = + new JpaTaskAuthorizationProvider( + JpaTaskOwnershipRepository.create(factory, transactionManager)); staleCallerSnapshot(programmatic, "programmatic"); } - private void staleCallerSnapshot(JpaTaskAuthorizationProvider testedProvider, String suffix) throws Exception { + private void staleCallerSnapshot(JpaTaskAuthorizationProvider testedProvider, String suffix) + throws Exception { String taskId = "race-" + suffix; String markerId = "marker-" + suffix; CountDownLatch snapshotEstablished = new CountDownLatch(1); CountDownLatch winnerCommitted = new CountDownLatch(1); ExecutorService worker = Executors.newSingleThreadExecutor(); - Future winner = worker.submit(() -> { - await(snapshotEstablished); - testedProvider.recordOwnership(context("winner"), taskId, TaskOperation.MESSAGE_SEND); - winnerCommitted.countDown(); - }); + Future winner = + worker.submit( + () -> { + await(snapshotEstablished); + testedProvider.recordOwnership( + context("winner"), taskId, TaskOperation.MESSAGE_SEND); + winnerCommitted.countDown(); + }); transactionManager.begin(); try (EntityManager caller = factory.createEntityManager()) { caller.joinTransaction(); - caller.unwrap(Session.class).doWork(connection -> { - connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); - assertEquals(Connection.TRANSACTION_REPEATABLE_READ, connection.getTransactionIsolation()); - }); + caller.unwrap(Session.class) + .doWork( + connection -> { + connection.setTransactionIsolation( + Connection.TRANSACTION_REPEATABLE_READ); + assertEquals( + Connection.TRANSACTION_REPEATABLE_READ, + connection.getTransactionIsolation()); + }); assertNull(owner(caller, taskId)); // Establish A's repeatable-read snapshot. caller.persist(JpaTask.createFromTask(task(markerId))); caller.flush(); @@ -181,40 +230,61 @@ private void staleCallerSnapshot(JpaTaskAuthorizationProvider testedProvider, St await(winnerCommitted); winner.get(30, TimeUnit.SECONDS); caller.clear(); - assertNull(owner(caller, taskId), "A must retain an actually stale snapshot after B commits"); + assertNull( + owner(caller, taskId), + "A must retain an actually stale snapshot after B commits"); assertEquals(Status.STATUS_ACTIVE, transactionManager.getStatus()); // The REQUIRES_NEW insert loses to B. Failed insertion must roll back before // confirmation; neither failed insert context nor A's stale context can confirm B. testedProvider.recordOwnership(context("loser"), taskId, TaskOperation.MESSAGE_SEND); assertEquals(Status.STATUS_ACTIVE, transactionManager.getStatus()); - caller.unwrap(Session.class).doWork(connection -> - assertEquals(Connection.TRANSACTION_REPEATABLE_READ, connection.getTransactionIsolation())); + caller.unwrap(Session.class) + .doWork( + connection -> + assertEquals( + Connection.TRANSACTION_REPEATABLE_READ, + connection.getTransactionIsolation())); caller.clear(); assertNull(owner(caller, taskId), "Recovery must not replace or alter A's snapshot"); transactionManager.commit(); } finally { - if (transactionManager.getTransaction() != null) { transactionManager.rollback(); } + if (transactionManager.getTransaction() != null) { + transactionManager.rollback(); + } worker.shutdownNow(); } - assertNotNull(store.get(markerId), "Caller marker must commit despite losing ownership insertion"); + assertNotNull( + store.get(markerId), + "Caller marker must commit despite losing ownership insertion"); assertArrayEquals("winner".getBytes(StandardCharsets.UTF_8), owner(taskId)); assertFalse(testedProvider.checkRead(context("loser"), taskId, TaskOperation.GET_TASK)); assertFalse(testedProvider.checkWrite(context("loser"), taskId, TaskOperation.CANCEL_TASK)); - System.out.println("Ownership stale snapshot: " + suffix + ", caller isolation=REPEATABLE_READ, marker committed"); + System.out.println( + "Ownership stale snapshot: " + + suffix + + ", caller isolation=REPEATABLE_READ, marker committed"); } @Test void ordinaryConcurrentRecordingPreservesOneOwner() throws Exception { - int expectedIsolation = mysql() ? Connection.TRANSACTION_REPEATABLE_READ : Connection.TRANSACTION_READ_COMMITTED; + int expectedIsolation = + mysql() + ? Connection.TRANSACTION_REPEATABLE_READ + : Connection.TRANSACTION_READ_COMMITTED; try (Connection connection = dataSource.getConnection()) { - assertEquals(expectedIsolation, connection.getTransactionIsolation(), "Verify real database default isolation"); + assertEquals( + expectedIsolation, + connection.getTransactionIsolation(), + "Verify real database default isolation"); } CountDownLatch ready = new CountDownLatch(2); CountDownLatch start = new CountDownLatch(1); ExecutorService workers = Executors.newFixedThreadPool(2); try { - Future first = workers.submit(() -> concurrentRecord("one", ready, start, expectedIsolation)); - Future second = workers.submit(() -> concurrentRecord("two", ready, start, expectedIsolation)); + Future first = + workers.submit(() -> concurrentRecord("one", ready, start, expectedIsolation)); + Future second = + workers.submit(() -> concurrentRecord("two", ready, start, expectedIsolation)); await(ready); start.countDown(); first.get(30, TimeUnit.SECONDS); @@ -222,24 +292,34 @@ void ordinaryConcurrentRecordingPreservesOneOwner() throws Exception { } finally { workers.shutdownNow(); } - boolean firstOwner = provider.checkRead(context("one"), "concurrent", TaskOperation.GET_TASK); - boolean secondOwner = provider.checkRead(context("two"), "concurrent", TaskOperation.GET_TASK); + boolean firstOwner = + provider.checkRead(context("one"), "concurrent", TaskOperation.GET_TASK); + boolean secondOwner = + provider.checkRead(context("two"), "concurrent", TaskOperation.GET_TASK); assertTrue(firstOwner ^ secondOwner); } - private void concurrentRecord(String subject, CountDownLatch ready, CountDownLatch start, int isolation) { + private void concurrentRecord( + String subject, CountDownLatch ready, CountDownLatch start, int isolation) { try { transactionManager.begin(); try (EntityManager caller = factory.createEntityManager()) { caller.joinTransaction(); - caller.unwrap(Session.class).doWork(connection -> assertEquals(isolation, connection.getTransactionIsolation())); + caller.unwrap(Session.class) + .doWork( + connection -> + assertEquals( + isolation, connection.getTransactionIsolation())); ready.countDown(); await(start); - provider.recordOwnership(context(subject), "concurrent", TaskOperation.MESSAGE_SEND); + provider.recordOwnership( + context(subject), "concurrent", TaskOperation.MESSAGE_SEND); assertEquals(Status.STATUS_ACTIVE, transactionManager.getStatus()); transactionManager.commit(); } finally { - if (transactionManager.getTransaction() != null) { transactionManager.rollback(); } + if (transactionManager.getTransaction() != null) { + transactionManager.rollback(); + } } } catch (Exception failure) { throw new IllegalStateException(failure); @@ -247,24 +327,36 @@ private void concurrentRecord(String subject, CountDownLatch ready, CountDownLat } protected void textColumnNegativeControl() throws Exception { - try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { - try (ResultSet result = statement.executeQuery("SELECT DEFAULT_COLLATION_NAME FROM information_schema.schemata " - + "WHERE SCHEMA_NAME=DATABASE()")) { + try (Connection connection = dataSource.getConnection(); + Statement statement = connection.createStatement()) { + try (ResultSet result = + statement.executeQuery( + "SELECT DEFAULT_COLLATION_NAME FROM information_schema.schemata " + + "WHERE SCHEMA_NAME=DATABASE()")) { assertTrue(result.next()); assertEquals("utf8mb4_0900_ai_ci", result.getString(1)); } - statement.execute("CREATE TEMPORARY TABLE a2a_text_owner_mutation " - + "(task_id VARCHAR(255) PRIMARY KEY, subject_key VARCHAR(1024) NOT NULL) " - + "CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"); + statement.execute( + "CREATE TEMPORARY TABLE a2a_text_owner_mutation (task_id VARCHAR(255) PRIMARY" + + " KEY, subject_key VARCHAR(1024) NOT NULL) CHARACTER SET utf8mb4 COLLATE" + + " utf8mb4_0900_ai_ci"); try { - statement.execute("INSERT INTO a2a_text_owner_mutation VALUES ('case-1','Alice'),('case-2','alice')," - + "('accent-1','Jose'),('accent-2','José')"); + statement.execute( + "INSERT INTO a2a_text_owner_mutation VALUES" + + " ('case-1','Alice'),('case-2','alice')," + + "('accent-1','Jose'),('accent-2','José')"); for (String subject : List.of("Alice", "Jose")) { - try (var query = connection.prepareStatement("SELECT COUNT(*) FROM a2a_text_owner_mutation WHERE subject_key=?")) { + try (var query = + connection.prepareStatement( + "SELECT COUNT(*) FROM a2a_text_owner_mutation WHERE" + + " subject_key=?")) { query.setString(1, subject); try (ResultSet result = query.executeQuery()) { assertTrue(result.next()); - assertEquals(2, result.getInt(1), "Text mutation must expose overbroad identity equality"); + assertEquals( + 2, + result.getInt(1), + "Text mutation must expose overbroad identity equality"); } } } @@ -281,8 +373,12 @@ private byte[] owner(String id) throws Exception { } private static byte[] owner(EntityManager em, String id) { - return em.createQuery("SELECT o.subjectKey FROM JpaTaskOwner o WHERE o.taskId=:id", byte[].class) - .setParameter("id", id).getResultStream().findFirst().orElse(null); + return em.createQuery( + "SELECT o.subjectKey FROM JpaTaskOwner o WHERE o.taskId=:id", byte[].class) + .setParameter("id", id) + .getResultStream() + .findFirst() + .orElse(null); } private void inTransaction(java.util.function.Consumer action) throws Exception { @@ -292,11 +388,15 @@ private void inTransaction(java.util.function.Consumer action) th action.accept(em); transactionManager.commit(); } finally { - if (transactionManager.getTransaction() != null) { transactionManager.rollback(); } + if (transactionManager.getTransaction() != null) { + transactionManager.rollback(); + } } } - private static void assertPages(List expected, ServerCallContext context, + private static void assertPages( + List expected, + ServerCallContext context, BiFunction listing) { List actual = new ArrayList<>(); String token = null; @@ -306,16 +406,25 @@ private static void assertPages(List expected, ServerCallContext context assertEquals(1, result.tasks().size()); actual.add(result.tasks().get(0).id()); token = result.nextPageToken(); - if (page < expected.size() - 1) { assertNotNull(token); } - else { assertNull(token); } + if (page < expected.size() - 1) { + assertNotNull(token); + } else { + assertNull(token); + } } assertEquals(expected, actual); } private DefaultRequestHandler handler(TaskAuthorizationProvider authorization) { - return DefaultRequestHandler.builder().agentExecutor(mock(AgentExecutor.class)).taskStore(store) - .queueManager(mock(QueueManager.class)).mainEventBusProcessor(mock(MainEventBusProcessor.class)) - .executor(Runnable::run).eventConsumerExecutor(Runnable::run).authorizationProvider(authorization).build(); + return DefaultRequestHandler.builder() + .agentExecutor(mock(AgentExecutor.class)) + .taskStore(store) + .queueManager(mock(QueueManager.class)) + .mainEventBusProcessor(mock(MainEventBusProcessor.class)) + .executor(Runnable::run) + .eventConsumerExecutor(Runnable::run) + .authorizationProvider(authorization) + .build(); } private static void await(CountDownLatch latch) { @@ -327,31 +436,59 @@ private static void await(CountDownLatch latch) { } } - private static String taskId(int subject, int page) { return "identity-" + subject + "-" + page; } + private static String taskId(int subject, int page) { + return "identity-" + subject + "-" + page; + } + private static Task task(String id) { - return Task.builder().id(id).contextId("identity-context") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, TIMESTAMP)).build(); + return Task.builder() + .id(id) + .contextId("identity-context") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, TIMESTAMP)) + .build(); } private static ServerCallContext context(String subject) { - return new ServerCallContext(new User() { - @Override public boolean isAuthenticated() { return true; } - @Override public String getUsername() { return subject; } - }, Map.of(), Set.of()); + return new ServerCallContext( + new User() { + @Override + public boolean isAuthenticated() { + return true; + } + + @Override + public String getUsername() { + return subject; + } + }, + Map.of(), + Set.of()); } - private record UnsupportedCapabilityProvider(TaskAuthorizationProvider delegate) implements TaskAuthorizationProvider { - @Override public boolean checkRead(ServerCallContext context, String id, TaskOperation operation) { + private record UnsupportedCapabilityProvider(TaskAuthorizationProvider delegate) + implements TaskAuthorizationProvider { + @Override + public boolean checkRead(ServerCallContext context, String id, TaskOperation operation) { return delegate.checkRead(context, id, operation); } - @Override public boolean checkWrite(ServerCallContext context, String id, TaskOperation operation) { + + @Override + public boolean checkWrite(ServerCallContext context, String id, TaskOperation operation) { return delegate.checkWrite(context, id, operation); } - @Override public boolean checkCreate(ServerCallContext context, TaskOperation operation) { + + @Override + public boolean checkCreate(ServerCallContext context, TaskOperation operation) { return delegate.checkCreate(context, operation); } - @Override public boolean isTaskRecorded(String id) { return delegate.isTaskRecorded(id); } - @Override public void recordOwnership(ServerCallContext context, String id, TaskOperation operation) { + + @Override + public boolean isTaskRecorded(String id) { + return delegate.isTaskRecorded(id); + } + + @Override + public void recordOwnership(ServerCallContext context, String id, TaskOperation operation) { delegate.recordOwnership(context, id, operation); } } diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationMySqlTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationMySqlTest.java index 607cddda5..ab14d97a6 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationMySqlTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationMySqlTest.java @@ -9,7 +9,10 @@ @TestProfile(JpaTaskAuthorizationDatabaseProfile.MySql.class) @DisabledIfSystemProperty(named = "skipDockerTests", matches = "true") public class JpaTaskAuthorizationMySqlTest extends JpaTaskAuthorizationDatabaseTest { - @Override protected boolean mysql() { return true; } + @Override + protected boolean mysql() { + return true; + } @Test void temporaryTextSchemaDemonstratesCaseAndAccentEqualityFailure() throws Exception { diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationPostgresTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationPostgresTest.java index b8731026c..dcbc8280e 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationPostgresTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationPostgresTest.java @@ -8,5 +8,8 @@ @TestProfile(JpaTaskAuthorizationDatabaseProfile.Postgres.class) @DisabledIfSystemProperty(named = "skipDockerTests", matches = "true") public class JpaTaskAuthorizationPostgresTest extends JpaTaskAuthorizationDatabaseTest { - @Override protected boolean mysql() { return false; } + @Override + protected boolean mysql() { + return false; + } } diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationQueryTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationQueryTest.java index 6bd8b4590..2236566ca 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationQueryTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskAuthorizationQueryTest.java @@ -8,15 +8,15 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; - -import jakarta.inject.Inject; -import jakarta.persistence.EntityManager; - import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.auth.TaskAuthorizationProvider; @@ -27,56 +27,87 @@ import org.a2aproject.sdk.spec.TaskStatus; import org.junit.jupiter.api.Test; -import io.quarkus.narayana.jta.QuarkusTransaction; -import io.quarkus.test.junit.QuarkusTest; - @QuarkusTest class JpaTaskAuthorizationQueryTest { @Inject JpaDatabaseTaskStore store; @Inject JpaTaskOwnershipRepository repository; @Inject EntityManager em; - @Test void exactIdentityIsolationWithEqualTimestampPagesAndFallback() { + @Test + void exactIdentityIsolationWithEqualTimestampPagesAndFallback() { JpaTaskAuthorizationProvider provider = new JpaTaskAuthorizationProvider(repository); - for (String[] pair : new String[][] {{"Alice", "alice"}, {"Jose", "José"}, {"é", "e\u0301"}, {"alice", "alice "}}) { + for (String[] pair : + new String[][] { + {"Alice", "alice"}, {"Jose", "José"}, {"é", "e\u0301"}, {"alice", "alice "} + }) { String group = UUID.randomUUID().toString(); for (int i = 0; i < 8; i++) { String owner = pair[i % 2]; String id = group + i; - persist(id, group, TaskState.TASK_STATE_WORKING, OffsetDateTime.parse("2026-01-01T00:00:00Z")); - provider.recordOwnership(JpaTaskOwnershipTest.context(owner), id, TaskOperation.LIST_TASKS); + persist( + id, + group, + TaskState.TASK_STATE_WORKING, + OffsetDateTime.parse("2026-01-01T00:00:00Z")); + provider.recordOwnership( + JpaTaskOwnershipTest.context(owner), id, TaskOperation.LIST_TASKS); } for (int subject = 0; subject < 2; subject++) { ServerCallContext context = JpaTaskOwnershipTest.context(pair[subject]); List expected = new ArrayList<>(); - for (int i = subject; i < 8; i += 2) { expected.add(group + i); } + for (int i = subject; i < 8; i += 2) { + expected.add(group + i); + } assertEquals(expected, pages(group, context, provider)); JpaTaskAuthorizationProvider observed = org.mockito.Mockito.spy(provider); - var handler = org.a2aproject.sdk.server.requesthandlers.DefaultRequestHandler.builder() - .taskStore(store) - .agentExecutor(mock(org.a2aproject.sdk.server.agentexecution.AgentExecutor.class)) - .queueManager(mock(org.a2aproject.sdk.server.events.QueueManager.class)) - .mainEventBusProcessor(mock(org.a2aproject.sdk.server.events.MainEventBusProcessor.class)) - .executor(Runnable::run).eventConsumerExecutor(Runnable::run) - .authorizationProvider(observed).build(); - var handlerPage = handler.onListTasks(new ListTasksParams(group, null, 1, null, null, null, null, null), context); + var handler = + org.a2aproject.sdk.server.requesthandlers.DefaultRequestHandler.builder() + .taskStore(store) + .agentExecutor( + mock( + org.a2aproject.sdk.server.agentexecution + .AgentExecutor.class)) + .queueManager( + mock(org.a2aproject.sdk.server.events.QueueManager.class)) + .mainEventBusProcessor( + mock( + org.a2aproject.sdk.server.events + .MainEventBusProcessor.class)) + .executor(Runnable::run) + .eventConsumerExecutor(Runnable::run) + .authorizationProvider(observed) + .build(); + var handlerPage = + handler.onListTasks( + new ListTasksParams(group, null, 1, null, null, null, null, null), + context); assertEquals(4, handlerPage.totalSize()); assertEquals(expected.get(0), handlerPage.tasks().get(0).id()); - org.mockito.Mockito.verify(observed).checkRead(context, "", TaskOperation.LIST_TASKS); - org.mockito.Mockito.verify(observed).checkRead(context, expected.get(0), TaskOperation.LIST_TASKS); - org.mockito.Mockito.verify(observed, org.mockito.Mockito.times(2)).checkRead(org.mockito.ArgumentMatchers.eq(context), - org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.eq(TaskOperation.LIST_TASKS)); + org.mockito.Mockito.verify(observed) + .checkRead(context, "", TaskOperation.LIST_TASKS); + org.mockito.Mockito.verify(observed) + .checkRead(context, expected.get(0), TaskOperation.LIST_TASKS); + org.mockito.Mockito.verify(observed, org.mockito.Mockito.times(2)) + .checkRead( + org.mockito.ArgumentMatchers.eq(context), + org.mockito.ArgumentMatchers.anyString(), + org.mockito.ArgumentMatchers.eq(TaskOperation.LIST_TASKS)); TaskAuthorizationProvider wrapper = legacy(provider); assertEquals(expected, pages(group, context, wrapper)); } } } - private List pages(String group, ServerCallContext context, TaskAuthorizationProvider provider) { + private List pages( + String group, ServerCallContext context, TaskAuthorizationProvider provider) { String token = null; List ids = new ArrayList<>(); do { - ListTasksResult page = store.listAuthorized(new ListTasksParams(group, null, 1, token, null, null, null, null), context, provider); + ListTasksResult page = + store.listAuthorized( + new ListTasksParams(group, null, 1, token, null, null, null, null), + context, + provider); assertEquals(4, page.totalSize()); assertEquals(1, page.tasks().size()); ids.add(page.tasks().get(0).id()); @@ -85,97 +116,186 @@ private List pages(String group, ServerCallContext context, TaskAuthoriz return ids; } - @Test void ordinaryFiltersAndAllNoneScopesApplyBeforeCountAndPage() { + @Test + void ordinaryFiltersAndAllNoneScopesApplyBeforeCountAndPage() { String group = UUID.randomUUID().toString(); OffsetDateTime timestamp = OffsetDateTime.parse("2026-01-01T00:00:00Z"); persist(group + "1", group, TaskState.TASK_STATE_WORKING, timestamp); persist(group + "2", group, TaskState.TASK_STATE_SUBMITTED, timestamp); persist(group + "3", group, TaskState.TASK_STATE_WORKING, timestamp.minusDays(1)); - ListTasksParams params = new ListTasksParams(group, TaskState.TASK_STATE_WORKING, 1, null, null, - timestamp.minusHours(1).toInstant(), null, null); + ListTasksParams params = + new ListTasksParams( + group, + TaskState.TASK_STATE_WORKING, + 1, + null, + null, + timestamp.minusHours(1).toInstant(), + null, + null); ServerCallContext context = JpaTaskOwnershipTest.context("filter-owner"); TaskAuthorizationProvider all = scoped(new JpaTaskReadScope.All()); ListTasksResult page = store.listAuthorized(params, context, all); assertEquals(1, page.totalSize()); assertEquals(group + "1", page.tasks().get(0).id()); assertNull(page.nextPageToken()); - ListTasksResult none = store.listAuthorized(params, context, scoped(new JpaTaskReadScope.None())); + ListTasksResult none = + store.listAuthorized(params, context, scoped(new JpaTaskReadScope.None())); assertEquals(0, none.totalSize()); assertTrue(none.tasks().isEmpty()); JpaTaskAuthorizationProvider owned = new JpaTaskAuthorizationProvider(repository); owned.recordOwnership(context, group + "1", TaskOperation.LIST_TASKS); assertEquals(1, store.listAuthorized(params, context, owned).totalSize()); - assertEquals(0, store.listAuthorized(params, JpaTaskOwnershipTest.context("someone else"), owned).totalSize()); + assertEquals( + 0, + store.listAuthorized(params, JpaTaskOwnershipTest.context("someone else"), owned) + .totalSize()); } - @Test void optimizedQueryExceedsTenThousandCandidatesWithoutIndividualChecks() { + @Test + void optimizedQueryExceedsTenThousandCandidatesWithoutIndividualChecks() { String group = UUID.randomUUID().toString(); OffsetDateTime timestamp = OffsetDateTime.parse("2026-01-01T00:00:00Z"); - QuarkusTransaction.requiringNew().run(() -> { - for (int i = 0; i < 10003; i++) { - try { - em.persist(JpaTask.createFromTask(task(group + String.format("%05d", i), group, TaskState.TASK_STATE_WORKING, timestamp))); - } catch (Exception e) { throw new IllegalStateException(e); } - if (i % 100 == 0) { em.flush(); em.clear(); } - } - }); + QuarkusTransaction.requiringNew() + .run( + () -> { + for (int i = 0; i < 10003; i++) { + try { + em.persist( + JpaTask.createFromTask( + task( + group + String.format("%05d", i), + group, + TaskState.TASK_STATE_WORKING, + timestamp))); + } catch (Exception e) { + throw new IllegalStateException(e); + } + if (i % 100 == 0) { + em.flush(); + em.clear(); + } + } + }); String subject = "large-owner"; - for (int i = 10000; i < 10003; i++) { repository.record(group + i, JpaSubjectKey.encode(subject)); } + for (int i = 10000; i < 10003; i++) { + repository.record(group + i, JpaSubjectKey.encode(subject)); + } TaskAuthorizationProvider provider = scoped(new JpaTaskReadScope.OwnedBy(subject)); ListTasksParams params = new ListTasksParams(group, null, 1, null, null, null, null, null); - ListTasksResult first = store.listAuthorized(params, JpaTaskOwnershipTest.context(subject), provider); + ListTasksResult first = + store.listAuthorized(params, JpaTaskOwnershipTest.context(subject), provider); assertEquals(3, first.totalSize()); assertEquals(group + "10000", first.tasks().get(0).id()); assertNotNull(first.nextPageToken()); - ListTasksResult second = store.listAuthorized(new ListTasksParams(group, null, 1, first.nextPageToken(), null, null, null, null), - JpaTaskOwnershipTest.context(subject), provider); + ListTasksResult second = + store.listAuthorized( + new ListTasksParams( + group, null, 1, first.nextPageToken(), null, null, null, null), + JpaTaskOwnershipTest.context(subject), + provider); assertEquals(3, second.totalSize()); assertEquals(group + "10001", second.tasks().get(0).id()); - ListTasksResult third = store.listAuthorized(new ListTasksParams(group, null, 1, second.nextPageToken(), null, null, null, null), - JpaTaskOwnershipTest.context(subject), provider); + ListTasksResult third = + store.listAuthorized( + new ListTasksParams( + group, null, 1, second.nextPageToken(), null, null, null, null), + JpaTaskOwnershipTest.context(subject), + provider); assertEquals(3, third.totalSize()); assertEquals(group + "10002", third.tasks().get(0).id()); assertNull(third.nextPageToken()); - org.mockito.Mockito.verify(provider, org.mockito.Mockito.never()).checkRead(org.mockito.ArgumentMatchers.any(), - org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any()); - assertThrows(org.a2aproject.sdk.server.tasks.TaskStoreException.class, - () -> store.listAuthorized(params, JpaTaskOwnershipTest.context(subject), legacy(new JpaTaskAuthorizationProvider(repository)))); + org.mockito.Mockito.verify(provider, org.mockito.Mockito.never()) + .checkRead( + org.mockito.ArgumentMatchers.any(), + org.mockito.ArgumentMatchers.anyString(), + org.mockito.ArgumentMatchers.any()); + assertThrows( + org.a2aproject.sdk.server.tasks.TaskStoreException.class, + () -> + store.listAuthorized( + params, + JpaTaskOwnershipTest.context(subject), + legacy(new JpaTaskAuthorizationProvider(repository)))); } - @Test void capabilityLookupAndScopeFailuresPropagate() { + @Test + void capabilityLookupAndScopeFailuresPropagate() { TaskAuthorizationProvider provider = mock(TaskAuthorizationProvider.class); - when(provider.getCapability(JpaTaskAuthorizationCapability.class)).thenThrow(new IllegalStateException("lookup")); - assertThrows(IllegalStateException.class, () -> store.listAuthorized(new ListTasksParams(), JpaTaskOwnershipTest.context("x"), provider)); + when(provider.getCapability(JpaTaskAuthorizationCapability.class)) + .thenThrow(new IllegalStateException("lookup")); + assertThrows( + IllegalStateException.class, + () -> + store.listAuthorized( + new ListTasksParams(), + JpaTaskOwnershipTest.context("x"), + provider)); org.mockito.Mockito.reset(provider); - when(provider.getCapability(JpaTaskAuthorizationCapability.class)).thenReturn(Optional.of((context, operation) -> { - throw new IllegalStateException("scope"); - })); - assertThrows(IllegalStateException.class, () -> store.listAuthorized(new ListTasksParams(), JpaTaskOwnershipTest.context("x"), provider)); + when(provider.getCapability(JpaTaskAuthorizationCapability.class)) + .thenReturn( + Optional.of( + (context, operation) -> { + throw new IllegalStateException("scope"); + })); + assertThrows( + IllegalStateException.class, + () -> + store.listAuthorized( + new ListTasksParams(), + JpaTaskOwnershipTest.context("x"), + provider)); } private static TaskAuthorizationProvider scoped(JpaTaskReadScope scope) { TaskAuthorizationProvider provider = mock(TaskAuthorizationProvider.class); - when(provider.getCapability(JpaTaskAuthorizationCapability.class)).thenReturn(Optional.of((context, operation) -> scope)); + when(provider.getCapability(JpaTaskAuthorizationCapability.class)) + .thenReturn(Optional.of((context, operation) -> scope)); return provider; } private static TaskAuthorizationProvider legacy(JpaTaskAuthorizationProvider delegate) { return new TaskAuthorizationProvider() { - public boolean checkRead(ServerCallContext c, String id, TaskOperation op) { return delegate.checkRead(c, id, op); } - public boolean checkWrite(ServerCallContext c, String id, TaskOperation op) { return delegate.checkWrite(c, id, op); } - public boolean checkCreate(ServerCallContext c, TaskOperation op) { return delegate.checkCreate(c, op); } - public boolean isTaskRecorded(String id) { return delegate.isTaskRecorded(id); } - public void recordOwnership(ServerCallContext c, String id, TaskOperation op) { delegate.recordOwnership(c, id, op); } + public boolean checkRead(ServerCallContext c, String id, TaskOperation op) { + return delegate.checkRead(c, id, op); + } + + public boolean checkWrite(ServerCallContext c, String id, TaskOperation op) { + return delegate.checkWrite(c, id, op); + } + + public boolean checkCreate(ServerCallContext c, TaskOperation op) { + return delegate.checkCreate(c, op); + } + + public boolean isTaskRecorded(String id) { + return delegate.isTaskRecorded(id); + } + + public void recordOwnership(ServerCallContext c, String id, TaskOperation op) { + delegate.recordOwnership(c, id, op); + } }; } private void persist(String id, String group, TaskState state, OffsetDateTime timestamp) { - QuarkusTransaction.requiringNew().run(() -> { - try { em.persist(JpaTask.createFromTask(task(id, group, state, timestamp))); } - catch (Exception e) { throw new IllegalStateException(e); } - }); + QuarkusTransaction.requiringNew() + .run( + () -> { + try { + em.persist( + JpaTask.createFromTask(task(id, group, state, timestamp))); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); } + private static Task task(String id, String group, TaskState state, OffsetDateTime timestamp) { - return Task.builder().id(id).contextId(group).status(new TaskStatus(state, null, timestamp)).build(); + return Task.builder() + .id(id) + .contextId(group) + .status(new TaskStatus(state, null, timestamp)) + .build(); } } diff --git a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipTest.java b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipTest.java index d720f1052..60da4b66b 100644 --- a/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipTest.java +++ b/extras/task-store-database-jpa/src/test/java/org/a2aproject/sdk/extras/taskstore/database/jpa/JpaTaskOwnershipTest.java @@ -7,26 +7,23 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.transaction.TransactionManager; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; - -import jakarta.inject.Inject; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; -import jakarta.transaction.TransactionManager; - import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.auth.TaskOperation; import org.a2aproject.sdk.server.auth.User; import org.junit.jupiter.api.Test; -import io.quarkus.narayana.jta.QuarkusTransaction; -import io.quarkus.test.junit.QuarkusTest; - @QuarkusTest class JpaTaskOwnershipTest { @Inject JpaTaskOwnershipRepository repository; @@ -34,29 +31,50 @@ class JpaTaskOwnershipTest { @Inject EntityManagerFactory factory; @Inject TransactionManager manager; @Inject JpaDatabaseTaskStore store; - @Inject @jakarta.enterprise.inject.Any jakarta.enterprise.inject.Instance providers; + + @Inject @jakarta.enterprise.inject.Any + jakarta.enterprise.inject.Instance + providers; static ServerCallContext context(String name) { - return new ServerCallContext(new User() { - public boolean isAuthenticated() { return true; } - public String getUsername() { return name; } - }, Map.of(), Set.of()); + return new ServerCallContext( + new User() { + public boolean isAuthenticated() { + return true; + } + + public String getUsername() { + return name; + } + }, + Map.of(), + Set.of()); } - @Test void providerRemainsOptIn() { + @Test + void providerRemainsOptIn() { assertTrue(providers.isUnsatisfied()); } - @Test void persistedExactIdentitiesAndRestart() { + @Test + void persistedExactIdentitiesAndRestart() { JpaTaskAuthorizationProvider provider = new JpaTaskAuthorizationProvider(repository); - for (String[] pair : new String[][] {{"Alice", "alice"}, {"Jose", "José"}, {"é", "e\u0301"}, {"alice", "alice "}, {"😀", "😁"}}) { + for (String[] pair : + new String[][] { + {"Alice", "alice"}, + {"Jose", "José"}, + {"é", "e\u0301"}, + {"alice", "alice "}, + {"😀", "😁"} + }) { String first = UUID.randomUUID().toString(); String second = UUID.randomUUID().toString(); provider.recordOwnership(context(pair[0]), first, TaskOperation.LIST_TASKS); provider.recordOwnership(context(pair[1]), second, TaskOperation.LIST_TASKS); QuarkusTransaction.requiringNew().run(() -> em.clear()); - JpaTaskAuthorizationProvider restarted = new JpaTaskAuthorizationProvider( - JpaTaskOwnershipRepository.create(factory, manager)); + JpaTaskAuthorizationProvider restarted = + new JpaTaskAuthorizationProvider( + JpaTaskOwnershipRepository.create(factory, manager)); assertTrue(restarted.checkRead(context(pair[0]), first, TaskOperation.LIST_TASKS)); assertTrue(restarted.checkWrite(context(pair[1]), second, TaskOperation.LIST_TASKS)); assertFalse(restarted.checkRead(context(pair[1]), first, TaskOperation.LIST_TASKS)); @@ -78,53 +96,107 @@ static ServerCallContext context(String name) { assertTrue(provider.getCapability(JpaTaskAuthorizationCapability.class).isPresent()); } - @Test void concurrentFirstWriterDoesNotPoisonCallerTransactions() throws Exception { + @Test + void concurrentFirstWriterDoesNotPoisonCallerTransactions() throws Exception { String id = UUID.randomUUID().toString(); CyclicBarrier start = new CyclicBarrier(2); var executor = Executors.newFixedThreadPool(2); try { - var futures = java.util.stream.Stream.of("first", "second").map(subject -> executor.submit(() -> { - start.await(20, TimeUnit.SECONDS); - QuarkusTransaction.requiringNew().run(() -> { - new JpaTaskAuthorizationProvider(repository).recordOwnership(context(subject), id, TaskOperation.LIST_TASKS); - em.createNativeQuery("INSERT INTO a2a_tasks(task_id, task_data) VALUES (:id, '{}')") - .setParameter("id", id + subject).executeUpdate(); - }); - return subject; - })).toList(); - for (var future : futures) { future.get(30, TimeUnit.SECONDS); } + var futures = + java.util.stream.Stream.of("first", "second") + .map( + subject -> + executor.submit( + () -> { + start.await(20, TimeUnit.SECONDS); + QuarkusTransaction.requiringNew() + .run( + () -> { + new JpaTaskAuthorizationProvider( + repository) + .recordOwnership( + context( + subject), + id, + TaskOperation + .LIST_TASKS); + em.createNativeQuery( + "INSERT" + + " INTO" + + " a2a_tasks(task_id," + + " task_data)" + + " VALUES" + + " (:id," + + " '{}')") + .setParameter( + "id", + id + + subject) + .executeUpdate(); + }); + return subject; + })) + .toList(); + for (var future : futures) { + future.get(30, TimeUnit.SECONDS); + } JpaTaskAuthorizationProvider provider = new JpaTaskAuthorizationProvider(repository); - assertTrue(provider.checkRead(context("first"), id, TaskOperation.LIST_TASKS) - ^ provider.checkRead(context("second"), id, TaskOperation.LIST_TASKS)); - QuarkusTransaction.requiringNew().run(() -> assertEquals(2L, - em.createQuery("SELECT COUNT(t) FROM JpaTask t WHERE t.id LIKE :prefix", Long.class) - .setParameter("prefix", id + "%").getSingleResult())); - } finally { executor.shutdownNow(); } + assertTrue( + provider.checkRead(context("first"), id, TaskOperation.LIST_TASKS) + ^ provider.checkRead(context("second"), id, TaskOperation.LIST_TASKS)); + QuarkusTransaction.requiringNew() + .run( + () -> + assertEquals( + 2L, + em.createQuery( + "SELECT COUNT(t) FROM JpaTask t WHERE" + + " t.id LIKE :prefix", + Long.class) + .setParameter("prefix", id + "%") + .getSingleResult())); + } finally { + executor.shutdownNow(); + } } - @Test void invalidSubjectsFailBeforeAnyPersistenceAccess() { + @Test + void invalidSubjectsFailBeforeAnyPersistenceAccess() { JpaTaskOwnershipRepository untouched = mock(JpaTaskOwnershipRepository.class); JpaTaskAuthorizationProvider provider = new JpaTaskAuthorizationProvider(untouched); for (String subject : new String[] {"\ud800", "é".repeat(513)}) { var context = context(subject); - assertThrows(IllegalArgumentException.class, () -> provider.checkRead(context, "", TaskOperation.LIST_TASKS)); - assertThrows(IllegalArgumentException.class, () -> provider.checkRead(context, "id", TaskOperation.LIST_TASKS)); - assertThrows(IllegalArgumentException.class, () -> provider.checkWrite(context, "id", TaskOperation.LIST_TASKS)); - assertThrows(IllegalArgumentException.class, () -> provider.checkCreate(context, TaskOperation.LIST_TASKS)); - assertThrows(IllegalArgumentException.class, () -> provider.recordOwnership(context, "id", TaskOperation.LIST_TASKS)); + assertThrows( + IllegalArgumentException.class, + () -> provider.checkRead(context, "", TaskOperation.LIST_TASKS)); + assertThrows( + IllegalArgumentException.class, + () -> provider.checkRead(context, "id", TaskOperation.LIST_TASKS)); + assertThrows( + IllegalArgumentException.class, + () -> provider.checkWrite(context, "id", TaskOperation.LIST_TASKS)); + assertThrows( + IllegalArgumentException.class, + () -> provider.checkCreate(context, TaskOperation.LIST_TASKS)); + assertThrows( + IllegalArgumentException.class, + () -> provider.recordOwnership(context, "id", TaskOperation.LIST_TASKS)); } org.mockito.Mockito.verifyNoInteractions(untouched); } - @Test void programmaticFailureRollsBackAndRestoresCaller() throws Exception { + @Test + void programmaticFailureRollsBackAndRestoresCaller() throws Exception { EntityManagerFactory failingFactory = mock(EntityManagerFactory.class); TransactionManager transactions = mock(TransactionManager.class); jakarta.transaction.Transaction caller = mock(jakarta.transaction.Transaction.class); jakarta.transaction.Transaction inner = mock(jakarta.transaction.Transaction.class); when(transactions.suspend()).thenReturn(caller); when(transactions.getTransaction()).thenReturn(inner); - when(failingFactory.createEntityManager()).thenThrow(new IllegalStateException("no persistence context")); - JpaTaskOwnershipRepository programmatic = JpaTaskOwnershipRepository.create(failingFactory, transactions); + when(failingFactory.createEntityManager()) + .thenThrow(new IllegalStateException("no persistence context")); + JpaTaskOwnershipRepository programmatic = + JpaTaskOwnershipRepository.create(failingFactory, transactions); assertThrows(IllegalStateException.class, () -> programmatic.owner("id")); org.mockito.InOrder order = org.mockito.Mockito.inOrder(transactions); order.verify(transactions).suspend(); @@ -134,14 +206,23 @@ static ServerCallContext context(String name) { order.verify(transactions).resume(caller); } - @Test void missingOwnerAndFailedConfirmationPreserveInsertFailure() { + @Test + void missingOwnerAndFailedConfirmationPreserveInsertFailure() { JpaTaskOwnershipTransactions transactions = mock(JpaTaskOwnershipTransactions.class); RuntimeException failure = new IllegalStateException("insert failure"); - org.mockito.Mockito.doThrow(failure).when(transactions).insert(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any()); + org.mockito.Mockito.doThrow(failure) + .when(transactions) + .insert( + org.mockito.ArgumentMatchers.anyString(), + org.mockito.ArgumentMatchers.any()); JpaTaskOwnershipRepository failing = new JpaTaskOwnershipRepository(transactions); - assertEquals(failure, assertThrows(RuntimeException.class, () -> failing.record("id", new byte[] {1}))); + assertEquals( + failure, + assertThrows(RuntimeException.class, () -> failing.record("id", new byte[] {1}))); when(transactions.owner("id")).thenThrow(new IllegalStateException("confirmation failure")); - assertEquals(failure, assertThrows(RuntimeException.class, () -> failing.record("id", new byte[] {1}))); + assertEquals( + failure, + assertThrows(RuntimeException.class, () -> failing.record("id", new byte[] {1}))); assertEquals(1, failure.getSuppressed().length); } } diff --git a/http-client/pom.xml b/http-client/pom.xml index fb7ad0cbd..cd37533b9 100644 --- a/http-client/pom.xml +++ b/http-client/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java index ff3c6b975..ee1beb841 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2ACardResolver.java @@ -1,12 +1,11 @@ package org.a2aproject.sdk.client.http; +import static org.a2aproject.sdk.util.Assert.checkNotNullParam; + import java.io.IOException; import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; - -import static org.a2aproject.sdk.util.Assert.checkNotNullParam; - import org.a2aproject.sdk.grpc.utils.JSONRPCUtils; import org.a2aproject.sdk.grpc.utils.ProtoUtils; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; @@ -86,7 +85,13 @@ public class A2ACardResolver { private final @Nullable String fallbackUrl; private final @Nullable Map authHeaders; - private A2ACardResolver(A2AHttpClient httpClient, String baseUrl, @Nullable String tenant, @Nullable String agentCardPath, @Nullable Map authHeaders) throws A2AClientError { + private A2ACardResolver( + A2AHttpClient httpClient, + String baseUrl, + @Nullable String tenant, + @Nullable String agentCardPath, + @Nullable Map authHeaders) + throws A2AClientError { checkNotNullParam("httpClient", httpClient); checkNotNullParam("baseUrl", baseUrl); this.httpClient = httpClient; @@ -124,8 +129,7 @@ public static class Builder { private @Nullable String agentCardPath; private @Nullable Map authHeaders; - private Builder() { - } + private Builder() {} /** * Sets the HTTP client to use for fetching agent cards. @@ -209,9 +213,10 @@ public Builder authHeader(String name, String value) { * @throws IllegalArgumentException if baseUrl is null */ public A2ACardResolver build() throws A2AClientError { - A2AHttpClient client = httpClient != null - ? httpClient - : A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT); + A2AHttpClient client = + httpClient != null + ? httpClient + : A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT); if (baseUrl == null) { throw new IllegalArgumentException("baseUrl must not be null"); } @@ -240,7 +245,12 @@ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError { return fetchAgentCard(cardUrl); } catch (A2AClientHTTPError e) { if (fallbackUrl != null && e.getCode() == 404) { - LOGGER.debug("Failed to fetch agent card from {} (status {}); retrying with provided URL: {}", cardUrl, e.getCode(), fallbackUrl); + LOGGER.debug( + "Failed to fetch agent card from {} (status {}); retrying with provided" + + " URL: {}", + cardUrl, + e.getCode(), + fallbackUrl); try { return fetchAgentCard(fallbackUrl); } catch (A2AClientError fallbackEx) { @@ -252,7 +262,12 @@ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError { } } - private static ResolvedUrls resolveUrls(String cleanBase, String originalBase, @Nullable String tenant, @Nullable String agentCardPath) throws URISyntaxException { + private static ResolvedUrls resolveUrls( + String cleanBase, + String originalBase, + @Nullable String tenant, + @Nullable String agentCardPath) + throws URISyntaxException { if (agentCardPath != null && !agentCardPath.isBlank()) { // Custom path: tenant goes as path prefix (explicit override); no fallback. String baseUrlWithTenant = Utils.buildBaseUrl(cleanBase, tenant); @@ -286,10 +301,12 @@ private static boolean isSameUrl(String a, String b) { } private AgentCard fetchAgentCard(String url) throws A2AClientError, A2AClientJSONError { - A2AHttpClient.GetBuilder builder = httpClient.createGet() - .url(url) - .followRedirects(true) - .addHeader("Content-Type", "application/json"); + A2AHttpClient.GetBuilder builder = + httpClient + .createGet() + .url(url) + .followRedirects(true) + .addHeader("Content-Type", "application/json"); if (authHeaders != null) { builder.addHeaders(authHeaders); @@ -300,8 +317,10 @@ private AgentCard fetchAgentCard(String url) throws A2AClientError, A2AClientJSO A2AHttpResponse response = builder.get(); if (!response.success()) { String msg = "Failed to obtain agent card: " + response.status(); - LOGGER.debug("Failed to fetch agent card from {}, status: {}", url, response.status()); - throw new A2AClientHTTPError(response.status(), msg, response.body(), response.headers().toMap()); + LOGGER.debug( + "Failed to fetch agent card from {}, status: {}", url, response.status()); + throw new A2AClientHTTPError( + response.status(), msg, response.body(), response.headers().toMap()); } body = response.body(); LOGGER.debug("Successfully fetched agent card from {}", url); @@ -313,7 +332,8 @@ private AgentCard fetchAgentCard(String url) throws A2AClientError, A2AClientJSO } try { - org.a2aproject.sdk.grpc.AgentCard.Builder agentCardBuilder = org.a2aproject.sdk.grpc.AgentCard.newBuilder(); + org.a2aproject.sdk.grpc.AgentCard.Builder agentCardBuilder = + org.a2aproject.sdk.grpc.AgentCard.newBuilder(); JSONRPCUtils.parseJsonString(body, agentCardBuilder, "", true); return ProtoUtils.FromProto.agentCard(agentCardBuilder); } catch (JsonProcessingException e) { diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClient.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClient.java index b93c9e55d..43a77434b 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClient.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClient.java @@ -46,10 +46,13 @@ public interface A2AHttpClient { /** HTTP Content-Type header name. */ String CONTENT_TYPE = "Content-Type"; + /** JSON content type value. */ String APPLICATION_JSON = "application/json"; + /** HTTP Accept header name. */ String ACCEPT = "Accept"; + /** SSE event stream content type. */ String EVENT_STREAM = "text/event-stream"; @@ -155,7 +158,8 @@ default GetBuilder followRedirects(boolean follow) { CompletableFuture getAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, - Runnable completeRunnable) throws IOException, InterruptedException; + Runnable completeRunnable) + throws IOException, InterruptedException; } /** @@ -220,7 +224,8 @@ default PostBuilder followRedirects(boolean follow) { CompletableFuture postAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, - Runnable completeRunnable) throws IOException, InterruptedException; + Runnable completeRunnable) + throws IOException, InterruptedException; } /** diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClientConfiguration.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClientConfiguration.java index 8218d91c0..c2a5f1b1c 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClientConfiguration.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClientConfiguration.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.client.http; import java.util.List; - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; @@ -17,7 +16,8 @@ public record A2AHttpClientConfiguration( @Nullable RedirectHeaderPolicy redirectHeaderPolicy) { /** The default configuration, which selects the highest-priority available provider. */ - public static final A2AHttpClientConfiguration DEFAULT = new A2AHttpClientConfiguration(List.of(), null, null); + public static final A2AHttpClientConfiguration DEFAULT = + new A2AHttpClientConfiguration(List.of(), null, null); public A2AHttpClientConfiguration { Assert.checkNotNullParam("excludedProviderNames", excludedProviderNames); @@ -52,8 +52,10 @@ private Builder() {} /** Sets provider names that must not be considered by the factory. */ public Builder excludedProviderNames(List excludedProviderNames) { - this.excludedProviderNames = List.copyOf(Assert.checkNotNullParam( - "excludedProviderNames", excludedProviderNames)); + this.excludedProviderNames = + List.copyOf( + Assert.checkNotNullParam( + "excludedProviderNames", excludedProviderNames)); return this; } @@ -71,7 +73,8 @@ public Builder redirectHeaderPolicy(@Nullable RedirectHeaderPolicy redirectHeade /** Creates the immutable configuration. */ public A2AHttpClientConfiguration build() { - return new A2AHttpClientConfiguration(excludedProviderNames, sseParserConfig, redirectHeaderPolicy); + return new A2AHttpClientConfiguration( + excludedProviderNames, sseParserConfig, redirectHeaderPolicy); } } } diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClientFactory.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClientFactory.java index fc25c9f6d..1f0be719d 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClientFactory.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpClientFactory.java @@ -8,7 +8,6 @@ import java.util.logging.Logger; import java.util.stream.Stream; import java.util.stream.StreamSupport; - import org.a2aproject.sdk.util.Assert; /** @@ -54,9 +53,15 @@ public final class A2AHttpClientFactory { private static final List PROVIDERS; static { - PROVIDERS = StreamSupport.stream(ServiceLoader.load(A2AHttpClientProvider.class, A2AHttpClientProvider.class.getClassLoader()).spliterator(), false) - .sorted(Comparator.comparingInt(A2AHttpClientProvider::priority).reversed()) - .toList(); + PROVIDERS = + StreamSupport.stream( + ServiceLoader.load( + A2AHttpClientProvider.class, + A2AHttpClientProvider.class.getClassLoader()) + .spliterator(), + false) + .sorted(Comparator.comparingInt(A2AHttpClientProvider::priority).reversed()) + .toList(); } private A2AHttpClientFactory() { @@ -95,9 +100,10 @@ public static A2AHttpClient create() { */ @Deprecated(forRemoval = false) public static A2AHttpClient createExcluding(String... excludedProviderNames) { - return create(A2AHttpClientConfiguration.builder() - .excludedProviderNames(List.of(excludedProviderNames)) - .build()); + return create( + A2AHttpClientConfiguration.builder() + .excludedProviderNames(List.of(excludedProviderNames)) + .build()); } /** @@ -114,9 +120,8 @@ public static A2AHttpClient createExcluding(String... excludedProviderNames) { */ @Deprecated(forRemoval = false) public static A2AHttpClient createWithSseConfig(SSEParserConfig sseParserConfig) { - return create(A2AHttpClientConfiguration.builder() - .sseParserConfig(sseParserConfig) - .build()); + return create( + A2AHttpClientConfiguration.builder().sseParserConfig(sseParserConfig).build()); } /** @@ -139,8 +144,11 @@ public static A2AHttpClient create(String providerName) { .filter(provider -> providerName.equals(provider.name())) .findFirst() .map(provider -> provider.create(A2AHttpClientConfiguration.DEFAULT)) - .orElseThrow(() -> new IllegalArgumentException( - "No A2AHttpClientProvider found with name: " + providerName)); + .orElseThrow( + () -> + new IllegalArgumentException( + "No A2AHttpClientProvider found with name: " + + providerName)); } /** @@ -156,29 +164,39 @@ public static A2AHttpClient create(String providerName) { public static A2AHttpClient create(A2AHttpClientConfiguration configuration) { Assert.checkNotNullParam("configuration", configuration); return createInternal( - p -> !configuration.excludedProviderNames().contains(p.name()) && supports(p, configuration), + p -> + !configuration.excludedProviderNames().contains(p.name()) + && supports(p, configuration), configuration); } - private static boolean supports(A2AHttpClientProvider provider, A2AHttpClientConfiguration configuration) { + private static boolean supports( + A2AHttpClientProvider provider, A2AHttpClientConfiguration configuration) { return (configuration.sseParserConfig() == null || provider.supportsSseConfig()) - && (configuration.redirectHeaderPolicy() == null || provider.supportsRedirectHeaderPolicy()); + && (configuration.redirectHeaderPolicy() == null + || provider.supportsRedirectHeaderPolicy()); } private static A2AHttpClient createInternal( Predicate filter, A2AHttpClientConfiguration configuration) { return PROVIDERS.stream() .filter(filter) - .flatMap(p -> { - try { - return Stream.of(p.create(configuration)); - } catch (Exception e) { - LOGGER.log(Level.WARNING, e, () -> "Provider " + p.name() + " skipped"); - return Stream.empty(); - } - }) + .flatMap( + p -> { + try { + return Stream.of(p.create(configuration)); + } catch (Exception e) { + LOGGER.log( + Level.WARNING, + e, + () -> "Provider " + p.name() + " skipped"); + return Stream.empty(); + } + }) .findFirst() - .orElseThrow(() -> new IllegalStateException("No A2AHttpClientProvider could be instantiated")); + .orElseThrow( + () -> + new IllegalStateException( + "No A2AHttpClientProvider could be instantiated")); } - } diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpHeaders.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpHeaders.java index f306dff7e..e7ba3079e 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpHeaders.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/A2AHttpHeaders.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Map; - import org.a2aproject.sdk.util.HttpHeaderUtils; import org.jspecify.annotations.Nullable; @@ -36,22 +35,23 @@ public interface A2AHttpHeaders { /** * Empty headers instance returned by default when headers are not available. */ - A2AHttpHeaders EMPTY = new A2AHttpHeaders() { - @Override - public @Nullable String firstValue(String name) { - return null; - } + A2AHttpHeaders EMPTY = + new A2AHttpHeaders() { + @Override + public @Nullable String firstValue(String name) { + return null; + } - @Override - public List allValues(String name) { - return List.of(); - } + @Override + public List allValues(String name) { + return List.of(); + } - @Override - public Map> toMap() { - return Map.of(); - } - }; + @Override + public Map> toMap() { + return Map.of(); + } + }; /** * Returns the first value for the given header name, or {@code null} if not present. @@ -59,8 +59,7 @@ public Map> toMap() { * @param name the header name (case-insensitive) * @return the first header value, or {@code null} */ - @Nullable - String firstValue(String name); + @Nullable String firstValue(String name); /** * Returns all values for the given header name. diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/JdkA2AHttpClient.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/JdkA2AHttpClient.java index 84ed234c5..2d16df802 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/JdkA2AHttpClient.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/JdkA2AHttpClient.java @@ -1,10 +1,10 @@ package org.a2aproject.sdk.client.http; -import static org.a2aproject.sdk.util.Assert.checkNotNullParam; import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_MULT_CHOICE; import static java.net.HttpURLConnection.HTTP_OK; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; +import static org.a2aproject.sdk.util.Assert.checkNotNullParam; import java.io.IOException; import java.net.URI; @@ -23,10 +23,9 @@ import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; -import org.jspecify.annotations.Nullable; - import org.a2aproject.sdk.common.A2AErrorMessages; import org.a2aproject.sdk.spec.A2AClientHTTPError; +import org.jspecify.annotations.Nullable; /** * Default HTTP client implementation using JDK 11+ {@link HttpClient}. @@ -99,7 +98,8 @@ public JdkA2AHttpClient() { * @param redirectHeaderPolicy policy applied to explicitly enabled POST redirects * @return a client with secure transport defaults and the supplied redirect policy */ - public static JdkA2AHttpClient withRedirectHeaderPolicy(RedirectHeaderPolicy redirectHeaderPolicy) { + public static JdkA2AHttpClient withRedirectHeaderPolicy( + RedirectHeaderPolicy redirectHeaderPolicy) { return withConfiguration(SSEParserConfig.DEFAULT, redirectHeaderPolicy); } @@ -131,7 +131,8 @@ public static JdkA2AHttpClient withSseConfig(SSEParserConfig sseParserConfig) { /** Creates a default JDK client with custom SSE and redirect settings. */ public static JdkA2AHttpClient withConfiguration( SSEParserConfig sseParserConfig, RedirectHeaderPolicy redirectHeaderPolicy) { - return new JdkA2AHttpClient(createDefaultHttpClient(), sseParserConfig, redirectHeaderPolicy); + return new JdkA2AHttpClient( + createDefaultHttpClient(), sseParserConfig, redirectHeaderPolicy); } private static HttpClient createDefaultHttpClient() { @@ -241,7 +242,7 @@ public T addHeader(String name, String value) { @Override public T addHeaders(Map headers) { - if(headers != null && ! headers.isEmpty()) { + if (headers != null && !headers.isEmpty()) { for (Map.Entry entry : headers.entrySet()) { addHeader(entry.getKey(), entry.getValue()); } @@ -272,126 +273,148 @@ protected CompletableFuture asyncRequest( HttpRequest request, Consumer messageConsumer, Consumer errorConsumer, - Runnable completeRunnable - ) { - ServerSentEventParser sseParser = new ServerSentEventParser(messageConsumer, errorConsumer, sseParserConfig); + Runnable completeRunnable) { + ServerSentEventParser sseParser = + new ServerSentEventParser(messageConsumer, errorConsumer, sseParserConfig); AtomicBoolean useSseParser = new AtomicBoolean(false); AtomicBoolean errorNotified = new AtomicBoolean(false); StringBuilder nonSseBodyBuffer = new StringBuilder(); - Flow.Subscriber subscriber = new Flow.Subscriber() { - private Flow.@Nullable Subscription subscription; + Flow.Subscriber subscriber = + new Flow.Subscriber() { + private Flow.@Nullable Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - this.subscription.request(1); - } - - @Override - public void onNext(String item) { - if (item != null) { - if (useSseParser.get()) { - sseParser.processLine(item); - } else { - // Preserve blank lines so JSON string values containing literal newlines survive intact - if (nonSseBodyBuffer.length() > 0) { - nonSseBodyBuffer.append('\n'); - } - nonSseBodyBuffer.append(item); - } - } - if (subscription != null) { - subscription.request(1); - } - } - - @Override - public void onError(Throwable throwable) { - if (errorNotified.compareAndSet(false, true)) { - if (!isCancellation(throwable)) { - errorConsumer.accept(throwable); - } - } - } - - @Override - public void onComplete() { - if (errorNotified.compareAndSet(false, true)) { - if (useSseParser.get()) { - sseParser.flush(); - } else { - String body = nonSseBodyBuffer.toString(); - if (!body.isEmpty()) { - messageConsumer.accept(new ServerSentEvent(body)); - } - } - completeRunnable.run(); - } - } - }; - - // Create a custom body handler that checks status and Content-Type before processing body - BodyHandler bodyHandler = responseInfo -> { - // Check for authentication/authorization errors only - if (responseInfo.statusCode() == HTTP_UNAUTHORIZED || responseInfo.statusCode() == HTTP_FORBIDDEN) { - final int statusCode = responseInfo.statusCode(); - final String errorMessage = statusCode == HTTP_UNAUTHORIZED - ? A2AErrorMessages.AUTHENTICATION_FAILED - : A2AErrorMessages.AUTHORIZATION_FAILED; - final A2AClientHTTPError httpError = new A2AClientHTTPError( - statusCode, errorMessage, null, responseInfo.headers().map()); - // Return a body subscriber that immediately signals error - return BodySubscribers.fromSubscriber(new Flow.Subscriber>() { @Override public void onSubscribe(Flow.Subscription subscription) { - subscriber.onError(new IOException(errorMessage, httpError)); + this.subscription = subscription; + this.subscription.request(1); } @Override - public void onNext(List item) { - // Should not be called + public void onNext(String item) { + if (item != null) { + if (useSseParser.get()) { + sseParser.processLine(item); + } else { + // Preserve blank lines so JSON string values containing literal + // newlines survive intact + if (nonSseBodyBuffer.length() > 0) { + nonSseBodyBuffer.append('\n'); + } + nonSseBodyBuffer.append(item); + } + } + if (subscription != null) { + subscription.request(1); + } } @Override public void onError(Throwable throwable) { - // Should not be called + if (errorNotified.compareAndSet(false, true)) { + if (!isCancellation(throwable)) { + errorConsumer.accept(throwable); + } + } } @Override public void onComplete() { - // Should not be called + if (errorNotified.compareAndSet(false, true)) { + if (useSseParser.get()) { + sseParser.flush(); + } else { + String body = nonSseBodyBuffer.toString(); + if (!body.isEmpty()) { + messageConsumer.accept(new ServerSentEvent(body)); + } + } + completeRunnable.run(); + } } - }); - } - // Use SSE parser only for actual SSE responses; plain body lines are forwarded directly - String contentType = responseInfo.headers().firstValue("Content-Type").orElse(""); - boolean isSse = JdkHttpResponse.success(responseInfo.statusCode()) - && contentType.contains(EVENT_STREAM); - useSseParser.set(isSse); - if (isSse) { - // Use a bounded byte-level subscriber so that the maxLineLength limit is - // enforced *before* a full line is materialised in memory, preventing memory - // exhaustion from a malicious server that never sends a newline. - return new BoundedLineBodySubscriber(subscriber, sseParserConfig.maxLineLength(), sseParser); - } - return BodyHandlers.fromLineSubscriber(subscriber).apply(responseInfo); - }; + }; + + // Create a custom body handler that checks status and Content-Type before processing + // body + BodyHandler bodyHandler = + responseInfo -> { + // Check for authentication/authorization errors only + if (responseInfo.statusCode() == HTTP_UNAUTHORIZED + || responseInfo.statusCode() == HTTP_FORBIDDEN) { + final int statusCode = responseInfo.statusCode(); + final String errorMessage = + statusCode == HTTP_UNAUTHORIZED + ? A2AErrorMessages.AUTHENTICATION_FAILED + : A2AErrorMessages.AUTHORIZATION_FAILED; + final A2AClientHTTPError httpError = + new A2AClientHTTPError( + statusCode, + errorMessage, + null, + responseInfo.headers().map()); + // Return a body subscriber that immediately signals error + return BodySubscribers.fromSubscriber( + new Flow.Subscriber>() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscriber.onError( + new IOException(errorMessage, httpError)); + } + + @Override + public void onNext(List item) { + // Should not be called + } + + @Override + public void onError(Throwable throwable) { + // Should not be called + } + + @Override + public void onComplete() { + // Should not be called + } + }); + } + // Use SSE parser only for actual SSE responses; plain body lines are + // forwarded directly + String contentType = + responseInfo.headers().firstValue("Content-Type").orElse(""); + boolean isSse = + JdkHttpResponse.success(responseInfo.statusCode()) + && contentType.contains(EVENT_STREAM); + useSseParser.set(isSse); + if (isSse) { + // Use a bounded byte-level subscriber so that the maxLineLength limit + // is + // enforced *before* a full line is materialised in memory, preventing + // memory + // exhaustion from a malicious server that never sends a newline. + return new BoundedLineBodySubscriber( + subscriber, sseParserConfig.maxLineLength(), sseParser); + } + return BodyHandlers.fromLineSubscriber(subscriber).apply(responseInfo); + }; // Send the response async, and let the subscriber handle the lines. // handle() catches failures before the body handler is reached (e.g. connection // errors, DNS failures) and routes them to errorConsumer, then always completes // normally — consistent with the Vert.x and Android implementations. return client.sendAsync(request, bodyHandler) - .handle((response, throwable) -> { - if (throwable != null && errorNotified.compareAndSet(false, true)) { - if (!isCancellation(throwable)) { - Throwable cause = throwable.getCause() != null ? throwable.getCause() : throwable; - errorConsumer.accept(cause); - } - } - return null; - }); + .handle( + (response, throwable) -> { + if (throwable != null && errorNotified.compareAndSet(false, true)) { + if (!isCancellation(throwable)) { + Throwable cause = + throwable.getCause() != null + ? throwable.getCause() + : throwable; + errorConsumer.accept(cause); + } + } + return null; + }); } } @@ -408,7 +431,8 @@ public GetBuilder followRedirects(boolean follow) { private HttpClient resolveClient() throws IOException { if (followRedirects && httpClient.followRedirects() != HttpClient.Redirect.NEVER) { throw new IOException( - "The supplied HttpClient must disable native redirects when followRedirects(true) is used"); + "The supplied HttpClient must disable native redirects when" + + " followRedirects(true) is used"); } return followRedirects ? httpClient : getNoRedirectClient(); } @@ -424,31 +448,47 @@ private HttpRequest.Builder createRequestBuilder(boolean SSE) throws IOException @Override public A2AHttpResponse get() throws IOException, InterruptedException { HttpRequest request = createRequestBuilder(false).build(); - HttpResponse response = resolveClient().send(request, BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + resolveClient().send(request, BodyHandlers.ofString(StandardCharsets.UTF_8)); if (followRedirects) { String location = response.headers().firstValue("Location").orElse(null); - RedirectHeaderPolicy.RedirectedRequest redirect = redirectHeaderPolicy - .redirectedRequest(request.uri(), response.statusCode(), location, headers) - .orElse(null); + RedirectHeaderPolicy.RedirectedRequest redirect = + redirectHeaderPolicy + .redirectedRequest( + request.uri(), response.statusCode(), location, headers) + .orElse(null); if (redirect != null) { - HttpClient redirectClient = RedirectHeaderPolicy.sameOrigin(request.uri(), redirect.target()) - ? resolveClient() - : getCredentialFreeNoRedirectClient(); - HttpRequest redirectRequest = createRequestBuilder(redirect.target(), redirect.headers()) - .GET().build(); - response = redirectClient.send(redirectRequest, BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpClient redirectClient = + RedirectHeaderPolicy.sameOrigin(request.uri(), redirect.target()) + ? resolveClient() + : getCredentialFreeNoRedirectClient(); + HttpRequest redirectRequest = + createRequestBuilder(redirect.target(), redirect.headers()) + .GET() + .build(); + response = + redirectClient.send( + redirectRequest, BodyHandlers.ofString(StandardCharsets.UTF_8)); } } if (response.statusCode() == HTTP_UNAUTHORIZED) { - throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED, - new A2AClientHTTPError(HTTP_UNAUTHORIZED, A2AErrorMessages.AUTHENTICATION_FAILED, - null, response.headers().map())); + throw new IOException( + A2AErrorMessages.AUTHENTICATION_FAILED, + new A2AClientHTTPError( + HTTP_UNAUTHORIZED, + A2AErrorMessages.AUTHENTICATION_FAILED, + null, + response.headers().map())); } else if (response.statusCode() == HTTP_FORBIDDEN) { - throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED, - new A2AClientHTTPError(HTTP_FORBIDDEN, A2AErrorMessages.AUTHORIZATION_FAILED, - null, response.headers().map())); + throw new IOException( + A2AErrorMessages.AUTHORIZATION_FAILED, + new A2AClientHTTPError( + HTTP_FORBIDDEN, + A2AErrorMessages.AUTHORIZATION_FAILED, + null, + response.headers().map())); } return new JdkHttpResponse(response); @@ -458,35 +498,48 @@ public A2AHttpResponse get() throws IOException, InterruptedException { public CompletableFuture getAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, - Runnable completeRunnable) throws IOException, InterruptedException { - HttpRequest request = createRequestBuilder(true) - .build(); - return super.asyncRequest(getNoRedirectClient(), request, messageConsumer, errorConsumer, completeRunnable); + Runnable completeRunnable) + throws IOException, InterruptedException { + HttpRequest request = createRequestBuilder(true).build(); + return super.asyncRequest( + getNoRedirectClient(), + request, + messageConsumer, + errorConsumer, + completeRunnable); } - } - private class JdkDeleteBuilder extends JdkBuilder implements A2AHttpClient.DeleteBuilder { + private class JdkDeleteBuilder extends JdkBuilder + implements A2AHttpClient.DeleteBuilder { @Override public A2AHttpResponse delete() throws IOException, InterruptedException { HttpRequest request = super.createRequestBuilder().DELETE().build(); HttpResponse response = - getNoRedirectClient().send(request, BodyHandlers.ofString(StandardCharsets.UTF_8)); + getNoRedirectClient() + .send(request, BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() == HTTP_UNAUTHORIZED) { - throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED, - new A2AClientHTTPError(HTTP_UNAUTHORIZED, A2AErrorMessages.AUTHENTICATION_FAILED, - null, response.headers().map())); + throw new IOException( + A2AErrorMessages.AUTHENTICATION_FAILED, + new A2AClientHTTPError( + HTTP_UNAUTHORIZED, + A2AErrorMessages.AUTHENTICATION_FAILED, + null, + response.headers().map())); } else if (response.statusCode() == HTTP_FORBIDDEN) { - throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED, - new A2AClientHTTPError(HTTP_FORBIDDEN, A2AErrorMessages.AUTHORIZATION_FAILED, - null, response.headers().map())); + throw new IOException( + A2AErrorMessages.AUTHORIZATION_FAILED, + new A2AClientHTTPError( + HTTP_FORBIDDEN, + A2AErrorMessages.AUTHORIZATION_FAILED, + null, + response.headers().map())); } return new JdkHttpResponse(response); } - } private HttpClient getNoRedirectClient() { @@ -521,11 +574,12 @@ private HttpClient getCredentialFreeNoRedirectClient() { } private HttpClient createNoRedirectClient(boolean includeCredentials) { - HttpClient.Builder builder = HttpClient.newBuilder() - .version(httpClient.version()) - .followRedirects(HttpClient.Redirect.NEVER) - .sslContext(httpClient.sslContext()) - .sslParameters(httpClient.sslParameters()); + HttpClient.Builder builder = + HttpClient.newBuilder() + .version(httpClient.version()) + .followRedirects(HttpClient.Redirect.NEVER) + .sslContext(httpClient.sslContext()) + .sslParameters(httpClient.sslParameters()); httpClient.connectTimeout().ifPresent(builder::connectTimeout); httpClient.proxy().ifPresent(builder::proxy); httpClient.executor().ifPresent(builder::executor); @@ -538,7 +592,8 @@ private HttpClient createNoRedirectClient(boolean includeCredentials) { return builder.build(); } - private class JdkPostBuilder extends JdkBuilder implements A2AHttpClient.PostBuilder { + private class JdkPostBuilder extends JdkBuilder + implements A2AHttpClient.PostBuilder { String body = ""; boolean followRedirects = false; @@ -555,8 +610,11 @@ public PostBuilder followRedirects(boolean follow) { } private HttpRequest.Builder createRequestBuilder(boolean SSE) throws IOException { - HttpRequest.Builder builder = super.createRequestBuilder() - .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)); + HttpRequest.Builder builder = + super.createRequestBuilder() + .POST( + HttpRequest.BodyPublishers.ofString( + body, StandardCharsets.UTF_8)); if (SSE) { builder.header(ACCEPT, EVENT_STREAM); } @@ -566,16 +624,21 @@ private HttpRequest.Builder createRequestBuilder(boolean SSE) throws IOException private HttpClient resolveClient() throws IOException { if (followRedirects && httpClient.followRedirects() != HttpClient.Redirect.NEVER) { throw new IOException( - "The supplied HttpClient must disable native redirects when followRedirects(true) is used"); + "The supplied HttpClient must disable native redirects when" + + " followRedirects(true) is used"); } return followRedirects ? httpClient : getNoRedirectClient(); } private HttpRequest createRedirectRequest(RedirectHeaderPolicy.RedirectedRequest redirect) { - HttpRequest.Builder builder = createRequestBuilder(redirect.target(), redirect.headers()); + HttpRequest.Builder builder = + createRequestBuilder(redirect.target(), redirect.headers()); return redirect.redirectAsGet() ? builder.GET().build() - : builder.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build(); + : builder.POST( + HttpRequest.BodyPublishers.ofString( + body, StandardCharsets.UTF_8)) + .build(); } @Override @@ -586,28 +649,41 @@ public A2AHttpResponse post() throws IOException, InterruptedException { if (followRedirects) { String location = response.headers().firstValue("Location").orElse(null); - RedirectHeaderPolicy.RedirectedRequest redirect = redirectHeaderPolicy - .redirectedRequest(request.uri(), response.statusCode(), location, headers) - .orElse(null); + RedirectHeaderPolicy.RedirectedRequest redirect = + redirectHeaderPolicy + .redirectedRequest( + request.uri(), response.statusCode(), location, headers) + .orElse(null); if (redirect != null) { // A caller-supplied CookieHandler may add credentials after our explicit // header filtering. Do not use that client for a cross-origin follow-up. - HttpClient redirectClient = RedirectHeaderPolicy.sameOrigin(request.uri(), redirect.target()) - ? resolveClient() - : getCredentialFreeNoRedirectClient(); - response = redirectClient.send( - createRedirectRequest(redirect), BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpClient redirectClient = + RedirectHeaderPolicy.sameOrigin(request.uri(), redirect.target()) + ? resolveClient() + : getCredentialFreeNoRedirectClient(); + response = + redirectClient.send( + createRedirectRequest(redirect), + BodyHandlers.ofString(StandardCharsets.UTF_8)); } } if (response.statusCode() == HTTP_UNAUTHORIZED) { - throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED, - new A2AClientHTTPError(HTTP_UNAUTHORIZED, A2AErrorMessages.AUTHENTICATION_FAILED, - null, response.headers().map())); + throw new IOException( + A2AErrorMessages.AUTHENTICATION_FAILED, + new A2AClientHTTPError( + HTTP_UNAUTHORIZED, + A2AErrorMessages.AUTHENTICATION_FAILED, + null, + response.headers().map())); } else if (response.statusCode() == HTTP_FORBIDDEN) { - throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED, - new A2AClientHTTPError(HTTP_FORBIDDEN, A2AErrorMessages.AUTHORIZATION_FAILED, - null, response.headers().map())); + throw new IOException( + A2AErrorMessages.AUTHORIZATION_FAILED, + new A2AClientHTTPError( + HTTP_FORBIDDEN, + A2AErrorMessages.AUTHORIZATION_FAILED, + null, + response.headers().map())); } return new JdkHttpResponse(response); @@ -617,10 +693,11 @@ public A2AHttpResponse post() throws IOException, InterruptedException { public CompletableFuture postAsyncSSE( Consumer messageConsumer, Consumer errorConsumer, - Runnable completeRunnable) throws IOException, InterruptedException { - HttpRequest request = createRequestBuilder(true) - .build(); - return super.asyncRequest(resolveClient(), request, messageConsumer, errorConsumer, completeRunnable); + Runnable completeRunnable) + throws IOException, InterruptedException { + HttpRequest request = createRequestBuilder(true).build(); + return super.asyncRequest( + resolveClient(), request, messageConsumer, errorConsumer, completeRunnable); } } @@ -662,16 +739,20 @@ public A2AHttpHeaders headers() { *

    When {@code maxLineBytes <= 0} the per-line byte cap is disabled, matching the * semantics of {@link SSEParserConfig#maxLineLength()} == 0. */ - private static final class BoundedLineBodySubscriber implements HttpResponse.BodySubscriber { + private static final class BoundedLineBodySubscriber + implements HttpResponse.BodySubscriber { private final Flow.Subscriber lineSubscriber; private final ServerSentEventParser sseParser; private final CompletableFuture result = new CompletableFuture<>(); private final BoundedLineAccumulator accumulator; + /** True when a bare CR was the last byte of the previous chunk (may start a CRLF pair). */ private boolean pendingCR = false; - BoundedLineBodySubscriber(Flow.Subscriber lineSubscriber, int maxLineBytes, + BoundedLineBodySubscriber( + Flow.Subscriber lineSubscriber, + int maxLineBytes, ServerSentEventParser sseParser) { this.lineSubscriber = lineSubscriber; this.sseParser = sseParser; @@ -691,10 +772,18 @@ public void onSubscribe(Flow.Subscription subscription) { subscription.request(Long.MAX_VALUE); // Give the line subscriber a no-op subscription: it cannot cancel the HTTP // connection from the line level, and back-pressure is already handled above. - lineSubscriber.onSubscribe(new Flow.Subscription() { - @Override public void request(long n) { /* no-op */ } - @Override public void cancel() { /* no-op */ } - }); + lineSubscriber.onSubscribe( + new Flow.Subscription() { + @Override + public void request(long n) { + /* no-op */ + } + + @Override + public void cancel() { + /* no-op */ + } + }); } private record TerminatorScan(int lineLen, int resumePos, boolean crAtBoundary) {} @@ -800,4 +889,3 @@ public void onComplete() { } } } - diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/RedirectHeaderPolicy.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/RedirectHeaderPolicy.java index 5e2d9b2f7..5d59e3449 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/RedirectHeaderPolicy.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/RedirectHeaderPolicy.java @@ -6,7 +6,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; @@ -19,27 +18,29 @@ */ public final class RedirectHeaderPolicy { - private static final Set DEFAULT_CROSS_ORIGIN_HEADERS = Set.of( - "accept", - "accept-charset", - "accept-encoding", - "accept-language", - "a2a-extensions", - "a2a-version", - "cache-control", - "content-type", - "pragma", - "user-agent"); - private static final Set ENTITY_HEADERS = Set.of( - "content-encoding", - "content-language", - "content-length", - "content-location", - "content-type", - "digest", - "expect", - "last-modified", - "transfer-encoding"); + private static final Set DEFAULT_CROSS_ORIGIN_HEADERS = + Set.of( + "accept", + "accept-charset", + "accept-encoding", + "accept-language", + "a2a-extensions", + "a2a-version", + "cache-control", + "content-type", + "pragma", + "user-agent"); + private static final Set ENTITY_HEADERS = + Set.of( + "content-encoding", + "content-language", + "content-length", + "content-location", + "content-type", + "digest", + "expect", + "last-modified", + "transfer-encoding"); /** The conservative policy used by every SDK HTTP client by default. */ public static final RedirectHeaderPolicy DEFAULT = new RedirectHeaderPolicy(Set.of(), Set.of()); @@ -62,12 +63,17 @@ public RedirectHeaderPolicy(Set additionalCrossOriginHeaders, Set t Assert.checkNotNullParam("additionalCrossOriginHeaders", additionalCrossOriginHeaders); Assert.checkNotNullParam("trustedOrigins", trustedOrigins); if (!additionalCrossOriginHeaders.isEmpty() && trustedOrigins.isEmpty()) { - throw new IllegalArgumentException("Additional redirect headers require trusted origins"); - } - this.additionalCrossOriginHeaders = additionalCrossOriginHeaders.stream() - .map(header -> Assert.checkNotNullParam("additionalCrossOriginHeader", header) - .toLowerCase(Locale.ROOT)) - .collect(java.util.stream.Collectors.toUnmodifiableSet()); + throw new IllegalArgumentException( + "Additional redirect headers require trusted origins"); + } + this.additionalCrossOriginHeaders = + additionalCrossOriginHeaders.stream() + .map( + header -> + Assert.checkNotNullParam( + "additionalCrossOriginHeader", header) + .toLowerCase(Locale.ROOT)) + .collect(java.util.stream.Collectors.toUnmodifiableSet()); Set trustedOriginsCopy = Set.copyOf(trustedOrigins); for (URI origin : trustedOriginsCopy) { Assert.checkNotNullParam("trustedOrigin", origin); @@ -79,7 +85,8 @@ public RedirectHeaderPolicy(Set additionalCrossOriginHeaders, Set t || origin.getRawFragment() != null || origin.getPort() == 0 || origin.getPort() > 65535) { - throw new IllegalArgumentException("Trusted redirect target must be an HTTP(S) origin: " + origin); + throw new IllegalArgumentException( + "Trusted redirect target must be an HTTP(S) origin: " + origin); } } this.trustedOrigins = trustedOriginsCopy; @@ -124,10 +131,11 @@ public Optional redirectedRequest( return Optional.empty(); } - return Optional.of(new RedirectedRequest( - target, - redirectAsGet, - headersForRedirect(headers, target, sameOrigin, redirectAsGet))); + return Optional.of( + new RedirectedRequest( + target, + redirectAsGet, + headersForRedirect(headers, target, sameOrigin, redirectAsGet))); } /** @@ -189,18 +197,25 @@ public static boolean sameOrigin(URI source, URI target) { private Map headersForRedirect( Map headers, URI target, boolean sameOrigin, boolean redirectAsGet) { Map filteredHeaders = new HashMap<>(headers); - boolean trustedTarget = trustedOrigins.stream().anyMatch(origin -> sameOrigin(origin, target)); - filteredHeaders.entrySet().removeIf(entry -> { - String name = entry.getKey(); - if (name == null) { - return true; - } - String normalizedName = name.toLowerCase(Locale.ROOT); - return (!sameOrigin - && !DEFAULT_CROSS_ORIGIN_HEADERS.contains(normalizedName) - && !(trustedTarget && additionalCrossOriginHeaders.contains(normalizedName))) - || (redirectAsGet && ENTITY_HEADERS.contains(normalizedName)); - }); + boolean trustedTarget = + trustedOrigins.stream().anyMatch(origin -> sameOrigin(origin, target)); + filteredHeaders + .entrySet() + .removeIf( + entry -> { + String name = entry.getKey(); + if (name == null) { + return true; + } + String normalizedName = name.toLowerCase(Locale.ROOT); + return (!sameOrigin + && !DEFAULT_CROSS_ORIGIN_HEADERS.contains( + normalizedName) + && !(trustedTarget + && additionalCrossOriginHeaders.contains( + normalizedName))) + || (redirectAsGet && ENTITY_HEADERS.contains(normalizedName)); + }); return Map.copyOf(filteredHeaders); } @@ -229,7 +244,8 @@ private static int effectivePort(URI uri) { } /** A prepared one-hop POST redirect request. */ - public record RedirectedRequest(URI target, boolean redirectAsGet, Map headers) { + public record RedirectedRequest( + URI target, boolean redirectAsGet, Map headers) { public RedirectedRequest { Assert.checkNotNullParam("target", target); Assert.checkNotNullParam("headers", headers); diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/SSEParserConfig.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/SSEParserConfig.java index 28538a7d7..169dcd0c4 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/SSEParserConfig.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/SSEParserConfig.java @@ -31,7 +31,8 @@ public record SSEParserConfig(int maxLineLength, int maxBufferLines, int maxBuff /** * Default configuration: 1 MB per-line and per-event character limit, 1 000 data lines per event. */ - public static final SSEParserConfig DEFAULT = new SSEParserConfig(1024 * 1024, 1000, 1024 * 1024); + public static final SSEParserConfig DEFAULT = + new SSEParserConfig(1024 * 1024, 1000, 1024 * 1024); public SSEParserConfig { if (maxLineLength < 0) { @@ -57,8 +58,7 @@ public static final class Builder { private int maxBufferLines = DEFAULT.maxBufferLines; private int maxBufferChars = DEFAULT.maxBufferChars; - Builder() { - } + Builder() {} /** * Sets the maximum number of bytes allowed in a single raw SSE line. diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/ServerSentEvent.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/ServerSentEvent.java index 2bbb1d4c2..7627915cf 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/ServerSentEvent.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/ServerSentEvent.java @@ -14,7 +14,8 @@ *

  • {@code retry} — the reconnection interval in milliseconds from the {@code retry:} field; null if absent
  • * */ -public record ServerSentEvent(String data, String eventType, @Nullable String id, @Nullable Long retry) { +public record ServerSentEvent( + String data, String eventType, @Nullable String id, @Nullable Long retry) { /** * Default event type per the SSE specification when no {@code event:} field is present. diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/ServerSentEventParser.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/ServerSentEventParser.java index bb12a23fc..04037365a 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/ServerSentEventParser.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/ServerSentEventParser.java @@ -24,9 +24,12 @@ public class ServerSentEventParser { private final List dataBuffer = new ArrayList<>(); private int dataBufferChars = 0; private @Nullable String eventType; - // currentEventId: the spec's "event ID buffer" — persists across events, overwritten by each "id:" field. - // lastEventId: the spec's "last event ID string" — copied from currentEventId only at dispatch (empty line), - // so a skipped/corrupt event block cannot advance it. Returned to callers as the reconnect Last-Event-ID. + // currentEventId: the spec's "event ID buffer" — persists across events, overwritten by each + // "id:" field. + // lastEventId: the spec's "last event ID string" — copied from currentEventId only at dispatch + // (empty line), + // so a skipped/corrupt event block cannot advance it. Returned to callers as the reconnect + // Last-Event-ID. private @Nullable String currentEventId; private @Nullable String lastEventId; private @Nullable Long retry; @@ -38,11 +41,14 @@ public ServerSentEventParser(Consumer eventConsumer) { this(eventConsumer, null, SSEParserConfig.DEFAULT); } - public ServerSentEventParser(Consumer eventConsumer, @Nullable Consumer errorConsumer) { + public ServerSentEventParser( + Consumer eventConsumer, @Nullable Consumer errorConsumer) { this(eventConsumer, errorConsumer, SSEParserConfig.DEFAULT); } - public ServerSentEventParser(Consumer eventConsumer, @Nullable Consumer errorConsumer, + public ServerSentEventParser( + Consumer eventConsumer, + @Nullable Consumer errorConsumer, SSEParserConfig config) { this.eventConsumer = eventConsumer; this.errorConsumer = errorConsumer; @@ -58,7 +64,9 @@ public ServerSentEventParser(Consumer eventConsumer, @Nullable * and reports the error, without requiring the caller to allocate a placeholder string. */ public void processLineTooLong() { - handleError(new IllegalArgumentException("Line exceeds maximum length of " + maxLineLength + " characters")); + handleError( + new IllegalArgumentException( + "Line exceeds maximum length of " + maxLineLength + " characters")); skippingCurrentEvent = true; dataBuffer.clear(); dataBufferChars = 0; @@ -76,7 +84,9 @@ public void processLine(@Nullable String line) { // Check line length to prevent DoS; corrupt the current event so it is not dispatched if (maxLineLength > 0 && line.length() > maxLineLength) { - handleError(new IllegalArgumentException("Line exceeds maximum length of " + maxLineLength + " characters")); + handleError( + new IllegalArgumentException( + "Line exceeds maximum length of " + maxLineLength + " characters")); skippingCurrentEvent = true; dataBuffer.clear(); dataBufferChars = 0; @@ -121,7 +131,11 @@ private void processField(String field, String value) { case "data" -> { // Check line count to prevent DoS; corrupt and skip the rest of this event block if (dataBuffer.size() >= maxBufferLines) { - handleError(new IllegalStateException("SSE data buffer exceeded maximum size of " + maxBufferLines + " lines")); + handleError( + new IllegalStateException( + "SSE data buffer exceeded maximum size of " + + maxBufferLines + + " lines")); skippingCurrentEvent = true; dataBuffer.clear(); dataBufferChars = 0; @@ -129,7 +143,11 @@ private void processField(String field, String value) { } // Check total char count to prevent OOM on large streams if (dataBufferChars + value.length() > maxBufferChars) { - handleError(new IllegalStateException("SSE data buffer exceeded maximum size of " + maxBufferChars + " chars")); + handleError( + new IllegalStateException( + "SSE data buffer exceeded maximum size of " + + maxBufferChars + + " chars")); skippingCurrentEvent = true; dataBuffer.clear(); dataBufferChars = 0; @@ -147,7 +165,8 @@ private void processField(String field, String value) { } } case "retry" -> { - // Per SSE spec: ignore the retry field unless the value consists entirely of ASCII digits. + // Per SSE spec: ignore the retry field unless the value consists entirely of ASCII + // digits. if (!value.isEmpty() && value.chars().allMatch(c -> c >= '0' && c <= '9')) { try { retry = Long.parseLong(value); @@ -167,13 +186,16 @@ private void processField(String field, String value) { } private void dispatchEvent() { - // Per SSE spec §9.2.6: copy currentEventId → lastEventId at dispatch, but only for blocks that - // were not skipped. A corrupt/oversized block must not advance the reconnect cursor even if its + // Per SSE spec §9.2.6: copy currentEventId → lastEventId at dispatch, but only for blocks + // that + // were not skipped. A corrupt/oversized block must not advance the reconnect cursor even if + // its // id: field was parsed before the violation was detected. if (!skippingCurrentEvent && currentEventId != null) { lastEventId = currentEventId; } else if (skippingCurrentEvent) { - // Roll back the event ID buffer so the skipped block's id: cannot leak into subsequent events. + // Roll back the event ID buffer so the skipped block's id: cannot leak into subsequent + // events. currentEventId = lastEventId; } @@ -188,12 +210,15 @@ private void dispatchEvent() { skippingCurrentEvent = false; // currentEventId is NOT reset — it persists per the SSE specification - // Per SSE spec: don't dispatch if data is empty (also covers limit-violation blocks, whose buffer was cleared) + // Per SSE spec: don't dispatch if data is empty (also covers limit-violation blocks, whose + // buffer was cleared) if (data.isEmpty()) { return; } - eventConsumer.accept(new ServerSentEvent(data, type != null ? type : ServerSentEvent.DEFAULT_EVENT_TYPE, id, retry)); + eventConsumer.accept( + new ServerSentEvent( + data, type != null ? type : ServerSentEvent.DEFAULT_EVENT_TYPE, id, retry)); } /** Dispatches any buffered data not yet followed by an empty line. Call when the stream ends. */ diff --git a/http-client/src/main/java/org/a2aproject/sdk/client/http/package-info.java b/http-client/src/main/java/org/a2aproject/sdk/client/http/package-info.java index 671e24532..b9e4cf059 100644 --- a/http-client/src/main/java/org/a2aproject/sdk/client/http/package-info.java +++ b/http-client/src/main/java/org/a2aproject/sdk/client/http/package-info.java @@ -57,4 +57,3 @@ package org.a2aproject.sdk.client.http; import org.jspecify.annotations.NullMarked; - diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/A2ACardResolverTest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/A2ACardResolverTest.java index a4d4a3da4..db407544f 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/A2ACardResolverTest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/A2ACardResolverTest.java @@ -39,14 +39,22 @@ private TestHttpClient createTestClient() { @Test public void testWellKnownUrl_trailingSlashStripped() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com/") + .build() + .getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); } @Test public void testWellKnownUrl_noTrailingSlash() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .build() + .getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); } @@ -54,14 +62,22 @@ public void testWellKnownUrl_noTrailingSlash() throws Exception { public void testWellKnownUrl_subPathPreserved_withTrailingSlash() throws Exception { TestHttpClient client = createTestClient(); // URI.resolve() would have silently dropped the sub-path; string concat must not. - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/jsonrpc/").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com/jsonrpc/") + .build() + .getAgentCard(); assertEquals("http://example.com/jsonrpc" + AGENT_CARD_PATH, client.url); } @Test public void testWellKnownUrl_subPathPreserved_noTrailingSlash() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/jsonrpc").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com/jsonrpc") + .build() + .getAgentCard(); assertEquals("http://example.com/jsonrpc" + AGENT_CARD_PATH, client.url); } @@ -72,42 +88,72 @@ public void testWellKnownUrl_subPathPreserved_noTrailingSlash() throws Exception @Test public void testCustomPath_withLeadingSlash() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").agentCardPath(AGENT_CARD_PATH).build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .agentCardPath(AGENT_CARD_PATH) + .build() + .getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); } @Test public void testCustomPath_withoutLeadingSlash() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").agentCardPath(AGENT_CARD_PATH.substring(1)).build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .agentCardPath(AGENT_CARD_PATH.substring(1)) + .build() + .getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); } @Test public void testCustomPath_baseUrlTrailingSlash_withLeadingSlash() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/").agentCardPath(AGENT_CARD_PATH).build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com/") + .agentCardPath(AGENT_CARD_PATH) + .build() + .getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); } @Test public void testCustomPath_baseUrlTrailingSlash_withoutLeadingSlash() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/").agentCardPath(AGENT_CARD_PATH.substring(1)).build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com/") + .agentCardPath(AGENT_CARD_PATH.substring(1)) + .build() + .getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); } @Test public void testCustomPath_doesNotIntroduceDoubleSlash() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/").agentCardPath("/custom/agent.json").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com/") + .agentCardPath("/custom/agent.json") + .build() + .getAgentCard(); assertEquals("http://example.com/custom/agent.json", client.url); } @Test public void testCustomPath_subBaseUrl() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/jsonrpc/").agentCardPath(AGENT_CARD_PATH).build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com/jsonrpc/") + .agentCardPath(AGENT_CARD_PATH) + .build() + .getAgentCard(); assertEquals("http://example.com/jsonrpc" + AGENT_CARD_PATH, client.url); } @@ -118,7 +164,12 @@ public void testCustomPath_subBaseUrl() throws Exception { @Test public void testGetAgentCard_success() throws Exception { TestHttpClient client = createTestClient(); - AgentCard card = A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/").build().getAgentCard(); + AgentCard card = + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com/") + .build() + .getAgentCard(); assertEquals(printAgentCard(unmarshalFrom(JsonMessages.AGENT_CARD)), printAgentCard(card)); } @@ -126,7 +177,8 @@ public void testGetAgentCard_success() throws Exception { public void testGetAgentCard_jsonDecodeError() throws Exception { TestHttpClient client = createTestClient(); client.body = "X" + JsonMessages.AGENT_CARD; - A2ACardResolver resolver = A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/").build(); + A2ACardResolver resolver = + A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/").build(); assertThrows(A2AClientJSONError.class, resolver::getAgentCard); } @@ -134,7 +186,8 @@ public void testGetAgentCard_jsonDecodeError() throws Exception { public void testGetAgentCard_httpErrorThrows() throws Exception { TestHttpClient client = createTestClient(); client.status = 503; - A2ACardResolver resolver = A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/").build(); + A2ACardResolver resolver = + A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com/").build(); A2AClientHTTPError error = assertThrows(A2AClientHTTPError.class, resolver::getAgentCard); assertTrue(error.getMessage().contains("503")); assertEquals(503, error.getCode()); @@ -144,11 +197,12 @@ public void testGetAgentCard_httpErrorThrows() throws Exception { public void testGetAgentCard_customPath_httpErrorThrows_noFallback() throws Exception { TestHttpClient client = createTestClient(); client.status = 404; - A2ACardResolver resolver = A2ACardResolver.builder() - .httpClient(client) - .baseUrl("http://example.com") - .agentCardPath("/custom/agent.json") - .build(); + A2ACardResolver resolver = + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .agentCardPath("/custom/agent.json") + .build(); A2AClientHTTPError error = assertThrows(A2AClientHTTPError.class, resolver::getAgentCard); assertEquals(404, error.getCode()); assertEquals(1, client.urlsCalled.size()); @@ -159,16 +213,20 @@ public void testGetAgentCard_customPath_httpErrorThrows_noFallback() throws Exce public void testGetAgentCard_ioExceptionThrows() throws Exception { TestHttpClient client = createTestClient(); client.throwIOException = true; - assertThrows(A2AClientError.class, - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").build()::getAgentCard); + assertThrows( + A2AClientError.class, + A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").build() + ::getAgentCard); } @Test public void testGetAgentCard_interruptedExceptionThrows() throws Exception { TestHttpClient client = createTestClient(); client.throwInterruptedException = true; - assertThrows(A2AClientError.class, - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").build()::getAgentCard); + assertThrows( + A2AClientError.class, + A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").build() + ::getAgentCard); } // ------------------------------------------------------------------------- @@ -178,7 +236,12 @@ public void testGetAgentCard_interruptedExceptionThrows() throws Exception { @Test public void testGetAgentCard_withTenant() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").tenant("my-tenant").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .tenant("my-tenant") + .build() + .getAgentCard(); assertEquals("http://example.com/.well-known/my-tenant/agent-card.json", client.url); } @@ -198,26 +261,44 @@ public void testGetAgentCard_withTenantAndCustomAgentCardPath() throws Exception @Test public void testGetAgentCard_withCustomPath_absoluteAndRelative() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").agentCardPath("/custom/agent.json").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .agentCardPath("/custom/agent.json") + .build() + .getAgentCard(); assertEquals("http://example.com/custom/agent.json", client.url); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").agentCardPath("custom/agent.json").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .agentCardPath("custom/agent.json") + .build() + .getAgentCard(); assertEquals("http://example.com/custom/agent.json", client.url); } @Test public void testGetAgentCard_withAuthHeadersMap() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com") - .authHeaders(Map.of("Authorization", "Bearer token123")).build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .authHeaders(Map.of("Authorization", "Bearer token123")) + .build() + .getAgentCard(); assertEquals("Bearer token123", client.capturedHeaders.get("Authorization")); } @Test public void testGetAgentCard_withAuthHeaderSingle() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com") - .authHeader("Authorization", "Bearer token123").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .authHeader("Authorization", "Bearer token123") + .build() + .getAgentCard(); assertEquals("Bearer token123", client.capturedHeaders.get("Authorization")); } @@ -228,19 +309,23 @@ public void testBuilder_nullBaseUrl_throws() { @Test public void testBuilder_malformedBaseUrl_throws() { - assertThrows(A2AClientError.class, () -> A2ACardResolver.builder().baseUrl("not-a-url").build()); + assertThrows( + A2AClientError.class, () -> A2ACardResolver.builder().baseUrl("not-a-url").build()); } @Test public void testFullWellKnownUrlWithTenant() throws Exception { - // Full well-known URL + tenant must strip the suffix before embedding tenant inside the path, - // not produce a malformed path like ...agent-card.json/.well-known/my-tenant/agent-card.json - TestHttpClient client = createTestClient(); - A2ACardResolver resolver = A2ACardResolver.builder() - .httpClient(client) - .baseUrl("https://example.com/.well-known/agent-card.json") - .tenant("my-tenant") - .build(); + // Full well-known URL + tenant must strip the suffix before embedding tenant inside the + // path, + // not produce a malformed path like + // ...agent-card.json/.well-known/my-tenant/agent-card.json + TestHttpClient client = createTestClient(); + A2ACardResolver resolver = + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("https://example.com/.well-known/agent-card.json") + .tenant("my-tenant") + .build(); resolver.getAgentCard(); assertEquals("https://example.com/.well-known/my-tenant/agent-card.json", client.url); } @@ -252,29 +337,46 @@ public void testFullWellKnownUrlWithTenant() throws Exception { @Test public void testSpec03PathPreservation_wellKnown() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("https://example.com/spec03").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("https://example.com/spec03") + .build() + .getAgentCard(); assertEquals("https://example.com/spec03" + AGENT_CARD_PATH, client.url); } @Test public void testSpec03PathPreservation_withTenant() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("https://example.com/spec03").tenant("my-tenant").build().getAgentCard(); - assertEquals("https://example.com/spec03/.well-known/my-tenant/agent-card.json", client.url); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("https://example.com/spec03") + .tenant("my-tenant") + .build() + .getAgentCard(); + assertEquals( + "https://example.com/spec03/.well-known/my-tenant/agent-card.json", client.url); } @Test public void testSpec03PathPreservation_withCustomPath() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("https://example.com/spec03").agentCardPath("/custom/card.json").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("https://example.com/spec03") + .agentCardPath("/custom/card.json") + .build() + .getAgentCard(); assertEquals("https://example.com/spec03/custom/card.json", client.url); } @Test public void testBaseUrl_alreadyContainsWellKnownPath() throws Exception { - String fullUrl = "https://agentbin.greensmoke-1163cb63.eastus.azurecontainerapps.io/spec03/.well-known/agent-card.json"; + String fullUrl = + "https://agentbin.greensmoke-1163cb63.eastus.azurecontainerapps.io/spec03/.well-known/agent-card.json"; TestHttpClient client = createTestClient(); - A2ACardResolver resolver = A2ACardResolver.builder().httpClient(client).baseUrl(fullUrl).build(); + A2ACardResolver resolver = + A2ACardResolver.builder().httpClient(client).baseUrl(fullUrl).build(); resolver.getAgentCard(); assertEquals(fullUrl, client.url); } @@ -282,11 +384,12 @@ public void testBaseUrl_alreadyContainsWellKnownPath() throws Exception { @Test public void testFullWellKnownUrlWithCustomAgentCardPath() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver resolver = A2ACardResolver.builder() - .httpClient(client) - .baseUrl("https://example.com/spec03/.well-known/agent-card.json") - .agentCardPath("/custom/card.json") - .build(); + A2ACardResolver resolver = + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("https://example.com/spec03/.well-known/agent-card.json") + .agentCardPath("/custom/card.json") + .build(); resolver.getAgentCard(); assertEquals("https://example.com/spec03/custom/card.json", client.url); } @@ -294,11 +397,12 @@ public void testFullWellKnownUrlWithCustomAgentCardPath() throws Exception { @Test public void testFullWellKnownUrlWithSameAgentCardPath() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver resolver = A2ACardResolver.builder() - .httpClient(client) - .baseUrl("https://example.com/spec03/.well-known/agent-card.json") - .agentCardPath("/.well-known/agent-card.json") - .build(); + A2ACardResolver resolver = + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("https://example.com/spec03/.well-known/agent-card.json") + .agentCardPath("/.well-known/agent-card.json") + .build(); resolver.getAgentCard(); assertEquals("https://example.com/spec03/.well-known/agent-card.json", client.url); } @@ -310,7 +414,12 @@ public void testFullWellKnownUrlWithSameAgentCardPath() throws Exception { @Test public void testTenantUrl_embeddedInWellKnownPath() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").tenant("acme").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .tenant("acme") + .build() + .getAgentCard(); assertEquals("http://example.com/.well-known/acme/agent-card.json", client.url); } @@ -319,15 +428,22 @@ public void testTenantUrl_fullTenantCardUrlProvidedAsBase_notDuplicated() throws // When the caller already supplies the tenant card URL, it must be used as-is. TestHttpClient client = createTestClient(); String tenantCardUrl = "http://example.com/.well-known/acme/agent-card.json"; - A2ACardResolver.builder().httpClient(client).baseUrl(tenantCardUrl).tenant("acme").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl(tenantCardUrl) + .tenant("acme") + .build() + .getAgentCard(); assertEquals(tenantCardUrl, client.url); assertEquals(1, client.urlsCalled.size()); } @Test - public void testTenantUrl_fullTenantCardUrlProvidedAsBase_withTrailingSlash_notDuplicated() throws Exception { + public void testTenantUrl_fullTenantCardUrlProvidedAsBase_withTrailingSlash_notDuplicated() + throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client) + A2ACardResolver.builder() + .httpClient(client) .baseUrl("http://example.com/.well-known/acme/agent-card.json/") .tenant("acme") .build() @@ -337,22 +453,34 @@ public void testTenantUrl_fullTenantCardUrlProvidedAsBase_withTrailingSlash_notD } @Test - public void testTenantUrl_fullTenantCardUrlProvidedAsBase_fallsBackToCleanBase() throws Exception { + public void testTenantUrl_fullTenantCardUrlProvidedAsBase_fallsBackToCleanBase() + throws Exception { // When the provided URL already IS the computed card URL, fall back to the clean base. TestHttpClient client = createTestClient(); client.statusSequence.add(404); client.statusSequence.add(200); String tenantCardUrl = "http://example.com/.well-known/acme/agent-card.json"; - A2ACardResolver.builder().httpClient(client).baseUrl(tenantCardUrl).tenant("acme").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl(tenantCardUrl) + .tenant("acme") + .build() + .getAgentCard(); assertEquals(2, client.urlsCalled.size()); - assertEquals("http://example.com/.well-known/acme/agent-card.json", client.urlsCalled.get(0)); + assertEquals( + "http://example.com/.well-known/acme/agent-card.json", client.urlsCalled.get(0)); assertEquals("http://example.com", client.urlsCalled.get(1)); } @Test public void testTenantUrl_withSubBasePath() throws Exception { TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("https://example.com/spec03").tenant("acme").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("https://example.com/spec03") + .tenant("acme") + .build() + .getAgentCard(); assertEquals("https://example.com/spec03/.well-known/acme/agent-card.json", client.url); } @@ -362,11 +490,16 @@ public void testTenantUrl_withSubBasePath() throws Exception { @Test public void testGetAgentCard_httpError_fallsBackToProvidedUrl() throws Exception { - // Primary URL (/.well-known/agent-card.json) returns 404; fallback to original base URL succeeds. + // Primary URL (/.well-known/agent-card.json) returns 404; fallback to original base URL + // succeeds. TestHttpClient client = createTestClient(); client.statusSequence.add(404); client.statusSequence.add(200); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .build() + .getAgentCard(); assertEquals(2, client.urlsCalled.size()); assertEquals("http://example.com" + AGENT_CARD_PATH, client.urlsCalled.get(0)); assertEquals("http://example.com", client.urlsCalled.get(1)); @@ -378,7 +511,11 @@ public void testGetAgentCard_doubleSlashBaseUrl_fallbackToOriginalUrl() throws E TestHttpClient client = createTestClient(); client.statusSequence.add(404); client.statusSequence.add(200); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com//").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com//") + .build() + .getAgentCard(); assertEquals(2, client.urlsCalled.size()); assertEquals("http://example.com" + AGENT_CARD_PATH, client.urlsCalled.get(0)); assertEquals("http://example.com//", client.urlsCalled.get(1)); @@ -389,45 +526,58 @@ public void testGetAgentCard_doubleSlashWellKnownUrl_normalizedCardUrl() throws // A baseUrl that is already the well-known URL but with a double slash before /.well-known // must strip the well-known suffix and rebuild cleanly. TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com//.well-known/agent-card.json").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com//.well-known/agent-card.json") + .build() + .getAgentCard(); assertEquals(1, client.urlsCalled.size()); assertEquals("http://example.com" + AGENT_CARD_PATH, client.urlsCalled.get(0)); } @Test - public void testGetAgentCard_doubleSlashTenantCardUrl_sameTenant_normalizedCardUrl() throws Exception { - // baseUrl is the tenant card URL with a double slash; tenant matches — must normalize cleanly. + public void testGetAgentCard_doubleSlashTenantCardUrl_sameTenant_normalizedCardUrl() + throws Exception { + // baseUrl is the tenant card URL with a double slash; tenant matches — must normalize + // cleanly. TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client) + A2ACardResolver.builder() + .httpClient(client) .baseUrl("http://example.com//.well-known/acme/agent-card.json") .tenant("acme") .build() .getAgentCard(); assertEquals(1, client.urlsCalled.size()); - assertEquals("http://example.com/.well-known/acme/agent-card.json", client.urlsCalled.get(0)); + assertEquals( + "http://example.com/.well-known/acme/agent-card.json", client.urlsCalled.get(0)); } @Test - public void testGetAgentCard_doubleSlashTenantCardUrl_differentTenant_normalizedCardUrl() throws Exception { + public void testGetAgentCard_doubleSlashTenantCardUrl_differentTenant_normalizedCardUrl() + throws Exception { // baseUrl is a tenant card URL with a double slash; a different tenant is requested — the // suffix must be stripped before the new tenant path is embedded. TestHttpClient client = createTestClient(); - A2ACardResolver.builder().httpClient(client) + A2ACardResolver.builder() + .httpClient(client) .baseUrl("http://example.com//.well-known/acme/agent-card.json") .tenant("foo") .build() .getAgentCard(); assertEquals(1, client.urlsCalled.size()); - assertEquals("http://example.com/.well-known/foo/agent-card.json", client.urlsCalled.get(0)); + assertEquals( + "http://example.com/.well-known/foo/agent-card.json", client.urlsCalled.get(0)); } @Test public void testGetAgentCard_httpError_bothFail_throwsLastError() throws Exception { - // Both primary (/.well-known/agent-card.json) and fallback return 404; last error is propagated + // Both primary (/.well-known/agent-card.json) and fallback return 404; last error is + // propagated // and the primary error is attached as a suppressed exception. TestHttpClient client = createTestClient(); client.status = 404; - A2ACardResolver resolver = A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").build(); + A2ACardResolver resolver = + A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").build(); A2AClientHTTPError error = assertThrows(A2AClientHTTPError.class, resolver::getAgentCard); assertEquals(404, error.getCode()); assertEquals(2, client.urlsCalled.size()); @@ -436,13 +586,13 @@ public void testGetAgentCard_httpError_bothFail_throwsLastError() throws Excepti assertEquals(404, ((A2AClientHTTPError) error.getSuppressed()[0]).getCode()); } - @Test public void testGetAgentCard_nonNotFound_noFallback() throws Exception { // A 5xx from the primary URL is not a URL-format issue, so no fallback is attempted. TestHttpClient client = createTestClient(); client.status = 503; - A2ACardResolver resolver = A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").build(); + A2ACardResolver resolver = + A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").build(); A2AClientHTTPError error = assertThrows(A2AClientHTTPError.class, resolver::getAgentCard); assertEquals(503, error.getCode()); assertEquals(1, client.urlsCalled.size()); @@ -466,9 +616,15 @@ public void testGetAgentCard_withTenant_httpError_fallsBackToProvidedUrl() throw TestHttpClient client = createTestClient(); client.statusSequence.add(404); client.statusSequence.add(200); - A2ACardResolver.builder().httpClient(client).baseUrl("http://example.com").tenant("acme").build().getAgentCard(); + A2ACardResolver.builder() + .httpClient(client) + .baseUrl("http://example.com") + .tenant("acme") + .build() + .getAgentCard(); assertEquals(2, client.urlsCalled.size()); - assertEquals("http://example.com/.well-known/acme/agent-card.json", client.urlsCalled.get(0)); + assertEquals( + "http://example.com/.well-known/acme/agent-card.json", client.urlsCalled.get(0)); assertEquals("http://example.com", client.urlsCalled.get(1)); } @@ -477,7 +633,8 @@ public void testGetAgentCard_withTenant_httpError_fallsBackToProvidedUrl() throw // ------------------------------------------------------------------------- private AgentCard unmarshalFrom(String body) throws JsonProcessingException { - org.a2aproject.sdk.grpc.AgentCard.Builder agentCardBuilder = org.a2aproject.sdk.grpc.AgentCard.newBuilder(); + org.a2aproject.sdk.grpc.AgentCard.Builder agentCardBuilder = + org.a2aproject.sdk.grpc.AgentCard.newBuilder(); JSONRPCUtils.parseJsonString(body, agentCardBuilder, ""); return ProtoUtils.FromProto.agentCard(agentCardBuilder); } @@ -542,7 +699,11 @@ public String body() { } @Override - public CompletableFuture getAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { + public CompletableFuture getAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) + throws IOException, InterruptedException { return null; } diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientFactoryTest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientFactoryTest.java index 83f7da6a3..1787f9ffe 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientFactoryTest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientFactoryTest.java @@ -11,7 +11,6 @@ import java.net.URI; import java.util.List; import java.util.Set; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockserver.integration.ClientAndServer; @@ -41,9 +40,11 @@ public void testCreateReturnsNonNull() { @SuppressWarnings("deprecation") public void legacyFactoryOverloadsRemainAvailable() { assertInstanceOf(JdkA2AHttpClient.class, A2AHttpClientFactory.create("jdk")); - assertInstanceOf(JdkA2AHttpClient.class, + assertInstanceOf( + JdkA2AHttpClient.class, A2AHttpClientFactory.createExcluding("cdi", "android", "vertx")); - assertInstanceOf(JdkA2AHttpClient.class, + assertInstanceOf( + JdkA2AHttpClient.class, A2AHttpClientFactory.createWithSseConfig(SSEParserConfig.DEFAULT)); assertThrows(IllegalArgumentException.class, () -> A2AHttpClientFactory.create("unknown")); } @@ -52,57 +53,71 @@ public void legacyFactoryOverloadsRemainAvailable() { public void testCreateWithConfigurationAppliesRedirectPolicy() throws Exception { redirector = ClientAndServer.startClientAndServer(0); collector = ClientAndServer.startClientAndServer(0); - redirector.when(request().withMethod("POST").withPath("/redirect")) - .respond(response().withStatusCode(302).withHeader( - "Location", "http://127.0.0.1:" + collector.getLocalPort() + "/target")); - collector.when(request().withMethod("GET").withPath("/target")) + redirector + .when(request().withMethod("POST").withPath("/redirect")) + .respond( + response() + .withStatusCode(302) + .withHeader( + "Location", + "http://127.0.0.1:" + + collector.getLocalPort() + + "/target")); + collector + .when(request().withMethod("GET").withPath("/target")) .respond(response().withStatusCode(200).withBody("redirected")); - RedirectHeaderPolicy redirectHeaderPolicy = new RedirectHeaderPolicy( - Set.of("X-Trusted"), - Set.of(URI.create("http://127.0.0.1:" + collector.getLocalPort()))); - A2AHttpClientConfiguration configuration = A2AHttpClientConfiguration.builder() - .redirectHeaderPolicy(redirectHeaderPolicy) - .build(); + RedirectHeaderPolicy redirectHeaderPolicy = + new RedirectHeaderPolicy( + Set.of("X-Trusted"), + Set.of(URI.create("http://127.0.0.1:" + collector.getLocalPort()))); + A2AHttpClientConfiguration configuration = + A2AHttpClientConfiguration.builder() + .redirectHeaderPolicy(redirectHeaderPolicy) + .build(); A2AHttpClient client = A2AHttpClientFactory.create(configuration); - A2AHttpResponse result = client.createPost() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/redirect") - .addHeader("Authorization", "Bearer secret") - .addHeader("X-Trusted", "allowed") - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + client.createPost() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/redirect") + .addHeader("Authorization", "Bearer secret") + .addHeader("X-Trusted", "allowed") + .body("{}") + .followRedirects(true) + .post(); assertInstanceOf(JdkA2AHttpClient.class, client); assertEquals(200, result.status()); - org.mockserver.model.HttpRequest[] requests = collector.retrieveRecordedRequests( - request().withMethod("GET").withPath("/target")); + org.mockserver.model.HttpRequest[] requests = + collector.retrieveRecordedRequests(request().withMethod("GET").withPath("/target")); assertEquals(1, requests.length); assertEquals("allowed", requests[0].getFirstHeader("X-Trusted")); - assertTrue(requests[0].getFirstHeader("Authorization") == null - || requests[0].getFirstHeader("Authorization").isEmpty()); + assertTrue( + requests[0].getFirstHeader("Authorization") == null + || requests[0].getFirstHeader("Authorization").isEmpty()); } @Test public void testCreateWithConfigurationHonoursExcludedProviders() { - A2AHttpClientConfiguration configuration = A2AHttpClientConfiguration.builder() - .excludedProviderNames(List.of("jdk")) - .build(); + A2AHttpClientConfiguration configuration = + A2AHttpClientConfiguration.builder().excludedProviderNames(List.of("jdk")).build(); assertThrows(IllegalStateException.class, () -> A2AHttpClientFactory.create(configuration)); } @Test public void testCreateWithConfigurationIgnoresUnknownExcludedProvider() { - A2AHttpClientConfiguration configuration = A2AHttpClientConfiguration.builder() - .excludedProviderNames(List.of("nonexistent-provider")) - .build(); + A2AHttpClientConfiguration configuration = + A2AHttpClientConfiguration.builder() + .excludedProviderNames(List.of("nonexistent-provider")) + .build(); A2AHttpClient client = A2AHttpClientFactory.create(configuration); assertNotNull(client); - assertInstanceOf(JdkA2AHttpClient.class, client, + assertInstanceOf( + JdkA2AHttpClient.class, + client, "An unknown excluded provider should not prevent selecting the JDK client"); } @@ -111,15 +126,16 @@ public void testCreateReturnsJdkClient() { // When Vertx is not on classpath, JDK client should be used A2AHttpClient client = A2AHttpClientFactory.create(); assertNotNull(client); - assertInstanceOf(JdkA2AHttpClient.class, client, - "Factory should return JdkA2AHttpClient when Vertx is not available"); + assertInstanceOf( + JdkA2AHttpClient.class, + client, + "Factory should return JdkA2AHttpClient when Vertx is not available"); } @Test public void testCreateWithNullSseConfigUsesDefaultConfiguration() { - A2AHttpClientConfiguration configuration = A2AHttpClientConfiguration.builder() - .sseParserConfig(null) - .build(); + A2AHttpClientConfiguration configuration = + A2AHttpClientConfiguration.builder().sseParserConfig(null).build(); A2AHttpClient client = A2AHttpClientFactory.create(configuration); assertNotNull(client); @@ -129,14 +145,15 @@ public void testCreateWithNullSseConfigUsesDefaultConfiguration() { @Test public void testCreateWithConfigurationContainingSseConfigReturnsJdkClient() { SSEParserConfig config = SSEParserConfig.builder().maxBufferChars(4 * 1024 * 1024).build(); - A2AHttpClientConfiguration configuration = A2AHttpClientConfiguration.builder() - .sseParserConfig(config) - .build(); + A2AHttpClientConfiguration configuration = + A2AHttpClientConfiguration.builder().sseParserConfig(config).build(); A2AHttpClient client = A2AHttpClientFactory.create(configuration); assertNotNull(client); - assertInstanceOf(JdkA2AHttpClient.class, client, - "Factory should return JdkA2AHttpClient with custom SSEParserConfig"); + assertInstanceOf( + JdkA2AHttpClient.class, + client, + "Factory should return JdkA2AHttpClient with custom SSEParserConfig"); } @Test @@ -144,9 +161,8 @@ public void testCreateWithConfigurationAppliesSseConfig() throws Exception { // Verify the config is actually applied by creating a client with a restrictive // maxLineLength and checking that it rejects oversized SSE lines. SSEParserConfig restrictive = SSEParserConfig.builder().maxLineLength(50).build(); - A2AHttpClientConfiguration configuration = A2AHttpClientConfiguration.builder() - .sseParserConfig(restrictive) - .build(); + A2AHttpClientConfiguration configuration = + A2AHttpClientConfiguration.builder().sseParserConfig(restrictive).build(); A2AHttpClient client = A2AHttpClientFactory.create(configuration); assertNotNull(client); diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientFactoryUsageExample.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientFactoryUsageExample.java index 06aea525f..d87e5a5bd 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientFactoryUsageExample.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientFactoryUsageExample.java @@ -21,10 +21,11 @@ public void basicUsage() throws IOException, InterruptedException { A2AHttpClient client = A2AHttpClientFactory.create(); try { - A2AHttpResponse response = client.createGet() - .url("https://api.example.com/data") - .addHeader("Accept", "application/json") - .get(); + A2AHttpResponse response = + client.createGet() + .url("https://api.example.com/data") + .addHeader("Accept", "application/json") + .get(); if (response.success()) { System.out.println("Response: " + response.body()); @@ -50,21 +51,23 @@ public void tryWithResourcesUsage() throws Exception { // Only use try-with-resources if the client is AutoCloseable if (client instanceof AutoCloseable) { try (AutoCloseable closeableClient = (AutoCloseable) client) { - A2AHttpResponse response = client.createPost() - .url("https://api.example.com/submit") - .addHeader("Content-Type", "application/json") - .body("{\"key\":\"value\"}") - .post(); + A2AHttpResponse response = + client.createPost() + .url("https://api.example.com/submit") + .addHeader("Content-Type", "application/json") + .body("{\"key\":\"value\"}") + .post(); System.out.println("Status: " + response.status()); } } else { // Non-closeable client, use normally - A2AHttpResponse response = client.createPost() - .url("https://api.example.com/submit") - .addHeader("Content-Type", "application/json") - .body("{\"key\":\"value\"}") - .post(); + A2AHttpResponse response = + client.createPost() + .url("https://api.example.com/submit") + .addHeader("Content-Type", "application/json") + .body("{\"key\":\"value\"}") + .post(); System.out.println("Status: " + response.status()); } @@ -76,18 +79,18 @@ public void tryWithResourcesUsage() throws Exception { public void specificProviderUsage() throws IOException, InterruptedException { // Force the use of JDK client even if Vert.x is available A2AHttpClient jdkClient = A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT); - A2AHttpResponse response = jdkClient.createGet() - .url("https://api.example.com/data") - .get(); + A2AHttpResponse response = jdkClient.createGet().url("https://api.example.com/data").get(); System.out.println("Using JDK client: " + response.status()); // Or explicitly use Vert.x client - try (AutoCloseable vertxClient = (AutoCloseable) A2AHttpClientFactory.create( - A2AHttpClientConfiguration.DEFAULT)) { - A2AHttpResponse vertxResponse = ((A2AHttpClient) vertxClient).createGet() - .url("https://api.example.com/data") - .get(); + try (AutoCloseable vertxClient = + (AutoCloseable) A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT)) { + A2AHttpResponse vertxResponse = + ((A2AHttpClient) vertxClient) + .createGet() + .url("https://api.example.com/data") + .get(); System.out.println("Using Vert.x client: " + vertxResponse.status()); } catch (Exception e) { @@ -103,9 +106,7 @@ public void defensiveUsage() throws IOException, InterruptedException { try { client = A2AHttpClientFactory.create(); - A2AHttpResponse response = client.createGet() - .url("https://api.example.com/data") - .get(); + A2AHttpResponse response = client.createGet().url("https://api.example.com/data").get(); System.out.println("Response: " + response.body()); diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientProviderTest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientProviderTest.java index 0ca3adc6e..018c3213c 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientProviderTest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/A2AHttpClientProviderTest.java @@ -43,18 +43,20 @@ public void testJdkProviderSupportsSseConfig() { @Test public void testDefaultProviderDoesNotSupportSseConfig() { - A2AHttpClientProvider defaultProvider = new A2AHttpClientProvider() { - @Override - public A2AHttpClient create() { - return new JdkA2AHttpClient(); - } - - @Override - public String name() { - return "test"; - } - }; - assertFalse(defaultProvider.supportsSseConfig(), + A2AHttpClientProvider defaultProvider = + new A2AHttpClientProvider() { + @Override + public A2AHttpClient create() { + return new JdkA2AHttpClient(); + } + + @Override + public String name() { + return "test"; + } + }; + assertFalse( + defaultProvider.supportsSseConfig(), "Default interface implementation must return false"); } } diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientIntegrationTest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientIntegrationTest.java index d3eca90c7..b788e339b 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientIntegrationTest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientIntegrationTest.java @@ -12,7 +12,6 @@ import java.io.IOException; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.common.A2AErrorMessages; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -49,9 +48,7 @@ public void testGetRequestSuccess() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(200).withBody("success")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .get(); + A2AHttpResponse response = client.createGet().url(getBaseUrl() + "/test").get(); assertEquals(200, response.status()); assertTrue(response.success()); @@ -61,16 +58,15 @@ public void testGetRequestSuccess() throws Exception { @Test public void testPostRequestSuccess() throws Exception { mockServer - .when(request() - .withMethod("POST") - .withPath("/test") - .withBody("{\"key\":\"value\"}")) + .when( + request() + .withMethod("POST") + .withPath("/test") + .withBody("{\"key\":\"value\"}")) .respond(response().withStatusCode(201).withBody("created")); - A2AHttpResponse response = client.createPost() - .url(getBaseUrl() + "/test") - .body("{\"key\":\"value\"}") - .post(); + A2AHttpResponse response = + client.createPost().url(getBaseUrl() + "/test").body("{\"key\":\"value\"}").post(); assertEquals(201, response.status()); assertTrue(response.success()); @@ -83,9 +79,7 @@ public void testDeleteRequestSuccess() throws Exception { .when(request().withMethod("DELETE").withPath("/test")) .respond(response().withStatusCode(204)); - A2AHttpResponse response = client.createDelete() - .url(getBaseUrl() + "/test") - .delete(); + A2AHttpResponse response = client.createDelete().url(getBaseUrl() + "/test").delete(); assertEquals(204, response.status()); assertTrue(response.success()); @@ -97,11 +91,12 @@ public void test401AuthenticationErrorOnGet() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(401)); - Exception exception = assertThrows(IOException.class, () -> { - client.createGet() - .url(getBaseUrl() + "/test") - .get(); - }); + Exception exception = + assertThrows( + IOException.class, + () -> { + client.createGet().url(getBaseUrl() + "/test").get(); + }); assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage()); } @@ -112,11 +107,12 @@ public void test403AuthorizationErrorOnGet() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(403)); - Exception exception = assertThrows(IOException.class, () -> { - client.createGet() - .url(getBaseUrl() + "/test") - .get(); - }); + Exception exception = + assertThrows( + IOException.class, + () -> { + client.createGet().url(getBaseUrl() + "/test").get(); + }); assertEquals(A2AErrorMessages.AUTHORIZATION_FAILED, exception.getMessage()); } @@ -127,12 +123,12 @@ public void test401AuthenticationErrorOnPost() throws Exception { .when(request().withMethod("POST").withPath("/test")) .respond(response().withStatusCode(401)); - Exception exception = assertThrows(IOException.class, () -> { - client.createPost() - .url(getBaseUrl() + "/test") - .body("{}") - .post(); - }); + Exception exception = + assertThrows( + IOException.class, + () -> { + client.createPost().url(getBaseUrl() + "/test").body("{}").post(); + }); assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage()); } @@ -143,12 +139,12 @@ public void test403AuthorizationErrorOnPost() throws Exception { .when(request().withMethod("POST").withPath("/test")) .respond(response().withStatusCode(403)); - Exception exception = assertThrows(IOException.class, () -> { - client.createPost() - .url(getBaseUrl() + "/test") - .body("{}") - .post(); - }); + Exception exception = + assertThrows( + IOException.class, + () -> { + client.createPost().url(getBaseUrl() + "/test").body("{}").post(); + }); assertEquals(A2AErrorMessages.AUTHORIZATION_FAILED, exception.getMessage()); } @@ -159,11 +155,12 @@ public void test401AuthenticationErrorOnDelete() throws Exception { .when(request().withMethod("DELETE").withPath("/test")) .respond(response().withStatusCode(401)); - Exception exception = assertThrows(IOException.class, () -> { - client.createDelete() - .url(getBaseUrl() + "/test") - .delete(); - }); + Exception exception = + assertThrows( + IOException.class, + () -> { + client.createDelete().url(getBaseUrl() + "/test").delete(); + }); assertEquals(A2AErrorMessages.AUTHENTICATION_FAILED, exception.getMessage()); } @@ -171,18 +168,20 @@ public void test401AuthenticationErrorOnDelete() throws Exception { @Test public void testHeaderPropagation() throws Exception { mockServer - .when(request() - .withMethod("GET") - .withPath("/test") - .withHeader("Authorization", "Bearer token") - .withHeader("X-Custom-Header", "custom-value")) + .when( + request() + .withMethod("GET") + .withPath("/test") + .withHeader("Authorization", "Bearer token") + .withHeader("X-Custom-Header", "custom-value")) .respond(response().withStatusCode(200).withBody("ok")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .addHeader("Authorization", "Bearer token") - .addHeader("X-Custom-Header", "custom-value") - .get(); + A2AHttpResponse response = + client.createGet() + .url(getBaseUrl() + "/test") + .addHeader("Authorization", "Bearer token") + .addHeader("X-Custom-Header", "custom-value") + .get(); assertEquals(200, response.status()); assertEquals("ok", response.body()); @@ -194,9 +193,7 @@ public void testNonSuccessStatusCode() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(500).withBody("Internal Server Error")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .get(); + A2AHttpResponse response = client.createGet().url(getBaseUrl() + "/test").get(); assertEquals(500, response.status()); assertFalse(response.success()); @@ -209,9 +206,7 @@ public void test404NotFound() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(404).withBody("Not Found")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .get(); + A2AHttpResponse response = client.createGet().url(getBaseUrl() + "/test").get(); assertEquals(404, response.status()); assertFalse(response.success()); @@ -222,15 +217,14 @@ public void test404NotFound() throws Exception { public void testResponseHeadersOnSuccess() throws Exception { mockServer .when(request().withMethod("GET").withPath("/test")) - .respond(response() - .withStatusCode(200) - .withHeader("X-Custom-Header", "custom-value") - .withHeader("X-Request-Id", "abc-123") - .withBody("ok")); + .respond( + response() + .withStatusCode(200) + .withHeader("X-Custom-Header", "custom-value") + .withHeader("X-Request-Id", "abc-123") + .withBody("ok")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .get(); + A2AHttpResponse response = client.createGet().url(getBaseUrl() + "/test").get(); assertEquals(200, response.status()); A2AHttpHeaders headers = response.headers(); @@ -243,14 +237,13 @@ public void testResponseHeadersOnSuccess() throws Exception { public void testResponseHeadersCaseInsensitive() throws Exception { mockServer .when(request().withMethod("GET").withPath("/test")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "application/json") - .withBody("{}")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "application/json") + .withBody("{}")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .get(); + A2AHttpResponse response = client.createGet().url(getBaseUrl() + "/test").get(); A2AHttpHeaders headers = response.headers(); assertEquals(headers.firstValue("Content-Type"), headers.firstValue("content-type")); @@ -260,15 +253,14 @@ public void testResponseHeadersCaseInsensitive() throws Exception { public void testResponseHeadersOnErrorStatus() throws Exception { mockServer .when(request().withMethod("POST").withPath("/test")) - .respond(response() - .withStatusCode(429) - .withHeader("Retry-After", "120") - .withBody("Too Many Requests")); + .respond( + response() + .withStatusCode(429) + .withHeader("Retry-After", "120") + .withBody("Too Many Requests")); - A2AHttpResponse response = client.createPost() - .url(getBaseUrl() + "/test") - .body("{}") - .post(); + A2AHttpResponse response = + client.createPost().url(getBaseUrl() + "/test").body("{}").post(); assertEquals(429, response.status()); assertFalse(response.success()); @@ -282,9 +274,7 @@ public void testResponseHeadersMissingHeader() throws Exception { .when(request().withMethod("GET").withPath("/test")) .respond(response().withStatusCode(200).withBody("ok")); - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .get(); + A2AHttpResponse response = client.createGet().url(getBaseUrl() + "/test").get(); A2AHttpHeaders headers = response.headers(); assertNull(headers.firstValue("X-Nonexistent")); @@ -295,14 +285,13 @@ public void testResponseHeadersMissingHeader() throws Exception { public void testResponseHeadersToMap() throws Exception { mockServer .when(request().withMethod("GET").withPath("/test")) - .respond(response() - .withStatusCode(200) - .withHeader("X-Test", "value1") - .withBody("ok")); - - A2AHttpResponse response = client.createGet() - .url(getBaseUrl() + "/test") - .get(); + .respond( + response() + .withStatusCode(200) + .withHeader("X-Test", "value1") + .withBody("ok")); + + A2AHttpResponse response = client.createGet().url(getBaseUrl() + "/test").get(); Map> headerMap = response.headers().toMap(); assertNotNull(headerMap); diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientRedirectTest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientRedirectTest.java index 2c39dcbe6..247c075f3 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientRedirectTest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientRedirectTest.java @@ -47,38 +47,43 @@ public void tearDown() { @Test public void synchronousGetDoesNotFollowRedirectByDefault() throws Exception { redirector = ClientAndServer.startClientAndServer(0); - redirector.when(request().withMethod("GET").withPath("/agent")) - .respond(response() - .withStatusCode(302) - .withHeader("Location", "/target")); - redirector.when(request().withMethod("GET").withPath("/target")) + redirector + .when(request().withMethod("GET").withPath("/agent")) + .respond(response().withStatusCode(302).withHeader("Location", "/target")); + redirector + .when(request().withMethod("GET").withPath("/target")) .respond(response().withStatusCode(200).withBody("redirected")); - A2AHttpResponse result = createClient().createGet() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .get(); + A2AHttpResponse result = + createClient() + .createGet() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .get(); - assertEquals(302, result.status(), - "Synchronous GET must not follow redirects by default"); + assertEquals(302, result.status(), "Synchronous GET must not follow redirects by default"); redirector.verify(request().withPath("/target"), exactly(0)); } @Test public void synchronousGetFollowsRedirectWhenExplicitlyEnabled() throws Exception { redirector = ClientAndServer.startClientAndServer(0); - redirector.when(request().withMethod("GET").withPath("/agent")) - .respond(response() - .withStatusCode(302) - .withHeader("Location", "/target")); - redirector.when(request().withMethod("GET").withPath("/target")) + redirector + .when(request().withMethod("GET").withPath("/agent")) + .respond(response().withStatusCode(302).withHeader("Location", "/target")); + redirector + .when(request().withMethod("GET").withPath("/target")) .respond(response().withStatusCode(200).withBody("redirected")); - A2AHttpResponse result = createClient().createGet() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .followRedirects(true) - .get(); + A2AHttpResponse result = + createClient() + .createGet() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .followRedirects(true) + .get(); - assertEquals(200, result.status(), + assertEquals( + 200, + result.status(), "Synchronous GET must follow redirect when explicitly enabled"); redirector.verify(request().withPath("/target"), exactly(1)); } @@ -90,17 +95,24 @@ public void synchronousGetDoesNotFollowCrossOriginMethodPreservingRedirect(int r redirector = ClientAndServer.startClientAndServer(0); collector = ClientAndServer.startClientAndServer(0); String location = "http://127.0.0.1:" + collector.getLocalPort() + "/collect"; - redirector.when(request().withMethod("GET").withPath("/agent")) - .respond(response().withStatusCode(redirectStatus).withHeader("Location", location)); - collector.when(request().withMethod("GET").withPath("/collect")) + redirector + .when(request().withMethod("GET").withPath("/agent")) + .respond( + response().withStatusCode(redirectStatus).withHeader("Location", location)); + collector + .when(request().withMethod("GET").withPath("/collect")) .respond(response().withStatusCode(200).withBody("redirected")); - A2AHttpResponse result = createClient().createGet() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .followRedirects(true) - .get(); + A2AHttpResponse result = + createClient() + .createGet() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .followRedirects(true) + .get(); - assertEquals(redirectStatus, result.status(), + assertEquals( + redirectStatus, + result.status(), "GET 307/308 redirects are deliberately not followed across origins"); collector.verify(request().withPath("/collect"), exactly(0)); } @@ -109,14 +121,15 @@ public void synchronousGetDoesNotFollowCrossOriginMethodPreservingRedirect(int r public void synchronousPostDoesNotFollowRedirectByDefault() throws Exception { startRedirectPair(); - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .addHeader("X-API-Key", "FULCRUM-SYNTHETIC-DEFAULT") - .body("{}") - .post(); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .addHeader("X-API-Key", "FULCRUM-SYNTHETIC-DEFAULT") + .body("{}") + .post(); - assertEquals(302, result.status(), - "Synchronous POST must not follow redirects by default"); + assertEquals(302, result.status(), "Synchronous POST must not follow redirects by default"); collector.verify(request().withPath("/collect"), exactly(0)); } @@ -124,32 +137,39 @@ public void synchronousPostDoesNotFollowRedirectByDefault() throws Exception { public void synchronousPostFollowsRedirectWhenExplicitlyEnabled() throws Exception { startRedirectPair(); - A2AHttpResponse result = createRedirectCapableClient().createPost() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .addHeader("X-API-Key", "FULCRUM-SYNTHETIC-EXPLICIT") - .body("{}") - .followRedirects(true) - .post(); - - assertEquals(200, result.status(), + A2AHttpResponse result = + createRedirectCapableClient() + .createPost() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .addHeader("X-API-Key", "FULCRUM-SYNTHETIC-EXPLICIT") + .body("{}") + .followRedirects(true) + .post(); + + assertEquals( + 200, + result.status(), "Synchronous POST must follow redirect when explicitly enabled"); collector.verify(request().withPath("/collect"), exactly(1)); } @ParameterizedTest @ValueSource(ints = {301, 302, 303}) - public void crossOriginPostToGetRedirectHandlesBodyAndCredentials(int redirectStatus) throws Exception { + public void crossOriginPostToGetRedirectHandlesBodyAndCredentials(int redirectStatus) + throws Exception { startRedirectPair(redirectStatus); - A2AHttpResponse result = createRedirectCapableClient().createPost() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .addHeader("Authorization", "Bearer secret") - .addHeader("X-API-Key", "secret") - .addHeader("X-Custom-Token", "secret") - .addHeader("A2A-Version", "1.0") - .body("{\"token\":\"secret\"}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + createRedirectCapableClient() + .createPost() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .addHeader("Authorization", "Bearer secret") + .addHeader("X-API-Key", "secret") + .addHeader("X-Custom-Token", "secret") + .addHeader("A2A-Version", "1.0") + .body("{\"token\":\"secret\"}") + .followRedirects(true) + .post(); if (!followsCrossOriginPostToGetRedirects()) { assertEquals(redirectStatus, result.status()); @@ -158,8 +178,8 @@ public void crossOriginPostToGetRedirectHandlesBodyAndCredentials(int redirectSt } assertEquals(200, result.status()); - org.mockserver.model.HttpRequest[] recorded = collector.retrieveRecordedRequests( - request().withPath("/collect")); + org.mockserver.model.HttpRequest[] recorded = + collector.retrieveRecordedRequests(request().withPath("/collect")); assertEquals(1, recorded.length); assertEquals("GET", recorded[0].getMethod().getValue()); String redirectedBody = recorded[0].getBodyAsString(); @@ -176,13 +196,17 @@ public void synchronousPostDoesNotFollowCrossOriginMethodPreservingRedirect(int throws Exception { startRedirectPair(redirectStatus); - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .body("{\"token\":\"secret\"}") - .followRedirects(true) - .post(); - - assertEquals(redirectStatus, result.status(), + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .body("{\"token\":\"secret\"}") + .followRedirects(true) + .post(); + + assertEquals( + redirectStatus, + result.status(), "Cross-origin method-preserving redirects must not forward the POST body"); collector.verify(request().withPath("/collect"), exactly(0)); } @@ -192,27 +216,34 @@ public void synchronousPostStopsAfterSameOrigin307ThenCrossOrigin302() throws Ex redirector = ClientAndServer.startClientAndServer(0); collector = ClientAndServer.startClientAndServer(0); String secondLocation = "http://127.0.0.1:" + collector.getLocalPort() + "/collect"; - redirector.when(request().withMethod("POST").withPath("/agent")) + redirector + .when(request().withMethod("POST").withPath("/agent")) .respond(response().withStatusCode(307).withHeader("Location", "/same-origin")); - redirector.when(request().withMethod("POST").withPath("/same-origin")) + redirector + .when(request().withMethod("POST").withPath("/same-origin")) .respond(response().withStatusCode(302).withHeader("Location", secondLocation)); - collector.when(request().withPath("/collect")) + collector + .when(request().withPath("/collect")) .respond(response().withStatusCode(200).withBody("redirected")); String body = "{\"token\":\"secret\"}"; - A2AHttpResponse result = createRedirectCapableClient().createPost() - .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") - .body(body) - .followRedirects(true) - .post(); + A2AHttpResponse result = + createRedirectCapableClient() + .createPost() + .url("http://127.0.0.1:" + redirector.getLocalPort() + "/agent") + .body(body) + .followRedirects(true) + .post(); assertEquals(302, result.status()); assertEquals(secondLocation, result.headers().firstValue("Location")); - org.mockserver.model.HttpRequest[] secondHop = redirector.retrieveRecordedRequests( - request().withPath("/same-origin")); + org.mockserver.model.HttpRequest[] secondHop = + redirector.retrieveRecordedRequests(request().withPath("/same-origin")); assertEquals(1, secondHop.length); assertEquals("POST", secondHop[0].getMethod().getValue()); - assertEquals(JsonParser.parseString(body), JsonParser.parseString(secondHop[0].getBodyAsString())); + assertEquals( + JsonParser.parseString(body), + JsonParser.parseString(secondHop[0].getBodyAsString())); collector.verify(request().withPath("/collect"), exactly(0)); } @@ -224,12 +255,18 @@ private void startRedirectPair(int redirectStatus) { redirector = ClientAndServer.startClientAndServer(0); collector = ClientAndServer.startClientAndServer(0); - redirector.when(request().withMethod("POST").withPath("/agent")) - .respond(response() - .withStatusCode(redirectStatus) - .withHeader("Location", - "http://127.0.0.1:" + collector.getLocalPort() + "/collect")); - collector.when(request().withPath("/collect")) + redirector + .when(request().withMethod("POST").withPath("/agent")) + .respond( + response() + .withStatusCode(redirectStatus) + .withHeader( + "Location", + "http://127.0.0.1:" + + collector.getLocalPort() + + "/collect")); + collector + .when(request().withPath("/collect")) .respond(response().withStatusCode(200).withBody("redirected")); } @@ -243,6 +280,7 @@ protected ClientAndServer getRedirector() { private static void assertHeaderAbsent(org.mockserver.model.HttpRequest request, String name) { String value = request.getFirstHeader(name); - assertTrue(value == null || value.isEmpty(), name + " must not reach a cross-origin target"); + assertTrue( + value == null || value.isEmpty(), name + " must not reach a cross-origin target"); } } diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientSSETest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientSSETest.java index 8b57a967b..aed1be47b 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientSSETest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/AbstractA2AHttpClientSSETest.java @@ -9,13 +9,6 @@ import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; -import org.a2aproject.sdk.common.A2AErrorMessages; -import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockserver.integration.ClientAndServer; - import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -23,6 +16,12 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import org.a2aproject.sdk.common.A2AErrorMessages; +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockserver.integration.ClientAndServer; public abstract class AbstractA2AHttpClientSSETest { @@ -61,10 +60,11 @@ private String getBaseUrl() { public void testGetAsyncSSE() throws Exception { mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: event1\n\ndata: event2\n\ndata: event3\n\n")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: event1\n\ndata: event2\n\ndata: event3\n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -72,11 +72,7 @@ public void testGetAsyncSSE() throws Exception { client.createGet() .url(getBaseUrl() + "/sse") - .getAsyncSSE( - event -> events.add(event.data()), - error::set, - latch::countDown - ); + .getAsyncSSE(event -> events.add(event.data()), error::set, latch::countDown); assertTrue(latch.await(5, TimeUnit.SECONDS), "Expected completion handler to be called"); assertNull(error.get(), "Expected no errors"); @@ -89,14 +85,16 @@ public void testGetAsyncSSE() throws Exception { @Test public void testPostAsyncSSE() throws Exception { mockServer - .when(request() - .withMethod("POST") - .withPath("/sse") - .withBody("{\"subscribe\":true}")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: message1\n\ndata: message2\n\n")); + .when( + request() + .withMethod("POST") + .withPath("/sse") + .withBody("{\"subscribe\":true}")) + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: message1\n\ndata: message2\n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -105,11 +103,7 @@ public void testPostAsyncSSE() throws Exception { client.createPost() .url(getBaseUrl() + "/sse") .body("{\"subscribe\":true}") - .postAsyncSSE( - event -> events.add(event.data()), - error::set, - latch::countDown - ); + .postAsyncSSE(event -> events.add(event.data()), error::set, latch::countDown); assertTrue(latch.await(5, TimeUnit.SECONDS), "Expected completion handler to be called"); assertNull(error.get(), "Expected no errors"); @@ -122,10 +116,14 @@ public void testPostAsyncSSE() throws Exception { public void testSSEDataPrefixStripping() throws Exception { mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: content here\n\ndata:no space\n\ndata: extra spaces \n\n")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody( + "data: content here\n\n" + + "data:no space\n\n" + + "data: extra spaces \n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -133,18 +131,15 @@ public void testSSEDataPrefixStripping() throws Exception { client.createGet() .url(getBaseUrl() + "/sse") - .getAsyncSSE( - event -> events.add(event.data()), - error::set, - latch::countDown - ); + .getAsyncSSE(event -> events.add(event.data()), error::set, latch::countDown); assertTrue(latch.await(5, TimeUnit.SECONDS)); assertNull(error.get()); assertTrue(events.contains("content here"), "Should have stripped 'data: ' prefix"); assertTrue(events.contains("no space"), "Should handle 'data:' without space"); // SSE spec: only first space after colon is removed, rest is preserved - assertTrue(events.contains(" extra spaces "), "Should preserve whitespace after first space"); + assertTrue( + events.contains(" extra spaces "), "Should preserve whitespace after first space"); } @Test @@ -165,14 +160,14 @@ public void testSSEAuthenticationError() throws Exception { error.set(e); errorLatch.countDown(); }, - () -> completed.set(true) - ); + () -> completed.set(true)); assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called"); assertNotNull(error.get(), "Expected an error"); assertTrue(error.get() instanceof IOException, "Expected IOException"); - assertTrue(error.get().getMessage().contains(A2AErrorMessages.AUTHENTICATION_FAILED), - "Expected authentication error message but got: " + error.get().getMessage()); + assertTrue( + error.get().getMessage().contains(A2AErrorMessages.AUTHENTICATION_FAILED), + "Expected authentication error message but got: " + error.get().getMessage()); assertFalse(completed.get(), "Should not call completion handler on error"); } @@ -194,14 +189,14 @@ public void testSSEAuthorizationError() throws Exception { error.set(e); errorLatch.countDown(); }, - () -> completed.set(true) - ); + () -> completed.set(true)); assertTrue(errorLatch.await(5, TimeUnit.SECONDS), "Expected error handler to be called"); assertNotNull(error.get(), "Expected an error"); assertTrue(error.get() instanceof IOException, "Expected IOException"); - assertTrue(error.get().getMessage().contains(A2AErrorMessages.AUTHORIZATION_FAILED), - "Expected authorization error message but got: " + error.get().getMessage()); + assertTrue( + error.get().getMessage().contains(A2AErrorMessages.AUTHORIZATION_FAILED), + "Expected authorization error message but got: " + error.get().getMessage()); assertFalse(completed.get(), "Should not call completion handler on error"); } @@ -209,10 +204,15 @@ public void testSSEAuthorizationError() throws Exception { public void testSSEEmptyLinesIgnored() throws Exception { mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: first\n\n\n\ndata: second\n\ndata: \n\ndata: third\n\n")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody( + "data: first\n\n\n\n" + + "data: second\n\n" + + "data: \n\n" + + "data: third\n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -220,11 +220,7 @@ public void testSSEEmptyLinesIgnored() throws Exception { client.createGet() .url(getBaseUrl() + "/sse") - .getAsyncSSE( - event -> events.add(event.data()), - error::set, - latch::countDown - ); + .getAsyncSSE(event -> events.add(event.data()), error::set, latch::countDown); assertTrue(latch.await(5, TimeUnit.SECONDS)); assertNull(error.get()); @@ -237,15 +233,17 @@ public void testSSEEmptyLinesIgnored() throws Exception { @Test public void testSSEHeaderPropagation() throws Exception { mockServer - .when(request() - .withMethod("GET") - .withPath("/sse") - .withHeader("Accept", "text/event-stream") - .withHeader("Authorization", "Bearer token")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: authenticated\n\n")); + .when( + request() + .withMethod("GET") + .withPath("/sse") + .withHeader("Accept", "text/event-stream") + .withHeader("Authorization", "Bearer token")) + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: authenticated\n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -254,11 +252,7 @@ public void testSSEHeaderPropagation() throws Exception { client.createGet() .url(getBaseUrl() + "/sse") .addHeader("Authorization", "Bearer token") - .getAsyncSSE( - event -> events.add(event.data()), - error::set, - latch::countDown - ); + .getAsyncSSE(event -> events.add(event.data()), error::set, latch::countDown); assertTrue(latch.await(5, TimeUnit.SECONDS)); assertNull(error.get()); @@ -269,10 +263,16 @@ public void testSSEHeaderPropagation() throws Exception { public void testSSETypedEvents() throws Exception { mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("event: update\ndata: payload1\n\nevent: delete\ndata: payload2\n\ndata: no-type\n\n")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody( + "event: update\n" + + "data: payload1\n\n" + + "event: delete\n" + + "data: payload2\n\n" + + "data: no-type\n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -289,17 +289,21 @@ public void testSSETypedEvents() throws Exception { assertEquals("payload1", events.get(0).data()); assertEquals("delete", events.get(1).eventType()); assertEquals("payload2", events.get(1).data()); - assertEquals("message", events.get(2).eventType(), "Event without 'event:' field should default to 'message'"); + assertEquals( + "message", + events.get(2).eventType(), + "Event without 'event:' field should default to 'message'"); } @Test public void testSSEEventIdAndLastEventId() throws Exception { mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("id: 42\ndata: identified\n\ndata: no-id\n\n")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("id: 42\ndata: identified\n\ndata: no-id\n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -313,17 +317,21 @@ public void testSSEEventIdAndLastEventId() throws Exception { assertNull(error.get()); assertEquals(2, events.size()); assertEquals("42", events.get(0).id()); - assertEquals("42", events.get(1).id(), "Event without 'id:' field inherits last event ID per SSE spec"); + assertEquals( + "42", + events.get(1).id(), + "Event without 'id:' field inherits last event ID per SSE spec"); } @Test public void testSSEMultiLineData() throws Exception { mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: line1\ndata: line2\ndata: line3\n\n")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: line1\ndata: line2\ndata: line3\n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -341,13 +349,15 @@ public void testSSEMultiLineData() throws Exception { @Test public void testSSEStreamEndingWithoutTrailingEmptyLine() throws Exception { - // Stream ends mid-event with no terminal empty line — flush() must dispatch the buffered data + // Stream ends mid-event with no terminal empty line — flush() must dispatch the buffered + // data mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: flushed-event")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: flushed-event")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -367,10 +377,11 @@ public void testSSEStreamEndingWithoutTrailingEmptyLine() throws Exception { public void testPostSSETypedEvents() throws Exception { mockServer .when(request().withMethod("POST").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("event: result\nid: 99\ndata: done\n\n")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("event: result\nid: 99\ndata: done\n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -391,23 +402,26 @@ public void testPostSSETypedEvents() throws Exception { @Test public void testCustomSSEParserConfigRejectsOversizedLine() throws Exception { - A2AHttpClient configClient = createClient(SSEParserConfig.builder().maxLineLength(50).build()); + A2AHttpClient configClient = + createClient(SSEParserConfig.builder().maxLineLength(50).build()); assumeTrue(configClient != null, "Implementation does not support SSEParserConfig"); // 60-char payload exceeds the 50-char per-line limit String oversizedPayload = "x".repeat(60); mockServer .when(request().withMethod("POST").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: " + oversizedPayload + "\n\n")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: " + oversizedPayload + "\n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - configClient.createPost() + configClient + .createPost() .url(getBaseUrl() + "/sse") .body("{}") .postAsyncSSE( @@ -416,8 +430,7 @@ public void testCustomSSEParserConfigRejectsOversizedLine() throws Exception { error.set(e); latch.countDown(); }, - latch::countDown - ); + latch::countDown); assertTrue(latch.await(5, TimeUnit.SECONDS)); assertNotNull(error.get(), "Custom maxLineLength should reject oversized SSE line"); @@ -426,30 +439,29 @@ public void testCustomSSEParserConfigRejectsOversizedLine() throws Exception { @Test public void testCustomSSEParserConfigAcceptsLineWithinLimit() throws Exception { - A2AHttpClient configClient = createClient(SSEParserConfig.builder().maxLineLength(200).build()); + A2AHttpClient configClient = + createClient(SSEParserConfig.builder().maxLineLength(200).build()); assumeTrue(configClient != null, "Implementation does not support SSEParserConfig"); // 100-char payload is within the 200-char per-line limit String payload = "x".repeat(100); mockServer .when(request().withMethod("POST").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: " + payload + "\n\n")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: " + payload + "\n\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - configClient.createPost() + configClient + .createPost() .url(getBaseUrl() + "/sse") .body("{}") - .postAsyncSSE( - events::add, - error::set, - latch::countDown - ); + .postAsyncSSE(events::add, error::set, latch::countDown); assertTrue(latch.await(5, TimeUnit.SECONDS)); assertNull(error.get(), "Payload within limit should not trigger an error"); @@ -461,10 +473,11 @@ public void testCustomSSEParserConfigAcceptsLineWithinLimit() throws Exception { public void testSSEWithCRLFLineEndings() throws Exception { mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: first\r\n\r\ndata: second\r\n\r\n")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: first\r\n\r\ndata: second\r\n\r\n")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -485,10 +498,11 @@ public void testSSEWithCRLFLineEndings() throws Exception { public void testSSEWithBareCRLineEndings() throws Exception { mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: first\r\rdata: second\r\r")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: first\r\rdata: second\r\r")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -510,10 +524,11 @@ public void testSSEWithMixedLineEndings() throws Exception { // Mix of LF, CRLF, and bare CR terminators mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody("data: lf\n\ndata: crlf\r\n\r\ndata: cr\r\r")); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody("data: lf\n\ndata: crlf\r\n\r\ndata: cr\r\r")); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); @@ -533,7 +548,8 @@ public void testSSEWithMixedLineEndings() throws Exception { @Test public void testSSEOversizedLineRejectedBeforeMemoryExhaustion() throws Exception { - A2AHttpClient configClient = createClient(SSEParserConfig.builder().maxLineLength(50).build()); + A2AHttpClient configClient = + createClient(SSEParserConfig.builder().maxLineLength(50).build()); assumeTrue(configClient != null, "Implementation does not support SSEParserConfig"); // Valid event followed by an oversized line in the same event block. @@ -543,16 +559,18 @@ public void testSSEOversizedLineRejectedBeforeMemoryExhaustion() throws Exceptio mockServer .when(request().withMethod("GET").withPath("/sse")) - .respond(response() - .withStatusCode(200) - .withHeader("Content-Type", "text/event-stream") - .withBody(body)); + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "text/event-stream") + .withBody(body)); CountDownLatch latch = new CountDownLatch(1); List events = new ArrayList<>(); List errors = new ArrayList<>(); - configClient.createGet() + configClient + .createGet() .url(getBaseUrl() + "/sse") .getAsyncSSE(events::add, errors::add, latch::countDown); diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/BoundedLineBodySubscriberTest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/BoundedLineBodySubscriberTest.java index 336279d92..556997f5b 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/BoundedLineBodySubscriberTest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/BoundedLineBodySubscriberTest.java @@ -23,14 +23,19 @@ public class BoundedLineBodySubscriberTest { @SuppressWarnings("unchecked") private static HttpResponse.BodySubscriber createSubscriber( - Flow.Subscriber lineSubscriber, int maxLineBytes, - ServerSentEventParser sseParser) throws Exception { - Class clazz = Class.forName( - "org.a2aproject.sdk.client.http.JdkA2AHttpClient$BoundedLineBodySubscriber"); - Constructor ctor = clazz.getDeclaredConstructor( - Flow.Subscriber.class, int.class, ServerSentEventParser.class); + Flow.Subscriber lineSubscriber, + int maxLineBytes, + ServerSentEventParser sseParser) + throws Exception { + Class clazz = + Class.forName( + "org.a2aproject.sdk.client.http.JdkA2AHttpClient$BoundedLineBodySubscriber"); + Constructor ctor = + clazz.getDeclaredConstructor( + Flow.Subscriber.class, int.class, ServerSentEventParser.class); ctor.setAccessible(true); - return (HttpResponse.BodySubscriber) ctor.newInstance(lineSubscriber, maxLineBytes, sseParser); + return (HttpResponse.BodySubscriber) + ctor.newInstance(lineSubscriber, maxLineBytes, sseParser); } private static List toBuffers(String data) { @@ -38,13 +43,18 @@ private static List toBuffers(String data) { } private static void subscribe(HttpResponse.BodySubscriber subscriber) { - subscriber.onSubscribe(new Flow.Subscription() { - @Override - public void request(long n) { /* no-op in tests */ } - - @Override - public void cancel() { /* no-op in tests */ } - }); + subscriber.onSubscribe( + new Flow.Subscription() { + @Override + public void request(long n) { + /* no-op in tests */ + } + + @Override + public void cancel() { + /* no-op in tests */ + } + }); } private static final class RecordingSubscriber implements Flow.Subscriber { @@ -75,7 +85,9 @@ public void onComplete() { private static ServerSentEventParser createParser(List errors) { return new ServerSentEventParser( - event -> { /* discard events */ }, + event -> { + /* discard events */ + }, errors::add, SSEParserConfig.DEFAULT); } @@ -221,7 +233,9 @@ public void testLineTooLongCallsParser() throws Exception { subscriber.onNext(toBuffers("short\n" + "x".repeat(20) + "\nok\n")); subscriber.onComplete(); - assertEquals(List.of("short", "ok"), rec.lines, + assertEquals( + List.of("short", "ok"), + rec.lines, "Too-long line should not appear in line subscriber output"); assertEquals(1, errors.size(), "One error should be reported for too-long line"); assertTrue(errors.get(0).getMessage().contains("maximum length")); @@ -253,8 +267,8 @@ public void testLineTooLongAtStreamEnd() throws Exception { subscriber.onComplete(); assertEquals(List.of("data: ok"), rec.lines); - assertEquals(1, errors.size(), - "Too-long line at stream end must trigger processLineTooLong"); + assertEquals( + 1, errors.size(), "Too-long line at stream end must trigger processLineTooLong"); } @Test @@ -324,9 +338,10 @@ public void testMultipleBuffersInSingleOnNext() throws Exception { var subscriber = createSubscriber(rec, 0, createParser(errors)); subscribe(subscriber); - List buffers = List.of( - ByteBuffer.wrap("first\n".getBytes(StandardCharsets.UTF_8)), - ByteBuffer.wrap("second\n".getBytes(StandardCharsets.UTF_8))); + List buffers = + List.of( + ByteBuffer.wrap("first\n".getBytes(StandardCharsets.UTF_8)), + ByteBuffer.wrap("second\n".getBytes(StandardCharsets.UTF_8))); subscriber.onNext(buffers); subscriber.onComplete(); diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientRedirectTest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientRedirectTest.java index e2d6955d5..566d98768 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientRedirectTest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientRedirectTest.java @@ -8,7 +8,6 @@ import java.io.IOException; import java.net.http.HttpClient; - import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.mockserver.integration.ClientAndServer; @@ -33,54 +32,75 @@ protected A2AHttpClient createClient() { public void getDoesNotFollowNativeRedirectsEvenWithRedirectFollowingClient() throws Exception { server = ClientAndServer.startClientAndServer(0); server.when(request().withMethod("GET").withPath("/agent")) - .respond(response().withStatusCode(302).withHeader("Location", "http://127.0.0.1:1/elsewhere")); - - JdkA2AHttpClient client = new JdkA2AHttpClient(HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.NORMAL) - .build()); - - A2AHttpResponse result = client.createGet() - .url("http://127.0.0.1:" + server.getLocalPort() + "/agent") - .get(); - - assertEquals(302, result.status(), - "GET must not follow redirects even when the underlying HttpClient is configured to do so"); + .respond( + response() + .withStatusCode(302) + .withHeader("Location", "http://127.0.0.1:1/elsewhere")); + + JdkA2AHttpClient client = + new JdkA2AHttpClient( + HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build()); + + A2AHttpResponse result = + client.createGet() + .url("http://127.0.0.1:" + server.getLocalPort() + "/agent") + .get(); + + assertEquals( + 302, + result.status(), + "GET must not follow redirects even when the underlying HttpClient is configured to" + + " do so"); } @Test - public void deleteDoesNotFollowNativeRedirectsEvenWithRedirectFollowingClient() throws Exception { + public void deleteDoesNotFollowNativeRedirectsEvenWithRedirectFollowingClient() + throws Exception { server = ClientAndServer.startClientAndServer(0); server.when(request().withMethod("DELETE").withPath("/task")) - .respond(response().withStatusCode(302).withHeader("Location", "http://127.0.0.1:1/elsewhere")); - - JdkA2AHttpClient client = new JdkA2AHttpClient(HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.NORMAL) - .build()); - - A2AHttpResponse result = client.createDelete() - .url("http://127.0.0.1:" + server.getLocalPort() + "/task") - .delete(); - - assertEquals(302, result.status(), - "DELETE must not follow redirects even when the underlying HttpClient is configured to do so"); + .respond( + response() + .withStatusCode(302) + .withHeader("Location", "http://127.0.0.1:1/elsewhere")); + + JdkA2AHttpClient client = + new JdkA2AHttpClient( + HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NORMAL) + .build()); + + A2AHttpResponse result = + client.createDelete() + .url("http://127.0.0.1:" + server.getLocalPort() + "/task") + .delete(); + + assertEquals( + 302, + result.status(), + "DELETE must not follow redirects even when the underlying HttpClient is configured" + + " to do so"); } @Test public void crossOriginRedirectUsesSharedHeaderPolicy() throws Exception { startRedirectPair(); - A2AHttpResponse result = createClient().createPost() - .url("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent") - .addHeader("Authorization", "Bearer secret") - .addHeader("X-Custom-Token", "secret") - .addHeader("A2A-Version", "1.0") - .body("{}") - .followRedirects(true) - .post(); + A2AHttpResponse result = + createClient() + .createPost() + .url("http://127.0.0.1:" + getRedirector().getLocalPort() + "/agent") + .addHeader("Authorization", "Bearer secret") + .addHeader("X-Custom-Token", "secret") + .addHeader("A2A-Version", "1.0") + .body("{}") + .followRedirects(true) + .post(); assertEquals(200, result.status()); - org.mockserver.model.HttpRequest[] recorded = getCollector().retrieveRecordedRequests( - request().withPath("/collect")); + org.mockserver.model.HttpRequest[] recorded = + getCollector().retrieveRecordedRequests(request().withPath("/collect")); assertEquals(1, recorded.length); assertHeaderAbsent(recorded[0], "Authorization"); assertHeaderAbsent(recorded[0], "X-Custom-Token"); @@ -89,14 +109,19 @@ public void crossOriginRedirectUsesSharedHeaderPolicy() throws Exception { @Test public void rejectsCallerConfiguredNativeRedirectsWhenFollowingIsEnabled() { - JdkA2AHttpClient client = new JdkA2AHttpClient(HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.ALWAYS) - .build()); - - assertThrows(IOException.class, () -> client.createPost() - .url("http://127.0.0.1:1/agent") - .followRedirects(true) - .post()); + JdkA2AHttpClient client = + new JdkA2AHttpClient( + HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .build()); + + assertThrows( + IOException.class, + () -> + client.createPost() + .url("http://127.0.0.1:1/agent") + .followRedirects(true) + .post()); } private static void assertHeaderAbsent(org.mockserver.model.HttpRequest request, String name) { diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientSSETest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientSSETest.java index c5543383d..5a6d8ff0b 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientSSETest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientSSETest.java @@ -9,8 +9,7 @@ protected A2AHttpClient createClient() { @Override protected A2AHttpClient createClient(SSEParserConfig sseParserConfig) { - return A2AHttpClientFactory.create(A2AHttpClientConfiguration.builder() - .sseParserConfig(sseParserConfig) - .build()); + return A2AHttpClientFactory.create( + A2AHttpClientConfiguration.builder().sseParserConfig(sseParserConfig).build()); } } diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientTest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientTest.java index e562778e9..a0abc9825 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientTest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientTest.java @@ -1,8 +1,14 @@ package org.a2aproject.sdk.client.http; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.mockserver.integration.ClientAndServer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; import java.io.IOException; import java.net.Authenticator; @@ -33,16 +39,9 @@ import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeFalse; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockserver.integration.ClientAndServer; public class JdkA2AHttpClientTest { @@ -67,9 +66,10 @@ public void testDefaultConstructorCreatesUsableClient() throws Exception { JdkA2AHttpClient client = new JdkA2AHttpClient(); - A2AHttpResponse response = client.createGet() - .url("http://localhost:" + server.getLocalPort() + "/default") - .get(); + A2AHttpResponse response = + client.createGet() + .url("http://localhost:" + server.getLocalPort() + "/default") + .get(); assertEquals(200, response.status()); assertEquals("ok", response.body()); @@ -85,15 +85,20 @@ public void testCustomHostHeaderRejectedByJdkHttpClient() { // // Skip if the JVM running this test already allows a restricted "Host" header: in that // (documented, supported) configuration the JDK client legitimately accepts it instead. - assumeFalse(jdkAllowsRestrictedHostHeader(), "JVM allows a restricted Host header " - + "via jdk.httpclient.allowRestrictedHeaders; the JDK client no longer rejects it"); + assumeFalse( + jdkAllowsRestrictedHostHeader(), + "JVM allows a restricted Host header via jdk.httpclient.allowRestrictedHeaders; the" + + " JDK client no longer rejects it"); JdkA2AHttpClient client = new JdkA2AHttpClient(); - assertThrows(IllegalArgumentException.class, () -> client.createGet() - .url("http://localhost:1/unused") - .addHeader("Host", "custom.example.com") - .get()); + assertThrows( + IllegalArgumentException.class, + () -> + client.createGet() + .url("http://localhost:1/unused") + .addHeader("Host", "custom.example.com") + .get()); } private static boolean jdkAllowsRestrictedHostHeader() { @@ -110,25 +115,29 @@ public void testConstructorUsesProvidedHttpClient() throws Exception { .respond(response().withStatusCode(200).withBody("ok")); TrackingProxySelector proxySelector = new TrackingProxySelector(); - HttpClient providedClient = HttpClient.newBuilder() - .proxy(proxySelector) - .build(); + HttpClient providedClient = HttpClient.newBuilder().proxy(proxySelector).build(); JdkA2AHttpClient client = new JdkA2AHttpClient(providedClient); - A2AHttpResponse response = client.createGet() - .url("http://localhost:" + server.getLocalPort() + "/custom") - .get(); + A2AHttpResponse response = + client.createGet() + .url("http://localhost:" + server.getLocalPort() + "/custom") + .get(); assertEquals(200, response.status()); assertEquals("ok", response.body()); - assertEquals(1, proxySelector.selectCount.get(), + assertEquals( + 1, + proxySelector.selectCount.get(), "Provided HttpClient should be used for request execution"); } @Test public void testConstructorRejectsNullHttpClient() { - assertThrows(IllegalArgumentException.class, () -> new JdkA2AHttpClient((java.net.http.HttpClient) null), "foo"); + assertThrows( + IllegalArgumentException.class, + () -> new JdkA2AHttpClient((java.net.http.HttpClient) null), + "foo"); } @Test @@ -136,21 +145,25 @@ public void testPostFollowRedirectsFalseDoesNotFollowRedirect() throws Exception server = ClientAndServer.startClientAndServer(0); int port = server.getLocalPort(); server.when(request().withMethod("POST").withPath("/redirect")) - .respond(response() - .withStatusCode(302) - .withHeader("Location", "http://localhost:" + port + "/target")); + .respond( + response() + .withStatusCode(302) + .withHeader("Location", "http://localhost:" + port + "/target")); server.when(request().withMethod("POST").withPath("/target")) .respond(response().withStatusCode(200).withBody("redirected")); JdkA2AHttpClient client = new JdkA2AHttpClient(); - A2AHttpResponse response = client.createPost() - .url("http://localhost:" + port + "/redirect") - .body("{}") - .followRedirects(false) - .post(); + A2AHttpResponse response = + client.createPost() + .url("http://localhost:" + port + "/redirect") + .body("{}") + .followRedirects(false) + .post(); - assertEquals(302, response.status(), + assertEquals( + 302, + response.status(), "With followRedirects(false), the 302 should be returned as-is"); } @@ -164,20 +177,23 @@ public void testPostWithoutRedirectsPreservesSuppliedCookieHandler() throws Exce CookieManager cookieManager = new CookieManager(); URI uri = URI.create("http://localhost:" + port + "/post"); cookieManager.put(uri, Map.of("Set-Cookie", List.of("session=secret; Path=/"))); - HttpClient suppliedClient = HttpClient.newBuilder() - .cookieHandler(cookieManager) - .followRedirects(HttpClient.Redirect.NORMAL) - .build(); - - A2AHttpResponse result = new JdkA2AHttpClient(suppliedClient).createPost() - .url(uri.toString()) - .body("{}") - .followRedirects(false) - .post(); + HttpClient suppliedClient = + HttpClient.newBuilder() + .cookieHandler(cookieManager) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + + A2AHttpResponse result = + new JdkA2AHttpClient(suppliedClient) + .createPost() + .url(uri.toString()) + .body("{}") + .followRedirects(false) + .post(); assertEquals(200, result.status()); - org.mockserver.model.HttpRequest[] requests = server.retrieveRecordedRequests( - request().withMethod("POST").withPath("/post")); + org.mockserver.model.HttpRequest[] requests = + server.retrieveRecordedRequests(request().withMethod("POST").withPath("/post")); assertEquals(1, requests.length); assertEquals("session=secret", requests[0].getFirstHeader("Cookie")); } @@ -188,10 +204,16 @@ public void testCrossOriginRedirectDoesNotUseSuppliedCookieHandler() throws Exce collector = ClientAndServer.startClientAndServer(0); int redirectPort = server.getLocalPort(); server.when(request().withMethod("POST").withPath("/redirect")) - .respond(response() - .withStatusCode(302) - .withHeader("Location", "http://127.0.0.1:" + collector.getLocalPort() + "/target")); - collector.when(request().withMethod("GET").withPath("/target")) + .respond( + response() + .withStatusCode(302) + .withHeader( + "Location", + "http://127.0.0.1:" + + collector.getLocalPort() + + "/target")); + collector + .when(request().withMethod("GET").withPath("/target")) .respond(response().withStatusCode(200).withBody("redirected")); CookieManager cookieManager = new CookieManager(); @@ -199,25 +221,31 @@ public void testCrossOriginRedirectDoesNotUseSuppliedCookieHandler() throws Exce URI.create("http://127.0.0.1:" + redirectPort + "/redirect"), Map.of("Set-Cookie", List.of("session=secret; Path=/"))); URI targetUri = URI.create("http://127.0.0.1:" + collector.getLocalPort() + "/target"); - assertEquals(List.of("session=secret"), cookieManager.get(targetUri, Map.of()).get("Cookie"), + assertEquals( + List.of("session=secret"), + cookieManager.get(targetUri, Map.of()).get("Cookie"), "The supplied CookieHandler must be capable of injecting the cross-port cookie"); - HttpClient suppliedClient = HttpClient.newBuilder() - .cookieHandler(cookieManager) - .followRedirects(HttpClient.Redirect.NEVER) - .build(); - - A2AHttpResponse result = new JdkA2AHttpClient(suppliedClient).createPost() - .url("http://127.0.0.1:" + redirectPort + "/redirect") - .body("{}") - .followRedirects(true) - .post(); + HttpClient suppliedClient = + HttpClient.newBuilder() + .cookieHandler(cookieManager) + .followRedirects(HttpClient.Redirect.NEVER) + .build(); + + A2AHttpResponse result = + new JdkA2AHttpClient(suppliedClient) + .createPost() + .url("http://127.0.0.1:" + redirectPort + "/redirect") + .body("{}") + .followRedirects(true) + .post(); assertEquals(200, result.status()); - org.mockserver.model.HttpRequest[] requests = collector.retrieveRecordedRequests( - request().withMethod("GET").withPath("/target")); + org.mockserver.model.HttpRequest[] requests = + collector.retrieveRecordedRequests(request().withMethod("GET").withPath("/target")); assertEquals(1, requests.length); String cookie = requests[0].getFirstHeader("Cookie"); - assertTrue(cookie == null || cookie.isEmpty(), + assertTrue( + cookie == null || cookie.isEmpty(), "Cross-origin redirect must not allow a supplied CookieHandler to inject cookies"); } @@ -227,26 +255,37 @@ public void testCrossOriginRedirectPreservesSuppliedProxySelector() throws Excep collector = ClientAndServer.startClientAndServer(0); int redirectPort = server.getLocalPort(); server.when(request().withMethod("POST").withPath("/redirect")) - .respond(response() - .withStatusCode(302) - .withHeader("Location", "http://127.0.0.1:" + collector.getLocalPort() + "/target")); - collector.when(request().withMethod("GET").withPath("/target")) + .respond( + response() + .withStatusCode(302) + .withHeader( + "Location", + "http://127.0.0.1:" + + collector.getLocalPort() + + "/target")); + collector + .when(request().withMethod("GET").withPath("/target")) .respond(response().withStatusCode(200).withBody("redirected")); TrackingProxySelector proxySelector = new TrackingProxySelector(); - HttpClient suppliedClient = HttpClient.newBuilder() - .proxy(proxySelector) - .followRedirects(HttpClient.Redirect.NEVER) - .build(); - - A2AHttpResponse result = new JdkA2AHttpClient(suppliedClient).createPost() - .url("http://127.0.0.1:" + redirectPort + "/redirect") - .body("{}") - .followRedirects(true) - .post(); + HttpClient suppliedClient = + HttpClient.newBuilder() + .proxy(proxySelector) + .followRedirects(HttpClient.Redirect.NEVER) + .build(); + + A2AHttpResponse result = + new JdkA2AHttpClient(suppliedClient) + .createPost() + .url("http://127.0.0.1:" + redirectPort + "/redirect") + .body("{}") + .followRedirects(true) + .post(); assertEquals(200, result.status()); - assertEquals(2, proxySelector.selectCount.get(), + assertEquals( + 2, + proxySelector.selectCount.get(), "Supplied ProxySelector must be used for both redirect hops"); } @@ -255,18 +294,22 @@ public void testDefaultClientDoesNotFollowRedirects() throws Exception { server = ClientAndServer.startClientAndServer(0); server.when(request().withMethod("GET").withPath("/redirect")) - .respond(response() - .withStatusCode(302) - .withHeader("Location", "http://localhost:" + server.getLocalPort() + "/target")); + .respond( + response() + .withStatusCode(302) + .withHeader( + "Location", + "http://localhost:" + server.getLocalPort() + "/target")); server.when(request().withMethod("GET").withPath("/target")) .respond(response().withStatusCode(200).withBody("redirected")); JdkA2AHttpClient client = new JdkA2AHttpClient(); - A2AHttpResponse response = client.createGet() - .url("http://localhost:" + server.getLocalPort() + "/redirect") - .get(); + A2AHttpResponse response = + client.createGet() + .url("http://localhost:" + server.getLocalPort() + "/redirect") + .get(); assertEquals(302, response.status()); assertFalse(response.success()); @@ -275,28 +318,34 @@ public void testDefaultClientDoesNotFollowRedirects() throws Exception { } @Test - public void testGetNeverFollowsNativeRedirectsEvenWithRedirectFollowingClient() throws Exception { + public void testGetNeverFollowsNativeRedirectsEvenWithRedirectFollowingClient() + throws Exception { server = ClientAndServer.startClientAndServer(0); server.when(request().withMethod("GET").withPath("/redirect")) - .respond(response() - .withStatusCode(302) - .withHeader("Location", "http://localhost:" + server.getLocalPort() + "/target")); + .respond( + response() + .withStatusCode(302) + .withHeader( + "Location", + "http://localhost:" + server.getLocalPort() + "/target")); server.when(request().withMethod("GET").withPath("/target")) .respond(response().withStatusCode(200).withBody("redirected")); - HttpClient customClient = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.NORMAL) - .build(); + HttpClient customClient = + HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); JdkA2AHttpClient client = new JdkA2AHttpClient(customClient); - A2AHttpResponse response = client.createGet() - .url("http://localhost:" + server.getLocalPort() + "/redirect") - .get(); + A2AHttpResponse response = + client.createGet() + .url("http://localhost:" + server.getLocalPort() + "/redirect") + .get(); - assertEquals(302, response.status(), + assertEquals( + 302, + response.status(), "GET must not follow native redirects; the SDK always uses a no-redirect client"); } @@ -306,18 +355,19 @@ public void testCancellationExceptionViaSubscriberOnErrorIsNotPropagated() throw AtomicBoolean completed = new AtomicBoolean(false); CountDownLatch errorPathReached = new CountDownLatch(1); - HttpClient fakeClient = new StubHttpClient() { - @Override - public CompletableFuture> sendAsync( - HttpRequest request, HttpResponse.BodyHandler handler) { - HttpResponse.BodySubscriber bodySubscriber = - handler.apply(new FakeResponseInfo(200, "text/plain")); - bodySubscriber.onSubscribe(new NoOpSubscription()); - bodySubscriber.onError(new CancellationException()); - errorPathReached.countDown(); - return new CompletableFuture<>(); // never completes - } - }; + HttpClient fakeClient = + new StubHttpClient() { + @Override + public CompletableFuture> sendAsync( + HttpRequest request, HttpResponse.BodyHandler handler) { + HttpResponse.BodySubscriber bodySubscriber = + handler.apply(new FakeResponseInfo(200, "text/plain")); + bodySubscriber.onSubscribe(new NoOpSubscription()); + bodySubscriber.onError(new CancellationException()); + errorPathReached.countDown(); + return new CompletableFuture<>(); // never completes + } + }; new JdkA2AHttpClient(fakeClient) .createGet() @@ -325,7 +375,8 @@ public CompletableFuture> sendAsync( .getAsyncSSE(event -> {}, e -> capturedError.set(e), () -> completed.set(true)); assertTrue(errorPathReached.await(5, TimeUnit.SECONDS)); - assertNull(capturedError.get(), "CancellationException should not reach the error consumer"); + assertNull( + capturedError.get(), "CancellationException should not reach the error consumer"); assertFalse(completed.get(), "Complete handler must not be called after cancellation"); } @@ -334,23 +385,27 @@ public void testCancellationExceptionViaFutureFailureIsNotPropagated() throws Ex AtomicReference capturedError = new AtomicReference<>(); AtomicBoolean completed = new AtomicBoolean(false); - HttpClient fakeClient = new StubHttpClient() { - @Override - public CompletableFuture> sendAsync( - HttpRequest request, HttpResponse.BodyHandler handler) { - CompletableFuture> future = new CompletableFuture<>(); - future.completeExceptionally(new CancellationException()); - return future; - } - }; - - CompletableFuture result = new JdkA2AHttpClient(fakeClient) - .createGet() - .url("http://example.com/sse") - .getAsyncSSE(event -> {}, e -> capturedError.set(e), () -> completed.set(true)); + HttpClient fakeClient = + new StubHttpClient() { + @Override + public CompletableFuture> sendAsync( + HttpRequest request, HttpResponse.BodyHandler handler) { + CompletableFuture> future = new CompletableFuture<>(); + future.completeExceptionally(new CancellationException()); + return future; + } + }; + + CompletableFuture result = + new JdkA2AHttpClient(fakeClient) + .createGet() + .url("http://example.com/sse") + .getAsyncSSE( + event -> {}, e -> capturedError.set(e), () -> completed.set(true)); result.get(5, TimeUnit.SECONDS); - assertNull(capturedError.get(), "CancellationException should not reach the error consumer"); + assertNull( + capturedError.get(), "CancellationException should not reach the error consumer"); assertFalse(completed.get(), "Complete handler must not be called after cancellation"); } @@ -360,20 +415,27 @@ public void testRealErrorsAreStillPropagatedToErrorConsumer() throws Exception { CountDownLatch errorLatch = new CountDownLatch(1); IOException expectedError = new IOException("connection refused"); - HttpClient fakeClient = new StubHttpClient() { - @Override - public CompletableFuture> sendAsync( - HttpRequest request, HttpResponse.BodyHandler handler) { - CompletableFuture> future = new CompletableFuture<>(); - future.completeExceptionally(expectedError); - return future; - } - }; + HttpClient fakeClient = + new StubHttpClient() { + @Override + public CompletableFuture> sendAsync( + HttpRequest request, HttpResponse.BodyHandler handler) { + CompletableFuture> future = new CompletableFuture<>(); + future.completeExceptionally(expectedError); + return future; + } + }; new JdkA2AHttpClient(fakeClient) .createGet() .url("http://example.com/sse") - .getAsyncSSE(event -> {}, e -> { capturedError.set(e); errorLatch.countDown(); }, () -> {}); + .getAsyncSSE( + event -> {}, + e -> { + capturedError.set(e); + errorLatch.countDown(); + }, + () -> {}); assertTrue(errorLatch.await(5, TimeUnit.SECONDS)); assertNotNull(capturedError.get()); @@ -381,18 +443,68 @@ public CompletableFuture> sendAsync( } private abstract static class StubHttpClient extends HttpClient { - @Override public Optional cookieHandler() { return Optional.empty(); } - @Override public Optional connectTimeout() { return Optional.empty(); } - @Override public HttpClient.Redirect followRedirects() { return HttpClient.Redirect.NEVER; } - @Override public Optional proxy() { return Optional.empty(); } - @Override public SSLContext sslContext() { throw new UnsupportedOperationException(); } - @Override public SSLParameters sslParameters() { return new SSLParameters(); } - @Override public Optional authenticator() { return Optional.empty(); } - @Override public HttpClient.Version version() { return HttpClient.Version.HTTP_1_1; } - @Override public Optional executor() { return Optional.empty(); } - @Override public HttpResponse send(HttpRequest r, HttpResponse.BodyHandler h) { throw new UnsupportedOperationException(); } - @Override public CompletableFuture> sendAsync(HttpRequest r, HttpResponse.BodyHandler h, HttpResponse.PushPromiseHandler p) { return sendAsync(r, h); } - @Override public WebSocket.Builder newWebSocketBuilder() { throw new UnsupportedOperationException(); } + @Override + public Optional cookieHandler() { + return Optional.empty(); + } + + @Override + public Optional connectTimeout() { + return Optional.empty(); + } + + @Override + public HttpClient.Redirect followRedirects() { + return HttpClient.Redirect.NEVER; + } + + @Override + public Optional proxy() { + return Optional.empty(); + } + + @Override + public SSLContext sslContext() { + throw new UnsupportedOperationException(); + } + + @Override + public SSLParameters sslParameters() { + return new SSLParameters(); + } + + @Override + public Optional authenticator() { + return Optional.empty(); + } + + @Override + public HttpClient.Version version() { + return HttpClient.Version.HTTP_1_1; + } + + @Override + public Optional executor() { + return Optional.empty(); + } + + @Override + public HttpResponse send(HttpRequest r, HttpResponse.BodyHandler h) { + throw new UnsupportedOperationException(); + } + + @Override + public CompletableFuture> sendAsync( + HttpRequest r, + HttpResponse.BodyHandler h, + HttpResponse.PushPromiseHandler p) { + return sendAsync(r, h); + } + + @Override + public WebSocket.Builder newWebSocketBuilder() { + throw new UnsupportedOperationException(); + } } private static final class FakeResponseInfo implements HttpResponse.ResponseInfo { @@ -401,17 +513,32 @@ private static final class FakeResponseInfo implements HttpResponse.ResponseInfo FakeResponseInfo(int statusCode, String contentType) { this.statusCode = statusCode; - this.headers = HttpHeaders.of(Map.of("Content-Type", List.of(contentType)), (k, v) -> true); + this.headers = + HttpHeaders.of(Map.of("Content-Type", List.of(contentType)), (k, v) -> true); } - @Override public int statusCode() { return statusCode; } - @Override public HttpHeaders headers() { return headers; } - @Override public HttpClient.Version version() { return HttpClient.Version.HTTP_1_1; } + @Override + public int statusCode() { + return statusCode; + } + + @Override + public HttpHeaders headers() { + return headers; + } + + @Override + public HttpClient.Version version() { + return HttpClient.Version.HTTP_1_1; + } } private static final class NoOpSubscription implements Flow.Subscription { - @Override public void request(long n) {} - @Override public void cancel() {} + @Override + public void request(long n) {} + + @Override + public void cancel() {} } private static final class TrackingProxySelector extends ProxySelector { diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/JsonMessages.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/JsonMessages.java index 1f6cd24cd..7f10461db 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/JsonMessages.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/JsonMessages.java @@ -6,7 +6,8 @@ */ public class JsonMessages { - static final String AGENT_CARD = """ + static final String AGENT_CARD = + """ { "name": "GeoSpatial Route Planner Agent", "description": "Provides advanced route planning, traffic analysis, and custom map generation services. This agent can calculate optimal routes, estimate travel times considering real-time traffic, and create personalized maps with points of interest.", @@ -77,5 +78,6 @@ public class JsonMessages { "signature": "QFdkNLNszlGj3z3u0YQGt_T9LixY3qtdQpZmsTdDHDe3fXV9y9-B3m2-XgCpzuhiLt8E0tV6HXoZKHv4GtHgKQ" } ] - }"""; -} \ No newline at end of file + }\ + """; +} diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/RedirectHeaderPolicyTest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/RedirectHeaderPolicyTest.java index d82c7ae1a..0505b23ac 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/RedirectHeaderPolicyTest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/RedirectHeaderPolicyTest.java @@ -8,24 +8,24 @@ import java.net.URI; import java.util.Map; import java.util.Set; - import org.junit.jupiter.api.Test; public class RedirectHeaderPolicyTest { @Test public void defaultPolicyStripsCredentialsAndRetainsA2AHeadersCrossOrigin() { - RedirectHeaderPolicy.RedirectedRequest redirect = RedirectHeaderPolicy.DEFAULT - .redirectedRequest( - URI.create("http://origin.example/agent"), - 302, - "https://target.example/collect", - Map.of( - "Authorization", "Bearer secret", - "X-Custom-Token", "secret", - "A2A-Version", "1.0", - "A2A-Extensions", "extension-a")) - .orElseThrow(); + RedirectHeaderPolicy.RedirectedRequest redirect = + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create("http://origin.example/agent"), + 302, + "https://target.example/collect", + Map.of( + "Authorization", "Bearer secret", + "X-Custom-Token", "secret", + "A2A-Version", "1.0", + "A2A-Extensions", "extension-a")) + .orElseThrow(); assertTrue(redirect.redirectAsGet()); assertFalse(redirect.headers().containsKey("Authorization")); @@ -36,19 +36,20 @@ public void defaultPolicyStripsCredentialsAndRetainsA2AHeadersCrossOrigin() { @Test public void postToGetRedirectRemovesEntityHeadersOnSameOrigin() { - RedirectHeaderPolicy.RedirectedRequest redirect = RedirectHeaderPolicy.DEFAULT - .redirectedRequest( - URI.create("https://agent.example/send"), - 302, - "/collect", - Map.of( - "Authorization", "Bearer same-origin", - "Content-Length", "2", - "Content-Type", "application/json", - "Content-Encoding", "identity", - "dIgEsT", "sha-256=YWJj", - "lAsT-MoDiFiEd", "Wed, 21 Oct 2015 07:28:00 GMT")) - .orElseThrow(); + RedirectHeaderPolicy.RedirectedRequest redirect = + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create("https://agent.example/send"), + 302, + "/collect", + Map.of( + "Authorization", "Bearer same-origin", + "Content-Length", "2", + "Content-Type", "application/json", + "Content-Encoding", "identity", + "dIgEsT", "sha-256=YWJj", + "lAsT-MoDiFiEd", "Wed, 21 Oct 2015 07:28:00 GMT")) + .orElseThrow(); assertTrue(redirect.redirectAsGet()); assertEquals("Bearer same-origin", redirect.headers().get("Authorization")); @@ -61,43 +62,45 @@ public void postToGetRedirectRemovesEntityHeadersOnSameOrigin() { @Test public void customPolicyAddsHeadersOnlyForTrustedTargetOrigin() { - RedirectHeaderPolicy policy = new RedirectHeaderPolicy( - Set.of("Authorization", "X-Trusted-Header"), - Set.of(URI.create("https://target.example"))); - Map headers = Map.of( - "Authorization", "Bearer secret", - "A2A-Version", "1.0", - "X-Trusted-Header", "value"); - - RedirectHeaderPolicy.RedirectedRequest trusted = policy - .redirectedRequest( - URI.create("https://origin.example/send"), - 302, - "https://target.example:443/collect", - headers) - .orElseThrow(); + RedirectHeaderPolicy policy = + new RedirectHeaderPolicy( + Set.of("Authorization", "X-Trusted-Header"), + Set.of(URI.create("https://target.example"))); + Map headers = + Map.of( + "Authorization", "Bearer secret", + "A2A-Version", "1.0", + "X-Trusted-Header", "value"); + + RedirectHeaderPolicy.RedirectedRequest trusted = + policy.redirectedRequest( + URI.create("https://origin.example/send"), + 302, + "https://target.example:443/collect", + headers) + .orElseThrow(); assertEquals("Bearer secret", trusted.headers().get("Authorization")); assertEquals("1.0", trusted.headers().get("A2A-Version")); assertEquals("value", trusted.headers().get("X-Trusted-Header")); - for (String target : Set.of( - "https://other.example/collect", - "https://target.example:8443/collect")) { - RedirectHeaderPolicy.RedirectedRequest untrusted = policy - .redirectedRequest(URI.create("https://origin.example/send"), 302, target, headers) - .orElseThrow(); + for (String target : + Set.of("https://other.example/collect", "https://target.example:8443/collect")) { + RedirectHeaderPolicy.RedirectedRequest untrusted = + policy.redirectedRequest( + URI.create("https://origin.example/send"), 302, target, headers) + .orElseThrow(); assertFalse(untrusted.headers().containsKey("Authorization"), target); assertFalse(untrusted.headers().containsKey("X-Trusted-Header"), target); assertEquals("1.0", untrusted.headers().get("A2A-Version"), target); } - RedirectHeaderPolicy.RedirectedRequest differentScheme = policy - .redirectedRequest( - URI.create("http://origin.example/send"), - 302, - "http://target.example/collect", - headers) - .orElseThrow(); + RedirectHeaderPolicy.RedirectedRequest differentScheme = + policy.redirectedRequest( + URI.create("http://origin.example/send"), + 302, + "http://target.example/collect", + headers) + .orElseThrow(); assertFalse(differentScheme.headers().containsKey("Authorization")); assertFalse(differentScheme.headers().containsKey("X-Trusted-Header")); } @@ -106,57 +109,70 @@ public void customPolicyAddsHeadersOnlyForTrustedTargetOrigin() { public void httpsToHttpRedirectsAreNotFollowed() { URI source = URI.create("https://agent.example/send"); for (int status : new int[] {301, 302, 303, 307, 308}) { - assertTrue(RedirectHeaderPolicy.DEFAULT - .redirectedRequest(source, status, "http://agent.example/collect", Map.of()) - .isEmpty(), "status " + status); + assertTrue( + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + source, status, "http://agent.example/collect", Map.of()) + .isEmpty(), + "status " + status); } - assertTrue(RedirectHeaderPolicy.DEFAULT - .redirectedRequest( - URI.create("https://agent.example:8443/send"), - 302, - "http://agent.example:8443/collect", - Map.of()) - .isEmpty()); - RedirectHeaderPolicy trustedHttpPolicy = new RedirectHeaderPolicy( - Set.of("Authorization"), Set.of(URI.create("http://agent.example"))); - assertTrue(trustedHttpPolicy - .redirectedRequest( - source, - 302, - "http://agent.example/collect", - Map.of("Authorization", "Bearer secret")) - .isEmpty()); - assertTrue(RedirectHeaderPolicy.DEFAULT - .redirectedRequest( - URI.create("http://agent.example/send"), - 302, - "https://agent.example/collect", - Map.of()) - .isPresent()); + assertTrue( + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create("https://agent.example:8443/send"), + 302, + "http://agent.example:8443/collect", + Map.of()) + .isEmpty()); + RedirectHeaderPolicy trustedHttpPolicy = + new RedirectHeaderPolicy( + Set.of("Authorization"), Set.of(URI.create("http://agent.example"))); + assertTrue( + trustedHttpPolicy + .redirectedRequest( + source, + 302, + "http://agent.example/collect", + Map.of("Authorization", "Bearer secret")) + .isEmpty()); + assertTrue( + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create("http://agent.example/send"), + 302, + "https://agent.example/collect", + Map.of()) + .isPresent()); } @Test public void customPolicyRequiresValidTrustedOrigins() { - assertThrows(IllegalArgumentException.class, + assertThrows( + IllegalArgumentException.class, () -> new RedirectHeaderPolicy(Set.of("Authorization"), Set.of())); - for (String origin : Set.of( - "https://target.example/path", - "https://target.example?query=1", - "https://target.example#fragment", - "https://user@target.example", - "file:///target")) { - assertThrows(IllegalArgumentException.class, - () -> new RedirectHeaderPolicy(Set.of("Authorization"), Set.of(URI.create(origin))), + for (String origin : + Set.of( + "https://target.example/path", + "https://target.example?query=1", + "https://target.example#fragment", + "https://user@target.example", + "file:///target")) { + assertThrows( + IllegalArgumentException.class, + () -> + new RedirectHeaderPolicy( + Set.of("Authorization"), Set.of(URI.create(origin))), origin); } } @Test public void queryOnlyLocationRetainsSourcePath() { - RedirectHeaderPolicy.RedirectedRequest redirect = RedirectHeaderPolicy.DEFAULT - .redirectedRequest( - URI.create("http://host/rpc/send?old=1"), 302, "?new=2", Map.of()) - .orElseThrow(); + RedirectHeaderPolicy.RedirectedRequest redirect = + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create("http://host/rpc/send?old=1"), 302, "?new=2", Map.of()) + .orElseThrow(); assertEquals(URI.create("http://host/rpc/send?new=2"), redirect.target()); } @@ -167,56 +183,74 @@ public void resolvesRelativeAndProtocolRelativeLocations() { assertEquals( URI.create("https://host:3000/rpc/collect"), - RedirectHeaderPolicy.DEFAULT.redirectedRequest(source, 302, "collect", Map.of()) + RedirectHeaderPolicy.DEFAULT + .redirectedRequest(source, 302, "collect", Map.of()) .orElseThrow() .target()); assertEquals( URI.create("https://elsewhere.example/collect"), - RedirectHeaderPolicy.DEFAULT.redirectedRequest(source, 302, "//elsewhere.example/collect", Map.of()) + RedirectHeaderPolicy.DEFAULT + .redirectedRequest(source, 302, "//elsewhere.example/collect", Map.of()) .orElseThrow() .target()); } @Test public void crossOriginMethodPreservingRedirectsAreRejected() { - assertTrue(RedirectHeaderPolicy.DEFAULT - .redirectedRequest( - URI.create("https://origin.example/send"), - 307, - "https://target.example/collect", - Map.of()) - .isEmpty()); - assertTrue(RedirectHeaderPolicy.DEFAULT - .redirectedRequest( - URI.create("https://origin.example/send"), - 308, - "https://target.example/collect", - Map.of()) - .isEmpty()); + assertTrue( + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create("https://origin.example/send"), + 307, + "https://target.example/collect", + Map.of()) + .isEmpty()); + assertTrue( + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create("https://origin.example/send"), + 308, + "https://target.example/collect", + Map.of()) + .isEmpty()); } @Test public void normalizesOriginsAndRejectsMalformedOrUnsupportedRedirects() { - assertTrue(RedirectHeaderPolicy.sameOrigin( - URI.create("HTTP://HOST:80/rpc"), URI.create("http://host/collect"))); - assertFalse(RedirectHeaderPolicy.sameOrigin( - URI.create("http://host/rpc"), URI.create("https://host/rpc"))); - assertFalse(RedirectHeaderPolicy.sameOrigin( - URI.create("http://host/rpc"), URI.create("/collect"))); - assertTrue(RedirectHeaderPolicy.DEFAULT - .redirectedRequest(URI.create("http://host/rpc"), 302, "/pa th", Map.of()) - .isEmpty()); - assertTrue(RedirectHeaderPolicy.DEFAULT - .redirectedRequest(URI.create("http://host/rpc"), 302, "file:///tmp/collect", Map.of()) - .isEmpty()); - assertTrue(RedirectHeaderPolicy.DEFAULT - .redirectedRequest(URI.create("http://host/rpc"), 302, "mailto:agent@example.com", Map.of()) - .isEmpty()); - assertTrue(RedirectHeaderPolicy.DEFAULT - .redirectedRequest(URI.create("http://host/rpc"), 302, "http:/missing-host", Map.of()) - .isEmpty()); - assertTrue(RedirectHeaderPolicy.DEFAULT - .redirectedRequest(URI.create("http://host/rpc"), 300, "/collect", Map.of()) - .isEmpty()); + assertTrue( + RedirectHeaderPolicy.sameOrigin( + URI.create("HTTP://HOST:80/rpc"), URI.create("http://host/collect"))); + assertFalse( + RedirectHeaderPolicy.sameOrigin( + URI.create("http://host/rpc"), URI.create("https://host/rpc"))); + assertFalse( + RedirectHeaderPolicy.sameOrigin( + URI.create("http://host/rpc"), URI.create("/collect"))); + assertTrue( + RedirectHeaderPolicy.DEFAULT + .redirectedRequest(URI.create("http://host/rpc"), 302, "/pa th", Map.of()) + .isEmpty()); + assertTrue( + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create("http://host/rpc"), 302, "file:///tmp/collect", Map.of()) + .isEmpty()); + assertTrue( + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create("http://host/rpc"), + 302, + "mailto:agent@example.com", + Map.of()) + .isEmpty()); + assertTrue( + RedirectHeaderPolicy.DEFAULT + .redirectedRequest( + URI.create("http://host/rpc"), 302, "http:/missing-host", Map.of()) + .isEmpty()); + assertTrue( + RedirectHeaderPolicy.DEFAULT + .redirectedRequest(URI.create("http://host/rpc"), 300, "/collect", Map.of()) + .isEmpty()); } } diff --git a/http-client/src/test/java/org/a2aproject/sdk/client/http/ServerSentEventParserTest.java b/http-client/src/test/java/org/a2aproject/sdk/client/http/ServerSentEventParserTest.java index 5774e4b77..d0de4a707 100644 --- a/http-client/src/test/java/org/a2aproject/sdk/client/http/ServerSentEventParserTest.java +++ b/http-client/src/test/java/org/a2aproject/sdk/client/http/ServerSentEventParserTest.java @@ -145,7 +145,8 @@ public void testEmptyIdClearsLastEventId() { assertEquals(2, events.size()); assertEquals("initial", events.get(0).id()); assertEquals("", events.get(1).id(), "Empty id: should set currentEventId to empty string"); - assertEquals("", parser.getLastEventId(), "Empty id: should clear lastEventId to empty string"); + assertEquals( + "", parser.getLastEventId(), "Empty id: should clear lastEventId to empty string"); } @Test @@ -277,7 +278,10 @@ public void testEventTypeResetBetweenEvents() { assertEquals(2, events.size()); assertEquals("custom", events.get(0).eventType()); - assertEquals("message", events.get(1).eventType(), "Event type should reset to 'message' after dispatch"); + assertEquals( + "message", + events.get(1).eventType(), + "Event type should reset to 'message' after dispatch"); } @Test @@ -296,7 +300,10 @@ public void testIdPersistsAcrossEvents() { assertEquals(2, events.size()); assertEquals("100", events.get(0).id()); - assertEquals("100", events.get(1).id(), "ID should carry over to subsequent events per SSE spec"); + assertEquals( + "100", + events.get(1).id(), + "ID should carry over to subsequent events per SSE spec"); assertEquals("100", parser.getLastEventId(), "lastEventId should persist"); } @@ -428,7 +435,8 @@ public void testErrorConsumerCalledForBufferByteOverflow() { AtomicReference error = new AtomicReference<>(); ServerSentEventParser parser = new ServerSentEventParser(events::add, error::set); - // Each value is 65530 chars; 17 such lines (17 * 65530 = 1,114,010 chars) exceed the 1 MB buffer char limit. + // Each value is 65530 chars; 17 such lines (17 * 65530 = 1,114,010 chars) exceed the 1 MB + // buffer char limit. String bigValue = "x".repeat(65530); for (int i = 0; i < 17; i++) { parser.processLine("data: " + bigValue); @@ -517,12 +525,14 @@ public void testCRLFLineTerminatorsPreservedInValue() { public void testLargeJsonRpcResponseRejectedByOldLimit() { // Reproducer: a 70 KB payload exceeds the old 64 KB per-line limit but fits within the // new 1 MB default, proving the limit raise fixes real-world large JSON-RPC responses. - String hugeJson = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"" + "x".repeat(70_000) + "\"}"; + String hugeJson = + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"" + "x".repeat(70_000) + "\"}"; List events = new ArrayList<>(); List errors = new ArrayList<>(); SSEParserConfig oldLimit = SSEParserConfig.builder().maxLineLength(65536).build(); - ServerSentEventParser parser = new ServerSentEventParser(events::add, errors::add, oldLimit); + ServerSentEventParser parser = + new ServerSentEventParser(events::add, errors::add, oldLimit); parser.processLine("data: " + hugeJson); parser.processLine(""); @@ -548,7 +558,8 @@ public void testLargeJsonRpcResponseRejectedByOldLimit() { @Test public void testLargeJsonRpcResponseAcceptedByDefault() { // With the raised 1 MB default the same payload parses on a single line - String hugeJson = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"" + "x".repeat(70_000) + "\"}"; + String hugeJson = + "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"" + "x".repeat(70_000) + "\"}"; List events = new ArrayList<>(); List errors = new ArrayList<>(); @@ -582,10 +593,8 @@ public void testLargeSingleLineEventAcceptedByDefault() { public void testDisabledLineLengthCheck() { List events = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - SSEParserConfig config = SSEParserConfig.builder() - .maxLineLength(0) - .maxBufferChars(2 * 1024 * 1024) - .build(); + SSEParserConfig config = + SSEParserConfig.builder().maxLineLength(0).maxBufferChars(2 * 1024 * 1024).build(); ServerSentEventParser parser = new ServerSentEventParser(events::add, error::set, config); // With maxLineLength=0 (disabled), even very large lines are accepted @@ -602,9 +611,7 @@ public void testDisabledLineLengthCheck() { public void testCustomBufferLineLimit() { List events = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - SSEParserConfig config = SSEParserConfig.builder() - .maxBufferLines(5) - .build(); + SSEParserConfig config = SSEParserConfig.builder().maxBufferLines(5).build(); ServerSentEventParser parser = new ServerSentEventParser(events::add, error::set, config); for (int i = 0; i < 5; i++) { @@ -613,7 +620,9 @@ public void testCustomBufferLineLimit() { assertNull(error.get(), "No error expected at exactly the limit"); parser.processLine("data: overflow"); - assertNotNull(error.get(), "errorConsumer should be called when custom buffer line limit exceeded"); + assertNotNull( + error.get(), + "errorConsumer should be called when custom buffer line limit exceeded"); parser.processLine(""); assertEquals(0, events.size(), "Corrupted event block must not be dispatched"); } @@ -622,13 +631,13 @@ public void testCustomBufferLineLimit() { public void testCustomBufferCharLimit() { List events = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - SSEParserConfig config = SSEParserConfig.builder() - .maxBufferChars(100) - .build(); + SSEParserConfig config = SSEParserConfig.builder().maxBufferChars(100).build(); ServerSentEventParser parser = new ServerSentEventParser(events::add, error::set, config); parser.processLine("data: " + "x".repeat(101)); - assertNotNull(error.get(), "errorConsumer should be called when custom buffer char limit exceeded"); + assertNotNull( + error.get(), + "errorConsumer should be called when custom buffer char limit exceeded"); parser.processLine(""); assertEquals(0, events.size(), "Corrupted event block must not be dispatched"); } @@ -637,15 +646,15 @@ public void testCustomBufferCharLimit() { public void testParserRecoveryAfterCustomLimitViolation() { List events = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - SSEParserConfig config = SSEParserConfig.builder() - .maxBufferLines(2) - .build(); + SSEParserConfig config = SSEParserConfig.builder().maxBufferLines(2).build(); ServerSentEventParser parser = new ServerSentEventParser(events::add, error::set, config); parser.processLine("data: line0"); parser.processLine("data: line1"); parser.processLine("data: overflow"); - assertNotNull(error.get(), "errorConsumer should be called when custom buffer line limit exceeded"); + assertNotNull( + error.get(), + "errorConsumer should be called when custom buffer line limit exceeded"); parser.processLine(""); assertEquals(0, events.size(), "Corrupted event block must not be dispatched"); @@ -666,11 +675,12 @@ public void testSSEParserConfigDefaults() { @Test public void testSSEParserConfigBuilder() { - SSEParserConfig config = SSEParserConfig.builder() - .maxLineLength(500_000) - .maxBufferLines(2000) - .maxBufferChars(4 * 1024 * 1024) - .build(); + SSEParserConfig config = + SSEParserConfig.builder() + .maxLineLength(500_000) + .maxBufferLines(2000) + .maxBufferChars(4 * 1024 * 1024) + .build(); assertEquals(500_000, config.maxLineLength()); assertEquals(2000, config.maxBufferLines()); assertEquals(4 * 1024 * 1024, config.maxBufferChars()); @@ -678,16 +688,20 @@ public void testSSEParserConfigBuilder() { @Test public void testSSEParserConfigValidation() { - assertDoesNotThrow(() -> SSEParserConfig.builder().maxLineLength(0).build(), + assertDoesNotThrow( + () -> SSEParserConfig.builder().maxLineLength(0).build(), "maxLineLength=0 (disabled) should be allowed"); - assertThrows(IllegalArgumentException.class, + assertThrows( + IllegalArgumentException.class, () -> SSEParserConfig.builder().maxLineLength(-1).build()); - assertThrows(IllegalArgumentException.class, + assertThrows( + IllegalArgumentException.class, () -> SSEParserConfig.builder().maxBufferLines(0).build()); - assertThrows(IllegalArgumentException.class, + assertThrows( + IllegalArgumentException.class, () -> SSEParserConfig.builder().maxBufferChars(0).build()); } @@ -698,7 +712,8 @@ public void testLastEventIdNotAdvancedWhenBlockSkippedByLineTooLong() { // Regression: id: in a corrupt block must not update the reconnect cursor. List events = new ArrayList<>(); List errors = new ArrayList<>(); - // Limit long enough for "id: good-id" (11) and "data: ok" (8), but shorter than the oversized data line. + // Limit long enough for "id: good-id" (11) and "data: ok" (8), but shorter than the + // oversized data line. SSEParserConfig config = SSEParserConfig.builder().maxLineLength(50).build(); ServerSentEventParser parser = new ServerSentEventParser(events::add, errors::add, config); @@ -714,7 +729,10 @@ public void testLastEventIdNotAdvancedWhenBlockSkippedByLineTooLong() { parser.processLine("data: " + "x".repeat(51)); // triggers skip parser.processLine(""); // end of corrupt block assertEquals(1, events.size(), "Corrupt block must not be dispatched"); - assertEquals("good-id", parser.getLastEventId(), "lastEventId must not advance for a skipped block"); + assertEquals( + "good-id", + parser.getLastEventId(), + "lastEventId must not advance for a skipped block"); } @Test @@ -735,7 +753,10 @@ public void testLastEventIdNotAdvancedWhenBlockSkippedByBufferLineOverflow() { parser.processLine("data: overflow"); // triggers skip parser.processLine(""); assertEquals(1, events.size(), "Only the first good event should be dispatched"); - assertEquals("good-id", parser.getLastEventId(), "lastEventId must not advance for a skipped block"); + assertEquals( + "good-id", + parser.getLastEventId(), + "lastEventId must not advance for a skipped block"); } @Test @@ -763,8 +784,14 @@ public void testSkippedBlockIdDoesNotPoisonNextEventByLineTooLong() { parser.processLine(""); assertEquals(2, events.size()); - assertEquals("good", events.get(1).id(), "Next event must carry the pre-skip id, not the skipped block's id"); - assertEquals("good", parser.getLastEventId(), "lastEventId must not be poisoned by the skipped block"); + assertEquals( + "good", + events.get(1).id(), + "Next event must carry the pre-skip id, not the skipped block's id"); + assertEquals( + "good", + parser.getLastEventId(), + "lastEventId must not be poisoned by the skipped block"); } @Test @@ -789,8 +816,14 @@ public void testSkippedBlockIdDoesNotPoisonNextEventByBufferOverflow() { parser.processLine(""); assertEquals(2, events.size()); - assertEquals("good", events.get(1).id(), "Next event must carry the pre-skip id, not the skipped block's id"); - assertEquals("good", parser.getLastEventId(), "lastEventId must not be poisoned by the skipped block"); + assertEquals( + "good", + events.get(1).id(), + "Next event must carry the pre-skip id, not the skipped block's id"); + assertEquals( + "good", + parser.getLastEventId(), + "lastEventId must not be poisoned by the skipped block"); } @Test @@ -813,8 +846,14 @@ public void testSkippedBlockIdDoesNotPoisonNextEventByCharOverflow() { parser.processLine(""); assertEquals(2, events.size()); - assertEquals("good", events.get(1).id(), "Next event must carry the pre-skip id, not the skipped block's id"); - assertEquals("good", parser.getLastEventId(), "lastEventId must not be poisoned by the skipped block"); + assertEquals( + "good", + events.get(1).id(), + "Next event must carry the pre-skip id, not the skipped block's id"); + assertEquals( + "good", + parser.getLastEventId(), + "lastEventId must not be poisoned by the skipped block"); } @Test @@ -833,6 +872,9 @@ public void testLastEventIdNotAdvancedWhenBlockSkippedByBufferCharOverflow() { parser.processLine("data: " + "x".repeat(21)); // triggers skip parser.processLine(""); assertEquals(1, events.size(), "Only the first good event should be dispatched"); - assertEquals("good-id", parser.getLastEventId(), "lastEventId must not advance for a skipped block"); + assertEquals( + "good-id", + parser.getLastEventId(), + "lastEventId must not advance for a skipped block"); } } diff --git a/integrations/microprofile-config/pom.xml b/integrations/microprofile-config/pom.xml index f9deb65f7..63dd7f943 100644 --- a/integrations/microprofile-config/pom.xml +++ b/integrations/microprofile-config/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/integrations/microprofile-config/src/main/java/org/a2aproject/sdk/integrations/microprofile/MicroProfileConfigProvider.java b/integrations/microprofile-config/src/main/java/org/a2aproject/sdk/integrations/microprofile/MicroProfileConfigProvider.java index 9cae3962d..02a6c9a67 100644 --- a/integrations/microprofile-config/src/main/java/org/a2aproject/sdk/integrations/microprofile/MicroProfileConfigProvider.java +++ b/integrations/microprofile-config/src/main/java/org/a2aproject/sdk/integrations/microprofile/MicroProfileConfigProvider.java @@ -1,12 +1,10 @@ package org.a2aproject.sdk.integrations.microprofile; -import java.util.Optional; - +import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Alternative; -import jakarta.annotation.Priority; import jakarta.inject.Inject; - +import java.util.Optional; import org.a2aproject.sdk.server.config.A2AConfigProvider; import org.a2aproject.sdk.server.config.DefaultValuesConfigProvider; import org.eclipse.microprofile.config.Config; @@ -41,11 +39,9 @@ public class MicroProfileConfigProvider implements A2AConfigProvider { private static final Logger LOGGER = LoggerFactory.getLogger(MicroProfileConfigProvider.class); - @Inject - Config mpConfig; + @Inject Config mpConfig; - @Inject - DefaultValuesConfigProvider defaultValues; + @Inject DefaultValuesConfigProvider defaultValues; @Override public String getValue(String name) { @@ -57,7 +53,8 @@ public String getValue(String name) { // Fallback to defaults String defaultValue = defaultValues.getValue(name); - LOGGER.trace("Config value '{}' = '{}' (from DefaultValuesConfigProvider)", name, defaultValue); + LOGGER.trace( + "Config value '{}' = '{}' (from DefaultValuesConfigProvider)", name, defaultValue); return defaultValue; } @@ -65,14 +62,19 @@ public String getValue(String name) { public Optional getOptionalValue(String name) { Optional value = mpConfig.getOptionalValue(name, String.class); if (value.isPresent()) { - LOGGER.trace("Optional config value '{}' = '{}' (from MicroProfile Config)", name, value.get()); + LOGGER.trace( + "Optional config value '{}' = '{}' (from MicroProfile Config)", + name, + value.get()); return value; } // Fallback to defaults Optional defaultValue = defaultValues.getOptionalValue(name); - LOGGER.trace("Optional config value '{}' = '{}' (from DefaultValuesConfigProvider)", - name, defaultValue.orElse("")); + LOGGER.trace( + "Optional config value '{}' = '{}' (from DefaultValuesConfigProvider)", + name, + defaultValue.orElse("")); return defaultValue; } } diff --git a/integrations/microprofile-config/src/test/java/org/a2aproject/sdk/integrations/microprofile/MicroProfileConfigProviderTest.java b/integrations/microprofile-config/src/test/java/org/a2aproject/sdk/integrations/microprofile/MicroProfileConfigProviderTest.java index 3fc0e429f..b32834d58 100644 --- a/integrations/microprofile-config/src/test/java/org/a2aproject/sdk/integrations/microprofile/MicroProfileConfigProviderTest.java +++ b/integrations/microprofile-config/src/test/java/org/a2aproject/sdk/integrations/microprofile/MicroProfileConfigProviderTest.java @@ -6,12 +6,10 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.Optional; - +import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; - +import java.util.Optional; import org.a2aproject.sdk.server.config.A2AConfigProvider; -import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; /** @@ -27,14 +25,15 @@ public class MicroProfileConfigProviderTest { private static final String A2A_BLOCKING_RECONCILIATION_TIMEOUT_SECONDS = "a2a.blocking.reconciliation.timeout.seconds"; - @Inject - A2AConfigProvider configProvider; + @Inject A2AConfigProvider configProvider; @Test public void testIsMicroProfileConfigProvider() { // Verify that when microprofile-config module is on classpath, // the injected A2AConfigProvider is the MicroProfile implementation - assertInstanceOf(MicroProfileConfigProvider.class, configProvider, + assertInstanceOf( + MicroProfileConfigProvider.class, + configProvider, "A2AConfigProvider should be MicroProfileConfigProvider when module is present"); } @@ -43,15 +42,20 @@ public void testGetValueFromMicroProfileConfig() { // Test that values from application.properties override defaults // The test application.properties sets a2a.executor.core-pool-size=15 String value = configProvider.getValue(A2A_EXECUTOR_CORE_POOL_SIZE); - assertEquals("15", value, "Should get value from MicroProfile Config (application.properties)"); + assertEquals( + "15", value, "Should get value from MicroProfile Config (application.properties)"); } @Test public void testGetValueFallbackToDefaults() { - // Test that values not in application.properties fall back to META-INF/a2a-defaults.properties + // Test that values not in application.properties fall back to + // META-INF/a2a-defaults.properties // a2a.executor.max-pool-size is not in test application.properties, so should use default String value = configProvider.getValue(A2A_EXECUTOR_MAX_POOL_SIZE); - assertEquals("50", value, "Should fall back to default value from META-INF/a2a-defaults.properties"); + assertEquals( + "50", + value, + "Should fall back to default value from META-INF/a2a-defaults.properties"); } @Test @@ -93,7 +97,8 @@ public void testGetOptionalValueNotFound() { @Test public void testGetValueThrowsForNonExistent() { // Test that required getValue() throws for non-existent property - assertThrows(IllegalArgumentException.class, + assertThrows( + IllegalArgumentException.class, () -> configProvider.getValue("non.existent.property"), "Should throw IllegalArgumentException for non-existent required property"); } @@ -106,7 +111,9 @@ public void testSystemPropertyOverride() { try { System.setProperty("a2a.test.system.property", "from-system-property"); String value = configProvider.getValue("a2a.test.system.property"); - assertEquals("from-system-property", value, + assertEquals( + "from-system-property", + value, "System property should override application.properties"); } finally { if (originalValue != null) { diff --git a/itk/pom.xml b/itk/pom.xml index c2dbc0dae..d68df83e1 100644 --- a/itk/pom.xml +++ b/itk/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -15,6 +13,16 @@ Java SDK A2A ITK Agent Integration Test Kit agent for cross-SDK interoperability testing + + + ${project.basedir}/a2a-itk/protos + + @@ -98,16 +106,6 @@ - - - ${project.basedir}/a2a-itk/protos - - @@ -131,10 +129,10 @@ - generate-sources generate + generate-sources @@ -142,6 +140,9 @@ io.quarkus quarkus-maven-plugin true + + --add-opens=java.base/java.lang=ALL-UNNAMED + @@ -151,9 +152,6 @@ - - --add-opens=java.base/java.lang=ALL-UNNAMED - diff --git a/itk/src/main/java/org/a2aproject/sdk/itk/ActsAuth.java b/itk/src/main/java/org/a2aproject/sdk/itk/ActsAuth.java index 03a73770a..33f347db5 100644 --- a/itk/src/main/java/org/a2aproject/sdk/itk/ActsAuth.java +++ b/itk/src/main/java/org/a2aproject/sdk/itk/ActsAuth.java @@ -1,19 +1,17 @@ package org.a2aproject.sdk.itk; +import io.grpc.Metadata; +import io.grpc.Status; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; import java.util.List; import java.util.Map; - import org.a2aproject.sdk.spec.HTTPAuthSecurityScheme; import org.a2aproject.sdk.spec.SecurityRequirement; import org.a2aproject.sdk.spec.SecurityScheme; import org.jspecify.annotations.Nullable; -import io.grpc.Metadata; -import io.grpc.Status; -import io.vertx.core.json.JsonArray; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.RoutingContext; - /** * ACTS authentication support. * @@ -54,8 +52,7 @@ final class ActsAuth { private static final String EXTENDED_CARD_SUFFIX = "/extendedAgentCard"; - private ActsAuth() { - } + private ActsAuth() {} static boolean enforced() { String value = System.getenv("ITK_ACTS_AUTH"); @@ -74,7 +71,8 @@ static boolean enforced() { } return Map.of( SCHEME_ID, - new HTTPAuthSecurityScheme("opaque", "Bearer", "Bearer token presented by the ACTS runner.")); + new HTTPAuthSecurityScheme( + "opaque", "Bearer", "Bearer token presented by the ACTS runner.")); } /** @@ -134,7 +132,8 @@ static void guard(RoutingContext ctx) { switch (presented(ctx.request().getHeader("Authorization"))) { case VALID -> ctx.next(); - case INSUFFICIENT -> reject(ctx, 403, "PERMISSION_DENIED", "Token lacks the required scope."); + case INSUFFICIENT -> + reject(ctx, 403, "PERMISSION_DENIED", "Token lacks the required scope."); case UNUSABLE -> reject(ctx, 401, "UNAUTHENTICATED", "A bearer token is required."); } } @@ -149,31 +148,38 @@ static void guard(RoutingContext ctx) { if (!enforced()) { return null; } - String header = metadata.get(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER)); + String header = + metadata.get(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER)); return switch (presented(header)) { case VALID -> null; - case INSUFFICIENT -> Status.PERMISSION_DENIED.withDescription("Token lacks the required scope."); + case INSUFFICIENT -> + Status.PERMISSION_DENIED.withDescription("Token lacks the required scope."); case UNUSABLE -> Status.UNAUTHENTICATED.withDescription("A bearer token is required."); }; } /** Answers with the {@code google.rpc.Status} shape A2A §11.6 requires of an error. */ private static void reject(RoutingContext ctx, int status, String reason, String message) { - JsonObject errorInfo = new JsonObject() - .put("@type", "type.googleapis.com/google.rpc.ErrorInfo") - .put("reason", reason) - .put("domain", "a2a-protocol.org"); - JsonObject body = new JsonObject() - .put( - "error", - new JsonObject() - .put("code", status) - .put("status", reason) - .put("message", message) - .put("details", new JsonArray().add(errorInfo))); + JsonObject errorInfo = + new JsonObject() + .put("@type", "type.googleapis.com/google.rpc.ErrorInfo") + .put("reason", reason) + .put("domain", "a2a-protocol.org"); + JsonObject body = + new JsonObject() + .put( + "error", + new JsonObject() + .put("code", status) + .put("status", reason) + .put("message", message) + .put("details", new JsonArray().add(errorInfo))); if (status == 401) { - ctx.response().putHeader("WWW-Authenticate", "Bearer realm=\"a2a\", scheme=\"" + SCHEME_ID + "\""); + ctx.response() + .putHeader( + "WWW-Authenticate", + "Bearer realm=\"a2a\", scheme=\"" + SCHEME_ID + "\""); } ctx.response() .setStatusCode(status) diff --git a/itk/src/main/java/org/a2aproject/sdk/itk/ActsAuthInterceptor.java b/itk/src/main/java/org/a2aproject/sdk/itk/ActsAuthInterceptor.java index 5d8c6f8dd..820fb6488 100644 --- a/itk/src/main/java/org/a2aproject/sdk/itk/ActsAuthInterceptor.java +++ b/itk/src/main/java/org/a2aproject/sdk/itk/ActsAuthInterceptor.java @@ -1,15 +1,13 @@ package org.a2aproject.sdk.itk; -import jakarta.enterprise.context.ApplicationScoped; - -import org.jspecify.annotations.Nullable; - import io.grpc.Metadata; import io.grpc.ServerCall; import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.Status; import io.quarkus.grpc.GlobalInterceptor; +import jakarta.enterprise.context.ApplicationScoped; +import org.jspecify.annotations.Nullable; /** * Applies the ACTS credential rule to every gRPC call. @@ -28,8 +26,7 @@ public ServerCall.Listener interceptCall( @Nullable Status rejection = ActsAuth.grpcRejection(metadata); if (rejection != null) { call.close(rejection, new Metadata()); - return new ServerCall.Listener<>() { - }; + return new ServerCall.Listener<>() {}; } return next.startCall(call, metadata); } diff --git a/itk/src/main/java/org/a2aproject/sdk/itk/ActsBehaviors.java b/itk/src/main/java/org/a2aproject/sdk/itk/ActsBehaviors.java index c99a6523e..ac27edf61 100644 --- a/itk/src/main/java/org/a2aproject/sdk/itk/ActsBehaviors.java +++ b/itk/src/main/java/org/a2aproject/sdk/itk/ActsBehaviors.java @@ -11,7 +11,6 @@ import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; - import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; import org.a2aproject.sdk.spec.DataPart; @@ -55,12 +54,13 @@ final class ActsBehaviors { * {@code sut-behaviors.yaml}'s {@code terminal_state} key, though two of them are interrupted * states rather than terminal ones. */ - private static final Map TERMINAL_STATES = Map.of( - "tck-complete-task", TaskState.TASK_STATE_COMPLETED, - "tck-task-failure", TaskState.TASK_STATE_FAILED, - "tck-reject-task", TaskState.TASK_STATE_REJECTED, - "tck-input-required", TaskState.TASK_STATE_INPUT_REQUIRED, - "tck-auth-required", TaskState.TASK_STATE_AUTH_REQUIRED); + private static final Map TERMINAL_STATES = + Map.of( + "tck-complete-task", TaskState.TASK_STATE_COMPLETED, + "tck-task-failure", TaskState.TASK_STATE_FAILED, + "tck-reject-task", TaskState.TASK_STATE_REJECTED, + "tck-input-required", TaskState.TASK_STATE_INPUT_REQUIRED, + "tck-auth-required", TaskState.TASK_STATE_AUTH_REQUIRED); private static final String MULTI_TURN_DONE = "done"; @@ -225,7 +225,10 @@ void released(String taskId) { /** Fails the task rather than completing it: a silent success would report conformance the agent never demonstrated. */ private void unimplemented(AgentEmitter emitter, String behavior) { - complete(emitter, TaskState.TASK_STATE_FAILED, "unimplemented ACTS behaviour \"" + behavior + "\""); + complete( + emitter, + TaskState.TASK_STATE_FAILED, + "unimplemented ACTS behaviour \"" + behavior + "\""); } private void complete(AgentEmitter emitter, TaskState state, String text) { @@ -277,7 +280,8 @@ private void longRunning(AgentEmitter emitter) { // CORE-EXEC-001 polls to completion and then asserts the finished task carries at least one // artifact, so the work has to leave one behind even though §11.2 describes this behaviour // only as delayed completion. - emitter.addArtifact(List.of(new TextPart("long running result")), null, "long-running", null); + emitter.addArtifact( + List.of(new TextPart("long running result")), null, "long-running", null); complete(emitter, TaskState.TASK_STATE_COMPLETED, "long running work finished"); } @@ -285,10 +289,20 @@ private List> artifactParts(String behavior) { return switch (behavior) { case "tck-artifact-text" -> List.of(new TextPart("generated text content")); case "tck-artifact-data" -> List.of(new DataPart(dataArtifact())); - case "tck-artifact-file" -> List.of(new FilePart(new FileWithBytes( - "text/plain", "document.txt", "file bytes".getBytes(StandardCharsets.UTF_8)))); - case "tck-artifact-file-url" -> List.of(new FilePart(new FileWithUri( - "text/plain", "document.txt", "https://example.com/document.txt"))); + case "tck-artifact-file" -> + List.of( + new FilePart( + new FileWithBytes( + "text/plain", + "document.txt", + "file bytes".getBytes(StandardCharsets.UTF_8)))); + case "tck-artifact-file-url" -> + List.of( + new FilePart( + new FileWithUri( + "text/plain", + "document.txt", + "https://example.com/document.txt"))); default -> null; }; } @@ -305,7 +319,8 @@ private Map dataArtifact() { * frame; a single combined update would satisfy {@code min_count} only by accident. */ private void stream(AgentEmitter emitter, String behavior) { - emitter.startWork(emitter.newAgentMessage(List.of(new TextPart("streaming started")), null)); + emitter.startWork( + emitter.newAgentMessage(List.of(new TextPart("streaming started")), null)); if ("tck-stream-chunked".equals(behavior)) { List chunks = List.of("chunk one ", "chunk two ", "chunk three"); @@ -313,11 +328,17 @@ private void stream(AgentEmitter emitter, String behavior) { for (int i = 0; i < chunks.size(); i++) { // The first chunk goes out with append=false; an update naming an artifact the task // has not seen yet has nothing to append to. - emitter.addArtifact(List.of(new TextPart(chunks.get(i))), artifactId, "chunked", null, - i > 0, i == chunks.size() - 1); + emitter.addArtifact( + List.of(new TextPart(chunks.get(i))), + artifactId, + "chunked", + null, + i > 0, + i == chunks.size() - 1); } } else { - emitter.addArtifact(List.of(new TextPart("streamed content")), null, "streamed", null, false, true); + emitter.addArtifact( + List.of(new TextPart("streamed content")), null, "streamed", null, false, true); } complete(emitter, TaskState.TASK_STATE_COMPLETED, behavior + " ok"); diff --git a/itk/src/main/java/org/a2aproject/sdk/itk/ActsClientParse.java b/itk/src/main/java/org/a2aproject/sdk/itk/ActsClientParse.java index 317651957..a8b8b425f 100644 --- a/itk/src/main/java/org/a2aproject/sdk/itk/ActsClientParse.java +++ b/itk/src/main/java/org/a2aproject/sdk/itk/ActsClientParse.java @@ -1,5 +1,8 @@ package org.a2aproject.sdk.itk; +import com.google.gson.Gson; +import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.util.JsonFormat; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -7,11 +10,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; - -import com.google.gson.Gson; -import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.util.JsonFormat; - import org.a2aproject.sdk.A2A; import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientEvent; @@ -80,13 +78,17 @@ final class ActsClientParse { * without it a capability the client correctly parsed as {@code false} vanishes from the * rendering, and CLIENT-CAP-001 asserts on exactly that. */ - private static final JsonFormat.Printer PRINTER = JsonFormat.printer().alwaysPrintFieldsWithNoPresence(); + private static final JsonFormat.Printer PRINTER = + JsonFormat.printer().alwaysPrintFieldsWithNoPresence(); void run(RequestContext context, AgentEmitter emitter) { Map request = requestIn(context.getMessage()); if (request == null) { - emitter.updateStatus(TaskState.TASK_STATE_FAILED, emitter.newAgentMessage( - List.of(new TextPart(BEHAVIOR + " needs {operation, wire_payload}")), null)); + emitter.updateStatus( + TaskState.TASK_STATE_FAILED, + emitter.newAgentMessage( + List.of(new TextPart(BEHAVIOR + " needs {operation, wire_payload}")), + null)); return; } @@ -96,7 +98,8 @@ void run(RequestContext context, AgentEmitter emitter) { Map parsed = parse(operation, payload); emitter.addArtifact(List.of(new DataPart(parsed)), null, BEHAVIOR, null); - emitter.updateStatus(TaskState.TASK_STATE_COMPLETED, + emitter.updateStatus( + TaskState.TASK_STATE_COMPLETED, emitter.newAgentMessage(List.of(new TextPart(operation + " parsed")), null)); } @@ -106,7 +109,8 @@ private Map requestIn(Message message) { return null; } for (Part part : message.parts()) { - if (part instanceof DataPart dataPart && dataPart.data() instanceof Map data + if (part instanceof DataPart dataPart + && dataPart.data() instanceof Map data && data.get("operation") instanceof String) { return (Map) data; } @@ -146,8 +150,10 @@ private Map parse(String operation, String payload) { // corpus writes them — correctly, since a card is fetched over plain HTTP on every // binding, so there is no envelope to unwrap. case "get_agent_card" -> wire(ProtoUtils.ToProto.agentCard(parseCard(payload))); - case "get_extended_agent_card" -> wire(ProtoUtils.ToProto.agentCard(parseExtendedCard(payload))); - case "send_message" -> wire(ProtoUtils.ToProto.taskOrMessage(parseSendMessage(payload))); + case "get_extended_agent_card" -> + wire(ProtoUtils.ToProto.agentCard(parseExtendedCard(payload))); + case "send_message" -> + wire(ProtoUtils.ToProto.taskOrMessage(parseSendMessage(payload))); case "get_task" -> wire(ProtoUtils.ToProto.task(parseGetTask(payload))); default -> error("unsupported client operation \"" + operation + "\"", null); }; @@ -204,8 +210,10 @@ private Task parseGetTask(String payload) throws Exception { private Client client(String payload, AtomicReference captured) throws Exception { return Client.builder(syntheticCard()) .clientConfig(new ClientConfig.Builder().setStreaming(false).build()) - .withTransport(JSONRPCTransport.class, - new JSONRPCTransportConfigBuilder().httpClient(new FixedHttpClient(payload))) + .withTransport( + JSONRPCTransport.class, + new JSONRPCTransportConfigBuilder() + .httpClient(new FixedHttpClient(payload))) .addConsumer((event, card) -> captured.set(event)) .build(); } @@ -226,7 +234,9 @@ private AgentCard syntheticCard() { private boolean isEnveloped(String payload) { Map body = envelopeOf(payload); return body != null - && (body.containsKey("jsonrpc") || body.containsKey("result") || body.containsKey("error")); + && (body.containsKey("jsonrpc") + || body.containsKey("result") + || body.containsKey("error")); } @SuppressWarnings("unchecked") @@ -349,7 +359,8 @@ public String body() { /** No §10 payload is a stream; a client that asks for one has taken a path the test did not intend. */ private CompletableFuture unsupportedStream(Consumer errorConsumer) { - IllegalStateException failure = new IllegalStateException("ACTS client parse serves no stream"); + IllegalStateException failure = + new IllegalStateException("ACTS client parse serves no stream"); errorConsumer.accept(failure); return CompletableFuture.failedFuture(failure); } @@ -376,9 +387,10 @@ public A2AHttpResponse get() { } @Override - public CompletableFuture getAsyncSSE(Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) { + public CompletableFuture getAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) { return unsupportedStream(errorConsumer); } } @@ -413,9 +425,10 @@ public A2AHttpResponse post() { } @Override - public CompletableFuture postAsyncSSE(Consumer messageConsumer, - Consumer errorConsumer, - Runnable completeRunnable) { + public CompletableFuture postAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) { return unsupportedStream(errorConsumer); } } diff --git a/itk/src/main/java/org/a2aproject/sdk/itk/AdditionalRoutes.java b/itk/src/main/java/org/a2aproject/sdk/itk/AdditionalRoutes.java index a853f186f..94536e843 100644 --- a/itk/src/main/java/org/a2aproject/sdk/itk/AdditionalRoutes.java +++ b/itk/src/main/java/org/a2aproject/sdk/itk/AdditionalRoutes.java @@ -17,27 +17,37 @@ void addPrefixedRoutes(@Observes Router router) { router.route().order(Integer.MIN_VALUE + 1).handler(ActsAuth::guard); router.get("/jsonrpc/.well-known/agent-card.json") - .handler(ctx -> { - LOGGER.info("Rerouting GET /jsonrpc/.well-known/agent-card.json -> /.well-known/agent-card.json"); - ctx.reroute("/.well-known/agent-card.json"); - }); + .handler( + ctx -> { + LOGGER.info( + "Rerouting GET /jsonrpc/.well-known/agent-card.json ->" + + " /.well-known/agent-card.json"); + ctx.reroute("/.well-known/agent-card.json"); + }); - router.route("/jsonrpc/*").handler(ctx -> { - LOGGER.info("Rerouting /jsonrpc -> /"); - ctx.reroute("/"); - }); - router.route("/rest/*").handler(ctx -> { - LOGGER.info("Rerouting /rest -> /"); - ctx.reroute("/"); - }); + router.route("/jsonrpc/*") + .handler( + ctx -> { + LOGGER.info("Rerouting /jsonrpc -> /"); + ctx.reroute("/"); + }); + router.route("/rest/*") + .handler( + ctx -> { + LOGGER.info("Rerouting /rest -> /"); + ctx.reroute("/"); + }); - router.route().order(Integer.MIN_VALUE) - .handler(ctx -> { - String method = ctx.request().method().name(); - String path = ctx.request().path(); - String contentType = ctx.request().getHeader("Content-Type"); - LOGGER.info("Incoming {} {} Content-Type={}", method, path, contentType); - ctx.next(); - }); + router.route() + .order(Integer.MIN_VALUE) + .handler( + ctx -> { + String method = ctx.request().method().name(); + String path = ctx.request().path(); + String contentType = ctx.request().getHeader("Content-Type"); + LOGGER.info( + "Incoming {} {} Content-Type={}", method, path, contentType); + ctx.next(); + }); } } diff --git a/itk/src/main/java/org/a2aproject/sdk/itk/AgentCardProducer.java b/itk/src/main/java/org/a2aproject/sdk/itk/AgentCardProducer.java index ce5e67d9c..240700015 100644 --- a/itk/src/main/java/org/a2aproject/sdk/itk/AgentCardProducer.java +++ b/itk/src/main/java/org/a2aproject/sdk/itk/AgentCardProducer.java @@ -1,11 +1,10 @@ package org.a2aproject.sdk.itk; -import java.util.List; -import java.util.Map; - +import io.quarkus.arc.DefaultBean; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - +import java.util.List; +import java.util.Map; import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; import org.a2aproject.sdk.server.ExtendedAgentCard; @@ -18,8 +17,6 @@ import org.a2aproject.sdk.spec.SecurityScheme; import org.eclipse.microprofile.config.inject.ConfigProperty; -import io.quarkus.arc.DefaultBean; - @ApplicationScoped public class AgentCardProducer { @@ -51,26 +48,30 @@ private AgentCapabilities capabilities() { @ExtendedAgentCard public AgentCard agentCard() { String url = "http://127.0.0.1:" + httpPort; - List interfaces = List.of( - new AgentInterface("JSONRPC", url), - new AgentInterface("HTTP+JSON", url), - new AgentInterface("GRPC", "127.0.0.1:" + grpcPort)); + List interfaces = + List.of( + new AgentInterface("JSONRPC", url), + new AgentInterface("HTTP+JSON", url), + new AgentInterface("GRPC", "127.0.0.1:" + grpcPort)); - AgentCard.Builder builder = AgentCard.builder() - .name("ITK Current Agent") - .description("Java agent using A2A SDK (current source).") - .version("1.0.0") - .supportedInterfaces(interfaces) - .capabilities(capabilities()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of(AgentSkill.builder() - .id("itk_current") - .name("ITK Current") - .description("Processes ITK instruction traversals") - .tags(List.of("itk")) - .examples(List.of()) - .build())); + AgentCard.Builder builder = + AgentCard.builder() + .name("ITK Current Agent") + .description("Java agent using A2A SDK (current source).") + .version("1.0.0") + .supportedInterfaces(interfaces) + .capabilities(capabilities()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills( + List.of( + AgentSkill.builder() + .id("itk_current") + .name("ITK Current") + .description("Processes ITK instruction traversals") + .tags(List.of("itk")) + .examples(List.of()) + .build())); // Authentication is not a capability — AgentCapabilities has no member for it — so it is // declared at the top level, where the ACTS authentication precondition reads both of diff --git a/itk/src/main/java/org/a2aproject/sdk/itk/AgentExecutorProducer.java b/itk/src/main/java/org/a2aproject/sdk/itk/AgentExecutorProducer.java index 695281ae3..2de9cf897 100644 --- a/itk/src/main/java/org/a2aproject/sdk/itk/AgentExecutorProducer.java +++ b/itk/src/main/java/org/a2aproject/sdk/itk/AgentExecutorProducer.java @@ -1,5 +1,10 @@ package org.a2aproject.sdk.itk; +import io.grpc.ManagedChannelBuilder; +import itk.InstructionOuterClass.CallAgent; +import itk.InstructionOuterClass.Instruction; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; import java.util.ArrayList; import java.util.Base64; import java.util.Collections; @@ -9,10 +14,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Produces; - import org.a2aproject.sdk.A2A; import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientEvent; @@ -27,8 +28,8 @@ import org.a2aproject.sdk.client.transport.rest.RestTransport; import org.a2aproject.sdk.client.transport.rest.RestTransportConfigBuilder; import org.a2aproject.sdk.compat03.A2A_v0_3; -import org.a2aproject.sdk.compat03.client.Client_v0_3; import org.a2aproject.sdk.compat03.client.ClientEvent_v0_3; +import org.a2aproject.sdk.compat03.client.Client_v0_3; import org.a2aproject.sdk.compat03.client.MessageEvent_v0_3; import org.a2aproject.sdk.compat03.client.TaskEvent_v0_3; import org.a2aproject.sdk.compat03.client.TaskUpdateEvent_v0_3; @@ -61,11 +62,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.grpc.ManagedChannelBuilder; - -import itk.InstructionOuterClass.CallAgent; -import itk.InstructionOuterClass.Instruction; - @ApplicationScoped public class AgentExecutorProducer { @@ -115,12 +111,15 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro if (shouldHold(instruction)) { LOGGER.info("Holding task {} as requested", emitter.getTaskId()); - Message holdMsg = emitter.newAgentMessage( - List.of(new TextPart(response + "\ntask-finished")), null); + Message holdMsg = + emitter.newAgentMessage( + List.of(new TextPart(response + "\ntask-finished")), null); emitter.updateStatus(TaskState.TASK_STATE_WORKING, holdMsg); for (int i = 0; i < HOLD_ITERATIONS; i++) { - LOGGER.info("Emitting periodic status update for held task {}", emitter.getTaskId()); + LOGGER.info( + "Emitting periodic status update for held task {}", + emitter.getTaskId()); try { Thread.sleep(HOLD_INTERVAL_MS); } catch (InterruptedException e) { @@ -130,12 +129,13 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro } } LOGGER.info("Held task {} timed out, auto-completing", emitter.getTaskId()); - Message completeMsg = emitter.newAgentMessage( - List.of(new TextPart(response + "\ntask-finished")), null); + Message completeMsg = + emitter.newAgentMessage( + List.of(new TextPart(response + "\ntask-finished")), null); emitter.complete(completeMsg); } else { - Message completeMsg = emitter.newAgentMessage( - List.of(new TextPart(response)), null); + Message completeMsg = + emitter.newAgentMessage(List.of(new TextPart(response)), null); emitter.complete(completeMsg); } } catch (TimeoutException e) { @@ -143,15 +143,21 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro emitter.fail(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - LOGGER.error("Task {} interrupted while calling remote agent", emitter.getTaskId(), e); + LOGGER.error( + "Task {} interrupted while calling remote agent", emitter.getTaskId(), e); emitter.fail(); } catch (ExecutionException e) { - LOGGER.error("Remote agent call failed for task {}: {}", - emitter.getTaskId(), e.getCause().getMessage(), e.getCause()); + LOGGER.error( + "Remote agent call failed for task {}: {}", + emitter.getTaskId(), + e.getCause().getMessage(), + e.getCause()); emitter.fail(); } catch (Exception e) { - LOGGER.error("Unexpected error handling instruction for task {}", - emitter.getTaskId(), e); + LOGGER.error( + "Unexpected error handling instruction for task {}", + emitter.getTaskId(), + e); emitter.fail(); } } @@ -220,18 +226,22 @@ private List handleCallAgent(CallAgent call) throws Exception { AgentCard remoteCard = A2A.getAgentCard(call.getAgentCardUri()); if (remoteCard.supportedInterfaces().isEmpty()) { - LOGGER.info("Agent {} has no supportedInterfaces, falling back to v0.3 client", call.getAgentCardUri()); + LOGGER.info( + "Agent {} has no supportedInterfaces, falling back to v0.3 client", + call.getAgentCardUri()); return handleCallAgentV03(call); } - ClientConfig.Builder configBuilder = new ClientConfig.Builder() - .setStreaming(call.getStreaming() || isGrpc(call.getTransport())) - .setUseClientPreference(true); + ClientConfig.Builder configBuilder = + new ClientConfig.Builder() + .setStreaming(call.getStreaming() || isGrpc(call.getTransport())) + .setUseClientPreference(true); if (call.hasPushNotification()) { String url = call.getPushNotification().getUrl(); if (url.isEmpty()) { - throw new IllegalArgumentException("URL not specified in push_notification behavior"); + throw new IllegalArgumentException( + "URL not specified in push_notification behavior"); } if (!url.startsWith("http://") && !url.startsWith("https://")) { url = "http://" + url; @@ -244,45 +254,55 @@ private List handleCallAgent(CallAgent call) throws Exception { .build()); } - var clientBuilder = Client.builder(remoteCard) - .clientConfig(configBuilder.build()); + var clientBuilder = Client.builder(remoteCard).clientConfig(configBuilder.build()); addTransport(clientBuilder, call.getTransport()); byte[] instBytes = call.getInstruction().toByteArray(); - Message wrappedMsg = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new FilePart( - new FileWithBytes("application/x-protobuf", "instruction.bin", instBytes)))) - .build(); + Message wrappedMsg = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts( + List.of( + new FilePart( + new FileWithBytes( + "application/x-protobuf", + "instruction.bin", + instBytes)))) + .build(); CompletableFuture> resultFuture = new CompletableFuture<>(); List responses = Collections.synchronizedList(new ArrayList<>()); - clientBuilder.addConsumer((event, card) -> { - List texts = extractTextFromEvent(event); - if (call.hasResubscribe()) { - String finished = findTaskFinishedText(texts); - if (finished != null) { - responses.add(finished); - if (!resultFuture.isDone()) { + clientBuilder.addConsumer( + (event, card) -> { + List texts = extractTextFromEvent(event); + if (call.hasResubscribe()) { + String finished = findTaskFinishedText(texts); + if (finished != null) { + responses.add(finished); + if (!resultFuture.isDone()) { + resultFuture.complete(responses); + } + return; + } + } + responses.addAll(texts); + if (!resultFuture.isDone() && isTerminalEvent(event)) { resultFuture.complete(responses); } - return; - } - } - responses.addAll(texts); - if (!resultFuture.isDone() && isTerminalEvent(event)) { - resultFuture.complete(responses); - } - }); - clientBuilder.streamingErrorHandler(error -> { - LOGGER.error("Streaming error calling {} via {}", - call.getAgentCardUri(), call.getTransport(), error); - if (!resultFuture.isDone()) { - resultFuture.completeExceptionally(error); - } - }); + }); + clientBuilder.streamingErrorHandler( + error -> { + LOGGER.error( + "Streaming error calling {} via {}", + call.getAgentCardUri(), + call.getTransport(), + error); + if (!resultFuture.isDone()) { + resultFuture.completeExceptionally(error); + } + }); try (Client client = clientBuilder.build()) { client.sendMessage(wrappedMsg, (TaskPushNotificationConfig) null, null, null); @@ -294,66 +314,79 @@ private List handleCallAgent(CallAgent call) throws Exception { private List handleCallAgentV03(CallAgent call) throws Exception { AgentCard_v0_3 v03Card = A2A_v0_3.getAgentCard(call.getAgentCardUri()); - ClientConfig_v0_3.Builder configBuilder = new ClientConfig_v0_3.Builder() - .setStreaming(call.getStreaming() || isGrpc(call.getTransport())) - .setUseClientPreference(true); + ClientConfig_v0_3.Builder configBuilder = + new ClientConfig_v0_3.Builder() + .setStreaming(call.getStreaming() || isGrpc(call.getTransport())) + .setUseClientPreference(true); PushNotificationConfig_v0_3 pushConfig = null; if (call.hasPushNotification()) { String url = call.getPushNotification().getUrl(); if (url.isEmpty()) { - throw new IllegalArgumentException("URL not specified in push_notification behavior"); + throw new IllegalArgumentException( + "URL not specified in push_notification behavior"); } if (!url.startsWith("http://") && !url.startsWith("https://")) { url = "http://" + url; } - pushConfig = new PushNotificationConfig_v0_3.Builder() - .id(UUID.randomUUID().toString()) - .url(url + "/notifications") - .token("itk-token") - .build(); + pushConfig = + new PushNotificationConfig_v0_3.Builder() + .id(UUID.randomUUID().toString()) + .url(url + "/notifications") + .token("itk-token") + .build(); configBuilder.setPushNotificationConfig(pushConfig); } - var clientBuilder = Client_v0_3.builder(v03Card) - .clientConfig(configBuilder.build()); + var clientBuilder = Client_v0_3.builder(v03Card).clientConfig(configBuilder.build()); addTransportV03(clientBuilder, call.getTransport()); byte[] instBytes = call.getInstruction().toByteArray(); String instBase64 = Base64.getEncoder().encodeToString(instBytes); - Message_v0_3 wrappedMsg = new Message_v0_3.Builder() - .role(Message_v0_3.Role.USER) - .parts(List.of(new FilePart_v0_3( - new FileWithBytes_v0_3("application/x-protobuf", "instruction.bin", instBase64)))) - .build(); + Message_v0_3 wrappedMsg = + new Message_v0_3.Builder() + .role(Message_v0_3.Role.USER) + .parts( + List.of( + new FilePart_v0_3( + new FileWithBytes_v0_3( + "application/x-protobuf", + "instruction.bin", + instBase64)))) + .build(); CompletableFuture> resultFuture = new CompletableFuture<>(); List responses = Collections.synchronizedList(new ArrayList<>()); - clientBuilder.addConsumer((event, card) -> { - List texts = extractTextFromEventV03(event); - if (call.hasResubscribe()) { - String finished = findTaskFinishedText(texts); - if (finished != null) { - responses.add(finished); - if (!resultFuture.isDone()) { + clientBuilder.addConsumer( + (event, card) -> { + List texts = extractTextFromEventV03(event); + if (call.hasResubscribe()) { + String finished = findTaskFinishedText(texts); + if (finished != null) { + responses.add(finished); + if (!resultFuture.isDone()) { + resultFuture.complete(responses); + } + return; + } + } + responses.addAll(texts); + if (!resultFuture.isDone() && isTerminalEventV03(event)) { resultFuture.complete(responses); } - return; - } - } - responses.addAll(texts); - if (!resultFuture.isDone() && isTerminalEventV03(event)) { - resultFuture.complete(responses); - } - }); - clientBuilder.streamingErrorHandler(error -> { - LOGGER.error("Streaming error calling v0.3 agent {}", call.getAgentCardUri(), error); - if (!resultFuture.isDone()) { - resultFuture.completeExceptionally(error); - } - }); + }); + clientBuilder.streamingErrorHandler( + error -> { + LOGGER.error( + "Streaming error calling v0.3 agent {}", + call.getAgentCardUri(), + error); + if (!resultFuture.isDone()) { + resultFuture.completeExceptionally(error); + } + }); Client_v0_3 client = clientBuilder.build(); try { @@ -383,11 +416,15 @@ private List extractTextFromEvent(ClientEvent event) { if (event instanceof MessageEvent me) { message = me.getMessage(); } else if (event instanceof TaskEvent te) { - if (te.getTask() != null && te.getTask().status() != null && te.getTask().status().message() != null) { + if (te.getTask() != null + && te.getTask().status() != null + && te.getTask().status().message() != null) { message = te.getTask().status().message(); } } else if (event instanceof TaskUpdateEvent tue) { - if (tue.getTask() != null && tue.getTask().status() != null && tue.getTask().status().message() != null) { + if (tue.getTask() != null + && tue.getTask().status() != null + && tue.getTask().status().message() != null) { message = tue.getTask().status().message(); } } @@ -423,10 +460,14 @@ private boolean isTerminalEvent(ClientEvent event) { return true; } if (event instanceof TaskEvent te) { - return te.getTask() != null && te.getTask().status() != null && te.getTask().status().state().isFinal(); + return te.getTask() != null + && te.getTask().status() != null + && te.getTask().status().state().isFinal(); } if (event instanceof TaskUpdateEvent tue) { - return tue.getTask() != null && tue.getTask().status() != null && tue.getTask().status().state().isFinal(); + return tue.getTask() != null + && tue.getTask().status() != null + && tue.getTask().status().state().isFinal(); } return false; } @@ -439,7 +480,9 @@ private List extractTextFromMessageV03(Message_v0_3 message) { List texts = new ArrayList<>(); if (message != null && message.parts() != null) { for (Part_v0_3 part : message.parts()) { - if (part instanceof TextPart_v0_3 tp && tp.text() != null && !tp.text().isEmpty()) { + if (part instanceof TextPart_v0_3 tp + && tp.text() != null + && !tp.text().isEmpty()) { texts.add(tp.text()); } } @@ -453,11 +496,15 @@ private List extractTextFromEventV03(ClientEvent_v0_3 event) { if (event instanceof MessageEvent_v0_3 me) { message = me.getMessage(); } else if (event instanceof TaskEvent_v0_3 te) { - if (te.getTask() != null && te.getTask().status() != null && te.getTask().status().message() != null) { + if (te.getTask() != null + && te.getTask().status() != null + && te.getTask().status().message() != null) { message = te.getTask().status().message(); } } else if (event instanceof TaskUpdateEvent_v0_3 tue) { - if (tue.getTask() != null && tue.getTask().status() != null && tue.getTask().status().message() != null) { + if (tue.getTask() != null + && tue.getTask().status() != null + && tue.getTask().status().message() != null) { message = tue.getTask().status().message(); } } @@ -470,41 +517,60 @@ private boolean isTerminalEventV03(ClientEvent_v0_3 event) { return true; } if (event instanceof TaskEvent_v0_3 te) { - return te.getTask() != null && te.getTask().status() != null && te.getTask().status().state().isFinal(); + return te.getTask() != null + && te.getTask().status() != null + && te.getTask().status().state().isFinal(); } if (event instanceof TaskUpdateEvent_v0_3 tue) { - return tue.getTask() != null && tue.getTask().status() != null && tue.getTask().status().state().isFinal(); + return tue.getTask() != null + && tue.getTask().status() != null + && tue.getTask().status().state().isFinal(); } return false; } @SuppressWarnings("unchecked") - private void addTransport(org.a2aproject.sdk.client.ClientBuilder builder, String transport) { + private void addTransport( + org.a2aproject.sdk.client.ClientBuilder builder, String transport) { switch (transport.toUpperCase()) { - case "GRPC" -> builder.withTransport(GrpcTransport.class, - new GrpcTransportConfigBuilder() - .channelFactory(url -> ManagedChannelBuilder.forTarget(url) - .usePlaintext() - .build())); - case "REST", "HTTP_JSON", "HTTP+JSON" -> builder.withTransport( - RestTransport.class, new RestTransportConfigBuilder()); - default -> builder.withTransport(JSONRPCTransport.class, - new JSONRPCTransportConfigBuilder()); + case "GRPC" -> + builder.withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder() + .channelFactory( + url -> + ManagedChannelBuilder.forTarget(url) + .usePlaintext() + .build())); + case "REST", "HTTP_JSON", "HTTP+JSON" -> + builder.withTransport( + RestTransport.class, new RestTransportConfigBuilder()); + default -> + builder.withTransport( + JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); } } @SuppressWarnings("unchecked") - private void addTransportV03(org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3 builder, String transport) { + private void addTransportV03( + org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3 builder, String transport) { switch (transport.toUpperCase()) { - case "GRPC" -> builder.withTransport(GrpcTransport_v0_3.class, - new GrpcTransportConfigBuilder_v0_3() - .channelFactory(url -> ManagedChannelBuilder.forTarget(url) - .usePlaintext() - .build())); - case "REST", "HTTP_JSON", "HTTP+JSON" -> builder.withTransport( - RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3()); - default -> builder.withTransport(JSONRPCTransport_v0_3.class, - new JSONRPCTransportConfigBuilder_v0_3()); + case "GRPC" -> + builder.withTransport( + GrpcTransport_v0_3.class, + new GrpcTransportConfigBuilder_v0_3() + .channelFactory( + url -> + ManagedChannelBuilder.forTarget(url) + .usePlaintext() + .build())); + case "REST", "HTTP_JSON", "HTTP+JSON" -> + builder.withTransport( + RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3()); + default -> + builder.withTransport( + JSONRPCTransport_v0_3.class, + new JSONRPCTransportConfigBuilder_v0_3()); } } } diff --git a/itk/src/main/java/org/a2aproject/sdk/itk/Main.java b/itk/src/main/java/org/a2aproject/sdk/itk/Main.java index 67b677431..2814232bb 100644 --- a/itk/src/main/java/org/a2aproject/sdk/itk/Main.java +++ b/itk/src/main/java/org/a2aproject/sdk/itk/Main.java @@ -33,10 +33,7 @@ public static void main(String[] args) throws IOException, InterruptedException command.add("-jar"); command.add(jarPath.toString()); - Process process = new ProcessBuilder(command) - .inheritIO() - .directory(new File(".")) - .start(); + Process process = new ProcessBuilder(command).inheritIO().directory(new File(".")).start(); Runtime.getRuntime().addShutdownHook(new Thread(() -> process.destroyForcibly())); diff --git a/jsonrpc-common/pom.xml b/jsonrpc-common/pom.xml index 33b9222db..ccac38c08 100644 --- a/jsonrpc-common/pom.xml +++ b/jsonrpc-common/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/json/JsonMappingException.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/json/JsonMappingException.java index 591bc2874..605053433 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/json/JsonMappingException.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/json/JsonMappingException.java @@ -60,7 +60,8 @@ public JsonMappingException(@Nullable Object reference, String message) { * @param message the detail message explaining the mapping error * @param cause the underlying cause of the mapping error (may be null) */ - public JsonMappingException(@Nullable Object reference, String message, @Nullable Throwable cause) { + public JsonMappingException( + @Nullable Object reference, String message, @Nullable Throwable cause) { super(message, cause); this.reference = reference; } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/json/JsonUtil.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/json/JsonUtil.java index ff33b4cd9..cca2ad506 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/json/JsonUtil.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/json/JsonUtil.java @@ -1,23 +1,10 @@ package org.a2aproject.sdk.jsonrpc.common.json; +import static java.lang.String.format; +import static java.util.Collections.emptyMap; import static org.a2aproject.sdk.jsonrpc.common.json.JsonUtil.ThrowableTypeAdapter.THROWABLE_MARKER_FIELD; -import org.a2aproject.sdk.spec.A2AErrorCodes; import static org.a2aproject.sdk.spec.DataPart.DATA; import static org.a2aproject.sdk.spec.TextPart.TEXT; -import static java.lang.String.format; -import static java.util.Collections.emptyMap; - -import java.lang.reflect.Type; -import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -30,8 +17,19 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; - +import java.lang.reflect.Type; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; import org.a2aproject.sdk.spec.A2AError; +import org.a2aproject.sdk.spec.A2AErrorCodes; import org.a2aproject.sdk.spec.APIKeySecurityScheme; import org.a2aproject.sdk.spec.ContentTypeNotSupportedError; import org.a2aproject.sdk.spec.DataPart; @@ -65,7 +63,6 @@ import org.a2aproject.sdk.spec.TextPart; import org.a2aproject.sdk.spec.UnsupportedOperationError; import org.a2aproject.sdk.spec.VersionNotSupportedError; - import org.jspecify.annotations.Nullable; /** @@ -77,10 +74,10 @@ private static GsonBuilder createBaseGsonBuilder() { return new GsonBuilder() .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeTypeAdapter()) - .registerTypeAdapter(SecurityRequirement.class, new SecurityRequirementTypeAdapter()) + .registerTypeAdapter( + SecurityRequirement.class, new SecurityRequirementTypeAdapter()) .registerTypeHierarchyAdapter(A2AError.class, new A2AErrorTypeAdapter()) .registerTypeHierarchyAdapter(FileContent.class, new FileContentTypeAdapter()); - } /** @@ -93,12 +90,15 @@ private static GsonBuilder createBaseGsonBuilder() { * * @see JsonUtil#createBaseGsonBuilder() */ - public static final Gson OBJECT_MAPPER = createBaseGsonBuilder() - .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) - .registerTypeAdapter(EventKind.class, new EventKindTypeAdapter()) - .registerTypeAdapter(StreamingEventKind.class, new StreamingEventKindTypeAdapter()) - .registerTypeHierarchyAdapter(SecurityScheme.class, new SecuritySchemeTypeAdapter()) - .create(); + public static final Gson OBJECT_MAPPER = + createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) + .registerTypeAdapter(EventKind.class, new EventKindTypeAdapter()) + .registerTypeAdapter( + StreamingEventKind.class, new StreamingEventKindTypeAdapter()) + .registerTypeHierarchyAdapter( + SecurityScheme.class, new SecuritySchemeTypeAdapter()) + .create(); /** * Deserializes JSON string to an object of the specified class. @@ -159,7 +159,8 @@ public static String toJson(Object data) throws JsonProcessingException { * @return JSON string representation of the wrapped streaming event * @throws JsonProcessingException if conversion fails */ - public static String toJsonStreamingEvent(StreamingEventKind data) throws JsonProcessingException { + public static String toJsonStreamingEvent(StreamingEventKind data) + throws JsonProcessingException { try { return OBJECT_MAPPER.toJson(data, StreamingEventKind.class); } catch (JsonSyntaxException e) { @@ -171,7 +172,8 @@ public static String toJsonStreamingEvent(StreamingEventKind data) throws JsonPr * Writes a JSON-RPC {@code id} field. Handles null, String, and Number values, * preserving fractional precision for non-integer numeric IDs. */ - public static void writeJsonRpcId(JsonWriter out, @Nullable Object id) throws java.io.IOException { + public static void writeJsonRpcId(JsonWriter out, @Nullable Object id) + throws java.io.IOException { out.name("id"); if (id == null) { // JSON-RPC 2.0 section 5: "id" is required in a response and must be null when the @@ -182,7 +184,10 @@ public static void writeJsonRpcId(JsonWriter out, @Nullable Object id) throws ja out.nullValue(); out.setSerializeNulls(serializeNulls); } else if (id instanceof Number n) { - if (id instanceof Long || id instanceof Integer || id instanceof Short || id instanceof Byte) { + if (id instanceof Long + || id instanceof Integer + || id instanceof Short + || id instanceof Byte) { out.value(n.longValue()); } else { out.value(n); @@ -218,8 +223,7 @@ public void write(JsonWriter out, OffsetDateTime value) throws java.io.IOExcepti } @Override - public @Nullable - OffsetDateTime read(JsonReader in) throws java.io.IOException { + public @Nullable OffsetDateTime read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -228,7 +232,8 @@ OffsetDateTime read(JsonReader in) throws java.io.IOException { try { return OffsetDateTime.parse(dateTimeString, DateTimeFormatter.ISO_OFFSET_DATE_TIME); } catch (DateTimeParseException e) { - throw new JsonSyntaxException("Failed to parse OffsetDateTime: " + dateTimeString, e); + throw new JsonSyntaxException( + "Failed to parse OffsetDateTime: " + dateTimeString, e); } } } @@ -270,8 +275,7 @@ public void write(JsonWriter out, Throwable value) throws java.io.IOException { } @Override - public @Nullable - Throwable read(JsonReader in) throws java.io.IOException { + public @Nullable Throwable read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -284,12 +288,9 @@ Throwable read(JsonReader in) throws java.io.IOException { while (in.hasNext()) { String fieldName = in.nextName(); switch (fieldName) { - case "type" -> - type = in.nextString(); - case "message" -> - message = in.nextString(); - default -> - in.skipValue(); + case "type" -> type = in.nextString(); + case "message" -> message = in.nextString(); + default -> in.skipValue(); } } in.endObject(); @@ -365,8 +366,7 @@ public void write(JsonWriter out, A2AError value) throws java.io.IOException { } @Override - public @Nullable - A2AError read(JsonReader in) throws java.io.IOException { + public @Nullable A2AError read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -380,16 +380,13 @@ A2AError read(JsonReader in) throws java.io.IOException { while (in.hasNext()) { String fieldName = in.nextName(); switch (fieldName) { - case CODE_FIELD -> - code = in.nextInt(); - case MESSAGE_FIELD -> - message = in.nextString(); + case CODE_FIELD -> code = in.nextInt(); + case MESSAGE_FIELD -> message = in.nextString(); case DETAILS_FIELD -> { // Read details as a map details = readDetailsValue(in); } - default -> - in.skipValue(); + default -> in.skipValue(); } } in.endObject(); @@ -402,8 +399,8 @@ A2AError read(JsonReader in) throws java.io.IOException { * Reads the details field value as a map. */ @SuppressWarnings("unchecked") - private @Nullable - Map readDetailsValue(JsonReader in) throws java.io.IOException { + private @Nullable Map readDetailsValue(JsonReader in) + throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -418,7 +415,10 @@ Map readDetailsValue(JsonReader in) throws java.io.IOException { /** * Creates the appropriate A2AError subclass based on the error code. */ - private A2AError createErrorInstance(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + private A2AError createErrorInstance( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { if (code == null) { throw new JsonSyntaxException("A2AError must have a code field"); } @@ -435,12 +435,17 @@ private A2AError createErrorInstance(@Nullable Integer code, @Nullable String me case INTERNAL -> new org.a2aproject.sdk.spec.InternalError(code, message, details); case TASK_NOT_FOUND -> new TaskNotFoundError(message, details); case TASK_NOT_CANCELABLE -> new TaskNotCancelableError(code, message, details); - case PUSH_NOTIFICATION_NOT_SUPPORTED -> new PushNotificationNotSupportedError(code, message, details); + case PUSH_NOTIFICATION_NOT_SUPPORTED -> + new PushNotificationNotSupportedError(code, message, details); case UNSUPPORTED_OPERATION -> new UnsupportedOperationError(code, message, details); - case CONTENT_TYPE_NOT_SUPPORTED -> new ContentTypeNotSupportedError(code, message, details); - case INVALID_AGENT_RESPONSE -> new InvalidAgentResponseError(code, message, details); - case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new ExtendedAgentCardNotConfiguredError(code, message, details); - case EXTENSION_SUPPORT_REQUIRED -> new ExtensionSupportRequiredError(code, message, details); + case CONTENT_TYPE_NOT_SUPPORTED -> + new ContentTypeNotSupportedError(code, message, details); + case INVALID_AGENT_RESPONSE -> + new InvalidAgentResponseError(code, message, details); + case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> + new ExtendedAgentCardNotConfiguredError(code, message, details); + case EXTENSION_SUPPORT_REQUIRED -> + new ExtensionSupportRequiredError(code, message, details); case VERSION_NOT_SUPPORTED -> new VersionNotSupportedError(code, message, details); }; } @@ -454,10 +459,11 @@ private A2AError createErrorInstance(@Nullable Integer code, @Nullable String me * @param metadata the metadata map to write * @throws java.io.IOException if an I/O error occurs */ - public static void writeMetadata(JsonWriter out, @Nullable Map metadata) throws java.io.IOException { + public static void writeMetadata(JsonWriter out, @Nullable Map metadata) + throws java.io.IOException { if (metadata != null && !metadata.isEmpty()) { out.name("metadata"); - OBJECT_MAPPER.toJson(metadata, new TypeToken>(){}.getType(), out); + OBJECT_MAPPER.toJson(metadata, new TypeToken>() {}.getType(), out); } } @@ -471,7 +477,7 @@ public static String writeMetadata(@Nullable Map metadata) { if (metadata == null || metadata.isEmpty()) { return ""; } - return OBJECT_MAPPER.toJson(metadata, new TypeToken>(){}.getType()); + return OBJECT_MAPPER.toJson(metadata, new TypeToken>() {}.getType()); } /** @@ -482,7 +488,8 @@ public static String writeMetadata(@Nullable Map metadata) { */ public static Map readMetadata(com.google.gson.JsonObject jsonObject) { if (jsonObject.has("metadata")) { - return OBJECT_MAPPER.fromJson(jsonObject.get("metadata"), new TypeToken>(){}.getType()); + return OBJECT_MAPPER.fromJson( + jsonObject.get("metadata"), new TypeToken>() {}.getType()); } return Collections.emptyMap(); } @@ -494,7 +501,8 @@ public static Map readMetadata(com.google.gson.JsonObject jsonOb * @return the metadata map, or an empty map if the input is null, blank, or has no "metadata" field * @throws JsonProcessingException if the JSON is invalid */ - public static Map readMetadata(@Nullable String json) throws JsonProcessingException { + public static Map readMetadata(@Nullable String json) + throws JsonProcessingException { if (json == null || json.isBlank()) { return Collections.emptyMap(); } @@ -532,12 +540,13 @@ static class PartTypeAdapter extends TypeAdapter> { // The oneOf content-type discriminator keys in the flat JSON format. // Exactly one must be present (and non-null) in each Part object. private static final Set VALID_KEYS = Set.of(TEXT, RAW, URL, DATA); - private static final Type MAP_TYPE = new TypeToken>(){}.getType(); + private static final Type MAP_TYPE = new TypeToken>() {}.getType(); // Create separate Gson instance without the Part adapter to avoid recursion private final Gson delegateGson = createBaseGsonBuilder().create(); - private void writeMetadata(JsonWriter out, @Nullable Map metadata) throws java.io.IOException { + private void writeMetadata(JsonWriter out, @Nullable Map metadata) + throws java.io.IOException { if (metadata != null && !metadata.isEmpty()) { out.name("metadata"); delegateGson.toJson(metadata, MAP_TYPE, out); @@ -545,7 +554,8 @@ private void writeMetadata(JsonWriter out, @Nullable Map metadat } /** Writes a string field only when the value is non-null and non-empty. */ - private void writeNonEmpty(JsonWriter out, String name, String value) throws java.io.IOException { + private void writeNonEmpty(JsonWriter out, String name, String value) + throws java.io.IOException { if (!value.isEmpty()) { out.name(name).value(value); } @@ -572,7 +582,9 @@ public void write(JsonWriter out, Part value) throws java.io.IOException { writeNonEmpty(out, FILENAME, withUri.name()); writeNonEmpty(out, MEDIA_TYPE, withUri.mimeType()); } else { - throw new JsonSyntaxException("Unknown FileContent subclass: " + filePart.file().getClass().getName()); + throw new JsonSyntaxException( + "Unknown FileContent subclass: " + + filePart.file().getClass().getName()); } writeMetadata(out, filePart.metadata()); @@ -581,15 +593,15 @@ public void write(JsonWriter out, Part value) throws java.io.IOException { delegateGson.toJson(dataPart.data(), Object.class, out); JsonUtil.writeMetadata(out, dataPart.metadata()); } else { - throw new JsonSyntaxException("Unknown Part subclass: " + value.getClass().getName()); + throw new JsonSyntaxException( + "Unknown Part subclass: " + value.getClass().getName()); } out.endObject(); } @Override - public @Nullable - Part read(JsonReader in) throws java.io.IOException { + public @Nullable Part read(JsonReader in) throws java.io.IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; @@ -606,35 +618,57 @@ Part read(JsonReader in) throws java.io.IOException { Map metadata = JsonUtil.readMetadata(jsonObject); Set keys = jsonObject.keySet(); - // A producer that emits every content key, leaving the unset ones as "", would otherwise + // A producer that emits every content key, leaving the unset ones as "", would + // otherwise // have its payload discarded by whichever empty placeholder came first on the wire. // The fallback keeps a deliberately empty TextPart decodable. - List populatedKeys = contentKeys(jsonObject) - .filter(key -> !isEmptyString(jsonObject.get(key))) - .toList(); + List populatedKeys = + contentKeys(jsonObject) + .filter(key -> !isEmptyString(jsonObject.get(key))) + .toList(); if (populatedKeys.size() > 1) { throw new JsonSyntaxException( - format("Part is a oneOf: expected exactly one populated content key, but found: %s", populatedKeys)); + format( + "Part is a oneOf: expected exactly one populated content key, but" + + " found: %s", + populatedKeys)); } - String discriminator = populatedKeys.stream().findFirst() - .or(() -> contentKeys(jsonObject).findFirst()) - .orElseThrow(() -> new JsonSyntaxException(format("Part must have one of: %s (found: %s)", VALID_KEYS, keys))); + String discriminator = + populatedKeys.stream() + .findFirst() + .or(() -> contentKeys(jsonObject).findFirst()) + .orElseThrow( + () -> + new JsonSyntaxException( + format( + "Part must have one of: %s (found: %s)", + VALID_KEYS, keys))); return switch (discriminator) { case TEXT -> new TextPart(requireString(jsonObject, TEXT), metadata); - case RAW -> new FilePart(new FileWithBytes( - stringOrEmpty(jsonObject, MEDIA_TYPE), - stringOrEmpty(jsonObject, FILENAME), - requireString(jsonObject, RAW)), metadata); - case URL -> new FilePart(new FileWithUri( - stringOrEmpty(jsonObject, MEDIA_TYPE), - stringOrEmpty(jsonObject, FILENAME), - requireString(jsonObject, URL)), metadata); + case RAW -> + new FilePart( + new FileWithBytes( + stringOrEmpty(jsonObject, MEDIA_TYPE), + stringOrEmpty(jsonObject, FILENAME), + requireString(jsonObject, RAW)), + metadata); + case URL -> + new FilePart( + new FileWithUri( + stringOrEmpty(jsonObject, MEDIA_TYPE), + stringOrEmpty(jsonObject, FILENAME), + requireString(jsonObject, URL)), + metadata); case DATA -> { Object data = delegateGson.fromJson(jsonObject.get(DATA), Object.class); yield new DataPart(data, metadata); } - default -> throw new JsonSyntaxException(format("Part must have one of: %s (found: %s)", VALID_KEYS, discriminator)); + default -> + throw new JsonSyntaxException( + format( + "Part must have one of: %s (found: %s)", + VALID_KEYS, discriminator)); }; } @@ -642,14 +676,18 @@ Part read(JsonReader in) throws java.io.IOException { private Stream contentKeys(com.google.gson.JsonObject obj) { return obj.keySet().stream() .filter(VALID_KEYS::contains) - .filter(key -> { - JsonElement el = obj.get(key); - return el != null && !el.isJsonNull(); - }); + .filter( + key -> { + JsonElement el = obj.get(key); + return el != null && !el.isJsonNull(); + }); } private boolean isEmptyString(@Nullable JsonElement el) { - return el != null && el.isJsonPrimitive() && el.getAsJsonPrimitive().isString() && el.getAsString().isEmpty(); + return el != null + && el.isJsonPrimitive() + && el.getAsJsonPrimitive().isString() + && el.getAsString().isEmpty(); } /** Guards {@code getAsString()}, which throws an unchecked UnsupportedOperationException on an object or array. */ @@ -676,9 +714,10 @@ private String stringOrEmpty(com.google.gson.JsonObject obj, String key) { */ static class EventKindTypeAdapter extends TypeAdapter { - private final Gson delegateGson = createBaseGsonBuilder() - .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) - .create(); + private final Gson delegateGson = + createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) + .create(); private final StreamingEventKindTypeAdapter streamingEventKindTypeAdapter = new StreamingEventKindTypeAdapter(); @@ -719,9 +758,10 @@ public void write(JsonWriter out, EventKind value) throws java.io.IOException { static class StreamingEventKindTypeAdapter extends TypeAdapter { // Create separate Gson instance without the StreamingEventKind adapter to avoid recursion - private final Gson delegateGson = createBaseGsonBuilder() - .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) - .create(); + private final Gson delegateGson = + createBaseGsonBuilder() + .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) + .create(); @Override public void write(JsonWriter out, StreamingEventKind value) throws java.io.IOException { @@ -737,8 +777,7 @@ public void write(JsonWriter out, StreamingEventKind value) throws java.io.IOExc } @Override - public @Nullable - StreamingEventKind read(JsonReader in) throws java.io.IOException { + public @Nullable StreamingEventKind read(JsonReader in) throws java.io.IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; @@ -756,13 +795,16 @@ StreamingEventKind read(JsonReader in) throws java.io.IOException { if (jsonObject.has(Task.STREAMING_EVENT_ID)) { return delegateGson.fromJson(jsonObject.get(Task.STREAMING_EVENT_ID), Task.class); } else if (jsonObject.has(Message.STREAMING_EVENT_ID)) { - return delegateGson.fromJson(jsonObject.get(Message.STREAMING_EVENT_ID), Message.class); + return delegateGson.fromJson( + jsonObject.get(Message.STREAMING_EVENT_ID), Message.class); } else if (jsonObject.has(TaskStatusUpdateEvent.STREAMING_EVENT_ID)) { return delegateGson.fromJson( - jsonObject.get(TaskStatusUpdateEvent.STREAMING_EVENT_ID), TaskStatusUpdateEvent.class); + jsonObject.get(TaskStatusUpdateEvent.STREAMING_EVENT_ID), + TaskStatusUpdateEvent.class); } else if (jsonObject.has(TaskArtifactUpdateEvent.STREAMING_EVENT_ID)) { return delegateGson.fromJson( - jsonObject.get(TaskArtifactUpdateEvent.STREAMING_EVENT_ID), TaskArtifactUpdateEvent.class); + jsonObject.get(TaskArtifactUpdateEvent.STREAMING_EVENT_ID), + TaskArtifactUpdateEvent.class); } // Check for unwrapped format (direct Task/Message deserialization) @@ -781,7 +823,12 @@ StreamingEventKind read(JsonReader in) throws java.io.IOException { // This is an unwrapped TaskArtifactUpdateEvent return delegateGson.fromJson(jsonObject, TaskArtifactUpdateEvent.class); } else { - throw new JsonSyntaxException("StreamingEventKind must have wrapper (task/message/statusUpdate/artifactUpdate) or recognizable unwrapped fields (found: " + jsonObject.keySet() + ")"); + throw new JsonSyntaxException( + "StreamingEventKind must have wrapper" + + " (task/message/statusUpdate/artifactUpdate) or recognizable" + + " unwrapped fields (found: " + + jsonObject.keySet() + + ")"); } } } @@ -807,10 +854,11 @@ static class FileContentTypeAdapter extends TypeAdapter { // Create separate Gson instance without the FileContent adapter to avoid recursion, // but with an explicit FileWithBytes adapter to prevent field/path leakage. - private final Gson delegateGson = new GsonBuilder() - .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeTypeAdapter()) - .registerTypeAdapter(FileWithBytes.class, new FileWithBytesTypeAdapter()) - .create(); + private final Gson delegateGson = + new GsonBuilder() + .registerTypeAdapter(OffsetDateTime.class, new OffsetDateTimeTypeAdapter()) + .registerTypeAdapter(FileWithBytes.class, new FileWithBytesTypeAdapter()) + .create(); @Override public void write(JsonWriter out, FileContent value) throws java.io.IOException { @@ -823,8 +871,7 @@ public void write(JsonWriter out, FileContent value) throws java.io.IOException } @Override - public @Nullable - FileContent read(JsonReader in) throws java.io.IOException { + public @Nullable FileContent read(JsonReader in) throws java.io.IOException { if (in.peek() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -838,13 +885,15 @@ FileContent read(JsonReader in) throws java.io.IOException { com.google.gson.JsonObject jsonObject = jsonElement.getAsJsonObject(); - // Distinguish between FileWithBytes and FileWithUri by checking for "bytes" or "uri" field + // Distinguish between FileWithBytes and FileWithUri by checking for "bytes" or "uri" + // field if (jsonObject.has("bytes")) { return delegateGson.fromJson(jsonElement, FileWithBytes.class); } else if (jsonObject.has("uri")) { return delegateGson.fromJson(jsonElement, FileWithUri.class); } else { - throw new JsonSyntaxException("FileContent must have either 'bytes' or 'uri' field"); + throw new JsonSyntaxException( + "FileContent must have either 'bytes' or 'uri' field"); } } } @@ -915,7 +964,8 @@ public void write(JsonWriter out, FileWithBytes value) throws java.io.IOExceptio static class APIKeyLocationTypeAdapter extends TypeAdapter { @Override - public void write(JsonWriter out, APIKeySecurityScheme.Location value) throws java.io.IOException { + public void write(JsonWriter out, APIKeySecurityScheme.Location value) + throws java.io.IOException { if (value == null) { out.nullValue(); return; @@ -924,7 +974,8 @@ public void write(JsonWriter out, APIKeySecurityScheme.Location value) throws ja } @Override - public APIKeySecurityScheme.@Nullable Location read(JsonReader in) throws java.io.IOException { + public APIKeySecurityScheme.@Nullable Location read(JsonReader in) + throws java.io.IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; @@ -933,7 +984,8 @@ public void write(JsonWriter out, APIKeySecurityScheme.Location value) throws ja try { return APIKeySecurityScheme.Location.fromString(locationString); } catch (IllegalArgumentException e) { - throw new JsonSyntaxException("Invalid APIKeySecurityScheme.Location: " + locationString, e); + throw new JsonSyntaxException( + "Invalid APIKeySecurityScheme.Location: " + locationString, e); } } } @@ -985,17 +1037,22 @@ public void write(JsonWriter out, APIKeySecurityScheme.Location value) throws ja */ static class SecuritySchemeTypeAdapter extends TypeAdapter { - private static final Set VALID_KEYS = Set.of(APIKeySecurityScheme.TYPE, - HTTPAuthSecurityScheme.TYPE, - OAuth2SecurityScheme.TYPE, - OpenIdConnectSecurityScheme.TYPE, - MutualTLSSecurityScheme.TYPE); + private static final Set VALID_KEYS = + Set.of( + APIKeySecurityScheme.TYPE, + HTTPAuthSecurityScheme.TYPE, + OAuth2SecurityScheme.TYPE, + OpenIdConnectSecurityScheme.TYPE, + MutualTLSSecurityScheme.TYPE); // Create separate Gson instance without the SecurityScheme adapter to avoid recursion // Register custom adapter for APIKeySecurityScheme.Location enum - private final Gson delegateGson = createBaseGsonBuilder() - .registerTypeAdapter(APIKeySecurityScheme.Location.class, new APIKeyLocationTypeAdapter()) - .create(); + private final Gson delegateGson = + createBaseGsonBuilder() + .registerTypeAdapter( + APIKeySecurityScheme.Location.class, + new APIKeyLocationTypeAdapter()) + .create(); @Override public void write(JsonWriter out, SecurityScheme value) throws java.io.IOException { @@ -1012,8 +1069,7 @@ public void write(JsonWriter out, SecurityScheme value) throws java.io.IOExcepti } @Override - public @Nullable - SecurityScheme read(JsonReader in) throws java.io.IOException { + public @Nullable SecurityScheme read(JsonReader in) throws java.io.IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; @@ -1030,19 +1086,33 @@ SecurityScheme read(JsonReader in) throws java.io.IOException { // Check for member name discriminators Set keys = jsonObject.keySet(); if (keys.size() != 1) { - throw new JsonSyntaxException(format("A SecurityScheme object must have exactly one key, which must be one of: %s (found: %s)", VALID_KEYS, keys)); + throw new JsonSyntaxException( + format( + "A SecurityScheme object must have exactly one key, which must be" + + " one of: %s (found: %s)", + VALID_KEYS, keys)); } String discriminator = keys.iterator().next(); com.google.gson.JsonElement nestedObject = jsonObject.get(discriminator); return switch (discriminator) { - case APIKeySecurityScheme.TYPE -> delegateGson.fromJson(nestedObject, APIKeySecurityScheme.class); - case HTTPAuthSecurityScheme.TYPE -> delegateGson.fromJson(nestedObject, HTTPAuthSecurityScheme.class); - case OAuth2SecurityScheme.TYPE -> delegateGson.fromJson(nestedObject, OAuth2SecurityScheme.class); - case OpenIdConnectSecurityScheme.TYPE -> delegateGson.fromJson(nestedObject, OpenIdConnectSecurityScheme.class); - case MutualTLSSecurityScheme.TYPE -> delegateGson.fromJson(nestedObject, MutualTLSSecurityScheme.class); - default -> throw new JsonSyntaxException(format("Unknown SecurityScheme type. Must be one of: %s (found: %s)", VALID_KEYS, discriminator)); + case APIKeySecurityScheme.TYPE -> + delegateGson.fromJson(nestedObject, APIKeySecurityScheme.class); + case HTTPAuthSecurityScheme.TYPE -> + delegateGson.fromJson(nestedObject, HTTPAuthSecurityScheme.class); + case OAuth2SecurityScheme.TYPE -> + delegateGson.fromJson(nestedObject, OAuth2SecurityScheme.class); + case OpenIdConnectSecurityScheme.TYPE -> + delegateGson.fromJson(nestedObject, OpenIdConnectSecurityScheme.class); + case MutualTLSSecurityScheme.TYPE -> + delegateGson.fromJson(nestedObject, MutualTLSSecurityScheme.class); + default -> + throw new JsonSyntaxException( + format( + "Unknown SecurityScheme type. Must be one of: %s (found:" + + " %s)", + VALID_KEYS, discriminator)); }; } } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AErrorResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AErrorResponse.java index dd60a84c0..bf7ddeb92 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AErrorResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AErrorResponse.java @@ -23,9 +23,7 @@ */ public final class A2AErrorResponse extends A2AResponse { - private A2AErrorResponse() { - } - + private A2AErrorResponse() {} /** * Constructs a JSON-RPC error response with all fields. diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AMessage.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AMessage.java index 6687b7d8f..497600962 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AMessage.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AMessage.java @@ -43,5 +43,4 @@ public sealed interface A2AMessage permits A2ARequest, A2AResponse { * @return the correlation ID, or null for notifications */ Object getId(); - } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2ARequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2ARequest.java index 7e787c17f..fd95a544c 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2ARequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2ARequest.java @@ -28,7 +28,8 @@ * @see JSON-RPC 2.0 Specification * @see A2A Protocol Specification */ -public abstract sealed class A2ARequest implements A2AMessage permits NonStreamingJSONRPCRequest, StreamingJSONRPCRequest { +public abstract sealed class A2ARequest implements A2AMessage + permits NonStreamingJSONRPCRequest, StreamingJSONRPCRequest { /** The JSON-RPC protocol version. */ protected String jsonrpc; @@ -45,8 +46,7 @@ public abstract sealed class A2ARequest implements A2AMessage permits NonStre /** * Default constructor for JSON deserialization. */ - public A2ARequest() { - } + public A2ARequest() {} /** * {@inheritDoc} @@ -91,7 +91,8 @@ public T getParams() { * @param params the parameters * @param paramsIsRequired whether parameters are required */ - protected void validateAndSetJsonParameters(String jsonrpc, String method, Object id, T params, boolean paramsIsRequired) { + protected void validateAndSetJsonParameters( + String jsonrpc, String method, Object id, T params, boolean paramsIsRequired) { this.jsonrpc = Utils.defaultIfNull(jsonrpc, JSONRPC_VERSION); if (!JSONRPC_VERSION.equals(this.jsonrpc)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); @@ -105,5 +106,4 @@ protected void validateAndSetJsonParameters(String jsonrpc, String method, Objec } this.params = params; } - } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AResponse.java index 2ad38624e..bd862b1de 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/A2AResponse.java @@ -10,22 +10,35 @@ * * @param the type of the result value returned in successful responses */ -public abstract sealed class A2AResponse implements A2AMessage permits CancelTaskResponse, DeleteTaskPushNotificationConfigResponse, GetExtendedAgentCardResponse, GetTaskPushNotificationConfigResponse, GetTaskResponse, A2AErrorResponse, ListTaskPushNotificationConfigsResponse, ListTasksResponse, SendMessageResponse, SendStreamingMessageResponse, CreateTaskPushNotificationConfigResponse { +public abstract sealed class A2AResponse implements A2AMessage + permits CancelTaskResponse, + DeleteTaskPushNotificationConfigResponse, + GetExtendedAgentCardResponse, + GetTaskPushNotificationConfigResponse, + GetTaskResponse, + A2AErrorResponse, + ListTaskPushNotificationConfigsResponse, + ListTasksResponse, + SendMessageResponse, + SendStreamingMessageResponse, + CreateTaskPushNotificationConfigResponse { /** The JSON-RPC protocol version. */ protected String jsonrpc; + /** The request identifier this response corresponds to. */ protected Object id; + /** The result of the method invocation. */ protected T result; + /** The error object if the invocation failed. */ protected A2AError error; /** * Default constructor for deserialization. */ - public A2AResponse() { - } + public A2AResponse() {} /** * Constructs a JSON-RPC response. @@ -37,13 +50,13 @@ public A2AResponse() { * @param resultType the result type class */ public A2AResponse(String jsonrpc, Object id, T result, A2AError error, Class resultType) { - if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + if (jsonrpc != null && !jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } if (error != null && result != null) { throw new IllegalArgumentException("Invalid JSON-RPC error response"); } - if (error == null && result == null && ! Void.class.equals(resultType)) { + if (error == null && result == null && !Void.class.equals(resultType)) { throw new IllegalArgumentException("Invalid JSON-RPC success response"); } Assert.isValidJsonRpcId(id); diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CancelTaskRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CancelTaskRequest.java index c13a345d2..9f2e7040d 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CancelTaskRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CancelTaskRequest.java @@ -2,9 +2,8 @@ import static org.a2aproject.sdk.spec.A2AMethods.CANCEL_TASK_METHOD; -import org.a2aproject.sdk.spec.CancelTaskParams; import java.util.UUID; - +import org.a2aproject.sdk.spec.CancelTaskParams; import org.a2aproject.sdk.spec.TaskIdParams; import org.a2aproject.sdk.spec.TaskNotCancelableError; import org.a2aproject.sdk.spec.TaskState; @@ -28,9 +27,7 @@ */ public final class CancelTaskRequest extends NonStreamingJSONRPCRequest { - private CancelTaskRequest() { - } - + private CancelTaskRequest() {} /** * Creates a new CancelTaskRequest with the specified JSON-RPC parameters. @@ -78,8 +75,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the JSON-RPC protocol version. @@ -87,7 +83,6 @@ private Builder() { * @param jsonrpc the JSON-RPC version (optional, defaults to "2.0") * @return this builder for method chaining */ - public CancelTaskRequest.Builder jsonrpc(String jsonrpc) { this.jsonrpc = jsonrpc; return this; @@ -110,7 +105,6 @@ public CancelTaskRequest.Builder id(Object id) { * @param params the request parameters (required) * @return this builder for method chaining */ - public CancelTaskRequest.Builder params(CancelTaskParams params) { this.params = params; return this; diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CancelTaskResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CancelTaskResponse.java index 78417ba3c..a8fada788 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CancelTaskResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CancelTaskResponse.java @@ -21,12 +21,9 @@ * @see TaskNotCancelableError for the error when cancellation fails * @see A2A Protocol Specification */ - public final class CancelTaskResponse extends A2AResponse { - private CancelTaskResponse() { - } - + private CancelTaskResponse() {} /** * Constructs a CancelTaskResponse with full parameters. @@ -50,7 +47,6 @@ public CancelTaskResponse(Object id, A2AError error) { this(null, id, null, error); } - /** * Constructs a CancelTaskResponse with a successful result. * diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CreateTaskPushNotificationConfigRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CreateTaskPushNotificationConfigRequest.java index a471ca982..f0de5ae18 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CreateTaskPushNotificationConfigRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CreateTaskPushNotificationConfigRequest.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; import java.util.UUID; - import org.a2aproject.sdk.spec.TaskPushNotificationConfig; /** @@ -19,11 +18,10 @@ * @see TaskPushNotificationConfig for the parameter structure * @see A2A Protocol Specification */ -public final class CreateTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { - - private CreateTaskPushNotificationConfigRequest() { - } +public final class CreateTaskPushNotificationConfigRequest + extends NonStreamingJSONRPCRequest { + private CreateTaskPushNotificationConfigRequest() {} /** * Constructs request with all parameters. @@ -32,7 +30,8 @@ private CreateTaskPushNotificationConfigRequest() { * @param id the request ID * @param params the request parameters */ - public CreateTaskPushNotificationConfigRequest(String jsonrpc, Object id, TaskPushNotificationConfig params) { + public CreateTaskPushNotificationConfigRequest( + String jsonrpc, Object id, TaskPushNotificationConfig params) { super(jsonrpc, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, id, params); } @@ -42,7 +41,8 @@ public CreateTaskPushNotificationConfigRequest(String jsonrpc, Object id, TaskPu * @param id the request ID * @param taskPushConfig the task push notification configuration */ - public CreateTaskPushNotificationConfigRequest(String id, TaskPushNotificationConfig taskPushConfig) { + public CreateTaskPushNotificationConfigRequest( + String id, TaskPushNotificationConfig taskPushConfig) { this(null, id, taskPushConfig); } @@ -66,8 +66,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the JSON-RPC version. @@ -97,7 +96,8 @@ public CreateTaskPushNotificationConfigRequest.Builder id(Object id) { * @param params the request parameters * @return this builder for method chaining */ - public CreateTaskPushNotificationConfigRequest.Builder params(TaskPushNotificationConfig params) { + public CreateTaskPushNotificationConfigRequest.Builder params( + TaskPushNotificationConfig params) { this.params = params; return this; } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CreateTaskPushNotificationConfigResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CreateTaskPushNotificationConfigResponse.java index de9df6192..66c21aba1 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CreateTaskPushNotificationConfigResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/CreateTaskPushNotificationConfigResponse.java @@ -19,11 +19,10 @@ * @see PushNotificationNotSupportedError for the error when unsupported * @see A2A Protocol Specification */ -public final class CreateTaskPushNotificationConfigResponse extends A2AResponse { - - private CreateTaskPushNotificationConfigResponse() { - } +public final class CreateTaskPushNotificationConfigResponse + extends A2AResponse { + private CreateTaskPushNotificationConfigResponse() {} /** * Constructs response with all parameters. @@ -33,7 +32,8 @@ private CreateTaskPushNotificationConfigResponse() { * @param result the push notification configuration * @param error the error (if any) */ - public CreateTaskPushNotificationConfigResponse(String jsonrpc, Object id, TaskPushNotificationConfig result, A2AError error) { + public CreateTaskPushNotificationConfigResponse( + String jsonrpc, Object id, TaskPushNotificationConfig result, A2AError error) { super(jsonrpc, id, result, error, TaskPushNotificationConfig.class); } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/DeleteTaskPushNotificationConfigRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/DeleteTaskPushNotificationConfigRequest.java index b99f8ed76..548b03e1e 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/DeleteTaskPushNotificationConfigRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/DeleteTaskPushNotificationConfigRequest.java @@ -1,12 +1,10 @@ package org.a2aproject.sdk.jsonrpc.common.wrappers; +import static org.a2aproject.sdk.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; import java.util.UUID; - import org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams; -import static org.a2aproject.sdk.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; - /** * JSON-RPC request to delete a push notification configuration from a task. *

    @@ -20,11 +18,10 @@ * @see DeleteTaskPushNotificationConfigParams for the parameter structure * @see A2A Protocol Specification */ -public final class DeleteTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { - - private DeleteTaskPushNotificationConfigRequest() { - } +public final class DeleteTaskPushNotificationConfigRequest + extends NonStreamingJSONRPCRequest { + private DeleteTaskPushNotificationConfigRequest() {} /** * Creates a new DeleteTaskPushNotificationConfigRequest with the specified JSON-RPC parameters. @@ -34,7 +31,8 @@ private DeleteTaskPushNotificationConfigRequest() { * @param params the request parameters containing task and config IDs * @throws IllegalArgumentException if jsonrpc version is invalid, method is not "DeleteTaskPushNotificationConfig", or id is not a string/integer/null */ - public DeleteTaskPushNotificationConfigRequest(String jsonrpc, Object id, DeleteTaskPushNotificationConfigParams params) { + public DeleteTaskPushNotificationConfigRequest( + String jsonrpc, Object id, DeleteTaskPushNotificationConfigParams params) { super(jsonrpc, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, id, params); } @@ -45,7 +43,8 @@ public DeleteTaskPushNotificationConfigRequest(String jsonrpc, Object id, Delete * @param params the request parameters containing task and config IDs * @throws IllegalArgumentException if id is not a string/integer/null */ - public DeleteTaskPushNotificationConfigRequest(String id, DeleteTaskPushNotificationConfigParams params) { + public DeleteTaskPushNotificationConfigRequest( + String id, DeleteTaskPushNotificationConfigParams params) { this(null, id, params); } @@ -72,8 +71,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the JSON-RPC protocol version. diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/DeleteTaskPushNotificationConfigResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/DeleteTaskPushNotificationConfigResponse.java index 4d35afdca..db288d833 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/DeleteTaskPushNotificationConfigResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/DeleteTaskPushNotificationConfigResponse.java @@ -15,9 +15,7 @@ */ public final class DeleteTaskPushNotificationConfigResponse extends A2AResponse { - private DeleteTaskPushNotificationConfigResponse() { - } - + private DeleteTaskPushNotificationConfigResponse() {} /** * Creates a new DeleteTaskPushNotificationConfigResponse with full JSON-RPC parameters. @@ -27,7 +25,8 @@ private DeleteTaskPushNotificationConfigResponse() { * @param result the result (always null/Void for this response type) * @param error the error if the request failed, null on success */ - public DeleteTaskPushNotificationConfigResponse(String jsonrpc, Object id, Void result,A2AError error) { + public DeleteTaskPushNotificationConfigResponse( + String jsonrpc, Object id, Void result, A2AError error) { super(jsonrpc, id, result, error, Void.class); } @@ -49,5 +48,4 @@ public DeleteTaskPushNotificationConfigResponse(Object id, A2AError error) { public DeleteTaskPushNotificationConfigResponse(Object id) { this(null, id, null, null); } - } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardRequest.java index 7824bac47..9d53ebf4f 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardRequest.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD; import java.util.UUID; - import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.ExtendedAgentCardNotConfiguredError; @@ -34,10 +33,10 @@ * @see ExtendedAgentCardNotConfiguredError for the error when unsupported * @see A2A Protocol Specification */ -public final class GetExtendedAgentCardRequest extends NonStreamingJSONRPCRequest { +public final class GetExtendedAgentCardRequest + extends NonStreamingJSONRPCRequest { - private GetExtendedAgentCardRequest() { - } + private GetExtendedAgentCardRequest() {} /** * Constructs request with optional parameters. @@ -46,7 +45,8 @@ private GetExtendedAgentCardRequest() { * @param id the request ID * @param params the request parameters, may be {@code null} */ - public GetExtendedAgentCardRequest(String jsonrpc, Object id, @Nullable GetExtendedAgentCardParams params) { + public GetExtendedAgentCardRequest( + String jsonrpc, Object id, @Nullable GetExtendedAgentCardParams params) { // params are optional for getExtendedAgentCard (tenant is optional) validateAndSetJsonParameters(jsonrpc, GET_EXTENDED_AGENT_CARD_METHOD, id, params, false); } @@ -80,8 +80,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the JSON-RPC version. @@ -111,7 +110,8 @@ public GetExtendedAgentCardRequest.Builder id(Object id) { * @param params the request parameters * @return this builder for method chaining */ - public GetExtendedAgentCardRequest.Builder params(@Nullable GetExtendedAgentCardParams params) { + public GetExtendedAgentCardRequest.Builder params( + @Nullable GetExtendedAgentCardParams params) { this.params = params; return this; } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardResponse.java index 817add256..9fe9e1c8c 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardResponse.java @@ -22,9 +22,7 @@ */ public final class GetExtendedAgentCardResponse extends A2AResponse { - private GetExtendedAgentCardResponse() { - } - + private GetExtendedAgentCardResponse() {} /** * Constructs response with full parameters. @@ -34,7 +32,8 @@ private GetExtendedAgentCardResponse() { * @param result the agent card result * @param error the error if any */ - public GetExtendedAgentCardResponse(String jsonrpc, Object id, AgentCard result, A2AError error) { + public GetExtendedAgentCardResponse( + String jsonrpc, Object id, AgentCard result, A2AError error) { super(jsonrpc, id, result, error, AgentCard.class); } @@ -57,5 +56,4 @@ public GetExtendedAgentCardResponse(Object id, A2AError error) { public GetExtendedAgentCardResponse(Object id, AgentCard result) { this(null, id, result, null); } - } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskPushNotificationConfigRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskPushNotificationConfigRequest.java index e5232a3af..a98a4ac49 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskPushNotificationConfigRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskPushNotificationConfigRequest.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; import java.util.UUID; - import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; @@ -20,11 +19,10 @@ * @see TaskPushNotificationConfig for the returned configuration * @see A2A Protocol Specification */ -public final class GetTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { - - private GetTaskPushNotificationConfigRequest() { - } +public final class GetTaskPushNotificationConfigRequest + extends NonStreamingJSONRPCRequest { + private GetTaskPushNotificationConfigRequest() {} /** * Constructs request with all parameters. @@ -33,7 +31,8 @@ private GetTaskPushNotificationConfigRequest() { * @param id the request ID * @param params the request parameters */ - public GetTaskPushNotificationConfigRequest(String jsonrpc, Object id, GetTaskPushNotificationConfigParams params) { + public GetTaskPushNotificationConfigRequest( + String jsonrpc, Object id, GetTaskPushNotificationConfigParams params) { super(jsonrpc, GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, id, params); } @@ -43,7 +42,8 @@ public GetTaskPushNotificationConfigRequest(String jsonrpc, Object id, GetTaskPu * @param id the request ID * @param params the request parameters */ - public GetTaskPushNotificationConfigRequest(String id, GetTaskPushNotificationConfigParams params) { + public GetTaskPushNotificationConfigRequest( + String id, GetTaskPushNotificationConfigParams params) { this(null, id, params); } @@ -67,8 +67,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the jsonrpc. @@ -98,7 +97,8 @@ public GetTaskPushNotificationConfigRequest.Builder id(Object id) { * @param params the request parameters * @return this builder for method chaining */ - public GetTaskPushNotificationConfigRequest.Builder params(GetTaskPushNotificationConfigParams params) { + public GetTaskPushNotificationConfigRequest.Builder params( + GetTaskPushNotificationConfigParams params) { this.params = params; return this; } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskPushNotificationConfigResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskPushNotificationConfigResponse.java index 2cde30d23..3888bcb0c 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskPushNotificationConfigResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskPushNotificationConfigResponse.java @@ -16,11 +16,10 @@ * @see TaskPushNotificationConfig for the configuration structure * @see A2A Protocol Specification */ -public final class GetTaskPushNotificationConfigResponse extends A2AResponse { - - private GetTaskPushNotificationConfigResponse() { - } +public final class GetTaskPushNotificationConfigResponse + extends A2AResponse { + private GetTaskPushNotificationConfigResponse() {} /** * Constructs response with full parameters. @@ -30,7 +29,8 @@ private GetTaskPushNotificationConfigResponse() { * @param result the push notification config result * @param error the error if any */ - public GetTaskPushNotificationConfigResponse(String jsonrpc, Object id, TaskPushNotificationConfig result, A2AError error) { + public GetTaskPushNotificationConfigResponse( + String jsonrpc, Object id, TaskPushNotificationConfig result, A2AError error) { super(jsonrpc, id, result, error, TaskPushNotificationConfig.class); } @@ -53,5 +53,4 @@ public GetTaskPushNotificationConfigResponse(Object id, A2AError error) { public GetTaskPushNotificationConfigResponse(Object id, TaskPushNotificationConfig result) { this(null, id, result, null); } - } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskRequest.java index 1d68c9f4d..6aa9a6488 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskRequest.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.A2AMethods.GET_TASK_METHOD; import java.util.UUID; - import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskQueryParams; @@ -23,9 +22,7 @@ */ public final class GetTaskRequest extends NonStreamingJSONRPCRequest { - private GetTaskRequest() { - } - + private GetTaskRequest() {} /** * Constructs request with all parameters. @@ -68,8 +65,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the JSON-RPC version. diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskResponse.java index 3f373a398..b8e3e4d1c 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetTaskResponse.java @@ -20,9 +20,7 @@ */ public final class GetTaskResponse extends A2AResponse { - private GetTaskResponse() { - } - + private GetTaskResponse() {} /** * Constructs response with full parameters. diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTaskPushNotificationConfigsRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTaskPushNotificationConfigsRequest.java index d15f3671e..ebff8defa 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTaskPushNotificationConfigsRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTaskPushNotificationConfigsRequest.java @@ -1,13 +1,11 @@ package org.a2aproject.sdk.jsonrpc.common.wrappers; +import static org.a2aproject.sdk.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; import java.util.UUID; - import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; -import static org.a2aproject.sdk.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; - /** * JSON-RPC request to list all push notification configurations for a task. *

    @@ -21,11 +19,10 @@ * @see TaskPushNotificationConfig for the configuration structure * @see A2A Protocol Specification */ -public final class ListTaskPushNotificationConfigsRequest extends NonStreamingJSONRPCRequest { - - private ListTaskPushNotificationConfigsRequest() { - } +public final class ListTaskPushNotificationConfigsRequest + extends NonStreamingJSONRPCRequest { + private ListTaskPushNotificationConfigsRequest() {} /** * Constructs request with all parameters. @@ -34,7 +31,8 @@ private ListTaskPushNotificationConfigsRequest() { * @param id the request ID * @param params the request parameters */ - public ListTaskPushNotificationConfigsRequest(String jsonrpc, Object id, ListTaskPushNotificationConfigsParams params) { + public ListTaskPushNotificationConfigsRequest( + String jsonrpc, Object id, ListTaskPushNotificationConfigsParams params) { super(jsonrpc, LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, id, params); } @@ -44,7 +42,8 @@ public ListTaskPushNotificationConfigsRequest(String jsonrpc, Object id, ListTas * @param id the request ID * @param params the request parameters */ - public ListTaskPushNotificationConfigsRequest(String id, ListTaskPushNotificationConfigsParams params) { + public ListTaskPushNotificationConfigsRequest( + String id, ListTaskPushNotificationConfigsParams params) { this(null, id, params); } @@ -68,8 +67,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the jsonrpc. diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTaskPushNotificationConfigsResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTaskPushNotificationConfigsResponse.java index 0bdce5fb7..2435b1375 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTaskPushNotificationConfigsResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTaskPushNotificationConfigsResponse.java @@ -18,11 +18,10 @@ * @see TaskPushNotificationConfig for the configuration structure * @see A2A Protocol Specification */ -public final class ListTaskPushNotificationConfigsResponse extends A2AResponse { - - private ListTaskPushNotificationConfigsResponse() { - } +public final class ListTaskPushNotificationConfigsResponse + extends A2AResponse { + private ListTaskPushNotificationConfigsResponse() {} /** * Constructs response with all parameters. @@ -32,7 +31,11 @@ private ListTaskPushNotificationConfigsResponse() { * @param result the result containing list of push notification configurations and pagination info * @param error the error (if any) */ - public ListTaskPushNotificationConfigsResponse(String jsonrpc, Object id, ListTaskPushNotificationConfigsResult result, A2AError error) { + public ListTaskPushNotificationConfigsResponse( + String jsonrpc, + Object id, + ListTaskPushNotificationConfigsResult result, + A2AError error) { super(jsonrpc, id, result, error, ListTaskPushNotificationConfigsResult.class); } @@ -52,8 +55,8 @@ public ListTaskPushNotificationConfigsResponse(Object id, A2AError error) { * @param id the request ID * @param result the result containing list of push notification configurations and pagination info */ - public ListTaskPushNotificationConfigsResponse(Object id, ListTaskPushNotificationConfigsResult result) { + public ListTaskPushNotificationConfigsResponse( + Object id, ListTaskPushNotificationConfigsResult result) { this(null, id, result, null); } - } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksRequest.java index ebb9fee0c..edc5a90b0 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksRequest.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.A2AMethods.LIST_TASK_METHOD; import java.util.UUID; - import org.a2aproject.sdk.spec.ListTasksParams; /** @@ -11,9 +10,7 @@ */ public final class ListTasksRequest extends NonStreamingJSONRPCRequest { - private ListTasksRequest() { - } - + private ListTasksRequest() {} /** * Constructs request with all parameters. @@ -56,8 +53,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the jsonrpc. diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksResponse.java index bbbe307ab..20c73ca53 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksResponse.java @@ -7,9 +7,7 @@ */ public final class ListTasksResponse extends A2AResponse { - private ListTasksResponse() { - } - + private ListTasksResponse() {} /** * Constructs response with all parameters. diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksResult.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksResult.java index 7844547dd..e47ce4b2e 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksResult.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/ListTasksResult.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.jsonrpc.common.wrappers; import java.util.List; - import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; @@ -16,11 +15,7 @@ * @param nextPageToken Token for retrieving the next page of results (null if no more results) */ public record ListTasksResult( - List tasks, - int totalSize, - int pageSize, - @Nullable String nextPageToken -) { + List tasks, int totalSize, int pageSize, @Nullable String nextPageToken) { /** * Compact constructor for validation. * Validates parameters and creates a defensive copy of the tasks list. @@ -40,7 +35,12 @@ public record ListTasksResult( throw new IllegalArgumentException("pageSize must be non-negative"); } if (pageSize != tasks.size()) { - throw new IllegalArgumentException("pageSize must be equal to the number of tasks in the list. Got " + pageSize + ", but list has " + tasks.size() + " tasks."); + throw new IllegalArgumentException( + "pageSize must be equal to the number of tasks in the list. Got " + + pageSize + + ", but list has " + + tasks.size() + + " tasks."); } // Make defensive copy tasks = List.copyOf(tasks); @@ -85,8 +85,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the tasks list. diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/NonStreamingJSONRPCRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/NonStreamingJSONRPCRequest.java index b2b439715..80d8ebbcf 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/NonStreamingJSONRPCRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/NonStreamingJSONRPCRequest.java @@ -5,13 +5,18 @@ * * @param the type of the request parameters */ -public abstract sealed class NonStreamingJSONRPCRequest extends A2ARequest permits GetTaskRequest, - CancelTaskRequest, CreateTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, - SendMessageRequest, DeleteTaskPushNotificationConfigRequest, ListTaskPushNotificationConfigsRequest, - GetExtendedAgentCardRequest, ListTasksRequest { +public abstract sealed class NonStreamingJSONRPCRequest extends A2ARequest + permits GetTaskRequest, + CancelTaskRequest, + CreateTaskPushNotificationConfigRequest, + GetTaskPushNotificationConfigRequest, + SendMessageRequest, + DeleteTaskPushNotificationConfigRequest, + ListTaskPushNotificationConfigsRequest, + GetExtendedAgentCardRequest, + ListTasksRequest { - NonStreamingJSONRPCRequest() { - } + NonStreamingJSONRPCRequest() {} NonStreamingJSONRPCRequest(String jsonrpc, String method, Object id, T params) { validateAndSetJsonParameters(jsonrpc, method, id, params, true); diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendMessageRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendMessageRequest.java index b2aa03e05..79ca38f92 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendMessageRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendMessageRequest.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.A2AMethods.SEND_MESSAGE_METHOD; import java.util.UUID; - import org.a2aproject.sdk.spec.MessageSendParams; import org.a2aproject.sdk.spec.Task; @@ -34,9 +33,7 @@ */ public final class SendMessageRequest extends NonStreamingJSONRPCRequest { - private SendMessageRequest() { - } - + private SendMessageRequest() {} /** * Constructs a SendMessageRequest with the specified JSON-RPC fields. @@ -93,8 +90,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the JSON-RPC version. diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendMessageResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendMessageResponse.java index f5b52d62a..7fa06618f 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendMessageResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendMessageResponse.java @@ -8,9 +8,7 @@ */ public final class SendMessageResponse extends A2AResponse { - private SendMessageResponse() { - } - + private SendMessageResponse() {} /** * Constructs response with all parameters. diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendStreamingMessageRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendStreamingMessageRequest.java index ce1e6cc8d..f5d924b76 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendStreamingMessageRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendStreamingMessageRequest.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.A2AMethods.SEND_STREAMING_MESSAGE_METHOD; import java.util.UUID; - import org.a2aproject.sdk.spec.MessageSendParams; import org.a2aproject.sdk.spec.StreamingEventKind; @@ -26,9 +25,7 @@ */ public final class SendStreamingMessageRequest extends StreamingJSONRPCRequest { - private SendStreamingMessageRequest() { - } - + private SendStreamingMessageRequest() {} /** * Constructs request with all parameters. @@ -47,7 +44,7 @@ public SendStreamingMessageRequest(String jsonrpc, Object id, MessageSendParams * @param id the request ID * @param params the request parameters */ - public SendStreamingMessageRequest(Object id, MessageSendParams params) { + public SendStreamingMessageRequest(Object id, MessageSendParams params) { this(null, id, params); } @@ -67,59 +64,58 @@ public static Builder builder() { * of JSON-RPC protocol fields. */ public static class Builder { - private String jsonrpc; - private Object id; - private MessageSendParams params; + private String jsonrpc; + private Object id; + private MessageSendParams params; - /** - * Creates a new Builder with all fields unset. - */ - private Builder() { - } + /** + * Creates a new Builder with all fields unset. + */ + private Builder() {} - /** - * Sets the JSON-RPC protocol version. - * - * @param jsonrpc the JSON-RPC version (defaults to "2.0") - * @return this builder - */ - public Builder jsonrpc(String jsonrpc) { - this.jsonrpc = jsonrpc; - return this; - } + /** + * Sets the JSON-RPC protocol version. + * + * @param jsonrpc the JSON-RPC version (defaults to "2.0") + * @return this builder + */ + public Builder jsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; + return this; + } - /** - * Sets the JSON-RPC request ID. - * - * @param id the request identifier (auto-generated UUID if null) - * @return this builder - */ - public Builder id(Object id) { - this.id = id; - return this; - } + /** + * Sets the JSON-RPC request ID. + * + * @param id the request identifier (auto-generated UUID if null) + * @return this builder + */ + public Builder id(Object id) { + this.id = id; + return this; + } - /** - * Sets the message send parameters. - * - * @param params the parameters containing the message and configuration - * @return this builder - */ - public Builder params(MessageSendParams params) { - this.params = params; - return this; - } + /** + * Sets the message send parameters. + * + * @param params the parameters containing the message and configuration + * @return this builder + */ + public Builder params(MessageSendParams params) { + this.params = params; + return this; + } - /** - * Builds the {@link SendStreamingMessageRequest}. - * - * @return a new streaming message request instance - */ - public SendStreamingMessageRequest build() { - if (id == null) { - id = UUID.randomUUID().toString(); - } - return new SendStreamingMessageRequest(jsonrpc, id, params); + /** + * Builds the {@link SendStreamingMessageRequest}. + * + * @return a new streaming message request instance + */ + public SendStreamingMessageRequest build() { + if (id == null) { + id = UUID.randomUUID().toString(); } + return new SendStreamingMessageRequest(jsonrpc, id, params); } } +} diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendStreamingMessageResponse.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendStreamingMessageResponse.java index 89f556a3d..c1ee071d2 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendStreamingMessageResponse.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SendStreamingMessageResponse.java @@ -26,9 +26,7 @@ */ public final class SendStreamingMessageResponse extends A2AResponse { - private SendStreamingMessageResponse() { - } - + private SendStreamingMessageResponse() {} /** * Constructs response with all parameters. @@ -38,7 +36,8 @@ private SendStreamingMessageResponse() { * @param result the result * @param error the error if any */ - public SendStreamingMessageResponse(String jsonrpc, Object id, StreamingEventKind result, A2AError error) { + public SendStreamingMessageResponse( + String jsonrpc, Object id, StreamingEventKind result, A2AError error) { super(jsonrpc, id, result, error, StreamingEventKind.class); } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/StreamingJSONRPCRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/StreamingJSONRPCRequest.java index 8c2cac1cf..0fa7e0c92 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/StreamingJSONRPCRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/StreamingJSONRPCRequest.java @@ -29,14 +29,12 @@ * @see StreamingEventKind * @see A2A Protocol Specification */ -public abstract sealed class StreamingJSONRPCRequest extends A2ARequest permits SubscribeToTaskRequest, - SendStreamingMessageRequest { +public abstract sealed class StreamingJSONRPCRequest extends A2ARequest + permits SubscribeToTaskRequest, SendStreamingMessageRequest { - StreamingJSONRPCRequest() { - } + StreamingJSONRPCRequest() {} StreamingJSONRPCRequest(String jsonrpc, String method, Object id, T params) { validateAndSetJsonParameters(jsonrpc, method, id, params, true); } - } diff --git a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SubscribeToTaskRequest.java b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SubscribeToTaskRequest.java index ca93edc9b..e1945b3b3 100644 --- a/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SubscribeToTaskRequest.java +++ b/jsonrpc-common/src/main/java/org/a2aproject/sdk/jsonrpc/common/wrappers/SubscribeToTaskRequest.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD; import java.util.UUID; - import org.a2aproject.sdk.spec.StreamingEventKind; import org.a2aproject.sdk.spec.TaskIdParams; @@ -29,9 +28,7 @@ */ public final class SubscribeToTaskRequest extends StreamingJSONRPCRequest { - private SubscribeToTaskRequest() { - } - + private SubscribeToTaskRequest() {} /** * Constructs request with all parameters. @@ -41,7 +38,11 @@ private SubscribeToTaskRequest() { * @param params the request parameters */ public SubscribeToTaskRequest(String jsonrpc, Object id, TaskIdParams params) { - super(jsonrpc, SUBSCRIBE_TO_TASK_METHOD, id == null ? UUID.randomUUID().toString() : id, params); + super( + jsonrpc, + SUBSCRIBE_TO_TASK_METHOD, + id == null ? UUID.randomUUID().toString() : id, + params); } /** @@ -74,8 +75,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the JSON-RPC version. diff --git a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/A2AErrorSerializationTest.java b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/A2AErrorSerializationTest.java index 9c1935810..1ce216d53 100644 --- a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/A2AErrorSerializationTest.java +++ b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/A2AErrorSerializationTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import java.util.List; - import org.a2aproject.sdk.spec.A2AError; import org.a2aproject.sdk.spec.A2AErrorCodes; import org.a2aproject.sdk.spec.ContentTypeNotSupportedError; @@ -20,30 +19,46 @@ import org.a2aproject.sdk.spec.UnsupportedOperationError; import org.junit.jupiter.api.Test; - public class A2AErrorSerializationTest { @Test public void shouldDeserializeToCorrectA2AErrorSubclass() throws JsonProcessingException { - String jsonTemplate = """ + String jsonTemplate = + """ {"code": %s, "message": "error", "details": {"key": "anything"}} """; record ErrorCase(int code, Class clazz) {} - List cases = List.of( - new ErrorCase(A2AErrorCodes.JSON_PARSE.code(), JSONParseError.class), - new ErrorCase(A2AErrorCodes.INVALID_REQUEST.code(), InvalidRequestError.class), - new ErrorCase(A2AErrorCodes.METHOD_NOT_FOUND.code(), MethodNotFoundError.class), - new ErrorCase(A2AErrorCodes.INVALID_PARAMS.code(), InvalidParamsError.class), - new ErrorCase(A2AErrorCodes.INTERNAL.code(), InternalError.class), - new ErrorCase(A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED.code(), PushNotificationNotSupportedError.class), - new ErrorCase(A2AErrorCodes.UNSUPPORTED_OPERATION.code(), UnsupportedOperationError.class), - new ErrorCase(A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED.code(), ContentTypeNotSupportedError.class), - new ErrorCase(A2AErrorCodes.INVALID_AGENT_RESPONSE.code(), InvalidAgentResponseError.class), - new ErrorCase(A2AErrorCodes.TASK_NOT_CANCELABLE.code(), TaskNotCancelableError.class), - new ErrorCase(A2AErrorCodes.TASK_NOT_FOUND.code(), TaskNotFoundError.class), - new ErrorCase(Integer.MAX_VALUE, A2AError.class) // Any unknown code will be treated as A2AError - ); + List cases = + List.of( + new ErrorCase(A2AErrorCodes.JSON_PARSE.code(), JSONParseError.class), + new ErrorCase( + A2AErrorCodes.INVALID_REQUEST.code(), InvalidRequestError.class), + new ErrorCase( + A2AErrorCodes.METHOD_NOT_FOUND.code(), MethodNotFoundError.class), + new ErrorCase( + A2AErrorCodes.INVALID_PARAMS.code(), InvalidParamsError.class), + new ErrorCase(A2AErrorCodes.INTERNAL.code(), InternalError.class), + new ErrorCase( + A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED.code(), + PushNotificationNotSupportedError.class), + new ErrorCase( + A2AErrorCodes.UNSUPPORTED_OPERATION.code(), + UnsupportedOperationError.class), + new ErrorCase( + A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED.code(), + ContentTypeNotSupportedError.class), + new ErrorCase( + A2AErrorCodes.INVALID_AGENT_RESPONSE.code(), + InvalidAgentResponseError.class), + new ErrorCase( + A2AErrorCodes.TASK_NOT_CANCELABLE.code(), + TaskNotCancelableError.class), + new ErrorCase(A2AErrorCodes.TASK_NOT_FOUND.code(), TaskNotFoundError.class), + new ErrorCase( + Integer.MAX_VALUE, + A2AError.class) // Any unknown code will be treated as A2AError + ); for (ErrorCase errorCase : cases) { String json = jsonTemplate.formatted(errorCase.code()); @@ -53,6 +68,4 @@ record ErrorCase(int code, Class clazz) {} assertEquals("anything", error.getDetails().get("key")); } } - - } diff --git a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/JsonUtilTest.java b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/JsonUtilTest.java index b07afa814..572a2657d 100644 --- a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/JsonUtilTest.java +++ b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/JsonUtilTest.java @@ -3,12 +3,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.io.StringWriter; -import java.util.Map; - import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.stream.JsonWriter; +import java.io.StringWriter; +import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -19,7 +18,8 @@ public class JsonUtilTest { @Test public void testReadMetadataStringExtractsMetadataField() throws Exception { - String body = "{\"id\":\"task-1\",\"metadata\":{\"reason\":\"user_requested\",\"source\":\"web_ui\"}}"; + String body = + "{\"id\":\"task-1\",\"metadata\":{\"reason\":\"user_requested\",\"source\":\"web_ui\"}}"; Map metadata = JsonUtil.readMetadata(body); assertEquals(2, metadata.size()); assertEquals("user_requested", metadata.get("reason")); @@ -67,7 +67,8 @@ public void testReadMetadataStringConsistentWithJsonObjectOverload() throws Exce @ParameterizedTest @ValueSource(booleans = {true, false}) - public void testWriteJsonRpcIdWritesNullIdAndRestoresSerializeNulls(boolean serializeNulls) throws Exception { + public void testWriteJsonRpcIdWritesNullIdAndRestoresSerializeNulls(boolean serializeNulls) + throws Exception { StringWriter result = new StringWriter(); JsonWriter out = new JsonWriter(result); out.setSerializeNulls(serializeNulls); diff --git a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/PartSerializationTest.java b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/PartSerializationTest.java index 5798e6394..464a9ae4e 100644 --- a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/PartSerializationTest.java +++ b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/PartSerializationTest.java @@ -5,7 +5,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Map; - import org.a2aproject.sdk.spec.DataPart; import org.a2aproject.sdk.spec.FilePart; import org.a2aproject.sdk.spec.FileWithBytes; @@ -23,9 +22,10 @@ public class PartSerializationTest { @Test void testPopulatedDataWinsOverEmptyTextPlaceholder() throws JsonProcessingException { - String json = """ - {"text": "", "data": {"answer": "42"}} - """; + String json = + """ + {"text": "", "data": {"answer": "42"}} + """; Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(DataPart.class, part); @SuppressWarnings("unchecked") @@ -35,9 +35,10 @@ void testPopulatedDataWinsOverEmptyTextPlaceholder() throws JsonProcessingExcept @Test void testDataPartDecodesRegardlessOfContentKeyOrder() throws JsonProcessingException { - String json = """ - {"data": {"answer": "42"}, "text": ""} - """; + String json = + """ + {"data": {"answer": "42"}, "text": ""} + """; Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(DataPart.class, part); @SuppressWarnings("unchecked") @@ -47,9 +48,10 @@ void testDataPartDecodesRegardlessOfContentKeyOrder() throws JsonProcessingExcep @Test void testPopulatedDataWinsOverAllEmptyPlaceholders() throws JsonProcessingException { - String json = """ - {"text": "", "raw": "", "url": "", "data": {"answer": "42"}} - """; + String json = + """ + {"text": "", "raw": "", "url": "", "data": {"answer": "42"}} + """; Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(DataPart.class, part); @SuppressWarnings("unchecked") @@ -59,9 +61,10 @@ void testPopulatedDataWinsOverAllEmptyPlaceholders() throws JsonProcessingExcept @Test void testPopulatedUrlWinsOverEmptyTextPlaceholder() throws JsonProcessingException { - String json = """ - {"text": "", "url": "https://example.org/report.pdf", "mediaType": "application/pdf"} - """; + String json = + """ + {"text": "", "url": "https://example.org/report.pdf", "mediaType": "application/pdf"} + """; Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(FilePart.class, part); FileWithUri file = (FileWithUri) ((FilePart) part).file(); @@ -71,9 +74,10 @@ void testPopulatedUrlWinsOverEmptyTextPlaceholder() throws JsonProcessingExcepti @Test void testPopulatedRawWinsOverEmptyTextPlaceholder() throws JsonProcessingException { - String json = """ - {"text": "", "raw": "abc12w==", "filename": "diagram.png", "mediaType": "image/png"} - """; + String json = + """ + {"text": "", "raw": "abc12w==", "filename": "diagram.png", "mediaType": "image/png"} + """; Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(FilePart.class, part); FileWithBytes file = (FileWithBytes) ((FilePart) part).file(); @@ -85,16 +89,18 @@ void testPopulatedRawWinsOverEmptyTextPlaceholder() throws JsonProcessingExcepti /** Emptiness is decided on the JSON string only, so a falsy data payload still outranks a placeholder. */ @ParameterizedTest @ValueSource(strings = {"0", "false", "{}", "[]"}) - void testFalsyDataPayloadWinsOverEmptyTextPlaceholder(String payload) throws JsonProcessingException { + void testFalsyDataPayloadWinsOverEmptyTextPlaceholder(String payload) + throws JsonProcessingException { Part part = JsonUtil.fromJson("{\"text\": \"\", \"data\": " + payload + "}", Part.class); assertInstanceOf(DataPart.class, part); } @Test void testJsonNullContentKeyIsSkipped() throws JsonProcessingException { - String json = """ - {"text": "hello", "data": null} - """; + String json = + """ + {"text": "hello", "data": null} + """; Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(TextPart.class, part); assertEquals("hello", ((TextPart) part).text()); @@ -102,9 +108,10 @@ void testJsonNullContentKeyIsSkipped() throws JsonProcessingException { @Test void testPopulatedDataWinsOverJsonNullText() throws JsonProcessingException { - String json = """ - {"text": null, "data": {"answer": "42"}} - """; + String json = + """ + {"text": null, "data": {"answer": "42"}} + """; Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(DataPart.class, part); @SuppressWarnings("unchecked") @@ -114,9 +121,10 @@ void testPopulatedDataWinsOverJsonNullText() throws JsonProcessingException { @Test void testDeliberatelyEmptyTextPartDecodes() throws JsonProcessingException { - String json = """ - {"text": ""} - """; + String json = + """ + {"text": ""} + """; Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(TextPart.class, part); assertEquals("", ((TextPart) part).text()); @@ -132,9 +140,10 @@ void testEmptyTextPartRoundTrips() throws JsonProcessingException { @Test void testDataPartWithEmptyStringPayloadDecodes() throws JsonProcessingException { - String json = """ - {"data": ""} - """; + String json = + """ + {"data": ""} + """; Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(DataPart.class, part); assertEquals("", ((DataPart) part).data()); @@ -143,9 +152,10 @@ void testDataPartWithEmptyStringPayloadDecodes() throws JsonProcessingException /** Nothing distinguishes the intended content when every key is an empty string, so order decides. */ @Test void testAllEmptyContentKeysFallBackToTheFirst() throws JsonProcessingException { - String json = """ - {"text": "", "data": ""} - """; + String json = + """ + {"text": "", "data": ""} + """; Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(TextPart.class, part); assertEquals("", ((TextPart) part).text()); @@ -153,17 +163,19 @@ void testAllEmptyContentKeysFallBackToTheFirst() throws JsonProcessingException @Test void testMultiplePopulatedContentKeysAreRejected() { - String json = """ - {"text": "hello", "data": {"answer": "42"}} - """; + String json = + """ + {"text": "hello", "data": {"answer": "42"}} + """; assertThrows(JsonProcessingException.class, () -> JsonUtil.fromJson(json, Part.class)); } @Test void testNonStringContentKeyIsRejected() { - String json = """ - {"text": "", "raw": {"nested": 1}} - """; + String json = + """ + {"text": "", "raw": {"nested": 1}} + """; assertThrows(JsonProcessingException.class, () -> JsonUtil.fromJson(json, Part.class)); } } diff --git a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/SecurityRequirementSerializationTest.java b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/SecurityRequirementSerializationTest.java index cf82d9935..7ecdc2e99 100644 --- a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/SecurityRequirementSerializationTest.java +++ b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/SecurityRequirementSerializationTest.java @@ -5,10 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import java.util.List; - -import org.junit.jupiter.api.Test; - import org.a2aproject.sdk.spec.SecurityRequirement; +import org.junit.jupiter.api.Test; /** * Tests for SecurityRequirement serialization and deserialization with JSON. @@ -17,15 +15,16 @@ class SecurityRequirementSerializationTest { @Test void testSecurityRequirementSerializationWithSingleScheme() throws JsonProcessingException { - SecurityRequirement requirement = SecurityRequirement.builder() - .scheme("oauth2", List.of("read", "write")) - .build(); + SecurityRequirement requirement = + SecurityRequirement.builder().scheme("oauth2", List.of("read", "write")).build(); String json = JsonUtil.toJson(requirement); assertNotNull(json); - String expected = """ - {"schemes":{"oauth2":{"list":["read","write"]}}}"""; + String expected = + """ + {"schemes":{"oauth2":{"list":["read","write"]}}}\ + """; assertEquals(expected, json); SecurityRequirement deserialized = JsonUtil.fromJson(json, SecurityRequirement.class); @@ -34,16 +33,19 @@ void testSecurityRequirementSerializationWithSingleScheme() throws JsonProcessin @Test void testSecurityRequirementSerializationWithMultipleSchemes() throws JsonProcessingException { - SecurityRequirement requirement = SecurityRequirement.builder() - .scheme("oauth2", List.of("profile")) - .scheme("apiKey", List.of()) - .build(); + SecurityRequirement requirement = + SecurityRequirement.builder() + .scheme("oauth2", List.of("profile")) + .scheme("apiKey", List.of()) + .build(); String json = JsonUtil.toJson(requirement); assertNotNull(json); - String expected = """ - {"schemes":{"oauth2":{"list":["profile"]},"apiKey":{"list":[]}}}"""; + String expected = + """ + {"schemes":{"oauth2":{"list":["profile"]},"apiKey":{"list":[]}}}\ + """; assertEquals(expected, json); SecurityRequirement deserialized = JsonUtil.fromJson(json, SecurityRequirement.class); @@ -52,15 +54,16 @@ void testSecurityRequirementSerializationWithMultipleSchemes() throws JsonProces @Test void testSecurityRequirementSerializationWithEmptyScopes() throws JsonProcessingException { - SecurityRequirement requirement = SecurityRequirement.builder() - .scheme("apiKey", List.of()) - .build(); + SecurityRequirement requirement = + SecurityRequirement.builder().scheme("apiKey", List.of()).build(); String json = JsonUtil.toJson(requirement); assertNotNull(json); - String expected = """ - {"schemes":{"apiKey":{"list":[]}}}"""; + String expected = + """ + {"schemes":{"apiKey":{"list":[]}}}\ + """; assertEquals(expected, json); SecurityRequirement deserialized = JsonUtil.fromJson(json, SecurityRequirement.class); @@ -74,8 +77,10 @@ void testSecurityRequirementSerializationWithNullSchemes() throws JsonProcessing String json = JsonUtil.toJson(requirement); assertNotNull(json); - String expected = """ - {"schemes":{}}"""; + String expected = + """ + {"schemes":{}}\ + """; assertEquals(expected, json); SecurityRequirement deserialized = JsonUtil.fromJson(json, SecurityRequirement.class); diff --git a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/SecuritySchemeSerializationTest.java b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/SecuritySchemeSerializationTest.java index 9ae1bbf12..4a3c77eac 100644 --- a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/SecuritySchemeSerializationTest.java +++ b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/SecuritySchemeSerializationTest.java @@ -1,9 +1,9 @@ package org.a2aproject.sdk.jsonrpc.common.json; +import static java.lang.String.format; import static org.a2aproject.sdk.spec.APIKeySecurityScheme.Location.COOKIE; import static org.a2aproject.sdk.spec.APIKeySecurityScheme.Location.HEADER; import static org.a2aproject.sdk.spec.APIKeySecurityScheme.Location.QUERY; -import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -11,14 +11,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Map; - -import org.junit.jupiter.api.Test; - import org.a2aproject.sdk.spec.APIKeySecurityScheme; import org.a2aproject.sdk.spec.HTTPAuthSecurityScheme; import org.a2aproject.sdk.spec.MutualTLSSecurityScheme; import org.a2aproject.sdk.spec.OpenIdConnectSecurityScheme; import org.a2aproject.sdk.spec.SecurityScheme; +import org.junit.jupiter.api.Test; /** * Tests for SecurityScheme serialization and deserialization using Gson. @@ -27,23 +25,26 @@ class SecuritySchemeSerializationTest { @Test void testHTTPAuthSecuritySchemeSerialization() throws JsonProcessingException { - SecurityScheme scheme = HTTPAuthSecurityScheme.builder() - .scheme("basic") - .description("Basic HTTP authentication") - .build(); - - doTestSecuritySchemeSerialization(scheme, HTTPAuthSecurityScheme.TYPE, Map.of("scheme", "basic")); - + SecurityScheme scheme = + HTTPAuthSecurityScheme.builder() + .scheme("basic") + .description("Basic HTTP authentication") + .build(); + + doTestSecuritySchemeSerialization( + scheme, HTTPAuthSecurityScheme.TYPE, Map.of("scheme", "basic")); } @Test void testHTTPAuthSecuritySchemeDeserialization() throws JsonProcessingException { - String json = """ + String json = + """ { "httpAuthSecurityScheme" : { "scheme": "basic" } - }"""; + }\ + """; SecurityScheme securityScheme = JsonUtil.fromJson(json, SecurityScheme.class); assertInstanceOf(HTTPAuthSecurityScheme.class, securityScheme); HTTPAuthSecurityScheme scheme = (HTTPAuthSecurityScheme) securityScheme; @@ -53,28 +54,38 @@ void testHTTPAuthSecuritySchemeDeserialization() throws JsonProcessingException @Test void testHTTPAuthSecuritySchemeWithBearerFormatSerialization() throws JsonProcessingException { - SecurityScheme scheme = HTTPAuthSecurityScheme.builder() - .scheme("bearer") - .bearerFormat("JWT") - .description("JWT bearer token authentication") - .build(); - - doTestSecuritySchemeSerialization(scheme, HTTPAuthSecurityScheme.TYPE, - Map.of("scheme", "bearer", - "bearerFormat", "JWT", - "description", "JWT bearer token authentication")); + SecurityScheme scheme = + HTTPAuthSecurityScheme.builder() + .scheme("bearer") + .bearerFormat("JWT") + .description("JWT bearer token authentication") + .build(); + + doTestSecuritySchemeSerialization( + scheme, + HTTPAuthSecurityScheme.TYPE, + Map.of( + "scheme", + "bearer", + "bearerFormat", + "JWT", + "description", + "JWT bearer token authentication")); } @Test - void testHTTPAuthSecuritySchemeWithBearerFormatDeserialization() throws JsonProcessingException { - String json = """ + void testHTTPAuthSecuritySchemeWithBearerFormatDeserialization() + throws JsonProcessingException { + String json = + """ { "httpAuthSecurityScheme" : { "scheme": "bearer", "bearerFormat": "JWT", "description": "JWT authentication" } - }"""; + }\ + """; SecurityScheme securityScheme = JsonUtil.fromJson(json, SecurityScheme.class); assertInstanceOf(HTTPAuthSecurityScheme.class, securityScheme); HTTPAuthSecurityScheme scheme = (HTTPAuthSecurityScheme) securityScheme; @@ -83,27 +94,25 @@ void testHTTPAuthSecuritySchemeWithBearerFormatDeserialization() throws JsonProc assertEquals("JWT authentication", scheme.description()); } - @Test void testAPIKeySecuritySchemeSerialization() throws JsonProcessingException { - SecurityScheme scheme = APIKeySecurityScheme.builder() - .location(HEADER) - .name("bar") - .build(); + SecurityScheme scheme = APIKeySecurityScheme.builder().location(HEADER).name("bar").build(); - doTestSecuritySchemeSerialization(scheme, APIKeySecurityScheme.TYPE, Map.of("location", "header", - "name", "bar")); + doTestSecuritySchemeSerialization( + scheme, APIKeySecurityScheme.TYPE, Map.of("location", "header", "name", "bar")); } @Test void testAPIKeySecuritySchemeDeserialization() throws JsonProcessingException { - String json = """ + String json = + """ { "apiKeySecurityScheme" : { "location": "cookie", "name": "bar" } - }"""; + }\ + """; SecurityScheme securityScheme = JsonUtil.fromJson(json, SecurityScheme.class); assertInstanceOf(APIKeySecurityScheme.class, securityScheme); APIKeySecurityScheme scheme = (APIKeySecurityScheme) securityScheme; @@ -113,63 +122,84 @@ void testAPIKeySecuritySchemeDeserialization() throws JsonProcessingException { @Test void testAPIKeySecuritySchemeWithDescriptionSerialization() throws JsonProcessingException { - SecurityScheme scheme = APIKeySecurityScheme.builder() - .location(QUERY) - .name("api_key") - .description("API key authentication via query parameter") - .build(); - - doTestSecuritySchemeSerialization(scheme, APIKeySecurityScheme.TYPE, - Map.of("location", "query", - "name", "api_key", - "description", "API key authentication via query parameter")); + SecurityScheme scheme = + APIKeySecurityScheme.builder() + .location(QUERY) + .name("api_key") + .description("API key authentication via query parameter") + .build(); + + doTestSecuritySchemeSerialization( + scheme, + APIKeySecurityScheme.TYPE, + Map.of( + "location", + "query", + "name", + "api_key", + "description", + "API key authentication via query parameter")); } @Test void testOpenIdConnectSecuritySchemeSerialization() throws JsonProcessingException { - SecurityScheme scheme = OpenIdConnectSecurityScheme.builder() - .openIdConnectUrl("https://example.com/.well-known/openid-configuration") - .description("OpenID Connect authentication") - .build(); - - doTestSecuritySchemeSerialization(scheme, OpenIdConnectSecurityScheme.TYPE, - Map.of("openIdConnectUrl", "https://example.com/.well-known/openid-configuration", - "description", "OpenID Connect authentication")); + SecurityScheme scheme = + OpenIdConnectSecurityScheme.builder() + .openIdConnectUrl("https://example.com/.well-known/openid-configuration") + .description("OpenID Connect authentication") + .build(); + + doTestSecuritySchemeSerialization( + scheme, + OpenIdConnectSecurityScheme.TYPE, + Map.of( + "openIdConnectUrl", + "https://example.com/.well-known/openid-configuration", + "description", + "OpenID Connect authentication")); } @Test void testOpenIdConnectSecuritySchemeDeserialization() throws JsonProcessingException { - String json = """ + String json = + """ { "openIdConnectSecurityScheme" : { "openIdConnectUrl": "https://example.com/.well-known/openid-configuration", "description": "OIDC authentication" } - }"""; + }\ + """; SecurityScheme securityScheme = JsonUtil.fromJson(json, SecurityScheme.class); assertInstanceOf(OpenIdConnectSecurityScheme.class, securityScheme); OpenIdConnectSecurityScheme scheme = (OpenIdConnectSecurityScheme) securityScheme; - assertEquals("https://example.com/.well-known/openid-configuration", scheme.openIdConnectUrl()); + assertEquals( + "https://example.com/.well-known/openid-configuration", scheme.openIdConnectUrl()); assertEquals("OIDC authentication", scheme.description()); } @Test void testMutualTLSSecuritySchemeSerialization() throws JsonProcessingException { - SecurityScheme scheme = new MutualTLSSecurityScheme("Client certificate authentication required"); + SecurityScheme scheme = + new MutualTLSSecurityScheme("Client certificate authentication required"); - doTestSecuritySchemeSerialization(scheme, MutualTLSSecurityScheme.TYPE, + doTestSecuritySchemeSerialization( + scheme, + MutualTLSSecurityScheme.TYPE, Map.of("description", "Client certificate authentication required")); } @Test void testMutualTLSSecuritySchemeDeserialization() throws JsonProcessingException { - String json = """ + String json = + """ { "mtlsSecurityScheme" : { "description": "mTLS authentication" } - }"""; + }\ + """; SecurityScheme securityScheme = JsonUtil.fromJson(json, SecurityScheme.class); assertInstanceOf(MutualTLSSecurityScheme.class, securityScheme); @@ -178,12 +208,15 @@ void testMutualTLSSecuritySchemeDeserialization() throws JsonProcessingException } @Test - void testMutualTLSSecuritySchemeWithNullDescriptionDeserialization() throws JsonProcessingException { - String json = """ + void testMutualTLSSecuritySchemeWithNullDescriptionDeserialization() + throws JsonProcessingException { + String json = + """ { "mtlsSecurityScheme" : { } - }"""; + }\ + """; SecurityScheme securityScheme = JsonUtil.fromJson(json, SecurityScheme.class); assertInstanceOf(MutualTLSSecurityScheme.class, securityScheme); @@ -191,7 +224,9 @@ void testMutualTLSSecuritySchemeWithNullDescriptionDeserialization() throws Json assertNull(scheme.description()); } - void doTestSecuritySchemeSerialization(SecurityScheme scheme, String schemeType, Map expectedFields) throws JsonProcessingException { + void doTestSecuritySchemeSerialization( + SecurityScheme scheme, String schemeType, Map expectedFields) + throws JsonProcessingException { // Serialize to JSON String json = JsonUtil.toJson(scheme); @@ -208,4 +243,4 @@ void doTestSecuritySchemeSerialization(SecurityScheme scheme, String schemeType, assertEquals(scheme, deserialized); } -} \ No newline at end of file +} diff --git a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/StreamingEventKindSerializationTest.java b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/StreamingEventKindSerializationTest.java index 1c2c83ac5..6a321c017 100644 --- a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/StreamingEventKindSerializationTest.java +++ b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/StreamingEventKindSerializationTest.java @@ -7,7 +7,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; - import org.a2aproject.sdk.spec.Artifact; import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.StreamingEventKind; @@ -30,11 +29,12 @@ class StreamingEventKindSerializationTest { @Test void testTaskSerialization() throws JsonProcessingException { // Create a Task - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); // Serialize as StreamingEventKind String json = JsonUtil.toJsonStreamingEvent(task); @@ -60,13 +60,14 @@ void testTaskSerialization() throws JsonProcessingException { @Test void testMessageSerialization() throws JsonProcessingException { // Create a Message - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("Hello, agent!"))) - .taskId("task-789") - .messageId("msg-123") - .contextId("context-456") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("Hello, agent!"))) + .taskId("task-789") + .messageId("msg-123") + .contextId("context-456") + .build(); // Serialize as StreamingEventKind String json = JsonUtil.toJsonStreamingEvent(message); @@ -93,11 +94,12 @@ void testMessageSerialization() throws JsonProcessingException { @Test void testTaskStatusUpdateEventSerialization() throws JsonProcessingException { // Create a TaskStatusUpdateEvent - TaskStatusUpdateEvent statusEvent = TaskStatusUpdateEvent.builder() - .taskId("task-abc") - .contextId("context-def") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + TaskStatusUpdateEvent statusEvent = + TaskStatusUpdateEvent.builder() + .taskId("task-abc") + .contextId("context-def") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); // Serialize as StreamingEventKind String json = JsonUtil.toJsonStreamingEvent(statusEvent); @@ -124,17 +126,19 @@ void testTaskStatusUpdateEventSerialization() throws JsonProcessingException { @Test void testTaskArtifactUpdateEventSerialization() throws JsonProcessingException { // Create a TaskArtifactUpdateEvent - Artifact artifact = Artifact.builder() - .artifactId("artifact-xyz") - .name("Test Artifact") - .parts(List.of(new TextPart("Artifact content"))) - .build(); - - TaskArtifactUpdateEvent artifactEvent = TaskArtifactUpdateEvent.builder() - .taskId("task-123") - .contextId("context-456") - .artifact(artifact) - .build(); + Artifact artifact = + Artifact.builder() + .artifactId("artifact-xyz") + .name("Test Artifact") + .parts(List.of(new TextPart("Artifact content"))) + .build(); + + TaskArtifactUpdateEvent artifactEvent = + TaskArtifactUpdateEvent.builder() + .taskId("task-123") + .contextId("context-456") + .artifact(artifact) + .build(); // Serialize as StreamingEventKind String json = JsonUtil.toJsonStreamingEvent(artifactEvent); @@ -154,21 +158,23 @@ void testTaskArtifactUpdateEventSerialization() throws JsonProcessingException { assertInstanceOf(TaskArtifactUpdateEvent.class, deserialized); TaskArtifactUpdateEvent deserializedEvent = (TaskArtifactUpdateEvent) deserialized; assertEquals(artifactEvent.taskId(), deserializedEvent.taskId()); - assertEquals(artifactEvent.artifact().artifactId(), deserializedEvent.artifact().artifactId()); + assertEquals( + artifactEvent.artifact().artifactId(), deserializedEvent.artifact().artifactId()); } @Test void testUnwrappedTaskDeserialization() throws JsonProcessingException { // Test that unwrapped Task format (direct deserialization) still works - String json = """ - { - "id": "task-unwrapped", - "contextId": "context-999", - "status": { - "state": "TASK_STATE_COMPLETED" - } - } - """; + String json = + """ + { + "id": "task-unwrapped", + "contextId": "context-999", + "status": { + "state": "TASK_STATE_COMPLETED" + } + } + """; // Deserialize as StreamingEventKind StreamingEventKind deserialized = JsonUtil.fromJson(json, StreamingEventKind.class); @@ -184,18 +190,19 @@ void testUnwrappedTaskDeserialization() throws JsonProcessingException { @Test void testUnwrappedMessageDeserialization() throws JsonProcessingException { // Test that unwrapped Message format (direct deserialization) still works - String json = """ - { - "role": "ROLE_AGENT", - "parts": [ + String json = + """ { - "text": "Unwrapped message" + "role": "ROLE_AGENT", + "parts": [ + { + "text": "Unwrapped message" + } + ], + "messageId": "msg-unwrapped", + "taskId": "task-999" } - ], - "messageId": "msg-unwrapped", - "taskId": "task-999" - } - """; + """; // Deserialize as StreamingEventKind StreamingEventKind deserialized = JsonUtil.fromJson(json, StreamingEventKind.class); @@ -211,15 +218,16 @@ void testUnwrappedMessageDeserialization() throws JsonProcessingException { @Test void testUnwrappedTaskStatusUpdateEventDeserialization() throws JsonProcessingException { // Test that unwrapped TaskStatusUpdateEvent format still works - String json = """ - { - "taskId": "task-status-unwrapped", - "contextId": "context-999", - "status": { - "state": "TASK_STATE_WORKING" - } - } - """; + String json = + """ + { + "taskId": "task-status-unwrapped", + "contextId": "context-999", + "status": { + "state": "TASK_STATE_WORKING" + } + } + """; // Deserialize as StreamingEventKind StreamingEventKind deserialized = JsonUtil.fromJson(json, StreamingEventKind.class); @@ -235,20 +243,21 @@ void testUnwrappedTaskStatusUpdateEventDeserialization() throws JsonProcessingEx @Test void testUnwrappedTaskArtifactUpdateEventDeserialization() throws JsonProcessingException { // Test that unwrapped TaskArtifactUpdateEvent format still works - String json = """ - { - "taskId": "task-artifact-unwrapped", - "contextId": "context-999", - "artifact": { - "artifactId": "artifact-unwrapped", - "parts": [ - { - "text": "Unwrapped artifact" + String json = + """ + { + "taskId": "task-artifact-unwrapped", + "contextId": "context-999", + "artifact": { + "artifactId": "artifact-unwrapped", + "parts": [ + { + "text": "Unwrapped artifact" + } + ] } - ] - } - } - """; + } + """; // Deserialize as StreamingEventKind StreamingEventKind deserialized = JsonUtil.fromJson(json, StreamingEventKind.class); diff --git a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/TaskSerializationTest.java b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/TaskSerializationTest.java index 05110a9c1..a79d2303d 100644 --- a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/TaskSerializationTest.java +++ b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/json/TaskSerializationTest.java @@ -7,15 +7,14 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import java.nio.file.Files; import java.nio.file.Path; import java.time.OffsetDateTime; import java.util.Base64; import java.util.List; import java.util.Map; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; import org.a2aproject.sdk.jsonrpc.common.wrappers.SendMessageResponse; @@ -42,11 +41,12 @@ class TaskSerializationTest { @Test void testBasicTaskSerialization() throws JsonProcessingException { // Create a basic task - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); // Serialize to JSON String json = JsonUtil.toJson(task); @@ -68,19 +68,21 @@ void testBasicTaskSerialization() throws JsonProcessingException { @Test void testListTasksSerializationDoesNotWrapTaskItems() throws JsonProcessingException { - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); ListTasksResult result = new ListTasksResult(List.of(task)); String resultJson = JsonUtil.toJson(result); - JsonObject resultTaskJson = JsonParser.parseString(resultJson) - .getAsJsonObject() - .getAsJsonArray("tasks") - .get(0) - .getAsJsonObject(); + JsonObject resultTaskJson = + JsonParser.parseString(resultJson) + .getAsJsonObject() + .getAsJsonArray("tasks") + .get(0) + .getAsJsonObject(); assertFalse(resultTaskJson.has(Task.STREAMING_EVENT_ID)); assertEquals(task.id(), resultTaskJson.get("id").getAsString()); @@ -88,12 +90,13 @@ void testListTasksSerializationDoesNotWrapTaskItems() throws JsonProcessingExcep ListTasksResponse response = new ListTasksResponse("request-1", result); String responseJson = JsonUtil.toJson(response); - JsonObject responseTaskJson = JsonParser.parseString(responseJson) - .getAsJsonObject() - .getAsJsonObject("result") - .getAsJsonArray("tasks") - .get(0) - .getAsJsonObject(); + JsonObject responseTaskJson = + JsonParser.parseString(responseJson) + .getAsJsonObject() + .getAsJsonObject("result") + .getAsJsonArray("tasks") + .get(0) + .getAsJsonObject(); assertFalse(responseTaskJson.has(Task.STREAMING_EVENT_ID)); assertEquals(task.id(), responseTaskJson.get("id").getAsString()); @@ -101,18 +104,19 @@ void testListTasksSerializationDoesNotWrapTaskItems() throws JsonProcessingExcep } @Test - void testSendMessageResponseSerializationKeepsEventKindWrapper() throws JsonProcessingException { - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + void testSendMessageResponseSerializationKeepsEventKindWrapper() + throws JsonProcessingException { + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); SendMessageResponse response = new SendMessageResponse("request-1", task); String responseJson = JsonUtil.toJson(response); - JsonObject responseResultJson = JsonParser.parseString(responseJson) - .getAsJsonObject() - .getAsJsonObject("result"); + JsonObject responseResultJson = + JsonParser.parseString(responseJson).getAsJsonObject().getAsJsonObject("result"); assertTrue(responseResultJson.has(Task.STREAMING_EVENT_ID)); JsonObject taskJson = responseResultJson.getAsJsonObject(Task.STREAMING_EVENT_ID); @@ -124,11 +128,12 @@ void testSendMessageResponseSerializationKeepsEventKindWrapper() throws JsonProc void testTaskWithTimestamp() throws JsonProcessingException { OffsetDateTime timestamp = OffsetDateTime.now(); - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, timestamp)) - .build(); + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, timestamp)) + .build(); // Serialize String json = JsonUtil.toJson(task); @@ -143,22 +148,21 @@ void testTaskWithTimestamp() throws JsonProcessingException { @Test void testTaskWithArtifacts() throws JsonProcessingException { - Artifact artifact = Artifact.builder() - .artifactId("artifact-1") - .name("Test Artifact") - .description("Description of artifact") - .parts(List.of( - new TextPart("Hello"), - new TextPart("World") - )) - .build(); - - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .artifacts(List.of(artifact)) - .build(); + Artifact artifact = + Artifact.builder() + .artifactId("artifact-1") + .name("Test Artifact") + .description("Description of artifact") + .parts(List.of(new TextPart("Hello"), new TextPart("World"))) + .build(); + + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .artifacts(List.of(artifact)) + .build(); // Serialize String json = JsonUtil.toJson(task); @@ -180,17 +184,19 @@ void testTaskWithArtifacts() throws JsonProcessingException { @Test void testTaskWithHistory() throws JsonProcessingException { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("Test message"))) - .build(); - - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .history(List.of(message)) - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("Test message"))) + .build(); + + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .history(List.of(message)) + .build(); // Serialize String json = JsonUtil.toJson(task); @@ -213,28 +219,29 @@ void testTaskWithHistory() throws JsonProcessingException { void testTaskWithAllFields() throws JsonProcessingException { OffsetDateTime timestamp = OffsetDateTime.now(); - Task task = Task.builder() - .id("task-123") - .contextId("context-789") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, timestamp)) - .history(List.of( - Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of(new TextPart("User message"))) - .build(), - Message.builder() - .role(Message.Role.ROLE_AGENT) - .parts(List.of(new TextPart("Agent response"))) - .build() - )) - .artifacts(List.of( - Artifact.builder() - .artifactId("artifact-1") - .parts(List.of(new TextPart("Artifact content"))) - .build() - )) - .metadata(Map.of("key1", "value1", "key2", 42)) - .build(); + Task task = + Task.builder() + .id("task-123") + .contextId("context-789") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, timestamp)) + .history( + List.of( + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(List.of(new TextPart("User message"))) + .build(), + Message.builder() + .role(Message.Role.ROLE_AGENT) + .parts(List.of(new TextPart("Agent response"))) + .build())) + .artifacts( + List.of( + Artifact.builder() + .artifactId("artifact-1") + .parts(List.of(new TextPart("Artifact content"))) + .build())) + .metadata(Map.of("key1", "value1", "key2", 42)) + .build(); // Serialize String json = JsonUtil.toJson(task); @@ -256,11 +263,12 @@ void testTaskWithAllFields() throws JsonProcessingException { @Test void testTaskWithDifferentStates() throws JsonProcessingException { for (TaskState state : TaskState.values()) { - Task task = Task.builder() - .id("task-" + state) - .contextId("context-123") - .status(new TaskStatus(state)) - .build(); + Task task = + Task.builder() + .id("task-" + state) + .contextId("context-123") + .status(new TaskStatus(state)) + .build(); // Serialize String json = JsonUtil.toJson(task); @@ -278,12 +286,13 @@ void testTaskWithDifferentStates() throws JsonProcessingException { @Test void testTaskWithNullOptionalFields() throws JsonProcessingException { - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - // artifacts, history, metadata not set - .build(); + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + // artifacts, history, metadata not set + .build(); // Serialize String json = JsonUtil.toJson(task); @@ -305,24 +314,25 @@ void testTaskWithNullOptionalFields() throws JsonProcessingException { @Test void testTaskWithFilePartBytes() throws JsonProcessingException { - FilePart filePart = new FilePart(new FileWithBytes("application/pdf", "document.pdf", "base64data")); + FilePart filePart = + new FilePart(new FileWithBytes("application/pdf", "document.pdf", "base64data")); - Artifact artifact = Artifact.builder() - .artifactId("file-artifact") - .parts(List.of(filePart)) - .build(); + Artifact artifact = + Artifact.builder().artifactId("file-artifact").parts(List.of(filePart)).build(); - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .artifacts(List.of(artifact)) - .build(); + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .artifacts(List.of(artifact)) + .build(); // Serialize String json = JsonUtil.toJson(task); - // Verify JSON contains file part data in flat format (raw/filename/mediaType, not "file" wrapper) + // Verify JSON contains file part data in flat format (raw/filename/mediaType, not "file" + // wrapper) assertTrue(json.contains("\"raw\"")); assertFalse(json.contains("\"kind\"")); assertTrue(json.contains("document.pdf")); @@ -343,19 +353,20 @@ void testTaskWithFilePartBytes() throws JsonProcessingException { @Test void testTaskWithFilePartUri() throws JsonProcessingException { - FilePart filePart = new FilePart(new FileWithUri("image/png", "photo.png", "https://example.com/photo.png")); + FilePart filePart = + new FilePart( + new FileWithUri("image/png", "photo.png", "https://example.com/photo.png")); - Artifact artifact = Artifact.builder() - .artifactId("uri-artifact") - .parts(List.of(filePart)) - .build(); + Artifact artifact = + Artifact.builder().artifactId("uri-artifact").parts(List.of(filePart)).build(); - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .artifacts(List.of(artifact)) - .build(); + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .artifacts(List.of(artifact)) + .build(); // Serialize String json = JsonUtil.toJson(task); @@ -380,17 +391,16 @@ void testTaskWithFilePartUri() throws JsonProcessingException { void testTaskWithDataPart() throws JsonProcessingException { DataPart dataPart = new DataPart(Map.of("temperature", 22.5, "humidity", 65)); - Artifact artifact = Artifact.builder() - .artifactId("data-artifact") - .parts(List.of(dataPart)) - .build(); + Artifact artifact = + Artifact.builder().artifactId("data-artifact").parts(List.of(dataPart)).build(); - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .artifacts(List.of(artifact)) - .build(); + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .artifacts(List.of(artifact)) + .build(); // Serialize String json = JsonUtil.toJson(task); @@ -415,28 +425,34 @@ void testTaskRoundTrip() throws JsonProcessingException { // Create a comprehensive task with all part types OffsetDateTime timestamp = OffsetDateTime.now(); - Task original = Task.builder() - .id("task-123") - .contextId("context-789") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, timestamp)) - .history(List.of( - Message.builder() - .role(Message.Role.ROLE_USER) - .parts(List.of( - new TextPart("Text"), - new FilePart(new FileWithBytes("text/plain", "file.txt", "data")), - new DataPart(Map.of("key", "value")) - )) - .build() - )) - .artifacts(List.of( - Artifact.builder() - .artifactId("artifact-1") - .parts(List.of(new TextPart("Content"))) - .build() - )) - .metadata(Map.of("meta1", "value1")) - .build(); + Task original = + Task.builder() + .id("task-123") + .contextId("context-789") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING, null, timestamp)) + .history( + List.of( + Message.builder() + .role(Message.Role.ROLE_USER) + .parts( + List.of( + new TextPart("Text"), + new FilePart( + new FileWithBytes( + "text/plain", + "file.txt", + "data")), + new DataPart( + Map.of("key", "value")))) + .build())) + .artifacts( + List.of( + Artifact.builder() + .artifactId("artifact-1") + .parts(List.of(new TextPart("Content"))) + .build())) + .metadata(Map.of("meta1", "value1")) + .build(); // Serialize to JSON String json = JsonUtil.toJson(original); @@ -460,16 +476,18 @@ void testTaskRoundTrip() throws JsonProcessingException { @Test void testTaskStatusWithMessage() throws JsonProcessingException { - Message statusMessage = Message.builder() - .role(Message.Role.ROLE_AGENT) - .parts(List.of(new TextPart("Processing complete"))) - .build(); - - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, statusMessage, null)) - .build(); + Message statusMessage = + Message.builder() + .role(Message.Role.ROLE_AGENT) + .parts(List.of(new TextPart("Processing complete"))) + .build(); + + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, statusMessage, null)) + .build(); // Serialize String json = JsonUtil.toJson(task); @@ -490,15 +508,16 @@ void testTaskStatusWithMessage() throws JsonProcessingException { @Test void testDeserializeTaskFromJson() throws JsonProcessingException { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "TASK_STATE_SUBMITTED" - } - } - """; + String json = + """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "TASK_STATE_SUBMITTED" + } + } + """; Task task = JsonUtil.fromJson(json, Task.class); @@ -512,26 +531,27 @@ void testDeserializeTaskFromJson() throws JsonProcessingException { @Test void testDeserializeTaskWithArtifactsFromJson() throws JsonProcessingException { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "TASK_STATE_COMPLETED" - }, - "artifacts": [ + String json = + """ { - "artifactId": "artifact-1", - "name": "Result", - "parts": [ + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "TASK_STATE_COMPLETED" + }, + "artifacts": [ { - "text": "Hello World" + "artifactId": "artifact-1", + "name": "Result", + "parts": [ + { + "text": "Hello World" + } + ] } ] } - ] - } - """; + """; Task task = JsonUtil.fromJson(json, Task.class); @@ -547,27 +567,28 @@ void testDeserializeTaskWithArtifactsFromJson() throws JsonProcessingException { @Test void testDeserializeTaskWithFilePartBytesFromJson() throws JsonProcessingException { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "TASK_STATE_COMPLETED" - }, - "artifacts": [ + String json = + """ { - "artifactId": "file-artifact", - "parts": [ + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "TASK_STATE_COMPLETED" + }, + "artifacts": [ { - "raw": "base64encodeddata", - "filename": "document.pdf", - "mediaType": "application/pdf" + "artifactId": "file-artifact", + "parts": [ + { + "raw": "base64encodeddata", + "filename": "document.pdf", + "mediaType": "application/pdf" + } + ] } ] } - ] - } - """; + """; Task task = JsonUtil.fromJson(json, Task.class); @@ -585,27 +606,28 @@ void testDeserializeTaskWithFilePartBytesFromJson() throws JsonProcessingExcepti @Test void testDeserializeTaskWithFilePartUriFromJson() throws JsonProcessingException { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "TASK_STATE_COMPLETED" - }, - "artifacts": [ + String json = + """ { - "artifactId": "uri-artifact", - "parts": [ + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "TASK_STATE_COMPLETED" + }, + "artifacts": [ { - "url": "https://example.com/photo.png", - "filename": "photo.png", - "mediaType": "image/png" + "artifactId": "uri-artifact", + "parts": [ + { + "url": "https://example.com/photo.png", + "filename": "photo.png", + "mediaType": "image/png" + } + ] } ] } - ] - } - """; + """; Task task = JsonUtil.fromJson(json, Task.class); @@ -622,28 +644,29 @@ void testDeserializeTaskWithFilePartUriFromJson() throws JsonProcessingException @Test void testDeserializeTaskWithDataPartFromJson() throws JsonProcessingException { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "TASK_STATE_COMPLETED" - }, - "artifacts": [ + String json = + """ { - "artifactId": "data-artifact", - "parts": [ + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "TASK_STATE_COMPLETED" + }, + "artifacts": [ { - "data": { - "temperature": 22.5, - "humidity": 65 - } + "artifactId": "data-artifact", + "parts": [ + { + "data": { + "temperature": 22.5, + "humidity": 65 + } + } + ] } ] } - ] - } - """; + """; Task task = JsonUtil.fromJson(json, Task.class); @@ -656,35 +679,36 @@ void testDeserializeTaskWithDataPartFromJson() throws JsonProcessingException { @Test void testDeserializeTaskWithHistoryFromJson() throws JsonProcessingException { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "TASK_STATE_WORKING" - }, - "history": [ + String json = + """ { - "role": "ROLE_USER", - "parts": [ + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "TASK_STATE_WORKING" + }, + "history": [ { - "text": "User message" - } - ], - "messageId": "msg-1" - }, - { - "role": "ROLE_AGENT", - "parts": [ + "role": "ROLE_USER", + "parts": [ + { + "text": "User message" + } + ], + "messageId": "msg-1" + }, { - "text": "Agent response" + "role": "ROLE_AGENT", + "parts": [ + { + "text": "Agent response" + } + ], + "messageId": "msg-2" } - ], - "messageId": "msg-2" + ] } - ] - } - """; + """; Task task = JsonUtil.fromJson(json, Task.class); @@ -698,16 +722,17 @@ void testDeserializeTaskWithHistoryFromJson() throws JsonProcessingException { @Test void testDeserializeTaskWithTimestampFromJson() throws JsonProcessingException { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "TASK_STATE_WORKING", - "timestamp": "2023-10-01T12:00:00.234-05:00" - } - } - """; + String json = + """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "TASK_STATE_WORKING", + "timestamp": "2023-10-01T12:00:00.234-05:00" + } + } + """; Task task = JsonUtil.fromJson(json, Task.class); @@ -719,19 +744,20 @@ void testDeserializeTaskWithTimestampFromJson() throws JsonProcessingException { @Test void testDeserializeTaskWithMetadataFromJson() throws JsonProcessingException { - String json = """ - { - "id": "task-123", - "contextId": "context-456", - "status": { - "state": "TASK_STATE_COMPLETED" - }, - "metadata": { - "key1": "value1", - "key2": 42 - } - } - """; + String json = + """ + { + "id": "task-123", + "contextId": "context-456", + "status": { + "state": "TASK_STATE_COMPLETED" + }, + "metadata": { + "key1": "value1", + "key2": 42 + } + } + """; Task task = JsonUtil.fromJson(json, Task.class); @@ -742,22 +768,30 @@ void testDeserializeTaskWithMetadataFromJson() throws JsonProcessingException { @Test void testTaskWithMixedPartTypes() throws JsonProcessingException { - Artifact artifact = Artifact.builder() - .artifactId("mixed-artifact") - .parts(List.of( - new TextPart("Text content"), - new FilePart(new FileWithBytes("application/json", "data.json", "{}")), - new DataPart(Map.of("result", 42)), - new FilePart(new FileWithUri("image/png", "image.png", "https://example.com/img.png")) - )) - .build(); - - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .artifacts(List.of(artifact)) - .build(); + Artifact artifact = + Artifact.builder() + .artifactId("mixed-artifact") + .parts( + List.of( + new TextPart("Text content"), + new FilePart( + new FileWithBytes( + "application/json", "data.json", "{}")), + new DataPart(Map.of("result", 42)), + new FilePart( + new FileWithUri( + "image/png", + "image.png", + "https://example.com/img.png")))) + .build(); + + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .artifacts(List.of(artifact)) + .build(); // Serialize String json = JsonUtil.toJson(task); @@ -776,8 +810,7 @@ void testTaskWithMixedPartTypes() throws JsonProcessingException { // ========== FileContentTypeAdapter tests ========== - @TempDir - Path tempDir; + @TempDir Path tempDir; @Test void testFileWithBytesSerializationDoesNotLeakInternalFields() throws Exception { diff --git a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardRequestTest.java b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardRequestTest.java index fe83b5742..7c9601855 100644 --- a/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardRequestTest.java +++ b/jsonrpc-common/src/test/java/org/a2aproject/sdk/jsonrpc/common/wrappers/GetExtendedAgentCardRequestTest.java @@ -31,10 +31,8 @@ void convenienceConstructor() { @Test void builderWithParams() { GetExtendedAgentCardParams params = new GetExtendedAgentCardParams("acme"); - GetExtendedAgentCardRequest request = GetExtendedAgentCardRequest.builder() - .id("1") - .params(params) - .build(); + GetExtendedAgentCardRequest request = + GetExtendedAgentCardRequest.builder().id("1").params(params).build(); assertEquals("1", request.getId()); assertNotNull(request.getParams()); @@ -48,5 +46,4 @@ void builderAutoGeneratesId() { assertNotNull(request.getId()); assertEquals(A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD, request.getMethod()); } - } diff --git a/pom.xml b/pom.xml index 67d0e80d8..378034c80 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 org.a2aproject.sdk @@ -30,70 +28,153 @@ + + + boms/test-utils + + + client/base + client/transport/grpc + client/transport/jsonrpc + client/transport/rest + client/transport/spi + common + examples/helloworld + examples/stream-lifecycle + examples/cloud-deployment/server + extras/common + extras/opentelemetry + extras/task-store-database-jpa + extras/push-notification-config-store-database-jpa + extras/queue-manager-replicated + extras/http-client-vertx + extras/http-client-android + extras/http-client-cdi + extras/multitenancy + http-client + jsonrpc-common + integrations/microprofile-config + reference/common + reference/grpc + reference/jsonrpc + reference/rest + server-common + spec + spec-grpc + test-utils-docker + tests/server-common + transport/jsonrpc + transport/grpc + transport/rest + + + + + compat-0.3 + + + reference/multiversion-jsonrpc + reference/multiversion-rest + + + extras/multitenancy/tests/test-common + + + extras/multitenancy/tests/rest + + + extras/multitenancy/tests/jsonrpc + + + extras/multitenancy/tests/grpc + + + extras/multitenancy/tests/router-unit + + + tests/multiversion/jsonrpc + tests/multiversion/rest + tests/multiversion/grpc + + + boms/extras + boms/reference + boms/sdk + + + tck + + + scm:git:git://github.com/a2aproject/a2a-java.git scm:git:ssh://github.com:a2aproject/a2a-java.git https://github.com/a2aproject/a2a-java - - UTF-8 - 2026-06-30T10:56:39Z - - 1.81.0 - UTF-8 - 3.5.0 - 3.16.0 - 3.6.2 - 3.4.2 - 3.4.0 - 3.6.1 - 3.12.0 - 3.2.8 - 3.5.0 false 9.3 + -XDaddTypeAnnotationsToSymbol=true + 2.50.0 5.1.0 - 0.11.0 + 1.37.0 6.0.0 + + 1.81.0 2.14.0 4.1.0 2.0.1 2.1.3 3.1.0 6.1.3 - 1.6.3 + 1.6.3 1.74.0 + 1.6.3 + 3.5.0 + 3.5.0 + 3.16.0 + 3.6.2 + 3.2.8 + 3.4.2 + 3.12.0 + 3.4.0 + 3.6.1 + + + true 5.24.0 - 6.0.4.Final 8.0.0 - 4.12.0 1.3.1 + -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -Xep:RequireExplicitNullMarking:WARN -XepOpt:NullAway:ExhaustiveOverride=true -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:JSpecifyMode=true -XepExcludedPaths:.*/src/test/.* -XepDisableWarningsInGeneratedCode + 0.14.1 + 4.12.0 1.7.1 + 2026-06-30T10:56:39Z + UTF-8 + UTF-8 4.36.2 5.1.11 4.35.0 - 3.40.1.1 - 6.3.0 - 2.0.19 - 1.6.3 - 2.0.5 - 2.50.0 - 3.10.4 - 1.37.0 - 0.14.1 - -XDaddTypeAnnotationsToSymbol=true - -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -Xep:RequireExplicitNullMarking:WARN -XepOpt:NullAway:ExhaustiveOverride=true -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:JSpecifyMode=true -XepExcludedPaths:.*/src/test/.* -XepDisableWarningsInGeneratedCode - true - - - true --add-opens=java.base/java.lang=ALL-UNNAMED + 3.40.1.1 false + 6.3.0 + true + 2.0.19 + 0.11.0 + 3.10.4 + 2.0.5 + 6.0.4.Final @@ -408,22 +489,22 @@ ${project.groupId} a2a-java-sdk-tests-server-common + ${project.version} test-jar test - ${project.version} ${project.groupId} a2a-java-sdk-server-common + ${project.version} test-jar test - ${project.version} ${project.groupId} a2a-java-sdk-tests-multitenancy-test-common - test ${project.version} + test org.jspecify @@ -444,6 +525,20 @@ + + com.diffplug.spotless + spotless-maven-plugin + ${spotless-maven-plugin.version} + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + true + + ${quarkus.jvm.args} + + org.apache.maven.plugins maven-compiler-plugin @@ -500,6 +595,51 @@ + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + + sign + + verify + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.5.1 + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + maven-surefire-plugin ${maven-surefire-plugin.version} @@ -528,20 +668,6 @@ - - org.apache.maven.plugins - maven-jar-plugin - 3.5.1 - - - io.quarkus - quarkus-maven-plugin - true - ${quarkus.platform.version} - - ${quarkus.jvm.args} - - org.sonatype.central central-publishing-maven-plugin @@ -553,66 +679,9 @@ ${release.auto.publish} - - org.apache.maven.plugins - maven-source-plugin - ${maven-source-plugin.version} - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - ${maven-javadoc-plugin.version} - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - ${maven-gpg-plugin.version} - - - sign-artifacts - verify - - sign - - - - - - com.diffplug.spotless - spotless-maven-plugin - ${spotless-maven-plugin.version} - - - org.apache.maven.plugins - maven-compiler-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - org.sonatype.central - central-publishing-maven-plugin - com.diffplug.spotless spotless-maven-plugin @@ -626,8 +695,8 @@ .gitignore - - + + true 4 @@ -652,9 +721,9 @@ true false - - - + + + true 4 @@ -694,7 +763,7 @@ RELEASE.md SECURITY.md - + @@ -706,93 +775,21 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.sonatype.central + central-publishing-maven-plugin + - - - boms/test-utils - - - client/base - client/transport/grpc - client/transport/jsonrpc - client/transport/rest - client/transport/spi - common - examples/helloworld - examples/stream-lifecycle - examples/cloud-deployment/server - extras/common - extras/opentelemetry - extras/task-store-database-jpa - extras/push-notification-config-store-database-jpa - extras/queue-manager-replicated - extras/http-client-vertx - extras/http-client-android - extras/http-client-cdi - extras/multitenancy - http-client - jsonrpc-common - integrations/microprofile-config - reference/common - reference/grpc - reference/jsonrpc - reference/rest - server-common - spec - spec-grpc - test-utils-docker - tests/server-common - transport/jsonrpc - transport/grpc - transport/rest - - - - - compat-0.3 - - - reference/multiversion-jsonrpc - reference/multiversion-rest - - - extras/multitenancy/tests/test-common - - - extras/multitenancy/tests/rest - - - extras/multitenancy/tests/jsonrpc - - - extras/multitenancy/tests/grpc - - - extras/multitenancy/tests/router-unit - - - tests/multiversion/jsonrpc - tests/multiversion/rest - tests/multiversion/grpc - - - boms/extras - boms/reference - boms/sdk - - - tck - - - org.mapstruct @@ -114,10 +111,10 @@ - generate-sources generate + generate-sources diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2ACommonFieldMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2ACommonFieldMapper.java index 9704dd66c..37040af89 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2ACommonFieldMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2ACommonFieldMapper.java @@ -1,5 +1,9 @@ package org.a2aproject.sdk.grpc.mapper; +import com.google.protobuf.NullValue; +import com.google.protobuf.Struct; +import com.google.protobuf.Timestamp; +import com.google.protobuf.Value; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; @@ -8,16 +12,10 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; - -import com.google.protobuf.NullValue; -import com.google.protobuf.Struct; -import com.google.protobuf.Timestamp; -import com.google.protobuf.Value; +import org.a2aproject.sdk.spec.InvalidParamsError; import org.mapstruct.Mapper; import org.mapstruct.Named; -import org.a2aproject.sdk.spec.InvalidParamsError; - /** * Common field mapping utilities shared across all mappers. *

    @@ -31,7 +29,9 @@ *

  • Enum → null conversion (protobuf UNSPECIFIED/UNKNOWN handling)
  • * */ -@Mapper(config = A2AProtoMapperConfig.class, uses = {TaskStateMapper.class}) +@Mapper( + config = A2AProtoMapperConfig.class, + uses = {TaskStateMapper.class}) public interface A2ACommonFieldMapper { A2ACommonFieldMapper INSTANCE = A2AMappers.getMapper(A2ACommonFieldMapper.class); @@ -117,8 +117,7 @@ default OffsetDateTime protoTimestampToOffsetDateTime(Timestamp timestamp) { } return OffsetDateTime.ofInstant( Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()), - ZoneOffset.UTC - ); + ZoneOffset.UTC); } /** @@ -253,7 +252,8 @@ default Object valueToObject(Value value) { * @return protobuf ListValue */ default com.google.protobuf.ListValue listToListValue(List list) { - com.google.protobuf.ListValue.Builder listValueBuilder = com.google.protobuf.ListValue.newBuilder(); + com.google.protobuf.ListValue.Builder listValueBuilder = + com.google.protobuf.ListValue.newBuilder(); if (list != null) { list.forEach(o -> listValueBuilder.addValues(objectToValue(o))); } @@ -383,9 +383,11 @@ default long instantToMillis(Instant instant) { @Named("millisToInstant") default Instant millisToInstant(long millis) { if (millis < 0L) { - throw new InvalidParamsError(null, - "Timestamp must be a non-negative number of milliseconds since epoch, but got: " + millis, - null); + throw new InvalidParamsError( + null, + "Timestamp must be a non-negative number of milliseconds since epoch, but got: " + + millis, + null); } return millis > 0L ? Instant.ofEpochMilli(millis) : null; } @@ -443,19 +445,24 @@ default Instant protoTimestampToInstant(Timestamp timestamp) { * @throws InvalidParamsError if state is UNRECOGNIZED (invalid enum value) */ @Named("taskStateOrNull") - default org.a2aproject.sdk.spec.TaskState taskStateOrNull(org.a2aproject.sdk.grpc.TaskState state) { + default org.a2aproject.sdk.spec.TaskState taskStateOrNull( + org.a2aproject.sdk.grpc.TaskState state) { if (state == null || state == org.a2aproject.sdk.grpc.TaskState.TASK_STATE_UNSPECIFIED) { return null; } // Reject invalid enum values (e.g., "INVALID_STATUS" from JSON) if (state == org.a2aproject.sdk.grpc.TaskState.UNRECOGNIZED) { - String validStates = java.util.Arrays.stream(org.a2aproject.sdk.spec.TaskState.values()) - .filter(s -> s != org.a2aproject.sdk.spec.TaskState.TASK_STATE_UNSPECIFIED) - .map(Enum::name) - .collect(java.util.stream.Collectors.joining(", ")); - throw new InvalidParamsError(null, - "Invalid task state value. Must be one of: " + validStates, - null); + String validStates = + java.util.Arrays.stream(org.a2aproject.sdk.spec.TaskState.values()) + .filter( + s -> + s + != org.a2aproject.sdk.spec.TaskState + .TASK_STATE_UNSPECIFIED) + .map(Enum::name) + .collect(java.util.stream.Collectors.joining(", ")); + throw new InvalidParamsError( + null, "Invalid task state value. Must be one of: " + validStates, null); } org.a2aproject.sdk.spec.TaskState result = TaskStateMapper.INSTANCE.fromProto(state); return result; diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2AMappers.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2AMappers.java index eab580898..86d711108 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2AMappers.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2AMappers.java @@ -8,19 +8,21 @@ public class A2AMappers { static T getMapper(Class iface) { try { - @SuppressWarnings( "unchecked" ) + @SuppressWarnings("unchecked") Class implementation = - (Class) iface.getClassLoader().loadClass( iface.getName() + IMPLEMENTATION_SUFFIX ); + (Class) + iface.getClassLoader() + .loadClass(iface.getName() + IMPLEMENTATION_SUFFIX); Constructor constructor = implementation.getDeclaredConstructor(); - constructor.setAccessible( true ); + constructor.setAccessible(true); return constructor.newInstance(); - } catch (ClassNotFoundException | - NoSuchMethodException | - InstantiationException | - IllegalAccessException | - InvocationTargetException e) { + } catch (ClassNotFoundException + | NoSuchMethodException + | InstantiationException + | IllegalAccessException + | InvocationTargetException e) { throw new RuntimeException(e); } } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2AProtoMapperConfig.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2AProtoMapperConfig.java index d2122924a..d3fd89a08 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2AProtoMapperConfig.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/A2AProtoMapperConfig.java @@ -1,12 +1,5 @@ package org.a2aproject.sdk.grpc.mapper; -import java.nio.ByteBuffer; -import java.time.Instant; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; import com.google.protobuf.Int32Value; @@ -16,20 +9,25 @@ import com.google.protobuf.Struct; import com.google.protobuf.Timestamp; import com.google.protobuf.Value; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.mapstruct.MapperConfig; import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.ReportingPolicy; @MapperConfig( - // 1. FAIL THE BUILD if fields are missing in either Spec or Proto - unmappedTargetPolicy = ReportingPolicy.ERROR, + // 1. FAIL THE BUILD if fields are missing in either Spec or Proto + unmappedTargetPolicy = ReportingPolicy.ERROR, - // 2. Use the default component model (Singleton instance pattern) - componentModel = "default", + // 2. Use the default component model (Singleton instance pattern) + componentModel = "default", - // 3. IGNORE null values when mapping to protobuf (builders don't accept null) - nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE -) + // 3. IGNORE null values when mapping to protobuf (builders don't accept null) + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) public interface A2AProtoMapperConfig { // ======================================================================== @@ -52,9 +50,9 @@ default Instant map(Timestamp value) { default Timestamp map(Instant value) { if (value == null) return null; return Timestamp.newBuilder() - .setSeconds(value.getEpochSecond()) - .setNanos(value.getNano()) - .build(); + .setSeconds(value.getEpochSecond()) + .setNanos(value.getNano()) + .build(); } default java.time.Duration map(com.google.protobuf.Duration value) { @@ -65,9 +63,9 @@ default java.time.Duration map(com.google.protobuf.Duration value) { default com.google.protobuf.Duration map(java.time.Duration value) { if (value == null) return null; return com.google.protobuf.Duration.newBuilder() - .setSeconds(value.getSeconds()) - .setNanos(value.getNano()) - .build(); + .setSeconds(value.getSeconds()) + .setNanos(value.getNano()) + .build(); } // ======================================================================== @@ -98,6 +96,7 @@ default ByteString mapFromBuffer(ByteBuffer value) { default String map(StringValue value) { return value == null ? null : value.getValue(); } + default StringValue mapString(String value) { return value == null ? null : StringValue.of(value); } @@ -106,6 +105,7 @@ default StringValue mapString(String value) { default Integer map(Int32Value value) { return value == null ? null : value.getValue(); } + default Int32Value mapInt(Integer value) { return value == null ? null : Int32Value.of(value); } @@ -114,6 +114,7 @@ default Int32Value mapInt(Integer value) { default Long map(Int64Value value) { return value == null ? null : value.getValue(); } + default Int64Value mapLong(Long value) { return value == null ? null : Int64Value.of(value); } @@ -122,6 +123,7 @@ default Int64Value mapLong(Long value) { default Boolean map(BoolValue value) { return value == null ? null : value.getValue(); } + default BoolValue mapBool(Boolean value) { return value == null ? null : BoolValue.of(value); } @@ -153,27 +155,38 @@ default Struct mapStruct(Map map) { default Object map(Value value) { if (value == null) return null; switch (value.getKindCase()) { - case NULL_VALUE: return null; - case NUMBER_VALUE: return value.getNumberValue(); // Returns Double - case STRING_VALUE: return value.getStringValue(); - case BOOL_VALUE: return value.getBoolValue(); - case STRUCT_VALUE: return map(value.getStructValue()); + case NULL_VALUE: + return null; + case NUMBER_VALUE: + return value.getNumberValue(); // Returns Double + case STRING_VALUE: + return value.getStringValue(); + case BOOL_VALUE: + return value.getBoolValue(); + case STRUCT_VALUE: + return map(value.getStructValue()); case LIST_VALUE: return value.getListValue().getValuesList().stream() .map(this::map) .collect(Collectors.toList()); - default: return null; + default: + return null; } } default Value mapValue(Object object) { if (object == null) { - return Value.newBuilder().setNullValue(com.google.protobuf.NullValue.NULL_VALUE).build(); + return Value.newBuilder() + .setNullValue(com.google.protobuf.NullValue.NULL_VALUE) + .build(); } - if (object instanceof String) return Value.newBuilder().setStringValue((String) object).build(); - if (object instanceof Boolean) return Value.newBuilder().setBoolValue((Boolean) object).build(); - if (object instanceof Number) return Value.newBuilder().setNumberValue(((Number) object).doubleValue()).build(); + if (object instanceof String) + return Value.newBuilder().setStringValue((String) object).build(); + if (object instanceof Boolean) + return Value.newBuilder().setBoolValue((Boolean) object).build(); + if (object instanceof Number) + return Value.newBuilder().setNumberValue(((Number) object).doubleValue()).build(); if (object instanceof Map) { // Unchecked cast is unavoidable here unless we force Map @SuppressWarnings("unchecked") @@ -190,6 +203,7 @@ default Value mapValue(Object object) { // Fallback for unknown types (e.g. custom objects inside Map) -> convert to String? // For now, throw to catch unexpected types early - throw new IllegalArgumentException("Unsupported type for Proto Value conversion: " + object.getClass().getName()); + throw new IllegalArgumentException( + "Unsupported type for Proto Value conversion: " + object.getClass().getName()); } -} \ No newline at end of file +} diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/APIKeySecuritySchemeMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/APIKeySecuritySchemeMapper.java index f6934dcc6..ccb487a58 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/APIKeySecuritySchemeMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/APIKeySecuritySchemeMapper.java @@ -7,25 +7,33 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.APIKeySecurityScheme} and {@link org.a2aproject.sdk.grpc.APIKeySecurityScheme}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) public interface APIKeySecuritySchemeMapper { APIKeySecuritySchemeMapper INSTANCE = A2AMappers.getMapper(APIKeySecuritySchemeMapper.class); // location enum is converted to string via ProtoMapperConfig.map(Location) - @Mapping(target = "description", source = "description", conditionExpression = "java(domain.description() != null)") - org.a2aproject.sdk.grpc.APIKeySecurityScheme toProto(org.a2aproject.sdk.spec.APIKeySecurityScheme domain); + @Mapping( + target = "description", + source = "description", + conditionExpression = "java(domain.description() != null)") + org.a2aproject.sdk.grpc.APIKeySecurityScheme toProto( + org.a2aproject.sdk.spec.APIKeySecurityScheme domain); - default org.a2aproject.sdk.spec.APIKeySecurityScheme fromProto(org.a2aproject.sdk.grpc.APIKeySecurityScheme proto) { + default org.a2aproject.sdk.spec.APIKeySecurityScheme fromProto( + org.a2aproject.sdk.grpc.APIKeySecurityScheme proto) { if (proto == null) { return null; } org.a2aproject.sdk.spec.APIKeySecurityScheme.Location location = - org.a2aproject.sdk.spec.APIKeySecurityScheme.Location.fromString(proto.getLocation()); + org.a2aproject.sdk.spec.APIKeySecurityScheme.Location.fromString( + proto.getLocation()); String description = proto.getDescription().isEmpty() ? null : proto.getDescription(); - return new org.a2aproject.sdk.spec.APIKeySecurityScheme(location, proto.getName(), description); + return new org.a2aproject.sdk.spec.APIKeySecurityScheme( + location, proto.getName(), description); } } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCapabilitiesMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCapabilitiesMapper.java index d91db5256..5823a8fc0 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCapabilitiesMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCapabilitiesMapper.java @@ -6,14 +6,17 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.AgentCapabilities} and {@link org.a2aproject.sdk.grpc.AgentCapabilities}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = {AgentExtensionMapper.class}) public interface AgentCapabilitiesMapper { AgentCapabilitiesMapper INSTANCE = A2AMappers.getMapper(AgentCapabilitiesMapper.class); - org.a2aproject.sdk.grpc.AgentCapabilities toProto(org.a2aproject.sdk.spec.AgentCapabilities domain); + org.a2aproject.sdk.grpc.AgentCapabilities toProto( + org.a2aproject.sdk.spec.AgentCapabilities domain); - org.a2aproject.sdk.spec.AgentCapabilities fromProto(org.a2aproject.sdk.grpc.AgentCapabilities proto); + org.a2aproject.sdk.spec.AgentCapabilities fromProto( + org.a2aproject.sdk.grpc.AgentCapabilities proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardMapper.java index 9b6a72dce..1213460e8 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardMapper.java @@ -7,7 +7,8 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.AgentCard} and {@link org.a2aproject.sdk.grpc.AgentCard}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = { AgentProviderMapper.class, @@ -22,14 +23,32 @@ public interface AgentCardMapper { AgentCardMapper INSTANCE = A2AMappers.getMapper(AgentCardMapper.class); - @Mapping(target = "provider", source = "provider", conditionExpression = "java(domain.provider() != null)") - @Mapping(target = "documentationUrl", source = "documentationUrl", conditionExpression = "java(domain.documentationUrl() != null)") - @Mapping(target = "iconUrl", source = "iconUrl", conditionExpression = "java(domain.iconUrl() != null)") + @Mapping( + target = "provider", + source = "provider", + conditionExpression = "java(domain.provider() != null)") + @Mapping( + target = "documentationUrl", + source = "documentationUrl", + conditionExpression = "java(domain.documentationUrl() != null)") + @Mapping( + target = "iconUrl", + source = "iconUrl", + conditionExpression = "java(domain.iconUrl() != null)") org.a2aproject.sdk.grpc.AgentCard toProto(org.a2aproject.sdk.spec.AgentCard domain); - @Mapping(target = "provider", source = "provider", conditionExpression = "java(proto.hasProvider())") - @Mapping(target = "documentationUrl", source = "documentationUrl", conditionExpression = "java(!proto.getDocumentationUrl().isEmpty())") - @Mapping(target = "iconUrl", source = "iconUrl", conditionExpression = "java(!proto.getIconUrl().isEmpty())") + @Mapping( + target = "provider", + source = "provider", + conditionExpression = "java(proto.hasProvider())") + @Mapping( + target = "documentationUrl", + source = "documentationUrl", + conditionExpression = "java(!proto.getDocumentationUrl().isEmpty())") + @Mapping( + target = "iconUrl", + source = "iconUrl", + conditionExpression = "java(!proto.getIconUrl().isEmpty())") @Mapping(target = "url", ignore = true) @Mapping(target = "preferredTransport", ignore = true) @Mapping(target = "additionalInterfaces", ignore = true) diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardSignatureMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardSignatureMapper.java index e83a23d27..df6c35fe1 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardSignatureMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentCardSignatureMapper.java @@ -9,7 +9,8 @@ *

    * Uses CommonFieldMapper for struct conversion (header field). */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = {A2ACommonFieldMapper.class}) public interface AgentCardSignatureMapper { @@ -22,8 +23,13 @@ public interface AgentCardSignatureMapper { * Maps protectedHeader → protected field and header via struct conversion. */ @Mapping(source = "protectedHeader", target = "protected") - @Mapping(target = "header", source = "header", conditionExpression = "java(domain.header() != null)", qualifiedByName = "mapToStruct") - org.a2aproject.sdk.grpc.AgentCardSignature toProto(org.a2aproject.sdk.spec.AgentCardSignature domain); + @Mapping( + target = "header", + source = "header", + conditionExpression = "java(domain.header() != null)", + qualifiedByName = "mapToStruct") + org.a2aproject.sdk.grpc.AgentCardSignature toProto( + org.a2aproject.sdk.spec.AgentCardSignature domain); /** * Converts proto AgentCardSignature to domain AgentCardSignature. @@ -32,5 +38,6 @@ public interface AgentCardSignatureMapper { */ @Mapping(source = "protected", target = "protectedHeader") @Mapping(target = "header", source = "header", qualifiedByName = "structToMap") - org.a2aproject.sdk.spec.AgentCardSignature fromProto(org.a2aproject.sdk.grpc.AgentCardSignature proto); + org.a2aproject.sdk.spec.AgentCardSignature fromProto( + org.a2aproject.sdk.grpc.AgentCardSignature proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentExtensionMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentExtensionMapper.java index ff95b83b7..dde4ca078 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentExtensionMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentExtensionMapper.java @@ -9,7 +9,8 @@ *

    * Uses CommonFieldMapper for struct conversion (params field). */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = {A2ACommonFieldMapper.class}) public interface AgentExtensionMapper { @@ -21,7 +22,11 @@ public interface AgentExtensionMapper { *

    * Maps params field via struct conversion. */ - @Mapping(target = "params", source = "params", conditionExpression = "java(domain.params() != null)", qualifiedByName = "mapToStruct") + @Mapping( + target = "params", + source = "params", + conditionExpression = "java(domain.params() != null)", + qualifiedByName = "mapToStruct") org.a2aproject.sdk.grpc.AgentExtension toProto(org.a2aproject.sdk.spec.AgentExtension domain); /** diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentInterfaceMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentInterfaceMapper.java index d7b51224f..a5f49cc67 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentInterfaceMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentInterfaceMapper.java @@ -7,14 +7,18 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.AgentInterface} and {@link org.a2aproject.sdk.grpc.AgentInterface}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = A2ACommonFieldMapper.class) public interface AgentInterfaceMapper { AgentInterfaceMapper INSTANCE = A2AMappers.getMapper(AgentInterfaceMapper.class); - @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)") + @Mapping( + target = "tenant", + source = "tenant", + conditionExpression = "java(domain.tenant() != null)") org.a2aproject.sdk.grpc.AgentInterface toProto(org.a2aproject.sdk.spec.AgentInterface domain); @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentProviderMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentProviderMapper.java index 4a0e7293c..e52339f42 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentProviderMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentProviderMapper.java @@ -6,7 +6,8 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.AgentProvider} and {@link org.a2aproject.sdk.grpc.AgentProvider}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) public interface AgentProviderMapper { diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentSkillMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentSkillMapper.java index ce091fe01..09f8c0477 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentSkillMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AgentSkillMapper.java @@ -6,7 +6,8 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.AgentSkill} and {@link org.a2aproject.sdk.grpc.AgentSkill}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = SecurityRequirementMapper.class) public interface AgentSkillMapper { diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ArtifactMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ArtifactMapper.java index e6734883a..f5a6714a5 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ArtifactMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ArtifactMapper.java @@ -11,7 +11,8 @@ * Uses ADDER_PREFERRED strategy to use addAllExtensions() method instead of * trying to instantiate ProtocolStringList. Enables full compile-time validation! */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = {PartMapper.class, A2ACommonFieldMapper.class}) public interface ArtifactMapper { @@ -22,9 +23,15 @@ public interface ArtifactMapper { * Converts domain Artifact to proto Artifact. * ADDER_PREFERRED strategy ensures MapStruct uses addAllExtensions() method. */ - @Mapping(target = "artifactId", source = "artifactId", conditionExpression = "java(domain.artifactId() != null)") + @Mapping( + target = "artifactId", + source = "artifactId", + conditionExpression = "java(domain.artifactId() != null)") @Mapping(target = "name", source = "name", conditionExpression = "java(domain.name() != null)") - @Mapping(target = "description", source = "description", conditionExpression = "java(domain.description() != null)") + @Mapping( + target = "description", + source = "description", + conditionExpression = "java(domain.description() != null)") @Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataToProto") org.a2aproject.sdk.grpc.Artifact toProto(Artifact domain); diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AuthenticationInfoMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AuthenticationInfoMapper.java index 2a230bc8e..9d43668d5 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AuthenticationInfoMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AuthenticationInfoMapper.java @@ -1,6 +1,5 @@ package org.a2aproject.sdk.grpc.mapper; - import org.mapstruct.CollectionMappingStrategy; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -11,7 +10,8 @@ * Maps between domain AuthenticationInfo (schemes as List of String) and proto AuthenticationInfo (scheme as String). * The proto scheme field is mapped to/from the first element of the domain schemes list. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) public interface AuthenticationInfoMapper { @@ -21,14 +21,17 @@ public interface AuthenticationInfoMapper { * Converts domain AuthenticationInfo to proto AuthenticationInfo. * Takes the first scheme from the schemes list. */ - @Mapping(target = "credentials", source = "credentials", conditionExpression = "java(domain.credentials() != null)") - org.a2aproject.sdk.grpc.AuthenticationInfo toProto(org.a2aproject.sdk.spec.AuthenticationInfo domain); + @Mapping( + target = "credentials", + source = "credentials", + conditionExpression = "java(domain.credentials() != null)") + org.a2aproject.sdk.grpc.AuthenticationInfo toProto( + org.a2aproject.sdk.spec.AuthenticationInfo domain); /** * Converts proto AuthenticationInfo to domain AuthenticationInfo. * Wraps the single scheme in a list. */ - org.a2aproject.sdk.spec.AuthenticationInfo fromProto(org.a2aproject.sdk.grpc.AuthenticationInfo proto); - - + org.a2aproject.sdk.spec.AuthenticationInfo fromProto( + org.a2aproject.sdk.grpc.AuthenticationInfo proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AuthorizationCodeOAuthFlowMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AuthorizationCodeOAuthFlowMapper.java index 785822f26..e0d5532ee 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AuthorizationCodeOAuthFlowMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/AuthorizationCodeOAuthFlowMapper.java @@ -6,13 +6,17 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.AuthorizationCodeOAuthFlow} and {@link org.a2aproject.sdk.grpc.AuthorizationCodeOAuthFlow}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) public interface AuthorizationCodeOAuthFlowMapper { - AuthorizationCodeOAuthFlowMapper INSTANCE = A2AMappers.getMapper(AuthorizationCodeOAuthFlowMapper.class); + AuthorizationCodeOAuthFlowMapper INSTANCE = + A2AMappers.getMapper(AuthorizationCodeOAuthFlowMapper.class); - org.a2aproject.sdk.grpc.AuthorizationCodeOAuthFlow toProto(org.a2aproject.sdk.spec.AuthorizationCodeOAuthFlow domain); + org.a2aproject.sdk.grpc.AuthorizationCodeOAuthFlow toProto( + org.a2aproject.sdk.spec.AuthorizationCodeOAuthFlow domain); - org.a2aproject.sdk.spec.AuthorizationCodeOAuthFlow fromProto(org.a2aproject.sdk.grpc.AuthorizationCodeOAuthFlow proto); + org.a2aproject.sdk.spec.AuthorizationCodeOAuthFlow fromProto( + org.a2aproject.sdk.grpc.AuthorizationCodeOAuthFlow proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ClientCredentialsOAuthFlowMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ClientCredentialsOAuthFlowMapper.java index 26aabf6ac..ac1bbd26c 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ClientCredentialsOAuthFlowMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ClientCredentialsOAuthFlowMapper.java @@ -6,13 +6,17 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.ClientCredentialsOAuthFlow} and {@link org.a2aproject.sdk.grpc.ClientCredentialsOAuthFlow}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) public interface ClientCredentialsOAuthFlowMapper { - ClientCredentialsOAuthFlowMapper INSTANCE = A2AMappers.getMapper(ClientCredentialsOAuthFlowMapper.class); + ClientCredentialsOAuthFlowMapper INSTANCE = + A2AMappers.getMapper(ClientCredentialsOAuthFlowMapper.class); - org.a2aproject.sdk.grpc.ClientCredentialsOAuthFlow toProto(org.a2aproject.sdk.spec.ClientCredentialsOAuthFlow domain); + org.a2aproject.sdk.grpc.ClientCredentialsOAuthFlow toProto( + org.a2aproject.sdk.spec.ClientCredentialsOAuthFlow domain); - org.a2aproject.sdk.spec.ClientCredentialsOAuthFlow fromProto(org.a2aproject.sdk.grpc.ClientCredentialsOAuthFlow proto); + org.a2aproject.sdk.spec.ClientCredentialsOAuthFlow fromProto( + org.a2aproject.sdk.grpc.ClientCredentialsOAuthFlow proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/DeleteTaskPushNotificationConfigParamsMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/DeleteTaskPushNotificationConfigParamsMapper.java index a464dfab2..640c1b48b 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/DeleteTaskPushNotificationConfigParamsMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/DeleteTaskPushNotificationConfigParamsMapper.java @@ -12,7 +12,8 @@ @Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class) public interface DeleteTaskPushNotificationConfigParamsMapper { - DeleteTaskPushNotificationConfigParamsMapper INSTANCE = A2AMappers.getMapper(DeleteTaskPushNotificationConfigParamsMapper.class); + DeleteTaskPushNotificationConfigParamsMapper INSTANCE = + A2AMappers.getMapper(DeleteTaskPushNotificationConfigParamsMapper.class); /** * Converts proto DeleteTaskPushNotificationConfigRequest to domain DeleteTaskPushNotificationConfigParams. @@ -21,13 +22,18 @@ public interface DeleteTaskPushNotificationConfigParamsMapper { @Mapping(target = "taskId", source = "taskId") @Mapping(target = "id", source = "id") @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") - DeleteTaskPushNotificationConfigParams fromProto(org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest proto); + DeleteTaskPushNotificationConfigParams fromProto( + org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest proto); /** * Converts domain DeleteTaskPushNotificationConfigParams to proto DeleteTaskPushNotificationConfigRequest. */ @Mapping(target = "taskId", source = "taskId") @Mapping(target = "id", source = "id") - @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)") - org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest toProto(DeleteTaskPushNotificationConfigParams domain); + @Mapping( + target = "tenant", + source = "tenant", + conditionExpression = "java(domain.tenant() != null)") + org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest toProto( + DeleteTaskPushNotificationConfigParams domain); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/GetTaskPushNotificationConfigParamsMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/GetTaskPushNotificationConfigParamsMapper.java index e277b7ce0..984a26bb6 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/GetTaskPushNotificationConfigParamsMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/GetTaskPushNotificationConfigParamsMapper.java @@ -12,7 +12,8 @@ @Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class) public interface GetTaskPushNotificationConfigParamsMapper { - GetTaskPushNotificationConfigParamsMapper INSTANCE = A2AMappers.getMapper(GetTaskPushNotificationConfigParamsMapper.class); + GetTaskPushNotificationConfigParamsMapper INSTANCE = + A2AMappers.getMapper(GetTaskPushNotificationConfigParamsMapper.class); /** * Converts proto GetTaskPushNotificationConfigRequest to domain GetTaskPushNotificationConfigParams. @@ -21,13 +22,18 @@ public interface GetTaskPushNotificationConfigParamsMapper { @Mapping(target = "taskId", source = "taskId") @Mapping(target = "id", source = "id") @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") - GetTaskPushNotificationConfigParams fromProto(org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest proto); + GetTaskPushNotificationConfigParams fromProto( + org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest proto); /** * Converts domain GetTaskPushNotificationConfigParams to proto GetTaskPushNotificationConfigRequest. */ @Mapping(target = "taskId", source = "taskId") @Mapping(target = "id", source = "id", conditionExpression = "java(domain.id() != null)") - @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)") - org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest toProto(GetTaskPushNotificationConfigParams domain); + @Mapping( + target = "tenant", + source = "tenant", + conditionExpression = "java(domain.tenant() != null)") + org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest toProto( + GetTaskPushNotificationConfigParams domain); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/HTTPAuthSecuritySchemeMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/HTTPAuthSecuritySchemeMapper.java index 261c32ce7..46e9b5316 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/HTTPAuthSecuritySchemeMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/HTTPAuthSecuritySchemeMapper.java @@ -7,17 +7,27 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.HTTPAuthSecurityScheme} and {@link org.a2aproject.sdk.grpc.HTTPAuthSecurityScheme}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) public interface HTTPAuthSecuritySchemeMapper { - HTTPAuthSecuritySchemeMapper INSTANCE = A2AMappers.getMapper(HTTPAuthSecuritySchemeMapper.class); + HTTPAuthSecuritySchemeMapper INSTANCE = + A2AMappers.getMapper(HTTPAuthSecuritySchemeMapper.class); - @Mapping(target = "bearerFormat", source = "bearerFormat", conditionExpression = "java(domain.bearerFormat() != null)") - @Mapping(target = "description", source = "description", conditionExpression = "java(domain.description() != null)") - org.a2aproject.sdk.grpc.HTTPAuthSecurityScheme toProto(org.a2aproject.sdk.spec.HTTPAuthSecurityScheme domain); + @Mapping( + target = "bearerFormat", + source = "bearerFormat", + conditionExpression = "java(domain.bearerFormat() != null)") + @Mapping( + target = "description", + source = "description", + conditionExpression = "java(domain.description() != null)") + org.a2aproject.sdk.grpc.HTTPAuthSecurityScheme toProto( + org.a2aproject.sdk.spec.HTTPAuthSecurityScheme domain); - default org.a2aproject.sdk.spec.HTTPAuthSecurityScheme fromProto(org.a2aproject.sdk.grpc.HTTPAuthSecurityScheme proto) { + default org.a2aproject.sdk.spec.HTTPAuthSecurityScheme fromProto( + org.a2aproject.sdk.grpc.HTTPAuthSecurityScheme proto) { if (proto == null) { return null; } @@ -25,6 +35,7 @@ default org.a2aproject.sdk.spec.HTTPAuthSecurityScheme fromProto(org.a2aproject. String bearerFormat = proto.getBearerFormat().isEmpty() ? null : proto.getBearerFormat(); String description = proto.getDescription().isEmpty() ? null : proto.getDescription(); - return new org.a2aproject.sdk.spec.HTTPAuthSecurityScheme(bearerFormat, proto.getScheme(), description); + return new org.a2aproject.sdk.spec.HTTPAuthSecurityScheme( + bearerFormat, proto.getScheme(), description); } } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTaskPushNotificationConfigsParamsMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTaskPushNotificationConfigsParamsMapper.java index c69fa1925..50a2bc249 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTaskPushNotificationConfigsParamsMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTaskPushNotificationConfigsParamsMapper.java @@ -12,7 +12,8 @@ @Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class) public interface ListTaskPushNotificationConfigsParamsMapper { - ListTaskPushNotificationConfigsParamsMapper INSTANCE = A2AMappers.getMapper(ListTaskPushNotificationConfigsParamsMapper.class); + ListTaskPushNotificationConfigsParamsMapper INSTANCE = + A2AMappers.getMapper(ListTaskPushNotificationConfigsParamsMapper.class); /** * Converts proto ListTaskPushNotificationConfigsRequest to domain ListTaskPushNotificationConfigsParams. @@ -20,12 +21,17 @@ public interface ListTaskPushNotificationConfigsParamsMapper { @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "id", source = "taskId") @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") - ListTaskPushNotificationConfigsParams fromProto(org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest proto); + ListTaskPushNotificationConfigsParams fromProto( + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest proto); /** * Converts domain ListTaskPushNotificationConfigsParams to proto ListTaskPushNotificationConfigsRequest. */ @Mapping(target = "taskId", source = "id") - @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)") - org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest toProto(ListTaskPushNotificationConfigsParams domain); + @Mapping( + target = "tenant", + source = "tenant", + conditionExpression = "java(domain.tenant() != null)") + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest toProto( + ListTaskPushNotificationConfigsParams domain); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTasksParamsMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTasksParamsMapper.java index 7170804cc..4b96699c3 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTasksParamsMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTasksParamsMapper.java @@ -10,7 +10,9 @@ * Handles the conversion between domain ListTasksParams and protobuf ListTasksRequest, * with special handling for optional fields and timestamp conversions. */ -@Mapper(config = A2AProtoMapperConfig.class, uses = {TaskStateMapper.class, A2ACommonFieldMapper.class}) +@Mapper( + config = A2AProtoMapperConfig.class, + uses = {TaskStateMapper.class, A2ACommonFieldMapper.class}) public interface ListTasksParamsMapper { ListTasksParamsMapper INSTANCE = A2AMappers.getMapper(ListTasksParamsMapper.class); @@ -21,14 +23,38 @@ public interface ListTasksParamsMapper { * @param params the domain ListTasksParams * @return protobuf ListTasksRequest */ - @Mapping(target = "contextId", source = "contextId", conditionExpression = "java(params.contextId() != null)") - @Mapping(target = "status", source = "status", conditionExpression = "java(params.status() != null)") - @Mapping(target = "pageSize", source = "pageSize", conditionExpression = "java(params.pageSize() != null)") - @Mapping(target = "pageToken", source = "pageToken", conditionExpression = "java(params.pageToken() != null)") - @Mapping(target = "historyLength", source = "historyLength", conditionExpression = "java(params.historyLength() != null)") - @Mapping(target = "statusTimestampAfter", source = "statusTimestampAfter", qualifiedByName = "instantToProtoTimestamp") - @Mapping(target = "includeArtifacts", source = "includeArtifacts", conditionExpression = "java(params.includeArtifacts() != null)") - @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(params.tenant() != null)") + @Mapping( + target = "contextId", + source = "contextId", + conditionExpression = "java(params.contextId() != null)") + @Mapping( + target = "status", + source = "status", + conditionExpression = "java(params.status() != null)") + @Mapping( + target = "pageSize", + source = "pageSize", + conditionExpression = "java(params.pageSize() != null)") + @Mapping( + target = "pageToken", + source = "pageToken", + conditionExpression = "java(params.pageToken() != null)") + @Mapping( + target = "historyLength", + source = "historyLength", + conditionExpression = "java(params.historyLength() != null)") + @Mapping( + target = "statusTimestampAfter", + source = "statusTimestampAfter", + qualifiedByName = "instantToProtoTimestamp") + @Mapping( + target = "includeArtifacts", + source = "includeArtifacts", + conditionExpression = "java(params.includeArtifacts() != null)") + @Mapping( + target = "tenant", + source = "tenant", + conditionExpression = "java(params.tenant() != null)") ListTasksRequest toProto(org.a2aproject.sdk.spec.ListTasksParams params); /** @@ -39,13 +65,24 @@ public interface ListTasksParamsMapper { */ @Mapping(target = "contextId", source = "contextId", qualifiedByName = "emptyToNull") @Mapping(target = "status", source = "status", qualifiedByName = "taskStateOrNull") - // pageSize: Check if field is set using hasPageSize() to distinguish unset (null) from explicit 0 (validation error) - @Mapping(target = "pageSize", expression = "java(request.hasPageSize() ? request.getPageSize() : null)") + // pageSize: Check if field is set using hasPageSize() to distinguish unset (null) from explicit + // 0 (validation error) + @Mapping( + target = "pageSize", + expression = "java(request.hasPageSize() ? request.getPageSize() : null)") @Mapping(target = "pageToken", source = "pageToken", qualifiedByName = "emptyToNull") // historyLength: Check if field is set using hasHistoryLength() for consistency with pageSize - @Mapping(target = "historyLength", expression = "java(request.hasHistoryLength() ? request.getHistoryLength() : null)") - @Mapping(target = "statusTimestampAfter", source = "statusTimestampAfter", qualifiedByName = "protoTimestampToInstant") - @Mapping(target = "includeArtifacts", source = "includeArtifacts", qualifiedByName = "falseToNull") + @Mapping( + target = "historyLength", + expression = "java(request.hasHistoryLength() ? request.getHistoryLength() : null)") + @Mapping( + target = "statusTimestampAfter", + source = "statusTimestampAfter", + qualifiedByName = "protoTimestampToInstant") + @Mapping( + target = "includeArtifacts", + source = "includeArtifacts", + qualifiedByName = "falseToNull") @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") org.a2aproject.sdk.spec.ListTasksParams fromProto(ListTasksRequest request); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTasksResultMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTasksResultMapper.java index 637800991..362bb98da 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTasksResultMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTasksResultMapper.java @@ -10,7 +10,8 @@ * Handles conversion with null handling for nextPageToken field. * Uses ADDER_PREFERRED strategy to avoid ProtocolMessageList instantiation issues. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = org.mapstruct.CollectionMappingStrategy.ADDER_PREFERRED, uses = {TaskMapper.class}) public interface ListTasksResultMapper { @@ -21,9 +22,15 @@ public interface ListTasksResultMapper { * Converts domain ListTasksResult to proto ListTasksResponse. * Protobuf builders don't accept null, so nextPageToken is conditionally mapped. */ - @Mapping(target = "nextPageToken", source = "nextPageToken", conditionExpression = "java(domain.nextPageToken() != null)") + @Mapping( + target = "nextPageToken", + source = "nextPageToken", + conditionExpression = "java(domain.nextPageToken() != null)") org.a2aproject.sdk.grpc.ListTasksResponse toProto(ListTasksResult domain); - @Mapping(source = "nextPageToken", target = "nextPageToken", conditionExpression = "java(proto.getNextPageToken() != null)") + @Mapping( + source = "nextPageToken", + target = "nextPageToken", + conditionExpression = "java(proto.getNextPageToken() != null)") ListTasksResult fromProto(org.a2aproject.sdk.grpc.ListTasksResponse proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageMapper.java index 05b2bfccf..d9ffdfdbb 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageMapper.java @@ -13,7 +13,8 @@ * Uses ADDER_PREFERRED strategy for List fields (parts, extensions, referenceTaskIds) * to avoid ProtocolStringList instantiation issues. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = {RoleMapper.class, PartMapper.class, A2ACommonFieldMapper.class}) public interface MessageMapper { @@ -24,9 +25,18 @@ public interface MessageMapper { * Converts domain Message to proto Message. * Uses CommonFieldMapper for metadata conversion and ADDER_PREFERRED for lists. */ - @Mapping(target = "messageId", source = "messageId", conditionExpression = "java(domain.messageId() != null)") - @Mapping(target = "contextId", source = "contextId", conditionExpression = "java(domain.contextId() != null)") - @Mapping(target = "taskId", source = "taskId", conditionExpression = "java(domain.taskId() != null)") + @Mapping( + target = "messageId", + source = "messageId", + conditionExpression = "java(domain.messageId() != null)") + @Mapping( + target = "contextId", + source = "contextId", + conditionExpression = "java(domain.contextId() != null)") + @Mapping( + target = "taskId", + source = "taskId", + conditionExpression = "java(domain.taskId() != null)") @Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataToProto") org.a2aproject.sdk.grpc.Message toProto(Message domain); @@ -40,7 +50,13 @@ public interface MessageMapper { @Mapping(target = "contextId", source = "contextId", qualifiedByName = "emptyToNull") @Mapping(target = "taskId", source = "taskId", qualifiedByName = "emptyToNull") @Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataFromProto") - @Mapping(target = "extensions", expression = "java(org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.INSTANCE.emptyListToNull(proto.getExtensionsList()))") - @Mapping(target = "referenceTaskIds", expression = "java(org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.INSTANCE.emptyListToNull(proto.getReferenceTaskIdsList()))") + @Mapping( + target = "extensions", + expression = + "java(org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.INSTANCE.emptyListToNull(proto.getExtensionsList()))") + @Mapping( + target = "referenceTaskIds", + expression = + "java(org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.INSTANCE.emptyListToNull(proto.getReferenceTaskIdsList()))") Message fromProto(org.a2aproject.sdk.grpc.Message proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageSendConfigurationMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageSendConfigurationMapper.java index 8e291caf6..4264d4637 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageSendConfigurationMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageSendConfigurationMapper.java @@ -13,17 +13,22 @@ * Handles bidirectional mapping with null/empty list conversions and task push notification config delegation. * Uses ADDER_PREFERRED strategy to avoid ProtocolStringList instantiation issues. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = {TaskPushNotificationConfigMapper.class, A2ACommonFieldMapper.class}) public interface MessageSendConfigurationMapper { - MessageSendConfigurationMapper INSTANCE = A2AMappers.getMapper(MessageSendConfigurationMapper.class); + MessageSendConfigurationMapper INSTANCE = + A2AMappers.getMapper(MessageSendConfigurationMapper.class); /** * Converts domain MessageSendConfiguration to proto SendMessageConfiguration. */ - @Mapping(target = "taskPushNotificationConfig", source = "taskPushNotificationConfig", conditionExpression = "java(domain.taskPushNotificationConfig() != null)") + @Mapping( + target = "taskPushNotificationConfig", + source = "taskPushNotificationConfig", + conditionExpression = "java(domain.taskPushNotificationConfig() != null)") org.a2aproject.sdk.grpc.SendMessageConfiguration toProto(MessageSendConfiguration domain); /** @@ -31,6 +36,9 @@ public interface MessageSendConfigurationMapper { * Uses Builder pattern for record construction. */ @BeanMapping(builder = @Builder(buildMethod = "build")) - @Mapping(target = "acceptedOutputModes", expression = "java(org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.INSTANCE.emptyListToNull(proto.getAcceptedOutputModesList()))") + @Mapping( + target = "acceptedOutputModes", + expression = + "java(org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper.INSTANCE.emptyListToNull(proto.getAcceptedOutputModesList()))") MessageSendConfiguration fromProto(org.a2aproject.sdk.grpc.SendMessageConfiguration proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageSendParamsMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageSendParamsMapper.java index 0325ca527..4e14adb1d 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageSendParamsMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageSendParamsMapper.java @@ -11,7 +11,13 @@ *

    * Handles bidirectional mapping with message/request field name difference and Struct conversions. */ -@Mapper(config = A2AProtoMapperConfig.class, uses = {MessageMapper.class, MessageSendConfigurationMapper.class, A2ACommonFieldMapper.class}) +@Mapper( + config = A2AProtoMapperConfig.class, + uses = { + MessageMapper.class, + MessageSendConfigurationMapper.class, + A2ACommonFieldMapper.class + }) public interface MessageSendParamsMapper { MessageSendParamsMapper INSTANCE = A2AMappers.getMapper(MessageSendParamsMapper.class); @@ -20,9 +26,15 @@ public interface MessageSendParamsMapper { * Converts domain MessageSendParams to proto SendMessageRequest. * Maps domain "message" field to proto "message" field. */ - @Mapping(target = "configuration", source = "configuration", conditionExpression = "java(domain.configuration() != null)") + @Mapping( + target = "configuration", + source = "configuration", + conditionExpression = "java(domain.configuration() != null)") @Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataToProto") - @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)") + @Mapping( + target = "tenant", + source = "tenant", + conditionExpression = "java(domain.tenant() != null)") org.a2aproject.sdk.grpc.SendMessageRequest toProto(MessageSendParams domain); /** diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MutualTLSSecuritySchemeMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MutualTLSSecuritySchemeMapper.java index 4a56fc926..5c41189e1 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MutualTLSSecuritySchemeMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MutualTLSSecuritySchemeMapper.java @@ -7,16 +7,23 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.MutualTLSSecurityScheme} and {@link org.a2aproject.sdk.grpc.MutualTlsSecurityScheme}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) public interface MutualTLSSecuritySchemeMapper { - MutualTLSSecuritySchemeMapper INSTANCE = A2AMappers.getMapper(MutualTLSSecuritySchemeMapper.class); + MutualTLSSecuritySchemeMapper INSTANCE = + A2AMappers.getMapper(MutualTLSSecuritySchemeMapper.class); - @Mapping(target = "description", source = "description", conditionExpression = "java(domain.description() != null)") - org.a2aproject.sdk.grpc.MutualTlsSecurityScheme toProto(org.a2aproject.sdk.spec.MutualTLSSecurityScheme domain); + @Mapping( + target = "description", + source = "description", + conditionExpression = "java(domain.description() != null)") + org.a2aproject.sdk.grpc.MutualTlsSecurityScheme toProto( + org.a2aproject.sdk.spec.MutualTLSSecurityScheme domain); - default org.a2aproject.sdk.spec.MutualTLSSecurityScheme fromProto(org.a2aproject.sdk.grpc.MutualTlsSecurityScheme proto) { + default org.a2aproject.sdk.spec.MutualTLSSecurityScheme fromProto( + org.a2aproject.sdk.grpc.MutualTlsSecurityScheme proto) { if (proto == null) { return null; } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OAuth2SecuritySchemeMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OAuth2SecuritySchemeMapper.java index cae76a12b..25cc9aacd 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OAuth2SecuritySchemeMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OAuth2SecuritySchemeMapper.java @@ -7,26 +7,38 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.OAuth2SecurityScheme} and {@link org.a2aproject.sdk.grpc.OAuth2SecurityScheme}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = {OAuthFlowsMapper.class}) public interface OAuth2SecuritySchemeMapper { OAuth2SecuritySchemeMapper INSTANCE = A2AMappers.getMapper(OAuth2SecuritySchemeMapper.class); - @Mapping(target = "description", source = "description", conditionExpression = "java(domain.description() != null)") - @Mapping(target = "oauth2MetadataUrl", source = "oauth2MetadataUrl", conditionExpression = "java(domain.oauth2MetadataUrl() != null)") - org.a2aproject.sdk.grpc.OAuth2SecurityScheme toProto(org.a2aproject.sdk.spec.OAuth2SecurityScheme domain); + @Mapping( + target = "description", + source = "description", + conditionExpression = "java(domain.description() != null)") + @Mapping( + target = "oauth2MetadataUrl", + source = "oauth2MetadataUrl", + conditionExpression = "java(domain.oauth2MetadataUrl() != null)") + org.a2aproject.sdk.grpc.OAuth2SecurityScheme toProto( + org.a2aproject.sdk.spec.OAuth2SecurityScheme domain); - default org.a2aproject.sdk.spec.OAuth2SecurityScheme fromProto(org.a2aproject.sdk.grpc.OAuth2SecurityScheme proto) { + default org.a2aproject.sdk.spec.OAuth2SecurityScheme fromProto( + org.a2aproject.sdk.grpc.OAuth2SecurityScheme proto) { if (proto == null) { return null; } - org.a2aproject.sdk.spec.OAuthFlows flows = OAuthFlowsMapper.INSTANCE.fromProto(proto.getFlows()); + org.a2aproject.sdk.spec.OAuthFlows flows = + OAuthFlowsMapper.INSTANCE.fromProto(proto.getFlows()); String description = proto.getDescription().isEmpty() ? null : proto.getDescription(); - String oauth2MetadataUrl = proto.getOauth2MetadataUrl().isEmpty() ? null : proto.getOauth2MetadataUrl(); + String oauth2MetadataUrl = + proto.getOauth2MetadataUrl().isEmpty() ? null : proto.getOauth2MetadataUrl(); - return new org.a2aproject.sdk.spec.OAuth2SecurityScheme(flows, description, oauth2MetadataUrl); + return new org.a2aproject.sdk.spec.OAuth2SecurityScheme( + flows, description, oauth2MetadataUrl); } } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OAuthFlowsMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OAuthFlowsMapper.java index ecea584a4..a19e5d6d1 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OAuthFlowsMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OAuthFlowsMapper.java @@ -7,12 +7,10 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.OAuthFlows} and {@link org.a2aproject.sdk.grpc.OAuthFlows}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, - uses = { - AuthorizationCodeOAuthFlowMapper.class, - ClientCredentialsOAuthFlowMapper.class - }) + uses = {AuthorizationCodeOAuthFlowMapper.class, ClientCredentialsOAuthFlowMapper.class}) public interface OAuthFlowsMapper { OAuthFlowsMapper INSTANCE = A2AMappers.getMapper(OAuthFlowsMapper.class); diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OpenIdConnectSecuritySchemeMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OpenIdConnectSecuritySchemeMapper.java index a526a9308..6105ca525 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OpenIdConnectSecuritySchemeMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/OpenIdConnectSecuritySchemeMapper.java @@ -7,22 +7,30 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.OpenIdConnectSecurityScheme} and {@link org.a2aproject.sdk.grpc.OpenIdConnectSecurityScheme}. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) public interface OpenIdConnectSecuritySchemeMapper { - OpenIdConnectSecuritySchemeMapper INSTANCE = A2AMappers.getMapper(OpenIdConnectSecuritySchemeMapper.class); + OpenIdConnectSecuritySchemeMapper INSTANCE = + A2AMappers.getMapper(OpenIdConnectSecuritySchemeMapper.class); - @Mapping(target = "description", source = "description", conditionExpression = "java(domain.description() != null)") - org.a2aproject.sdk.grpc.OpenIdConnectSecurityScheme toProto(org.a2aproject.sdk.spec.OpenIdConnectSecurityScheme domain); + @Mapping( + target = "description", + source = "description", + conditionExpression = "java(domain.description() != null)") + org.a2aproject.sdk.grpc.OpenIdConnectSecurityScheme toProto( + org.a2aproject.sdk.spec.OpenIdConnectSecurityScheme domain); - default org.a2aproject.sdk.spec.OpenIdConnectSecurityScheme fromProto(org.a2aproject.sdk.grpc.OpenIdConnectSecurityScheme proto) { + default org.a2aproject.sdk.spec.OpenIdConnectSecurityScheme fromProto( + org.a2aproject.sdk.grpc.OpenIdConnectSecurityScheme proto) { if (proto == null) { return null; } String description = proto.getDescription().isEmpty() ? null : proto.getDescription(); - return new org.a2aproject.sdk.spec.OpenIdConnectSecurityScheme(proto.getOpenIdConnectUrl(), description); + return new org.a2aproject.sdk.spec.OpenIdConnectSecurityScheme( + proto.getOpenIdConnectUrl(), description); } } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/PartMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/PartMapper.java index cd8fb8600..4edaf12bc 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/PartMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/PartMapper.java @@ -1,9 +1,9 @@ package org.a2aproject.sdk.grpc.mapper; -import java.util.Base64; - import com.google.protobuf.ByteString; import com.google.protobuf.Value; +import java.util.Base64; +import java.util.Map; import org.a2aproject.sdk.spec.DataPart; import org.a2aproject.sdk.spec.FileContent; import org.a2aproject.sdk.spec.FilePart; @@ -12,7 +12,6 @@ import org.a2aproject.sdk.spec.InvalidRequestError; import org.a2aproject.sdk.spec.Part; import org.a2aproject.sdk.spec.TextPart; -import java.util.Map; import org.mapstruct.Mapper; /** @@ -29,7 +28,9 @@ * Manual Implementation Required: Must use manual instanceof dispatch to handle protobuf oneof pattern, * as MapStruct's @SubclassMapping maps to different target types, not different fields of the same type. */ -@Mapper(config = A2AProtoMapperConfig.class, uses = {A2ACommonFieldMapper.class}) +@Mapper( + config = A2AProtoMapperConfig.class, + uses = {A2ACommonFieldMapper.class}) public interface PartMapper { PartMapper INSTANCE = A2AMappers.getMapper(PartMapper.class); @@ -53,7 +54,8 @@ default org.a2aproject.sdk.grpc.Part toProto(Part domain) { if (fileContent instanceof FileWithBytes fileWithBytes) { // Map to raw (bytes), filename, and media_type - builder.setRaw(ByteString.copyFrom(Base64.getDecoder().decode(fileWithBytes.bytes()))); + builder.setRaw( + ByteString.copyFrom(Base64.getDecoder().decode(fileWithBytes.bytes()))); if (fileWithBytes.name() != null) { builder.setFilename(fileWithBytes.name()); } @@ -89,7 +91,8 @@ default Part fromProto(org.a2aproject.sdk.grpc.Part proto) { if (proto == null) { return null; } - Map metadata = A2ACommonFieldMapper.INSTANCE.metadataFromProto(proto.getMetadata()); + Map metadata = + A2ACommonFieldMapper.INSTANCE.metadataFromProto(proto.getMetadata()); if (proto.hasText()) { return new TextPart(proto.getText(), metadata); } else if (proto.hasRaw()) { diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ResourceNameParser.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ResourceNameParser.java index 653fe79de..d2cea7816 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ResourceNameParser.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ResourceNameParser.java @@ -21,7 +21,7 @@ public static String extractTaskId(String resourceName) { /** * Define the task name form its taskId as in "tasks/{taskId}". * - * @param taskId the taskId + * @param taskId the taskId * @return the task name */ public static String defineTaskName(String taskId) { @@ -40,9 +40,10 @@ public static String defineTaskName(String taskId) { public static String[] parseTaskPushNotificationConfigName(String resourceName) { String[] parts = resourceName.split("/"); if (parts.length < 4) { - throw new IllegalArgumentException("Invalid name format for TaskPushNotificationConfig: " + resourceName); + throw new IllegalArgumentException( + "Invalid name format for TaskPushNotificationConfig: " + resourceName); } - return new String[]{parts[1], parts[3]}; + return new String[] {parts[1], parts[3]}; } /** @@ -65,17 +66,20 @@ public static String[] parseGetTaskPushNotificationConfigName(String resourceNam // "tasks/{taskId}" - use taskId as configId configId = taskId; } else if (parts.length < 4) { - throw new IllegalArgumentException("Invalid name format for GetTaskPushNotificationConfigRequest: " + resourceName); + throw new IllegalArgumentException( + "Invalid name format for GetTaskPushNotificationConfigRequest: " + + resourceName); } else { // "tasks/{taskId}/pushNotificationConfigs/{configId}" configId = parts[3]; } - return new String[]{taskId, configId}; + return new String[] {taskId, configId}; } + public static String defineGetTaskPushNotificationConfigName(String taskId, String configId) { - String name= "tasks/" + taskId ; - if(configId != null && !configId.isBlank()) { + String name = "tasks/" + taskId; + if (configId != null && !configId.isBlank()) { name = name + "/pushNotificationConfigs/" + configId; } return name; diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SecurityRequirementMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SecurityRequirementMapper.java index 18bc645f9..1992dafdd 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SecurityRequirementMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SecurityRequirementMapper.java @@ -1,11 +1,10 @@ package org.a2aproject.sdk.grpc.mapper; +import com.google.protobuf.ProtocolStringList; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - -import com.google.protobuf.ProtocolStringList; import org.a2aproject.sdk.grpc.StringList; import org.mapstruct.Mapper; @@ -46,12 +45,14 @@ public interface SecurityRequirementMapper { * @param domainRequirement domain SecurityRequirement with schemes map * @return SecurityRequirement proto message, or null if input is null */ - default org.a2aproject.sdk.grpc.SecurityRequirement mapSecurityRequirement(org.a2aproject.sdk.spec.SecurityRequirement domainRequirement) { + default org.a2aproject.sdk.grpc.SecurityRequirement mapSecurityRequirement( + org.a2aproject.sdk.spec.SecurityRequirement domainRequirement) { if (domainRequirement == null) { return null; } - org.a2aproject.sdk.grpc.SecurityRequirement.Builder securityBuilder = org.a2aproject.sdk.grpc.SecurityRequirement.newBuilder(); + org.a2aproject.sdk.grpc.SecurityRequirement.Builder securityBuilder = + org.a2aproject.sdk.grpc.SecurityRequirement.newBuilder(); Map> schemes = domainRequirement.schemes(); if (schemes != null) { for (Map.Entry> entry : schemes.entrySet()) { @@ -74,12 +75,14 @@ default org.a2aproject.sdk.grpc.SecurityRequirement mapSecurityRequirement(org.a * @param domainSecurity list of SecurityRequirement domain objects * @return list of SecurityRequirement proto messages, or null if input is null */ - default List toProto(List domainSecurity) { + default List toProto( + List domainSecurity) { if (domainSecurity == null) { return null; } - List protoList = new ArrayList<>(domainSecurity.size()); + List protoList = + new ArrayList<>(domainSecurity.size()); for (org.a2aproject.sdk.spec.SecurityRequirement requirement : domainSecurity) { protoList.add(mapSecurityRequirement(requirement)); } @@ -92,12 +95,14 @@ default List toProto(List fromProto(List protoSecurity) { + default List fromProto( + List protoSecurity) { if (protoSecurity == null) { return null; } - List domainList = new ArrayList<>(protoSecurity.size()); + List domainList = + new ArrayList<>(protoSecurity.size()); for (org.a2aproject.sdk.grpc.SecurityRequirement security : protoSecurity) { Map> schemeMap = new LinkedHashMap<>(); for (Map.Entry entry : security.getSchemesMap().entrySet()) { diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SecuritySchemeMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SecuritySchemeMapper.java index 5c15e944f..13b0cf050 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SecuritySchemeMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SecuritySchemeMapper.java @@ -18,7 +18,8 @@ * Manual Implementation Required: Must use manual instanceof dispatch to handle sealed interface (5 permitted subtypes) * to protobuf oneof pattern, as MapStruct's @SubclassMapping cannot map different source types to different fields of the same target type. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = { APIKeySecuritySchemeMapper.class, @@ -41,31 +42,33 @@ public interface SecuritySchemeMapper { * @param domain the domain security scheme (sealed interface) * @return the protobuf SecurityScheme with the appropriate oneof field set */ - default org.a2aproject.sdk.grpc.SecurityScheme toProto(org.a2aproject.sdk.spec.SecurityScheme domain) { + default org.a2aproject.sdk.grpc.SecurityScheme toProto( + org.a2aproject.sdk.spec.SecurityScheme domain) { if (domain == null) { return null; } if (domain instanceof APIKeySecurityScheme s) { return org.a2aproject.sdk.grpc.SecurityScheme.newBuilder() - .setApiKeySecurityScheme(APIKeySecuritySchemeMapper.INSTANCE.toProto(s)) - .build(); + .setApiKeySecurityScheme(APIKeySecuritySchemeMapper.INSTANCE.toProto(s)) + .build(); } else if (domain instanceof HTTPAuthSecurityScheme s) { return org.a2aproject.sdk.grpc.SecurityScheme.newBuilder() - .setHttpAuthSecurityScheme(HTTPAuthSecuritySchemeMapper.INSTANCE.toProto(s)) - .build(); + .setHttpAuthSecurityScheme(HTTPAuthSecuritySchemeMapper.INSTANCE.toProto(s)) + .build(); } else if (domain instanceof OAuth2SecurityScheme s) { return org.a2aproject.sdk.grpc.SecurityScheme.newBuilder() - .setOauth2SecurityScheme(OAuth2SecuritySchemeMapper.INSTANCE.toProto(s)) - .build(); + .setOauth2SecurityScheme(OAuth2SecuritySchemeMapper.INSTANCE.toProto(s)) + .build(); } else if (domain instanceof OpenIdConnectSecurityScheme s) { return org.a2aproject.sdk.grpc.SecurityScheme.newBuilder() - .setOpenIdConnectSecurityScheme(OpenIdConnectSecuritySchemeMapper.INSTANCE.toProto(s)) - .build(); + .setOpenIdConnectSecurityScheme( + OpenIdConnectSecuritySchemeMapper.INSTANCE.toProto(s)) + .build(); } else if (domain instanceof MutualTLSSecurityScheme s) { return org.a2aproject.sdk.grpc.SecurityScheme.newBuilder() - .setMtlsSecurityScheme(MutualTLSSecuritySchemeMapper.INSTANCE.toProto(s)) - .build(); + .setMtlsSecurityScheme(MutualTLSSecuritySchemeMapper.INSTANCE.toProto(s)) + .build(); } throw new IllegalArgumentException("Unknown SecurityScheme type: " + domain.getClass()); @@ -81,24 +84,27 @@ default org.a2aproject.sdk.grpc.SecurityScheme toProto(org.a2aproject.sdk.spec.S * @param proto the protobuf SecurityScheme with a oneof field set * @return the domain security scheme (sealed interface implementation) */ - default org.a2aproject.sdk.spec.SecurityScheme fromProto(org.a2aproject.sdk.grpc.SecurityScheme proto) { + default org.a2aproject.sdk.spec.SecurityScheme fromProto( + org.a2aproject.sdk.grpc.SecurityScheme proto) { if (proto == null) { return null; } return switch (proto.getSchemeCase()) { case API_KEY_SECURITY_SCHEME -> - APIKeySecuritySchemeMapper.INSTANCE.fromProto(proto.getApiKeySecurityScheme()); + APIKeySecuritySchemeMapper.INSTANCE.fromProto(proto.getApiKeySecurityScheme()); case HTTP_AUTH_SECURITY_SCHEME -> - HTTPAuthSecuritySchemeMapper.INSTANCE.fromProto(proto.getHttpAuthSecurityScheme()); + HTTPAuthSecuritySchemeMapper.INSTANCE.fromProto( + proto.getHttpAuthSecurityScheme()); case OAUTH2_SECURITY_SCHEME -> - OAuth2SecuritySchemeMapper.INSTANCE.fromProto(proto.getOauth2SecurityScheme()); + OAuth2SecuritySchemeMapper.INSTANCE.fromProto(proto.getOauth2SecurityScheme()); case OPEN_ID_CONNECT_SECURITY_SCHEME -> - OpenIdConnectSecuritySchemeMapper.INSTANCE.fromProto(proto.getOpenIdConnectSecurityScheme()); + OpenIdConnectSecuritySchemeMapper.INSTANCE.fromProto( + proto.getOpenIdConnectSecurityScheme()); case MTLS_SECURITY_SCHEME -> - MutualTLSSecuritySchemeMapper.INSTANCE.fromProto(proto.getMtlsSecurityScheme()); + MutualTLSSecuritySchemeMapper.INSTANCE.fromProto(proto.getMtlsSecurityScheme()); case SCHEME_NOT_SET -> - throw new IllegalArgumentException("SecurityScheme oneof field not set"); + throw new IllegalArgumentException("SecurityScheme oneof field not set"); }; } } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/StreamResponseMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/StreamResponseMapper.java index 9028ad0d6..0d2e19fd8 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/StreamResponseMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/StreamResponseMapper.java @@ -22,8 +22,14 @@ * This mapper provides bidirectional conversion using instanceof checks (toProto) * and switch expressions on the oneof case (fromProto). */ -@Mapper(config = A2AProtoMapperConfig.class, - uses = {TaskMapper.class, MessageMapper.class, TaskStatusUpdateEventMapper.class, TaskArtifactUpdateEventMapper.class}) +@Mapper( + config = A2AProtoMapperConfig.class, + uses = { + TaskMapper.class, + MessageMapper.class, + TaskStatusUpdateEventMapper.class, + TaskArtifactUpdateEventMapper.class + }) public interface StreamResponseMapper { StreamResponseMapper INSTANCE = A2AMappers.getMapper(StreamResponseMapper.class); @@ -41,20 +47,29 @@ default org.a2aproject.sdk.grpc.StreamResponse toProto(StreamingEventKind domain } return switch (domain.kind()) { - case Task.STREAMING_EVENT_ID -> org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setTask(TaskMapper.INSTANCE.toProto((Task) domain)) - .build(); - case Message.STREAMING_EVENT_ID -> org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setMessage(MessageMapper.INSTANCE.toProto((Message) domain)) - .build(); - case TaskStatusUpdateEvent.STREAMING_EVENT_ID -> org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setStatusUpdate(TaskStatusUpdateEventMapper.INSTANCE.toProto((TaskStatusUpdateEvent) domain)) - .build(); - case TaskArtifactUpdateEvent.STREAMING_EVENT_ID -> org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setArtifactUpdate(TaskArtifactUpdateEventMapper.INSTANCE.toProto((TaskArtifactUpdateEvent) domain)) - .build(); + case Task.STREAMING_EVENT_ID -> + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setTask(TaskMapper.INSTANCE.toProto((Task) domain)) + .build(); + case Message.STREAMING_EVENT_ID -> + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setMessage(MessageMapper.INSTANCE.toProto((Message) domain)) + .build(); + case TaskStatusUpdateEvent.STREAMING_EVENT_ID -> + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setStatusUpdate( + TaskStatusUpdateEventMapper.INSTANCE.toProto( + (TaskStatusUpdateEvent) domain)) + .build(); + case TaskArtifactUpdateEvent.STREAMING_EVENT_ID -> + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setArtifactUpdate( + TaskArtifactUpdateEventMapper.INSTANCE.toProto( + (TaskArtifactUpdateEvent) domain)) + .build(); default -> - throw new IllegalArgumentException("Unknown StreamingEventKind type: " + domain.getClass().getName()); + throw new IllegalArgumentException( + "Unknown StreamingEventKind type: " + domain.getClass().getName()); }; } @@ -72,16 +87,15 @@ default StreamingEventKind fromProto(org.a2aproject.sdk.grpc.StreamResponse prot } return switch (proto.getPayloadCase()) { - case TASK -> - TaskMapper.INSTANCE.fromProto(proto.getTask()); - case MESSAGE -> - MessageMapper.INSTANCE.fromProto(proto.getMessage()); + case TASK -> TaskMapper.INSTANCE.fromProto(proto.getTask()); + case MESSAGE -> MessageMapper.INSTANCE.fromProto(proto.getMessage()); case STATUS_UPDATE -> - TaskStatusUpdateEventMapper.INSTANCE.fromProto(proto.getStatusUpdate()); + TaskStatusUpdateEventMapper.INSTANCE.fromProto(proto.getStatusUpdate()); case ARTIFACT_UPDATE -> - TaskArtifactUpdateEventMapper.INSTANCE.fromProto(proto.getArtifactUpdate()); + TaskArtifactUpdateEventMapper.INSTANCE.fromProto(proto.getArtifactUpdate()); case PAYLOAD_NOT_SET -> - throw new IllegalArgumentException("StreamResponse payload oneof field not set"); + throw new IllegalArgumentException( + "StreamResponse payload oneof field not set"); }; } } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SubscribeToTaskRequestMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SubscribeToTaskRequestMapper.java index 90fe97ade..7cc0e05f1 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SubscribeToTaskRequestMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/SubscribeToTaskRequestMapper.java @@ -16,10 +16,13 @@ * request parameters. The JSONRPC envelope (id, jsonrpc, method) is handled separately * by the transport layer. */ -@Mapper(config = A2AProtoMapperConfig.class, uses = {TaskIdParamsMapper.class}) +@Mapper( + config = A2AProtoMapperConfig.class, + uses = {TaskIdParamsMapper.class}) public interface SubscribeToTaskRequestMapper { - SubscribeToTaskRequestMapper INSTANCE = A2AMappers.getMapper(SubscribeToTaskRequestMapper.class); + SubscribeToTaskRequestMapper INSTANCE = + A2AMappers.getMapper(SubscribeToTaskRequestMapper.class); /** * Converts domain SubscribeToTaskRequest to proto SubscribeToTaskRequest. @@ -45,7 +48,7 @@ default org.a2aproject.sdk.grpc.SubscribeToTaskRequest toProto(SubscribeToTaskRe * @return the domain SubscribeToTaskRequest */ default SubscribeToTaskRequest fromProto(org.a2aproject.sdk.grpc.SubscribeToTaskRequest proto) { - if (proto == null || proto.getId()== null) { + if (proto == null || proto.getId() == null) { return null; } return SubscribeToTaskRequest.builder() diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskArtifactUpdateEventMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskArtifactUpdateEventMapper.java index 4e262a27e..d46091c25 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskArtifactUpdateEventMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskArtifactUpdateEventMapper.java @@ -11,17 +11,26 @@ *

    * Now fully declarative using Builder pattern with @BeanMapping. */ -@Mapper(config = A2AProtoMapperConfig.class, uses = {ArtifactMapper.class, A2ACommonFieldMapper.class}) +@Mapper( + config = A2AProtoMapperConfig.class, + uses = {ArtifactMapper.class, A2ACommonFieldMapper.class}) public interface TaskArtifactUpdateEventMapper { - TaskArtifactUpdateEventMapper INSTANCE = A2AMappers.getMapper(TaskArtifactUpdateEventMapper.class); + TaskArtifactUpdateEventMapper INSTANCE = + A2AMappers.getMapper(TaskArtifactUpdateEventMapper.class); /** * Converts domain TaskArtifactUpdateEvent to proto. * Uses declarative mapping with CommonFieldMapper for metadata conversion. */ - @Mapping(target = "append", source = "append", conditionExpression = "java(domain.append() != null)") - @Mapping(target = "lastChunk", source = "lastChunk", conditionExpression = "java(domain.lastChunk() != null)") + @Mapping( + target = "append", + source = "append", + conditionExpression = "java(domain.append() != null)") + @Mapping( + target = "lastChunk", + source = "lastChunk", + conditionExpression = "java(domain.lastChunk() != null)") @Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataToProto") org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent toProto(TaskArtifactUpdateEvent domain); diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskIdParamsMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskIdParamsMapper.java index d6f3a913d..42ff8c4a2 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskIdParamsMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskIdParamsMapper.java @@ -26,18 +26,20 @@ public interface TaskIdParamsMapper { @Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataFromProto") @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") CancelTaskParams fromProtoCancelTaskRequest(org.a2aproject.sdk.grpc.CancelTaskRequest proto); - - /** + + /** * Converts proto CancelTaskRequest to domain TaskIdParams. * Extracts task ID from the resource name. */ @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "id", source = "id") @Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataToProto") - @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)") + @Mapping( + target = "tenant", + source = "tenant", + conditionExpression = "java(domain.tenant() != null)") org.a2aproject.sdk.grpc.CancelTaskRequest toProtoCancelTaskRequest(CancelTaskParams domain); - /** * Converts proto SubscribeToTaskRequest to domain TaskIdParams. * Extracts task ID from the resource name. @@ -45,7 +47,8 @@ public interface TaskIdParamsMapper { @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "id", source = "id") @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") - TaskIdParams fromProtoSubscribeToTaskRequest(org.a2aproject.sdk.grpc.SubscribeToTaskRequest proto); + TaskIdParams fromProtoSubscribeToTaskRequest( + org.a2aproject.sdk.grpc.SubscribeToTaskRequest proto); /** * Converts domain TaskIdParams to proto SubscribeToTaskRequest. @@ -53,6 +56,10 @@ public interface TaskIdParamsMapper { */ @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "id", source = "id") - @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)") - org.a2aproject.sdk.grpc.SubscribeToTaskRequest toProtoSubscribeToTaskRequest(TaskIdParams domain); + @Mapping( + target = "tenant", + source = "tenant", + conditionExpression = "java(domain.tenant() != null)") + org.a2aproject.sdk.grpc.SubscribeToTaskRequest toProtoSubscribeToTaskRequest( + TaskIdParams domain); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskMapper.java index d83c31bc7..f911eb2a0 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskMapper.java @@ -13,9 +13,15 @@ * Uses ADDER_PREFERRED strategy for List fields (artifacts, history) * to use addAllArtifacts() and addAllHistory() methods. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, - uses = {TaskStatusMapper.class, ArtifactMapper.class, MessageMapper.class, A2ACommonFieldMapper.class}) + uses = { + TaskStatusMapper.class, + ArtifactMapper.class, + MessageMapper.class, + A2ACommonFieldMapper.class + }) public interface TaskMapper { TaskMapper INSTANCE = A2AMappers.getMapper(TaskMapper.class); @@ -25,7 +31,10 @@ public interface TaskMapper { * Uses CommonFieldMapper for metadata conversion and ADDER_PREFERRED for lists. */ @Mapping(target = "id", source = "id", conditionExpression = "java(domain.id() != null)") - @Mapping(target = "contextId", source = "contextId", conditionExpression = "java(domain.contextId() != null)") + @Mapping( + target = "contextId", + source = "contextId", + conditionExpression = "java(domain.contextId() != null)") @Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataToProto") org.a2aproject.sdk.grpc.Task toProto(Task domain); diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskPushNotificationConfigMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskPushNotificationConfigMapper.java index 2d815954e..6bd9b53af 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskPushNotificationConfigMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskPushNotificationConfigMapper.java @@ -11,12 +11,14 @@ * Maps between domain TaskPushNotificationConfig and proto TaskPushNotificationConfig. * The proto has direct id, task_id, url, token, authentication, and tenant fields. */ -@Mapper(config = A2AProtoMapperConfig.class, +@Mapper( + config = A2AProtoMapperConfig.class, collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = {AuthenticationInfoMapper.class, A2ACommonFieldMapper.class}) public interface TaskPushNotificationConfigMapper { - TaskPushNotificationConfigMapper INSTANCE = A2AMappers.getMapper(TaskPushNotificationConfigMapper.class); + TaskPushNotificationConfigMapper INSTANCE = + A2AMappers.getMapper(TaskPushNotificationConfigMapper.class); /** * Converts domain TaskPushNotificationConfig to protobuf TaskPushNotificationConfig. @@ -25,11 +27,23 @@ public interface TaskPushNotificationConfigMapper { * @return protobuf TaskPushNotificationConfig */ @Mapping(target = "id", source = "id", conditionExpression = "java(domain.id() != null)") - @Mapping(target = "taskId", source = "taskId", conditionExpression = "java(domain.taskId() != null)") + @Mapping( + target = "taskId", + source = "taskId", + conditionExpression = "java(domain.taskId() != null)") @Mapping(target = "url", source = "url", conditionExpression = "java(domain.url() != null)") - @Mapping(target = "token", source = "token", conditionExpression = "java(domain.token() != null)") - @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)") - @Mapping(target = "authentication", source = "authentication", conditionExpression = "java(domain.authentication() != null)") + @Mapping( + target = "token", + source = "token", + conditionExpression = "java(domain.token() != null)") + @Mapping( + target = "tenant", + source = "tenant", + conditionExpression = "java(domain.tenant() != null)") + @Mapping( + target = "authentication", + source = "authentication", + conditionExpression = "java(domain.authentication() != null)") org.a2aproject.sdk.grpc.TaskPushNotificationConfig toProto(TaskPushNotificationConfig domain); /** @@ -41,6 +55,9 @@ public interface TaskPushNotificationConfigMapper { @Mapping(target = "token", source = "token", qualifiedByName = "emptyToNull") @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") @Mapping(target = "taskId", source = "taskId", qualifiedByName = "emptyToNull") - @Mapping(target = "authentication", source = "authentication", conditionExpression = "java(proto.hasAuthentication())") + @Mapping( + target = "authentication", + source = "authentication", + conditionExpression = "java(proto.hasAuthentication())") TaskPushNotificationConfig fromProto(org.a2aproject.sdk.grpc.TaskPushNotificationConfig proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskQueryParamsMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskQueryParamsMapper.java index d95147b75..af86ccfe9 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskQueryParamsMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskQueryParamsMapper.java @@ -28,6 +28,9 @@ public interface TaskQueryParamsMapper { @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "id", source = "id") @Mapping(target = "historyLength", source = "historyLength") - @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)") + @Mapping( + target = "tenant", + source = "tenant", + conditionExpression = "java(domain.tenant() != null)") org.a2aproject.sdk.grpc.GetTaskRequest toProto(TaskQueryParams domain); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStateMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStateMapper.java index cf9c071ad..155c708ed 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStateMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStateMapper.java @@ -32,8 +32,10 @@ default org.a2aproject.sdk.grpc.TaskState toProto(org.a2aproject.sdk.spec.TaskSt return switch (domain) { case TASK_STATE_SUBMITTED -> org.a2aproject.sdk.grpc.TaskState.TASK_STATE_SUBMITTED; case TASK_STATE_WORKING -> org.a2aproject.sdk.grpc.TaskState.TASK_STATE_WORKING; - case TASK_STATE_INPUT_REQUIRED -> org.a2aproject.sdk.grpc.TaskState.TASK_STATE_INPUT_REQUIRED; - case TASK_STATE_AUTH_REQUIRED -> org.a2aproject.sdk.grpc.TaskState.TASK_STATE_AUTH_REQUIRED; + case TASK_STATE_INPUT_REQUIRED -> + org.a2aproject.sdk.grpc.TaskState.TASK_STATE_INPUT_REQUIRED; + case TASK_STATE_AUTH_REQUIRED -> + org.a2aproject.sdk.grpc.TaskState.TASK_STATE_AUTH_REQUIRED; case TASK_STATE_COMPLETED -> org.a2aproject.sdk.grpc.TaskState.TASK_STATE_COMPLETED; case TASK_STATE_CANCELED -> org.a2aproject.sdk.grpc.TaskState.TASK_STATE_CANCELED; case TASK_STATE_FAILED -> org.a2aproject.sdk.grpc.TaskState.TASK_STATE_FAILED; @@ -56,13 +58,16 @@ default org.a2aproject.sdk.spec.TaskState fromProto(org.a2aproject.sdk.grpc.Task return switch (proto) { case TASK_STATE_SUBMITTED -> org.a2aproject.sdk.spec.TaskState.TASK_STATE_SUBMITTED; case TASK_STATE_WORKING -> org.a2aproject.sdk.spec.TaskState.TASK_STATE_WORKING; - case TASK_STATE_INPUT_REQUIRED -> org.a2aproject.sdk.spec.TaskState.TASK_STATE_INPUT_REQUIRED; - case TASK_STATE_AUTH_REQUIRED -> org.a2aproject.sdk.spec.TaskState.TASK_STATE_AUTH_REQUIRED; + case TASK_STATE_INPUT_REQUIRED -> + org.a2aproject.sdk.spec.TaskState.TASK_STATE_INPUT_REQUIRED; + case TASK_STATE_AUTH_REQUIRED -> + org.a2aproject.sdk.spec.TaskState.TASK_STATE_AUTH_REQUIRED; case TASK_STATE_COMPLETED -> org.a2aproject.sdk.spec.TaskState.TASK_STATE_COMPLETED; case TASK_STATE_CANCELED -> org.a2aproject.sdk.spec.TaskState.TASK_STATE_CANCELED; case TASK_STATE_FAILED -> org.a2aproject.sdk.spec.TaskState.TASK_STATE_FAILED; case TASK_STATE_REJECTED -> org.a2aproject.sdk.spec.TaskState.TASK_STATE_REJECTED; - case TASK_STATE_UNSPECIFIED, UNRECOGNIZED -> org.a2aproject.sdk.spec.TaskState.TASK_STATE_UNSPECIFIED; + case TASK_STATE_UNSPECIFIED, UNRECOGNIZED -> + org.a2aproject.sdk.spec.TaskState.TASK_STATE_UNSPECIFIED; }; } } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStatusMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStatusMapper.java index 24fecb285..aa898b776 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStatusMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStatusMapper.java @@ -13,7 +13,9 @@ * Uses TaskStateMapper for state conversion, MessageMapper for message conversion, * and CommonFieldMapper for timestamp conversion. */ -@Mapper(config = A2AProtoMapperConfig.class, uses = {TaskStateMapper.class, MessageMapper.class, A2ACommonFieldMapper.class}) +@Mapper( + config = A2AProtoMapperConfig.class, + uses = {TaskStateMapper.class, MessageMapper.class, A2ACommonFieldMapper.class}) public interface TaskStatusMapper { TaskStatusMapper INSTANCE = A2AMappers.getMapper(TaskStatusMapper.class); @@ -22,9 +24,18 @@ public interface TaskStatusMapper { * Converts domain TaskStatus to proto TaskStatus. * Uses MessageMapper for message and CommonFieldMapper for timestamp conversion. */ - @Mapping(target = "state", source = "state", conditionExpression = "java(domain.state() != null)") - @Mapping(target = "message", source = "message", conditionExpression = "java(domain.message() != null)") - @Mapping(target = "timestamp", source = "timestamp", qualifiedByName = "offsetDateTimeToProtoTimestamp") + @Mapping( + target = "state", + source = "state", + conditionExpression = "java(domain.state() != null)") + @Mapping( + target = "message", + source = "message", + conditionExpression = "java(domain.message() != null)") + @Mapping( + target = "timestamp", + source = "timestamp", + qualifiedByName = "offsetDateTimeToProtoTimestamp") org.a2aproject.sdk.grpc.TaskStatus toProto(TaskStatus domain); /** @@ -32,6 +43,9 @@ public interface TaskStatusMapper { * Uses MessageMapper for message and CommonFieldMapper for timestamp conversion. */ @BeanMapping(builder = @Builder(buildMethod = "build")) - @Mapping(target = "timestamp", source = "timestamp", qualifiedByName = "protoTimestampToOffsetDateTime") + @Mapping( + target = "timestamp", + source = "timestamp", + qualifiedByName = "protoTimestampToOffsetDateTime") TaskStatus fromProto(org.a2aproject.sdk.grpc.TaskStatus proto); } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStatusUpdateEventMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStatusUpdateEventMapper.java index 54530d8e7..6f8569f9b 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStatusUpdateEventMapper.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/TaskStatusUpdateEventMapper.java @@ -8,7 +8,9 @@ /** * Mapper between {@link org.a2aproject.sdk.spec.TaskStatusUpdateEvent} and {@link org.a2aproject.sdk.grpc.TaskStatusUpdateEvent}. */ -@Mapper(config = A2AProtoMapperConfig.class, uses = {TaskStatusMapper.class, A2ACommonFieldMapper.class}) +@Mapper( + config = A2AProtoMapperConfig.class, + uses = {TaskStatusMapper.class, A2ACommonFieldMapper.class}) public interface TaskStatusUpdateEventMapper { TaskStatusUpdateEventMapper INSTANCE = A2AMappers.getMapper(TaskStatusUpdateEventMapper.class); @@ -31,7 +33,6 @@ default TaskStatusUpdateEvent fromProto(org.a2aproject.sdk.grpc.TaskStatusUpdate proto.getTaskId(), status, proto.getContextId(), - A2ACommonFieldMapper.INSTANCE.metadataFromProto(proto.getMetadata()) - ); + A2ACommonFieldMapper.INSTANCE.metadataFromProto(proto.getMetadata())); } } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtils.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtils.java index 89cb05c9b..21852c500 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtils.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtils.java @@ -1,11 +1,27 @@ package org.a2aproject.sdk.grpc.utils; -import org.a2aproject.sdk.spec.A2AErrorCodes; - import static org.a2aproject.sdk.spec.A2AMethods.CANCEL_TASK_METHOD; +import static org.a2aproject.sdk.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; import static org.a2aproject.sdk.spec.A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD; +import static org.a2aproject.sdk.spec.A2AMethods.GET_TASK_METHOD; +import static org.a2aproject.sdk.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; +import static org.a2aproject.sdk.spec.A2AMethods.LIST_TASK_METHOD; +import static org.a2aproject.sdk.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; +import static org.a2aproject.sdk.spec.A2AMethods.SEND_MESSAGE_METHOD; import static org.a2aproject.sdk.spec.A2AMethods.SEND_STREAMING_MESSAGE_METHOD; +import static org.a2aproject.sdk.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; +import static org.a2aproject.sdk.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import com.google.gson.Strictness; +import com.google.gson.stream.JsonWriter; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; import java.io.IOException; import java.io.StringWriter; import java.util.List; @@ -17,28 +33,20 @@ import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; -import com.google.gson.Strictness; -import com.google.gson.stream.JsonWriter; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.util.JsonFormat; import org.a2aproject.sdk.grpc.StreamResponse; import org.a2aproject.sdk.jsonrpc.common.json.IdJsonMappingException; import org.a2aproject.sdk.jsonrpc.common.json.InvalidParamsJsonMappingException; import org.a2aproject.sdk.jsonrpc.common.json.JsonMappingException; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; +import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.jsonrpc.common.json.MethodNotFoundJsonMappingException; import org.a2aproject.sdk.jsonrpc.common.wrappers.A2AMessage; import org.a2aproject.sdk.jsonrpc.common.wrappers.A2ARequest; import org.a2aproject.sdk.jsonrpc.common.wrappers.A2AResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.CancelTaskRequest; import org.a2aproject.sdk.jsonrpc.common.wrappers.CancelTaskResponse; +import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigRequest; +import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.DeleteTaskPushNotificationConfigRequest; import org.a2aproject.sdk.jsonrpc.common.wrappers.DeleteTaskPushNotificationConfigResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.GetExtendedAgentCardRequest; @@ -54,14 +62,13 @@ import org.a2aproject.sdk.jsonrpc.common.wrappers.SendMessageRequest; import org.a2aproject.sdk.jsonrpc.common.wrappers.SendMessageResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.SendStreamingMessageRequest; -import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigRequest; -import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigResponse; import org.a2aproject.sdk.jsonrpc.common.wrappers.SubscribeToTaskRequest; import org.a2aproject.sdk.spec.A2AError; +import org.a2aproject.sdk.spec.A2AErrorCodes; import org.a2aproject.sdk.spec.ContentTypeNotSupportedError; import org.a2aproject.sdk.spec.ExtendedAgentCardNotConfiguredError; -import org.a2aproject.sdk.spec.GetExtendedAgentCardParams; import org.a2aproject.sdk.spec.ExtensionSupportRequiredError; +import org.a2aproject.sdk.spec.GetExtendedAgentCardParams; import org.a2aproject.sdk.spec.InvalidAgentResponseError; import org.a2aproject.sdk.spec.InvalidParamsError; import org.a2aproject.sdk.spec.InvalidRequestError; @@ -76,17 +83,6 @@ import org.a2aproject.sdk.spec.util.Utils; import org.jspecify.annotations.Nullable; -import static org.a2aproject.sdk.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; -import static org.a2aproject.sdk.spec.A2AMethods.GET_TASK_METHOD; -import static org.a2aproject.sdk.spec.A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; -import static org.a2aproject.sdk.spec.A2AMethods.LIST_TASK_METHOD; -import static org.a2aproject.sdk.spec.A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; -import static org.a2aproject.sdk.spec.A2AMethods.SEND_MESSAGE_METHOD; -import static org.a2aproject.sdk.spec.A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; -import static org.a2aproject.sdk.spec.A2AMethods.SUBSCRIBE_TO_TASK_METHOD; - -import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; - /** * Utilities for converting between JSON-RPC 2.0 messages and Protocol Buffer objects. *

    @@ -173,22 +169,27 @@ public class JSONRPCUtils { private static final Logger LOGGER = Logger.getLogger(JSONRPCUtils.class.getName()); - private static final Gson GSON = new GsonBuilder() - .setStrictness(Strictness.STRICT) - .create(); - private static final Pattern EXTRACT_WRONG_VALUE = Pattern.compile("Expect (.*) but got: \".*\""); - private static final Pattern EXTRACT_WRONG_TYPE = Pattern.compile("Expected (.*) but found \".*\""); - static final String ERROR_MESSAGE = "Invalid request content: %s. Please verify the request matches the expected schema for this method."; + private static final Gson GSON = new GsonBuilder().setStrictness(Strictness.STRICT).create(); + private static final Pattern EXTRACT_WRONG_VALUE = + Pattern.compile("Expect (.*) but got: \".*\""); + private static final Pattern EXTRACT_WRONG_TYPE = + Pattern.compile("Expected (.*) but found \".*\""); + static final String ERROR_MESSAGE = + "Invalid request content: %s. Please verify the request matches the expected schema for" + + " this method."; - public static A2ARequest parseRequestBody(String body, @Nullable String tenant) throws JsonMappingException, JsonProcessingException { + public static A2ARequest parseRequestBody(String body, @Nullable String tenant) + throws JsonMappingException, JsonProcessingException { JsonElement jelement = JsonParser.parseString(body); if (!jelement.isJsonObject()) { - throw new JsonMappingException(null, "Invalid JSON-RPC request: the request must be a JSON object."); + throw new JsonMappingException( + null, "Invalid JSON-RPC request: the request must be a JSON object."); } JsonObject jsonRpc = jelement.getAsJsonObject(); if (!jsonRpc.has("method")) { throw new IdJsonMappingException( - "JSON-RPC request missing required 'method' field. Request must include: jsonrpc, id, method, and params.", + "JSON-RPC request missing required 'method' field. Request must include:" + + " jsonrpc, id, method, and params.", getIdIfPossible(jsonRpc)); } String version = getAndValidateJsonrpc(jsonRpc); @@ -198,98 +199,144 @@ public static A2ARequest parseRequestBody(String body, @Nullable String tenan try { return parseMethodRequest(version, id, method, paramsNode, tenant); } catch (InvalidParamsError e) { - throw new InvalidParamsJsonMappingException(Utils.defaultIfNull(e.getMessage(), "Invalid parameters"), id); + throw new InvalidParamsJsonMappingException( + Utils.defaultIfNull(e.getMessage(), "Invalid parameters"), id); } } - private static void setTenantIfAbsent(Supplier existingTenantGetter, Consumer setter, @Nullable String tenantOverride) { - if (tenantOverride != null && !tenantOverride.isBlank() && existingTenantGetter.get().isBlank()) { + private static void setTenantIfAbsent( + Supplier existingTenantGetter, + Consumer setter, + @Nullable String tenantOverride) { + if (tenantOverride != null + && !tenantOverride.isBlank() + && existingTenantGetter.get().isBlank()) { setter.accept(tenantOverride); } } - private static A2ARequest parseMethodRequest(String version, Object id, String method, @Nullable JsonElement paramsNode, @Nullable String tenant) throws InvalidParamsError, MethodNotFoundJsonMappingException, JsonProcessingException { + private static A2ARequest parseMethodRequest( + String version, + Object id, + String method, + @Nullable JsonElement paramsNode, + @Nullable String tenant) + throws InvalidParamsError, MethodNotFoundJsonMappingException, JsonProcessingException { // JSON-RPC 2.0 allows "params" to be omitted: parse it like an empty params object. JsonElement params = paramsNode == null ? new JsonObject() : paramsNode; switch (method) { case GET_TASK_METHOD -> { - org.a2aproject.sdk.grpc.GetTaskRequest.Builder builder = org.a2aproject.sdk.grpc.GetTaskRequest.newBuilder(); + org.a2aproject.sdk.grpc.GetTaskRequest.Builder builder = + org.a2aproject.sdk.grpc.GetTaskRequest.newBuilder(); parseRequestBody(params, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new GetTaskRequest(version, id, ProtoUtils.FromProto.taskQueryParams(builder)); + return new GetTaskRequest( + version, id, ProtoUtils.FromProto.taskQueryParams(builder)); } case CANCEL_TASK_METHOD -> { - org.a2aproject.sdk.grpc.CancelTaskRequest.Builder builder = org.a2aproject.sdk.grpc.CancelTaskRequest.newBuilder(); + org.a2aproject.sdk.grpc.CancelTaskRequest.Builder builder = + org.a2aproject.sdk.grpc.CancelTaskRequest.newBuilder(); parseRequestBody(params, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new CancelTaskRequest(version, id, ProtoUtils.FromProto.cancelTaskParams(builder)); + return new CancelTaskRequest( + version, id, ProtoUtils.FromProto.cancelTaskParams(builder)); } case LIST_TASK_METHOD -> { - org.a2aproject.sdk.grpc.ListTasksRequest.Builder builder = org.a2aproject.sdk.grpc.ListTasksRequest.newBuilder(); + org.a2aproject.sdk.grpc.ListTasksRequest.Builder builder = + org.a2aproject.sdk.grpc.ListTasksRequest.newBuilder(); parseRequestBody(params, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new ListTasksRequest(version, id, ProtoUtils.FromProto.listTasksParams(builder)); + return new ListTasksRequest( + version, id, ProtoUtils.FromProto.listTasksParams(builder)); } case SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { - org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); parseRequestBody(params, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new CreateTaskPushNotificationConfigRequest(version, id, ProtoUtils.FromProto.createTaskPushNotificationConfig(builder)); + return new CreateTaskPushNotificationConfigRequest( + version, + id, + ProtoUtils.FromProto.createTaskPushNotificationConfig(builder)); } case GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { - org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.Builder builder = org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.newBuilder(); + org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.Builder builder = + org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.newBuilder(); parseRequestBody(params, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new GetTaskPushNotificationConfigRequest(version, id, ProtoUtils.FromProto.getTaskPushNotificationConfigParams(builder)); + return new GetTaskPushNotificationConfigRequest( + version, + id, + ProtoUtils.FromProto.getTaskPushNotificationConfigParams(builder)); } case SEND_MESSAGE_METHOD -> { - org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); + org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = + org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); parseRequestBody(params, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new SendMessageRequest(version, id, ProtoUtils.FromProto.messageSendParams(builder)); + return new SendMessageRequest( + version, id, ProtoUtils.FromProto.messageSendParams(builder)); } case LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { - org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder builder = org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.newBuilder(); + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder builder = + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.newBuilder(); parseRequestBody(params, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new ListTaskPushNotificationConfigsRequest(version, id, ProtoUtils.FromProto.listTaskPushNotificationConfigsParams(builder)); + return new ListTaskPushNotificationConfigsRequest( + version, + id, + ProtoUtils.FromProto.listTaskPushNotificationConfigsParams(builder)); } case DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { - org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.Builder builder = org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.newBuilder(); + org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.Builder builder = + org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest + .newBuilder(); parseRequestBody(params, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new DeleteTaskPushNotificationConfigRequest(version, id, ProtoUtils.FromProto.deleteTaskPushNotificationConfigParams(builder)); + return new DeleteTaskPushNotificationConfigRequest( + version, + id, + ProtoUtils.FromProto.deleteTaskPushNotificationConfigParams(builder)); } case GET_EXTENDED_AGENT_CARD_METHOD -> { if (paramsNode != null && !paramsNode.isJsonNull()) { - org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.Builder builder = org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.newBuilder(); + org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.Builder builder = + org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.newBuilder(); parseRequestBody(paramsNode, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new GetExtendedAgentCardRequest(version, id, ProtoUtils.FromProto.getExtendedAgentCardParams(builder)); + return new GetExtendedAgentCardRequest( + version, id, ProtoUtils.FromProto.getExtendedAgentCardParams(builder)); } if (tenant != null && !tenant.isBlank()) { - return new GetExtendedAgentCardRequest(version, id, new GetExtendedAgentCardParams(tenant)); + return new GetExtendedAgentCardRequest( + version, id, new GetExtendedAgentCardParams(tenant)); } return new GetExtendedAgentCardRequest(version, id, null); } case SEND_STREAMING_MESSAGE_METHOD -> { - org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); + org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = + org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); parseRequestBody(params, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new SendStreamingMessageRequest(version, id, ProtoUtils.FromProto.messageSendParams(builder)); + return new SendStreamingMessageRequest( + version, id, ProtoUtils.FromProto.messageSendParams(builder)); } case SUBSCRIBE_TO_TASK_METHOD -> { - org.a2aproject.sdk.grpc.SubscribeToTaskRequest.Builder builder = org.a2aproject.sdk.grpc.SubscribeToTaskRequest.newBuilder(); + org.a2aproject.sdk.grpc.SubscribeToTaskRequest.Builder builder = + org.a2aproject.sdk.grpc.SubscribeToTaskRequest.newBuilder(); parseRequestBody(params, builder, id); setTenantIfAbsent(builder::getTenant, builder::setTenant, tenant); - return new SubscribeToTaskRequest(version, id, ProtoUtils.FromProto.taskIdParams(builder)); + return new SubscribeToTaskRequest( + version, id, ProtoUtils.FromProto.taskIdParams(builder)); } default -> - throw new MethodNotFoundJsonMappingException("Unsupported JSON-RPC method: '" + method + "'", id); + throw new MethodNotFoundJsonMappingException( + "Unsupported JSON-RPC method: '" + method + "'", id); } } - public static StreamResponse parseResponseEvent(String body) throws JsonMappingException, JsonProcessingException { + public static StreamResponse parseResponseEvent(String body) + throws JsonMappingException, JsonProcessingException { JsonElement jelement = JsonParser.parseString(body); JsonObject jsonRpc = jelement.getAsJsonObject(); String version = getAndValidateJsonrpc(jsonRpc); @@ -304,7 +351,8 @@ public static StreamResponse parseResponseEvent(String body) throws JsonMappingE return builder.build(); } - public static A2AResponse parseResponseBody(String body, String method) throws JsonMappingException, JsonProcessingException { + public static A2AResponse parseResponseBody(String body, String method) + throws JsonMappingException, JsonProcessingException { JsonElement jelement = JsonParser.parseString(body); JsonObject jsonRpc = jelement.getAsJsonObject(); String version = getAndValidateJsonrpc(jsonRpc); @@ -316,57 +364,74 @@ public static A2AResponse parseResponseBody(String body, String method) throw } switch (method) { case GET_TASK_METHOD -> { - org.a2aproject.sdk.grpc.Task.Builder builder = org.a2aproject.sdk.grpc.Task.newBuilder(); + org.a2aproject.sdk.grpc.Task.Builder builder = + org.a2aproject.sdk.grpc.Task.newBuilder(); parseRequestBody(paramsNode, builder, id); return new GetTaskResponse(id, ProtoUtils.FromProto.task(builder)); } case CANCEL_TASK_METHOD -> { - org.a2aproject.sdk.grpc.Task.Builder builder = org.a2aproject.sdk.grpc.Task.newBuilder(); + org.a2aproject.sdk.grpc.Task.Builder builder = + org.a2aproject.sdk.grpc.Task.newBuilder(); parseRequestBody(paramsNode, builder, id); return new CancelTaskResponse(id, ProtoUtils.FromProto.task(builder)); } case LIST_TASK_METHOD -> { - org.a2aproject.sdk.grpc.ListTasksResponse.Builder builder = org.a2aproject.sdk.grpc.ListTasksResponse.newBuilder(); + org.a2aproject.sdk.grpc.ListTasksResponse.Builder builder = + org.a2aproject.sdk.grpc.ListTasksResponse.newBuilder(); parseRequestBody(paramsNode, builder, id); return new ListTasksResponse(id, ProtoUtils.FromProto.listTasksResult(builder)); } case SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { - org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); parseRequestBody(paramsNode, builder, id); - return new CreateTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.taskPushNotificationConfig(builder)); + return new CreateTaskPushNotificationConfigResponse( + id, ProtoUtils.FromProto.taskPushNotificationConfig(builder)); } case GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { - org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); parseRequestBody(paramsNode, builder, id); - return new GetTaskPushNotificationConfigResponse(id, ProtoUtils.FromProto.taskPushNotificationConfig(builder)); + return new GetTaskPushNotificationConfigResponse( + id, ProtoUtils.FromProto.taskPushNotificationConfig(builder)); } case SEND_MESSAGE_METHOD -> { - org.a2aproject.sdk.grpc.SendMessageResponse.Builder builder = org.a2aproject.sdk.grpc.SendMessageResponse.newBuilder(); + org.a2aproject.sdk.grpc.SendMessageResponse.Builder builder = + org.a2aproject.sdk.grpc.SendMessageResponse.newBuilder(); parseRequestBody(paramsNode, builder, id); if (builder.hasMessage()) { - return new SendMessageResponse(id, ProtoUtils.FromProto.message(builder.getMessage())); + return new SendMessageResponse( + id, ProtoUtils.FromProto.message(builder.getMessage())); } return new SendMessageResponse(id, ProtoUtils.FromProto.task(builder.getTask())); } case LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { - org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.Builder builder = org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.newBuilder(); + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.Builder builder = + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse + .newBuilder(); parseRequestBody(paramsNode, builder, id); - return new ListTaskPushNotificationConfigsResponse(id, ProtoUtils.FromProto.listTaskPushNotificationConfigsResult(builder)); + return new ListTaskPushNotificationConfigsResponse( + id, ProtoUtils.FromProto.listTaskPushNotificationConfigsResult(builder)); } case DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD -> { return new DeleteTaskPushNotificationConfigResponse(id); } case GET_EXTENDED_AGENT_CARD_METHOD -> { - org.a2aproject.sdk.grpc.AgentCard.Builder builder = org.a2aproject.sdk.grpc.AgentCard.newBuilder(); + org.a2aproject.sdk.grpc.AgentCard.Builder builder = + org.a2aproject.sdk.grpc.AgentCard.newBuilder(); parseRequestBody(paramsNode, builder, id); - return new GetExtendedAgentCardResponse(id, ProtoUtils.FromProto.agentCard(builder)); + return new GetExtendedAgentCardResponse( + id, ProtoUtils.FromProto.agentCard(builder)); } default -> - throw new MethodNotFoundJsonMappingException("Unsupported JSON-RPC method: '" + method + "' in response parsing.", getIdIfPossible(jsonRpc)); + throw new MethodNotFoundJsonMappingException( + "Unsupported JSON-RPC method: '" + method + "' in response parsing.", + getIdIfPossible(jsonRpc)); } } - public static A2AResponse parseError(JsonObject error, Object id, String method) throws JsonMappingException { + public static A2AResponse parseError(JsonObject error, Object id, String method) + throws JsonMappingException { A2AError rpcError = processError(error); switch (method) { case GET_TASK_METHOD -> { @@ -394,7 +459,8 @@ public static A2AResponse parseError(JsonObject error, Object id, String meth return new DeleteTaskPushNotificationConfigResponse(id, rpcError); } default -> - throw new MethodNotFoundJsonMappingException("Unsupported JSON-RPC method: '" + method + "'", id); + throw new MethodNotFoundJsonMappingException( + "Unsupported JSON-RPC method: '" + method + "'", id); } } @@ -422,14 +488,22 @@ private static A2AError processError(JsonObject error) { case INVALID_REQUEST -> new InvalidRequestError(code, message, details); case METHOD_NOT_FOUND -> new MethodNotFoundError(code, message, details); case INVALID_PARAMS -> new InvalidParamsError(code, message, details); - case INTERNAL -> new org.a2aproject.sdk.spec.InternalError(code, message, details); - case PUSH_NOTIFICATION_NOT_SUPPORTED -> new PushNotificationNotSupportedError(code, message, details); - case UNSUPPORTED_OPERATION -> new UnsupportedOperationError(code, message, details); - case CONTENT_TYPE_NOT_SUPPORTED -> new ContentTypeNotSupportedError(code, message, details); - case INVALID_AGENT_RESPONSE -> new InvalidAgentResponseError(code, message, details); - case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> new ExtendedAgentCardNotConfiguredError(code, message, details); - case EXTENSION_SUPPORT_REQUIRED -> new ExtensionSupportRequiredError(code, message, details); - case VERSION_NOT_SUPPORTED -> new VersionNotSupportedError(code, message, details); + case INTERNAL -> + new org.a2aproject.sdk.spec.InternalError(code, message, details); + case PUSH_NOTIFICATION_NOT_SUPPORTED -> + new PushNotificationNotSupportedError(code, message, details); + case UNSUPPORTED_OPERATION -> + new UnsupportedOperationError(code, message, details); + case CONTENT_TYPE_NOT_SUPPORTED -> + new ContentTypeNotSupportedError(code, message, details); + case INVALID_AGENT_RESPONSE -> + new InvalidAgentResponseError(code, message, details); + case EXTENDED_AGENT_CARD_NOT_CONFIGURED -> + new ExtendedAgentCardNotConfiguredError(code, message, details); + case EXTENSION_SUPPORT_REQUIRED -> + new ExtensionSupportRequiredError(code, message, details); + case VERSION_NOT_SUPPORTED -> + new VersionNotSupportedError(code, message, details); case TASK_NOT_CANCELABLE -> new TaskNotCancelableError(code, message, details); case TASK_NOT_FOUND -> new TaskNotFoundError(message, details); }; @@ -439,17 +513,29 @@ private static A2AError processError(JsonObject error) { return new A2AError(A2AErrorCodes.INTERNAL.code(), message == null ? "" : message, details); } - protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf.Message.Builder builder, Object id) throws JsonProcessingException { + protected static void parseRequestBody( + JsonElement jsonRpc, com.google.protobuf.Message.Builder builder, Object id) + throws JsonProcessingException { parseJsonString(jsonRpc.toString(), builder, id); } - public static void parseJsonString(String body, com.google.protobuf.Message.Builder builder, Object id) throws JsonProcessingException { + public static void parseJsonString( + String body, com.google.protobuf.Message.Builder builder, Object id) + throws JsonProcessingException { parseJsonString(body, builder, id, false); } - public static void parseJsonString(String body, com.google.protobuf.Message.Builder builder, Object id, boolean ignoringUnknownFields) throws JsonProcessingException { + public static void parseJsonString( + String body, + com.google.protobuf.Message.Builder builder, + Object id, + boolean ignoringUnknownFields) + throws JsonProcessingException { try { - JsonFormat.Parser parser = ignoringUnknownFields ? JsonFormat.parser().ignoringUnknownFields() : JsonFormat.parser(); + JsonFormat.Parser parser = + ignoringUnknownFields + ? JsonFormat.parser().ignoringUnknownFields() + : JsonFormat.parser(); parser.merge(body, builder); } catch (InvalidProtocolBufferException e) { LOGGER.log(Level.FINE, "Protocol buffer parsing failed for JSON: {0}", body); @@ -482,9 +568,13 @@ public static void parseJsonString(String body, com.google.protobuf.Message.Buil * @param id the request ID if it could be extracted, null otherwise * @return an appropriate JsonProcessingException subtype based on the error and ID availability */ - private static JsonProcessingException convertProtoBufExceptionToJsonProcessingException(InvalidProtocolBufferException e, Object id) { + private static JsonProcessingException convertProtoBufExceptionToJsonProcessingException( + InvalidProtocolBufferException e, Object id) { // Log the original exception for debugging purposes - LOGGER.log(Level.FINE, "Converting protobuf parsing exception to JSON-RPC error. Request ID: {0}", id); + LOGGER.log( + Level.FINE, + "Converting protobuf parsing exception to JSON-RPC error. Request ID: {0}", + id); LOGGER.log(Level.FINE, "Original proto exception details", e); String message = e.getMessage(); @@ -493,10 +583,15 @@ private static JsonProcessingException convertProtoBufExceptionToJsonProcessingE } // Extract field name if present in error message - check common prefixes - String[] prefixes = {"Cannot find field: ", "Invalid value for", "Invalid enum value:", "Failed to parse"}; + String[] prefixes = { + "Cannot find field: ", "Invalid value for", "Invalid enum value:", "Failed to parse" + }; for (String prefix : prefixes) { if (message.contains(prefix)) { - return new InvalidParamsJsonMappingException(ERROR_MESSAGE.formatted(message.substring(message.indexOf(prefix) + prefix.length())), id); + return new InvalidParamsJsonMappingException( + ERROR_MESSAGE.formatted( + message.substring(message.indexOf(prefix) + prefix.length())), + id); } } @@ -504,12 +599,14 @@ private static JsonProcessingException convertProtoBufExceptionToJsonProcessingE Matcher matcher = EXTRACT_WRONG_TYPE.matcher(message); if (matcher.matches() && matcher.group(1) != null) { // ID is null -> use empty string sentinel value (see javadoc above) - return new InvalidParamsJsonMappingException(ERROR_MESSAGE.formatted(matcher.group(1)), Utils.defaultIfNull(id, "")); + return new InvalidParamsJsonMappingException( + ERROR_MESSAGE.formatted(matcher.group(1)), Utils.defaultIfNull(id, "")); } matcher = EXTRACT_WRONG_VALUE.matcher(message); if (matcher.matches() && matcher.group(1) != null) { // ID is null -> use empty string sentinel value (see javadoc above) - return new InvalidParamsJsonMappingException(ERROR_MESSAGE.formatted(matcher.group(1)), Utils.defaultIfNull(id, "")); + return new InvalidParamsJsonMappingException( + ERROR_MESSAGE.formatted(matcher.group(1)), Utils.defaultIfNull(id, "")); } // Generic error - couldn't match specific patterns @@ -558,18 +655,26 @@ protected static Object getAndValidateId(JsonObject jsonRpc) throws JsonMappingE // when the client uses a UUID as the request ID (the default). id = primitive.isNumber() ? primitive.getAsInt() : primitive.getAsString(); } else { - throw new JsonMappingException(null, "Invalid 'id' type: " + jsonRpc.get("id").getClass().getSimpleName() - + ". ID must be a JSON string or number, not an object or array."); + throw new JsonMappingException( + null, + "Invalid 'id' type: " + + jsonRpc.get("id").getClass().getSimpleName() + + ". ID must be a JSON string or number, not an object or array."); } } if (id == null) { - throw new JsonMappingException(null, "Request 'id' cannot be null. Use a string or number identifier."); + throw new JsonMappingException( + null, "Request 'id' cannot be null. Use a string or number identifier."); } return id; } - public static String toJsonRPCRequest(@Nullable String requestId, String method, com.google.protobuf.@Nullable MessageOrBuilder payload) { - try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) { + public static String toJsonRPCRequest( + @Nullable String requestId, + String method, + com.google.protobuf.@Nullable MessageOrBuilder payload) { + try (StringWriter result = new StringWriter(); + JsonWriter output = GSON.newJsonWriter(result)) { output.beginObject(); output.name("jsonrpc").value("2.0"); String id = requestId; @@ -581,38 +686,57 @@ public static String toJsonRPCRequest(@Nullable String requestId, String method, output.name("method").value(method); } if (payload != null) { - String resultValue = ProtoJsonUtils.toJson( - JsonFormat.printer().alwaysPrintFieldsWithNoPresence().omittingInsignificantWhitespace(), payload); + String resultValue = + ProtoJsonUtils.toJson( + JsonFormat.printer() + .alwaysPrintFieldsWithNoPresence() + .omittingInsignificantWhitespace(), + payload); output.name("params").jsonValue(resultValue); } output.endObject(); return result.toString(); } catch (IOException ex) { throw new RuntimeException( - "Failed to serialize JSON-RPC request for method '" + method + "'. " - + "This indicates an internal error in JSON generation. Request ID: " + requestId, ex); + "Failed to serialize JSON-RPC request for method '" + + method + + "'. " + + "This indicates an internal error in JSON generation. Request ID: " + + requestId, + ex); } } - public static String toJsonRPCResultResponse(@Nullable Object requestId, com.google.protobuf.MessageOrBuilder builder) { - try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) { + public static String toJsonRPCResultResponse( + @Nullable Object requestId, com.google.protobuf.MessageOrBuilder builder) { + try (StringWriter result = new StringWriter(); + JsonWriter output = GSON.newJsonWriter(result)) { output.beginObject(); output.name("jsonrpc").value("2.0"); JsonUtil.writeJsonRpcId(output, requestId); - String resultValue = ProtoJsonUtils.toJson( - JsonFormat.printer().alwaysPrintFieldsWithNoPresence().omittingInsignificantWhitespace(), builder); + String resultValue = + ProtoJsonUtils.toJson( + JsonFormat.printer() + .alwaysPrintFieldsWithNoPresence() + .omittingInsignificantWhitespace(), + builder); output.name("result").jsonValue(resultValue); output.endObject(); return result.toString(); } catch (IOException ex) { throw new RuntimeException( "Failed to serialize JSON-RPC success response. " - + "Proto type: " + builder.getClass().getSimpleName() + ", Request ID: " + requestId, ex); + + "Proto type: " + + builder.getClass().getSimpleName() + + ", Request ID: " + + requestId, + ex); } } public static String toJsonRPCErrorResponse(@Nullable Object requestId, A2AError error) { - try (StringWriter result = new StringWriter(); JsonWriter output = GSON.newJsonWriter(result)) { + try (StringWriter result = new StringWriter(); + JsonWriter output = GSON.newJsonWriter(result)) { output.beginObject(); output.name("jsonrpc").value("2.0"); JsonUtil.writeJsonRpcId(output, requestId); @@ -621,7 +745,8 @@ public static String toJsonRPCErrorResponse(@Nullable Object requestId, A2AError output.name("code").value(error.getCode()); output.name("message").value(error.getMessage()); A2AErrorCodes a2aErrorCode = A2AErrorCodes.fromCode(error.getCode()); - String reason = a2aErrorCode != null ? a2aErrorCode.name() : A2AErrorCodes.INTERNAL.name(); + String reason = + a2aErrorCode != null ? a2aErrorCode.name() : A2AErrorCodes.INTERNAL.name(); output.name("data"); GSON.toJson(List.of(ErrorDetail.of(reason, error.getDetails())), List.class, output); output.endObject(); @@ -630,7 +755,11 @@ public static String toJsonRPCErrorResponse(@Nullable Object requestId, A2AError } catch (IOException ex) { throw new RuntimeException( "Failed to serialize JSON-RPC error response. " - + "Error code: " + error.getCode() + ", Request ID: " + requestId, ex); + + "Error code: " + + error.getCode() + + ", Request ID: " + + requestId, + ex); } } } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/ProtoJsonUtils.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/ProtoJsonUtils.java index 2a40afd44..287305b42 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/ProtoJsonUtils.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/ProtoJsonUtils.java @@ -16,8 +16,7 @@ */ public final class ProtoJsonUtils { - private ProtoJsonUtils() { - } + private ProtoJsonUtils() {} /** * Serializes a protobuf message to JSON using the supplied printer, @@ -29,7 +28,8 @@ private ProtoJsonUtils() { * @param proto the protobuf message to serialize, or {@code null} * @return JSON string without HTML-escaped characters, or empty string if proto is null */ - public static String toJson(JsonFormat.Printer printer, @Nullable MessageOrBuilder proto) throws InvalidProtocolBufferException { + public static String toJson(JsonFormat.Printer printer, @Nullable MessageOrBuilder proto) + throws InvalidProtocolBufferException { Assert.checkNotNullParam("printer", printer); if (proto == null) { return ""; diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/ProtoUtils.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/ProtoUtils.java index 600eecfd4..713653d3f 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/ProtoUtils.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/ProtoUtils.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.List; - import org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest; import org.a2aproject.sdk.grpc.StreamResponse; import org.a2aproject.sdk.grpc.mapper.AgentCardMapper; @@ -57,7 +56,8 @@ public static org.a2aproject.sdk.grpc.AgentCard agentCard(AgentCard agentCard) { return AgentCardMapper.INSTANCE.toProto(agentCard); } - public static org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest extendedAgentCard(GetExtendedAgentCardParams params) { + public static org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest extendedAgentCard( + GetExtendedAgentCardParams params) { GetExtendedAgentCardRequest.Builder builder = GetExtendedAgentCardRequest.newBuilder(); if (params.tenant() != null) { builder.setTenant(params.tenant()); @@ -65,31 +65,40 @@ public static org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest extendedAgentC return builder.build(); } - public static org.a2aproject.sdk.grpc.GetTaskRequest getTaskRequest(TaskQueryParams params) { + public static org.a2aproject.sdk.grpc.GetTaskRequest getTaskRequest( + TaskQueryParams params) { return TaskQueryParamsMapper.INSTANCE.toProto(params); } - public static org.a2aproject.sdk.grpc.CancelTaskRequest cancelTaskRequest(CancelTaskParams params) { + public static org.a2aproject.sdk.grpc.CancelTaskRequest cancelTaskRequest( + CancelTaskParams params) { return TaskIdParamsMapper.INSTANCE.toProtoCancelTaskRequest(params); } - public static org.a2aproject.sdk.grpc.SubscribeToTaskRequest subscribeToTaskRequest(TaskIdParams params) { + public static org.a2aproject.sdk.grpc.SubscribeToTaskRequest subscribeToTaskRequest( + TaskIdParams params) { return TaskIdParamsMapper.INSTANCE.toProtoSubscribeToTaskRequest(params); } - public static org.a2aproject.sdk.grpc.TaskPushNotificationConfig createTaskPushNotificationConfigRequest(TaskPushNotificationConfig config) { + public static org.a2aproject.sdk.grpc.TaskPushNotificationConfig + createTaskPushNotificationConfigRequest(TaskPushNotificationConfig config) { return TaskPushNotificationConfigMapper.INSTANCE.toProto(config); } - public static org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest getTaskPushNotificationConfigRequest(GetTaskPushNotificationConfigParams params) { + public static org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest + getTaskPushNotificationConfigRequest(GetTaskPushNotificationConfigParams params) { return GetTaskPushNotificationConfigParamsMapper.INSTANCE.toProto(params); } - public static org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest deleteTaskPushNotificationConfigRequest(DeleteTaskPushNotificationConfigParams params) { + public static org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest + deleteTaskPushNotificationConfigRequest( + DeleteTaskPushNotificationConfigParams params) { return DeleteTaskPushNotificationConfigParamsMapper.INSTANCE.toProto(params); } - public static org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest listTaskPushNotificationConfigsRequest(ListTaskPushNotificationConfigsParams params) { + public static org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest + listTaskPushNotificationConfigsRequest( + ListTaskPushNotificationConfigsParams params) { return ListTaskPushNotificationConfigsParamsMapper.INSTANCE.toProto(params); } @@ -97,11 +106,13 @@ public static org.a2aproject.sdk.grpc.Task task(Task task) { return TaskMapper.INSTANCE.toProto(task); } - public static org.a2aproject.sdk.grpc.ListTasksResponse listTasksResult(ListTasksResult result) { + public static org.a2aproject.sdk.grpc.ListTasksResponse listTasksResult( + ListTasksResult result) { return ListTasksResultMapper.INSTANCE.toProto(result); } - public static org.a2aproject.sdk.grpc.ListTasksRequest listTasksParams(ListTasksParams params) { + public static org.a2aproject.sdk.grpc.ListTasksRequest listTasksParams( + ListTasksParams params) { return ListTasksParamsMapper.INSTANCE.toProto(params); } @@ -109,15 +120,18 @@ public static org.a2aproject.sdk.grpc.Message message(Message message) { return MessageMapper.INSTANCE.toProto(message); } - public static org.a2aproject.sdk.grpc.TaskPushNotificationConfig taskPushNotificationConfig(TaskPushNotificationConfig config) { + public static org.a2aproject.sdk.grpc.TaskPushNotificationConfig taskPushNotificationConfig( + TaskPushNotificationConfig config) { return TaskPushNotificationConfigMapper.INSTANCE.toProto(config); } - public static org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent taskArtifactUpdateEvent(TaskArtifactUpdateEvent event) { + public static org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent taskArtifactUpdateEvent( + TaskArtifactUpdateEvent event) { return TaskArtifactUpdateEventMapper.INSTANCE.toProto(event); } - public static org.a2aproject.sdk.grpc.TaskStatusUpdateEvent taskStatusUpdateEvent(TaskStatusUpdateEvent event) { + public static org.a2aproject.sdk.grpc.TaskStatusUpdateEvent taskStatusUpdateEvent( + TaskStatusUpdateEvent event) { return TaskStatusUpdateEventMapper.INSTANCE.toProto(event); } @@ -125,20 +139,27 @@ public static org.a2aproject.sdk.grpc.TaskState taskState(TaskState taskState) { return TaskStateMapper.INSTANCE.toProto(taskState); } - public static org.a2aproject.sdk.grpc.SendMessageConfiguration messageSendConfiguration(MessageSendConfiguration messageSendConfiguration) { + public static org.a2aproject.sdk.grpc.SendMessageConfiguration messageSendConfiguration( + MessageSendConfiguration messageSendConfiguration) { return MessageSendConfigurationMapper.INSTANCE.toProto(messageSendConfiguration); } - public static org.a2aproject.sdk.grpc.SendMessageRequest sendMessageRequest(MessageSendParams request) { + public static org.a2aproject.sdk.grpc.SendMessageRequest sendMessageRequest( + MessageSendParams request) { return MessageSendParamsMapper.INSTANCE.toProto(request); } - public static org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse listTaskPushNotificationConfigsResponse(ListTaskPushNotificationConfigsResult result) { - List confs = new ArrayList<>(result.configs().size()); + public static org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse + listTaskPushNotificationConfigsResponse( + ListTaskPushNotificationConfigsResult result) { + List confs = + new ArrayList<>(result.configs().size()); for (TaskPushNotificationConfig config : result.configs()) { confs.add(taskPushNotificationConfig(config)); } - org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.Builder builder = org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.newBuilder().addAllConfigs(confs); + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.Builder builder = + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.newBuilder() + .addAllConfigs(confs); if (result.nextPageToken() != null) { builder.setNextPageToken(result.nextPageToken()); } @@ -149,41 +170,56 @@ public static StreamResponse streamResponse(StreamingEventKind streamingEventKin return StreamResponseMapper.INSTANCE.toProto(streamingEventKind); } - public static org.a2aproject.sdk.grpc.SendMessageResponse taskOrMessage(EventKind eventKind) { + public static org.a2aproject.sdk.grpc.SendMessageResponse taskOrMessage( + EventKind eventKind) { return switch (eventKind.kind()) { - case Task.STREAMING_EVENT_ID -> org.a2aproject.sdk.grpc.SendMessageResponse.newBuilder() - .setTask(task((Task) eventKind)) - .build(); - case Message.STREAMING_EVENT_ID -> org.a2aproject.sdk.grpc.SendMessageResponse.newBuilder() - .setMessage(message((Message) eventKind)) - .build(); - default -> throw new IllegalArgumentException("Unsupported event type: " + eventKind); + case Task.STREAMING_EVENT_ID -> + org.a2aproject.sdk.grpc.SendMessageResponse.newBuilder() + .setTask(task((Task) eventKind)) + .build(); + case Message.STREAMING_EVENT_ID -> + org.a2aproject.sdk.grpc.SendMessageResponse.newBuilder() + .setMessage(message((Message) eventKind)) + .build(); + default -> + throw new IllegalArgumentException("Unsupported event type: " + eventKind); }; } - public static org.a2aproject.sdk.grpc.StreamResponse taskOrMessageStream(StreamingEventKind eventKind) { + public static org.a2aproject.sdk.grpc.StreamResponse taskOrMessageStream( + StreamingEventKind eventKind) { return switch (eventKind.kind()) { - case Task.STREAMING_EVENT_ID -> org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setTask(task((Task) eventKind)) - .build(); - case Message.STREAMING_EVENT_ID -> org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setMessage(message((Message) eventKind)) - .build(); - case TaskStatusUpdateEvent.STREAMING_EVENT_ID -> org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setStatusUpdate(taskStatusUpdateEvent((TaskStatusUpdateEvent) eventKind)) - .build(); - case TaskArtifactUpdateEvent.STREAMING_EVENT_ID -> org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setArtifactUpdate(taskArtifactUpdateEvent((TaskArtifactUpdateEvent) eventKind)) - .build(); - default -> throw new IllegalArgumentException("Unsupported event type: " + eventKind); + case Task.STREAMING_EVENT_ID -> + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setTask(task((Task) eventKind)) + .build(); + case Message.STREAMING_EVENT_ID -> + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setMessage(message((Message) eventKind)) + .build(); + case TaskStatusUpdateEvent.STREAMING_EVENT_ID -> + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setStatusUpdate( + taskStatusUpdateEvent((TaskStatusUpdateEvent) eventKind)) + .build(); + case TaskArtifactUpdateEvent.STREAMING_EVENT_ID -> + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setArtifactUpdate( + taskArtifactUpdateEvent( + (TaskArtifactUpdateEvent) eventKind)) + .build(); + default -> + throw new IllegalArgumentException("Unsupported event type: " + eventKind); }; } - public static org.a2aproject.sdk.grpc.TaskPushNotificationConfig createTaskPushNotificationConfigResponse(TaskPushNotificationConfig config) { + public static org.a2aproject.sdk.grpc.TaskPushNotificationConfig + createTaskPushNotificationConfigResponse(TaskPushNotificationConfig config) { return taskPushNotificationConfig(config); } - public static org.a2aproject.sdk.grpc.TaskPushNotificationConfig getTaskPushNotificationConfigResponse(TaskPushNotificationConfig config) { + public static org.a2aproject.sdk.grpc.TaskPushNotificationConfig + getTaskPushNotificationConfigResponse(TaskPushNotificationConfig config) { return taskPushNotificationConfig(config); } @@ -203,30 +239,37 @@ private static T convert(java.util.function.Supplier s) { } public static AgentCard agentCard(org.a2aproject.sdk.grpc.AgentCardOrBuilder agentCard) { - org.a2aproject.sdk.grpc.AgentCard agentCardProto = agentCard instanceof org.a2aproject.sdk.grpc.AgentCard - ? (org.a2aproject.sdk.grpc.AgentCard) agentCard - : ((org.a2aproject.sdk.grpc.AgentCard.Builder) agentCard).build(); + org.a2aproject.sdk.grpc.AgentCard agentCardProto = + agentCard instanceof org.a2aproject.sdk.grpc.AgentCard + ? (org.a2aproject.sdk.grpc.AgentCard) agentCard + : ((org.a2aproject.sdk.grpc.AgentCard.Builder) agentCard).build(); return convert(() -> AgentCardMapper.INSTANCE.fromProto(agentCardProto)); } - public static TaskQueryParams taskQueryParams(org.a2aproject.sdk.grpc.GetTaskRequestOrBuilder request) { - org.a2aproject.sdk.grpc.GetTaskRequest reqProto = request instanceof org.a2aproject.sdk.grpc.GetTaskRequest - ? (org.a2aproject.sdk.grpc.GetTaskRequest) request - : ((org.a2aproject.sdk.grpc.GetTaskRequest.Builder) request).build(); + public static TaskQueryParams taskQueryParams( + org.a2aproject.sdk.grpc.GetTaskRequestOrBuilder request) { + org.a2aproject.sdk.grpc.GetTaskRequest reqProto = + request instanceof org.a2aproject.sdk.grpc.GetTaskRequest + ? (org.a2aproject.sdk.grpc.GetTaskRequest) request + : ((org.a2aproject.sdk.grpc.GetTaskRequest.Builder) request).build(); return convert(() -> TaskQueryParamsMapper.INSTANCE.fromProto(reqProto)); } - public static ListTasksParams listTasksParams(org.a2aproject.sdk.grpc.ListTasksRequestOrBuilder request) { - org.a2aproject.sdk.grpc.ListTasksRequest reqProto = request instanceof org.a2aproject.sdk.grpc.ListTasksRequest - ? (org.a2aproject.sdk.grpc.ListTasksRequest) request - : ((org.a2aproject.sdk.grpc.ListTasksRequest.Builder) request).build(); + public static ListTasksParams listTasksParams( + org.a2aproject.sdk.grpc.ListTasksRequestOrBuilder request) { + org.a2aproject.sdk.grpc.ListTasksRequest reqProto = + request instanceof org.a2aproject.sdk.grpc.ListTasksRequest + ? (org.a2aproject.sdk.grpc.ListTasksRequest) request + : ((org.a2aproject.sdk.grpc.ListTasksRequest.Builder) request).build(); return convert(() -> ListTasksParamsMapper.INSTANCE.fromProto(reqProto)); } - public static CancelTaskParams cancelTaskParams(org.a2aproject.sdk.grpc.CancelTaskRequestOrBuilder request) { - org.a2aproject.sdk.grpc.CancelTaskRequest reqProto = request instanceof org.a2aproject.sdk.grpc.CancelTaskRequest - ? (org.a2aproject.sdk.grpc.CancelTaskRequest) request - : ((org.a2aproject.sdk.grpc.CancelTaskRequest.Builder) request).build(); + public static CancelTaskParams cancelTaskParams( + org.a2aproject.sdk.grpc.CancelTaskRequestOrBuilder request) { + org.a2aproject.sdk.grpc.CancelTaskRequest reqProto = + request instanceof org.a2aproject.sdk.grpc.CancelTaskRequest + ? (org.a2aproject.sdk.grpc.CancelTaskRequest) request + : ((org.a2aproject.sdk.grpc.CancelTaskRequest.Builder) request).build(); return convert(() -> TaskIdParamsMapper.INSTANCE.fromProtoCancelTaskRequest(reqProto)); } @@ -240,48 +283,70 @@ public static CancelTaskParams cancelTaskParams(org.a2aproject.sdk.grpc.CancelTa * @param request the protobuf request * @return the spec params, with an empty tenant normalized to {@code null} */ - public static GetExtendedAgentCardParams getExtendedAgentCardParams(org.a2aproject.sdk.grpc.GetExtendedAgentCardRequestOrBuilder request) { + public static GetExtendedAgentCardParams getExtendedAgentCardParams( + org.a2aproject.sdk.grpc.GetExtendedAgentCardRequestOrBuilder request) { String tenant = request.getTenant(); return new GetExtendedAgentCardParams(tenant.isBlank() ? null : tenant); } - public static MessageSendParams messageSendParams(org.a2aproject.sdk.grpc.SendMessageRequestOrBuilder request) { - org.a2aproject.sdk.grpc.SendMessageRequest requestProto = request instanceof org.a2aproject.sdk.grpc.SendMessageRequest - ? (org.a2aproject.sdk.grpc.SendMessageRequest) request - : ((org.a2aproject.sdk.grpc.SendMessageRequest.Builder) request).build(); + public static MessageSendParams messageSendParams( + org.a2aproject.sdk.grpc.SendMessageRequestOrBuilder request) { + org.a2aproject.sdk.grpc.SendMessageRequest requestProto = + request instanceof org.a2aproject.sdk.grpc.SendMessageRequest + ? (org.a2aproject.sdk.grpc.SendMessageRequest) request + : ((org.a2aproject.sdk.grpc.SendMessageRequest.Builder) request) + .build(); return convert(() -> MessageSendParamsMapper.INSTANCE.fromProto(requestProto)); } - public static TaskPushNotificationConfig createTaskPushNotificationConfig(org.a2aproject.sdk.grpc.TaskPushNotificationConfigOrBuilder config) { - org.a2aproject.sdk.grpc.TaskPushNotificationConfig proto = config instanceof org.a2aproject.sdk.grpc.TaskPushNotificationConfig - ? (org.a2aproject.sdk.grpc.TaskPushNotificationConfig) config - : ((org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder) config).build(); + public static TaskPushNotificationConfig createTaskPushNotificationConfig( + org.a2aproject.sdk.grpc.TaskPushNotificationConfigOrBuilder config) { + org.a2aproject.sdk.grpc.TaskPushNotificationConfig proto = + config instanceof org.a2aproject.sdk.grpc.TaskPushNotificationConfig + ? (org.a2aproject.sdk.grpc.TaskPushNotificationConfig) config + : ((org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder) config) + .build(); return convert(() -> TaskPushNotificationConfigMapper.INSTANCE.fromProto(proto)); } - public static TaskPushNotificationConfig taskPushNotificationConfig(org.a2aproject.sdk.grpc.TaskPushNotificationConfigOrBuilder config) { - org.a2aproject.sdk.grpc.TaskPushNotificationConfig proto = config instanceof org.a2aproject.sdk.grpc.TaskPushNotificationConfig - ? (org.a2aproject.sdk.grpc.TaskPushNotificationConfig) config - : ((org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder) config).build(); + public static TaskPushNotificationConfig taskPushNotificationConfig( + org.a2aproject.sdk.grpc.TaskPushNotificationConfigOrBuilder config) { + org.a2aproject.sdk.grpc.TaskPushNotificationConfig proto = + config instanceof org.a2aproject.sdk.grpc.TaskPushNotificationConfig + ? (org.a2aproject.sdk.grpc.TaskPushNotificationConfig) config + : ((org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder) config) + .build(); return convert(() -> TaskPushNotificationConfigMapper.INSTANCE.fromProto(proto)); } - public static GetTaskPushNotificationConfigParams getTaskPushNotificationConfigParams(org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequestOrBuilder request) { - org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest reqProto = request instanceof org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest - ? (org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest) request - : ((org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest.Builder) request).build(); - return convert(() -> GetTaskPushNotificationConfigParamsMapper.INSTANCE.fromProto(reqProto)); - } - - public static TaskIdParams taskIdParams(org.a2aproject.sdk.grpc.SubscribeToTaskRequestOrBuilder request) { - org.a2aproject.sdk.grpc.SubscribeToTaskRequest reqProto = request instanceof org.a2aproject.sdk.grpc.SubscribeToTaskRequest - ? (org.a2aproject.sdk.grpc.SubscribeToTaskRequest) request - : ((org.a2aproject.sdk.grpc.SubscribeToTaskRequest.Builder) request).build(); - return convert(() -> TaskIdParamsMapper.INSTANCE.fromProtoSubscribeToTaskRequest(reqProto)); - } - - public static ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigsResult(org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponseOrBuilder response) { - List configs = response.getConfigsList(); + public static GetTaskPushNotificationConfigParams getTaskPushNotificationConfigParams( + org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequestOrBuilder request) { + org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest reqProto = + request instanceof org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest + ? (org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest) request + : ((org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest + .Builder) + request) + .build(); + return convert( + () -> GetTaskPushNotificationConfigParamsMapper.INSTANCE.fromProto(reqProto)); + } + + public static TaskIdParams taskIdParams( + org.a2aproject.sdk.grpc.SubscribeToTaskRequestOrBuilder request) { + org.a2aproject.sdk.grpc.SubscribeToTaskRequest reqProto = + request instanceof org.a2aproject.sdk.grpc.SubscribeToTaskRequest + ? (org.a2aproject.sdk.grpc.SubscribeToTaskRequest) request + : ((org.a2aproject.sdk.grpc.SubscribeToTaskRequest.Builder) request) + .build(); + return convert( + () -> TaskIdParamsMapper.INSTANCE.fromProtoSubscribeToTaskRequest(reqProto)); + } + + public static ListTaskPushNotificationConfigsResult listTaskPushNotificationConfigsResult( + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponseOrBuilder response) { + List configs = + response.getConfigsList(); List result = new ArrayList<>(configs.size()); for (org.a2aproject.sdk.grpc.TaskPushNotificationConfig config : configs) { result.add(taskPushNotificationConfig(config)); @@ -293,24 +358,45 @@ public static ListTaskPushNotificationConfigsResult listTaskPushNotificationConf return new ListTaskPushNotificationConfigsResult(result, nextPageToken); } - public static ListTaskPushNotificationConfigsParams listTaskPushNotificationConfigsParams(org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequestOrBuilder request) { - org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest reqProto = request instanceof org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest - ? (org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest) request - : ((org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest.Builder) request).build(); - return convert(() -> ListTaskPushNotificationConfigsParamsMapper.INSTANCE.fromProto(reqProto)); - } - - public static DeleteTaskPushNotificationConfigParams deleteTaskPushNotificationConfigParams(org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequestOrBuilder request) { - org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest reqProto = request instanceof org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest - ? (org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest) request - : ((org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest.Builder) request).build(); - return convert(() -> DeleteTaskPushNotificationConfigParamsMapper.INSTANCE.fromProto(reqProto)); + public static ListTaskPushNotificationConfigsParams listTaskPushNotificationConfigsParams( + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequestOrBuilder request) { + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest reqProto = + request + instanceof + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest + ? (org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest) + request + : ((org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest + .Builder) + request) + .build(); + return convert( + () -> ListTaskPushNotificationConfigsParamsMapper.INSTANCE.fromProto(reqProto)); + } + + public static DeleteTaskPushNotificationConfigParams deleteTaskPushNotificationConfigParams( + org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequestOrBuilder request) { + org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest reqProto = + request + instanceof + org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest + ? (org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest) + request + : ((org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest + .Builder) + request) + .build(); + return convert( + () -> + DeleteTaskPushNotificationConfigParamsMapper.INSTANCE.fromProto( + reqProto)); } public static Task task(org.a2aproject.sdk.grpc.TaskOrBuilder task) { - org.a2aproject.sdk.grpc.Task taskProto = task instanceof org.a2aproject.sdk.grpc.Task - ? (org.a2aproject.sdk.grpc.Task) task - : ((org.a2aproject.sdk.grpc.Task.Builder) task).build(); + org.a2aproject.sdk.grpc.Task taskProto = + task instanceof org.a2aproject.sdk.grpc.Task + ? (org.a2aproject.sdk.grpc.Task) task + : ((org.a2aproject.sdk.grpc.Task.Builder) task).build(); return convert(() -> TaskMapper.INSTANCE.fromProto(taskProto)); } @@ -318,39 +404,56 @@ public static Message message(org.a2aproject.sdk.grpc.MessageOrBuilder message) if (Assert.isNullOrEmpty(message.getMessageId())) { throw new InvalidParamsError(); } - org.a2aproject.sdk.grpc.Message messageProto = message instanceof org.a2aproject.sdk.grpc.Message - ? (org.a2aproject.sdk.grpc.Message) message - : ((org.a2aproject.sdk.grpc.Message.Builder) message).build(); + org.a2aproject.sdk.grpc.Message messageProto = + message instanceof org.a2aproject.sdk.grpc.Message + ? (org.a2aproject.sdk.grpc.Message) message + : ((org.a2aproject.sdk.grpc.Message.Builder) message).build(); return convert(() -> MessageMapper.INSTANCE.fromProto(messageProto)); } - public static TaskStatusUpdateEvent taskStatusUpdateEvent(org.a2aproject.sdk.grpc.TaskStatusUpdateEventOrBuilder taskStatusUpdateEvent) { - org.a2aproject.sdk.grpc.TaskStatusUpdateEvent eventProto = taskStatusUpdateEvent instanceof org.a2aproject.sdk.grpc.TaskStatusUpdateEvent - ? (org.a2aproject.sdk.grpc.TaskStatusUpdateEvent) taskStatusUpdateEvent - : ((org.a2aproject.sdk.grpc.TaskStatusUpdateEvent.Builder) taskStatusUpdateEvent).build(); + public static TaskStatusUpdateEvent taskStatusUpdateEvent( + org.a2aproject.sdk.grpc.TaskStatusUpdateEventOrBuilder taskStatusUpdateEvent) { + org.a2aproject.sdk.grpc.TaskStatusUpdateEvent eventProto = + taskStatusUpdateEvent instanceof org.a2aproject.sdk.grpc.TaskStatusUpdateEvent + ? (org.a2aproject.sdk.grpc.TaskStatusUpdateEvent) taskStatusUpdateEvent + : ((org.a2aproject.sdk.grpc.TaskStatusUpdateEvent.Builder) + taskStatusUpdateEvent) + .build(); return convert(() -> TaskStatusUpdateEventMapper.INSTANCE.fromProto(eventProto)); } - public static TaskArtifactUpdateEvent taskArtifactUpdateEvent(org.a2aproject.sdk.grpc.TaskArtifactUpdateEventOrBuilder taskArtifactUpdateEvent) { - org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent eventProto = taskArtifactUpdateEvent instanceof org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent - ? (org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent) taskArtifactUpdateEvent - : ((org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent.Builder) taskArtifactUpdateEvent).build(); + public static TaskArtifactUpdateEvent taskArtifactUpdateEvent( + org.a2aproject.sdk.grpc.TaskArtifactUpdateEventOrBuilder taskArtifactUpdateEvent) { + org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent eventProto = + taskArtifactUpdateEvent + instanceof org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent + ? (org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent) + taskArtifactUpdateEvent + : ((org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent.Builder) + taskArtifactUpdateEvent) + .build(); return convert(() -> TaskArtifactUpdateEventMapper.INSTANCE.fromProto(eventProto)); } - public static ListTasksResult listTasksResult(org.a2aproject.sdk.grpc.ListTasksResponseOrBuilder listTasksResponse) { - org.a2aproject.sdk.grpc.ListTasksResponse eventProto = listTasksResponse instanceof org.a2aproject.sdk.grpc.ListTasksResponse - ? (org.a2aproject.sdk.grpc.ListTasksResponse) listTasksResponse - : ((org.a2aproject.sdk.grpc.ListTasksResponse.Builder) listTasksResponse).build(); + public static ListTasksResult listTasksResult( + org.a2aproject.sdk.grpc.ListTasksResponseOrBuilder listTasksResponse) { + org.a2aproject.sdk.grpc.ListTasksResponse eventProto = + listTasksResponse instanceof org.a2aproject.sdk.grpc.ListTasksResponse + ? (org.a2aproject.sdk.grpc.ListTasksResponse) listTasksResponse + : ((org.a2aproject.sdk.grpc.ListTasksResponse.Builder) + listTasksResponse) + .build(); return convert(() -> ListTasksResultMapper.INSTANCE.fromProto(eventProto)); } - public static StreamingEventKind streamingEventKind(org.a2aproject.sdk.grpc.StreamResponseOrBuilder streamResponse) { - org.a2aproject.sdk.grpc.StreamResponse streamResponseProto = streamResponse instanceof org.a2aproject.sdk.grpc.StreamResponse - ? (org.a2aproject.sdk.grpc.StreamResponse) streamResponse - : ((org.a2aproject.sdk.grpc.StreamResponse.Builder) streamResponse).build(); + public static StreamingEventKind streamingEventKind( + org.a2aproject.sdk.grpc.StreamResponseOrBuilder streamResponse) { + org.a2aproject.sdk.grpc.StreamResponse streamResponseProto = + streamResponse instanceof org.a2aproject.sdk.grpc.StreamResponse + ? (org.a2aproject.sdk.grpc.StreamResponse) streamResponse + : ((org.a2aproject.sdk.grpc.StreamResponse.Builder) streamResponse) + .build(); return convert(() -> StreamResponseMapper.INSTANCE.fromProto(streamResponseProto)); } } - } diff --git a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/package-info.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/package-info.java index b3d56fb8a..98044de69 100644 --- a/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/package-info.java +++ b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/utils/package-info.java @@ -2,4 +2,3 @@ package org.a2aproject.sdk.grpc.utils; import org.jspecify.annotations.NullMarked; - diff --git a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/A2ACommonFieldMapperTest.java b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/A2ACommonFieldMapperTest.java index 40598e2e9..c7fc74101 100644 --- a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/A2ACommonFieldMapperTest.java +++ b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/A2ACommonFieldMapperTest.java @@ -7,15 +7,13 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Struct; import com.google.protobuf.Value; import com.google.protobuf.util.JsonFormat; - +import java.util.Arrays; +import java.util.List; +import java.util.Map; import org.a2aproject.sdk.grpc.utils.ProtoUtils; import org.a2aproject.sdk.spec.MessageSendParams; import org.junit.jupiter.api.Test; @@ -45,7 +43,8 @@ void testObjectToValue_WithExplicitNull_SetsNullKind() { @Test void testStructToMap_WithNestedNulls_RoundTrips() throws InvalidProtocolBufferException { - String json = """ + String json = + """ { "nested": {"optional": null}, "items": [null, {"optional": null}, [], {}], @@ -90,7 +89,8 @@ void testValueToObject_WithNullListElement_RoundTrips() throws InvalidProtocolBu JsonFormat.parser().merge("[null,\"value\",{},[]]", builder); Value original = builder.build(); - List result = assertInstanceOf(List.class, A2ACommonFieldMapper.INSTANCE.valueToObject(original)); + List result = + assertInstanceOf(List.class, A2ACommonFieldMapper.INSTANCE.valueToObject(original)); assertEquals(Arrays.asList(null, "value", Map.of(), List.of()), result); assertEquals(original, A2ACommonFieldMapper.INSTANCE.objectToValue(result)); @@ -121,45 +121,47 @@ void testStructConversions_WithAbsentOrEmptyValues_PreserveDefaults() { @Test void testValueToObject_WithEmptyStruct_ReturnsEmptyMap() throws InvalidProtocolBufferException { // JSON containing an empty struct in "response" field - String json = "{\n" + - " \"message\": {\n" + - " \"messageId\": \"b3b1ab58-c3d0-4e6d-9e47-9d8a12fe0809\",\n" + - " \"role\": \"ROLE_USER\",\n" + - " \"parts\": [{\n" + - " \"text\": \"Hello\"\n" + - " }, {\n" + - " \"data\": {\n" + - " \"data\": {\n" + - " \"id\": \"call_94yo5ymj3qi5glbpkw5eicfd\",\n" + - " \"args\": {\n" + - " \"agent_name\": \"Default Agent\"\n" + - " },\n" + - " \"name\": \"transfer_to_agent\"\n" + - " }\n" + - " }\n" + - " }, {\n" + - " \"data\": {\n" + - " \"data\": {\n" + - " \"response\": {\n" + - " },\n" + - " \"id\": \"call_94yo5ymj3qi5glbpkw5eicfd\",\n" + - " \"name\": \"transfer_to_agent\"\n" + - " }\n" + - " }\n" + - " }, {\n" + - " \"text\": \"World\"\n" + - " }],\n" + - " \"metadata\": {\n" + - " }\n" + - " },\n" + - " \"configuration\": {\n" + - " \"returnImmediately\": false\n" + - " },\n" + - " \"metadata\": {\n" + - " }\n" + - "}"; - - org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); + String json = + "{\n" + + " \"message\": {\n" + + " \"messageId\": \"b3b1ab58-c3d0-4e6d-9e47-9d8a12fe0809\",\n" + + " \"role\": \"ROLE_USER\",\n" + + " \"parts\": [{\n" + + " \"text\": \"Hello\"\n" + + " }, {\n" + + " \"data\": {\n" + + " \"data\": {\n" + + " \"id\": \"call_94yo5ymj3qi5glbpkw5eicfd\",\n" + + " \"args\": {\n" + + " \"agent_name\": \"Default Agent\"\n" + + " },\n" + + " \"name\": \"transfer_to_agent\"\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"data\": {\n" + + " \"data\": {\n" + + " \"response\": {\n" + + " },\n" + + " \"id\": \"call_94yo5ymj3qi5glbpkw5eicfd\",\n" + + " \"name\": \"transfer_to_agent\"\n" + + " }\n" + + " }\n" + + " }, {\n" + + " \"text\": \"World\"\n" + + " }],\n" + + " \"metadata\": {\n" + + " }\n" + + " },\n" + + " \"configuration\": {\n" + + " \"returnImmediately\": false\n" + + " },\n" + + " \"metadata\": {\n" + + " }\n" + + "}"; + + org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = + org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); JsonFormat.parser().merge(json, builder); // This should not throw NullPointerException @@ -174,25 +176,28 @@ void testValueToObject_WithEmptyStruct_ReturnsEmptyMap() throws InvalidProtocolB * Test that valueToObject handles nested empty struct correctly. */ @Test - void testValueToObject_WithNestedEmptyStruct_ReturnsEmptyMap() throws InvalidProtocolBufferException { - String json = "{\n" + - " \"message\": {\n" + - " \"messageId\": \"test-id\",\n" + - " \"role\": \"ROLE_USER\",\n" + - " \"parts\": [{\n" + - " \"data\": {\n" + - " \"data\": {\n" + - " \"nested\": {\n" + - " \"empty\": {\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }]\n" + - " }\n" + - "}"; - - org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); + void testValueToObject_WithNestedEmptyStruct_ReturnsEmptyMap() + throws InvalidProtocolBufferException { + String json = + "{\n" + + " \"message\": {\n" + + " \"messageId\": \"test-id\",\n" + + " \"role\": \"ROLE_USER\",\n" + + " \"parts\": [{\n" + + " \"data\": {\n" + + " \"data\": {\n" + + " \"nested\": {\n" + + " \"empty\": {\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " }]\n" + + " }\n" + + "}"; + + org.a2aproject.sdk.grpc.SendMessageRequest.Builder builder = + org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); JsonFormat.parser().merge(json, builder); // This should not throw NullPointerException diff --git a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/ListTasksParamsMapperTest.java b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/ListTasksParamsMapperTest.java index 2139a6d4a..a43ff39db 100644 --- a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/ListTasksParamsMapperTest.java +++ b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/ListTasksParamsMapperTest.java @@ -11,7 +11,8 @@ public class ListTasksParamsMapperTest { @Test void toProtoWithDefaultBuilder() { - ListTasksRequest request = ListTasksParamsMapper.INSTANCE.toProto(ListTasksParams.builder().build()); + ListTasksRequest request = + ListTasksParamsMapper.INSTANCE.toProto(ListTasksParams.builder().build()); assertEquals("", request.getTenant()); } @@ -25,30 +26,34 @@ void toProtoWithDefaultConstructor() { @Test void toProtoWithTenant() { - ListTasksRequest request = ListTasksParamsMapper.INSTANCE.toProto( - ListTasksParams.builder().tenant("tenant-123").build()); + ListTasksRequest request = + ListTasksParamsMapper.INSTANCE.toProto( + ListTasksParams.builder().tenant("tenant-123").build()); assertEquals("tenant-123", request.getTenant()); } @Test void fromProtoWithoutTenant() { - ListTasksParams params = ListTasksParamsMapper.INSTANCE.fromProto(ListTasksRequest.newBuilder().build()); + ListTasksParams params = + ListTasksParamsMapper.INSTANCE.fromProto(ListTasksRequest.newBuilder().build()); assertNull(params.tenant()); } @Test void fromProtoWithTenant() { - ListTasksParams params = ListTasksParamsMapper.INSTANCE.fromProto( - ListTasksRequest.newBuilder().setTenant("tenant-123").build()); + ListTasksParams params = + ListTasksParamsMapper.INSTANCE.fromProto( + ListTasksRequest.newBuilder().setTenant("tenant-123").build()); assertEquals("tenant-123", params.tenant()); } @Test void nullTenantRoundTrip() { - ListTasksRequest request = ListTasksParamsMapper.INSTANCE.toProto(ListTasksParams.builder().build()); + ListTasksRequest request = + ListTasksParamsMapper.INSTANCE.toProto(ListTasksParams.builder().build()); ListTasksParams params = ListTasksParamsMapper.INSTANCE.fromProto(request); assertNull(params.tenant()); diff --git a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/StreamResponseMapperTest.java b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/StreamResponseMapperTest.java index a53527009..bf02d721c 100644 --- a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/StreamResponseMapperTest.java +++ b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/mapper/StreamResponseMapperTest.java @@ -6,7 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Collections; - import org.a2aproject.sdk.spec.Artifact; import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.StreamingEventKind; @@ -23,35 +22,44 @@ public class StreamResponseMapperTest { @Test void testConvertTask_ToProto() { // Arrange - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + Task task = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); // Act org.a2aproject.sdk.grpc.StreamResponse result = StreamResponseMapper.INSTANCE.toProto(task); // Assert assertNotNull(result); - assertEquals(org.a2aproject.sdk.grpc.StreamResponse.PayloadCase.TASK, result.getPayloadCase()); + assertEquals( + org.a2aproject.sdk.grpc.StreamResponse.PayloadCase.TASK, result.getPayloadCase()); assertEquals("task-123", result.getTask().getId()); assertEquals("context-456", result.getTask().getContextId()); - assertEquals(org.a2aproject.sdk.grpc.TaskState.TASK_STATE_COMPLETED, result.getTask().getStatus().getState()); + assertEquals( + org.a2aproject.sdk.grpc.TaskState.TASK_STATE_COMPLETED, + result.getTask().getStatus().getState()); } @Test void testConvertTask_FromProto() { // Arrange - org.a2aproject.sdk.grpc.StreamResponse proto = org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setTask(org.a2aproject.sdk.grpc.Task.newBuilder() - .setId("task-123") - .setContextId("context-456") - .setStatus(org.a2aproject.sdk.grpc.TaskStatus.newBuilder() - .setState(org.a2aproject.sdk.grpc.TaskState.TASK_STATE_COMPLETED) - .build()) - .build()) - .build(); + org.a2aproject.sdk.grpc.StreamResponse proto = + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setTask( + org.a2aproject.sdk.grpc.Task.newBuilder() + .setId("task-123") + .setContextId("context-456") + .setStatus( + org.a2aproject.sdk.grpc.TaskStatus.newBuilder() + .setState( + org.a2aproject.sdk.grpc.TaskState + .TASK_STATE_COMPLETED) + .build()) + .build()) + .build(); // Act StreamingEventKind result = StreamResponseMapper.INSTANCE.fromProto(proto); @@ -68,19 +76,23 @@ void testConvertTask_FromProto() { @Test void testConvertMessage_ToProto() { // Arrange - Message message = Message.builder() - .messageId("msg-123") - .contextId("context-456") - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("Hello"))) - .build(); + Message message = + Message.builder() + .messageId("msg-123") + .contextId("context-456") + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("Hello"))) + .build(); // Act - org.a2aproject.sdk.grpc.StreamResponse result = StreamResponseMapper.INSTANCE.toProto(message); + org.a2aproject.sdk.grpc.StreamResponse result = + StreamResponseMapper.INSTANCE.toProto(message); // Assert assertNotNull(result); - assertEquals(org.a2aproject.sdk.grpc.StreamResponse.PayloadCase.MESSAGE, result.getPayloadCase()); + assertEquals( + org.a2aproject.sdk.grpc.StreamResponse.PayloadCase.MESSAGE, + result.getPayloadCase()); assertEquals("msg-123", result.getMessage().getMessageId()); assertEquals("context-456", result.getMessage().getContextId()); assertEquals(org.a2aproject.sdk.grpc.Role.ROLE_USER, result.getMessage().getRole()); @@ -89,16 +101,19 @@ void testConvertMessage_ToProto() { @Test void testConvertMessage_FromProto() { // Arrange - org.a2aproject.sdk.grpc.StreamResponse proto = org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setMessage(org.a2aproject.sdk.grpc.Message.newBuilder() - .setMessageId("msg-123") - .setContextId("context-456") - .setRole(org.a2aproject.sdk.grpc.Role.ROLE_USER) - .addParts(org.a2aproject.sdk.grpc.Part.newBuilder() - .setText("Hello") - .build()) - .build()) - .build(); + org.a2aproject.sdk.grpc.StreamResponse proto = + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setMessage( + org.a2aproject.sdk.grpc.Message.newBuilder() + .setMessageId("msg-123") + .setContextId("context-456") + .setRole(org.a2aproject.sdk.grpc.Role.ROLE_USER) + .addParts( + org.a2aproject.sdk.grpc.Part.newBuilder() + .setText("Hello") + .build()) + .build()) + .build(); // Act StreamingEventKind result = StreamResponseMapper.INSTANCE.fromProto(proto); @@ -115,35 +130,46 @@ void testConvertMessage_FromProto() { @Test void testConvertTaskStatusUpdateEvent_ToProto() { // Arrange - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); // Act - org.a2aproject.sdk.grpc.StreamResponse result = StreamResponseMapper.INSTANCE.toProto(event); + org.a2aproject.sdk.grpc.StreamResponse result = + StreamResponseMapper.INSTANCE.toProto(event); // Assert assertNotNull(result); - assertEquals(org.a2aproject.sdk.grpc.StreamResponse.PayloadCase.STATUS_UPDATE, result.getPayloadCase()); + assertEquals( + org.a2aproject.sdk.grpc.StreamResponse.PayloadCase.STATUS_UPDATE, + result.getPayloadCase()); assertEquals("task-123", result.getStatusUpdate().getTaskId()); assertEquals("context-456", result.getStatusUpdate().getContextId()); - assertEquals(org.a2aproject.sdk.grpc.TaskState.TASK_STATE_WORKING, result.getStatusUpdate().getStatus().getState()); + assertEquals( + org.a2aproject.sdk.grpc.TaskState.TASK_STATE_WORKING, + result.getStatusUpdate().getStatus().getState()); } @Test void testConvertTaskStatusUpdateEvent_FromProto() { // Arrange - org.a2aproject.sdk.grpc.StreamResponse proto = org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setStatusUpdate(org.a2aproject.sdk.grpc.TaskStatusUpdateEvent.newBuilder() - .setTaskId("task-123") - .setContextId("context-456") - .setStatus(org.a2aproject.sdk.grpc.TaskStatus.newBuilder() - .setState(org.a2aproject.sdk.grpc.TaskState.TASK_STATE_WORKING) - .build()) - .build()) - .build(); + org.a2aproject.sdk.grpc.StreamResponse proto = + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setStatusUpdate( + org.a2aproject.sdk.grpc.TaskStatusUpdateEvent.newBuilder() + .setTaskId("task-123") + .setContextId("context-456") + .setStatus( + org.a2aproject.sdk.grpc.TaskStatus.newBuilder() + .setState( + org.a2aproject.sdk.grpc.TaskState + .TASK_STATE_WORKING) + .build()) + .build()) + .build(); // Act StreamingEventKind result = StreamResponseMapper.INSTANCE.fromProto(proto); @@ -161,22 +187,27 @@ void testConvertTaskStatusUpdateEvent_FromProto() { @Test void testConvertTaskArtifactUpdateEvent_ToProto() { // Arrange - TaskArtifactUpdateEvent event = TaskArtifactUpdateEvent.builder() - .taskId("task-123") - .contextId("context-456") - .artifact(Artifact.builder() - .artifactId("artifact-1") - .name("result") - .parts(new TextPart("Result text")) - .build()) - .build(); + TaskArtifactUpdateEvent event = + TaskArtifactUpdateEvent.builder() + .taskId("task-123") + .contextId("context-456") + .artifact( + Artifact.builder() + .artifactId("artifact-1") + .name("result") + .parts(new TextPart("Result text")) + .build()) + .build(); // Act - org.a2aproject.sdk.grpc.StreamResponse result = StreamResponseMapper.INSTANCE.toProto(event); + org.a2aproject.sdk.grpc.StreamResponse result = + StreamResponseMapper.INSTANCE.toProto(event); // Assert assertNotNull(result); - assertEquals(org.a2aproject.sdk.grpc.StreamResponse.PayloadCase.ARTIFACT_UPDATE, result.getPayloadCase()); + assertEquals( + org.a2aproject.sdk.grpc.StreamResponse.PayloadCase.ARTIFACT_UPDATE, + result.getPayloadCase()); assertEquals("task-123", result.getArtifactUpdate().getTaskId()); assertEquals("context-456", result.getArtifactUpdate().getContextId()); assertEquals("artifact-1", result.getArtifactUpdate().getArtifact().getArtifactId()); @@ -186,19 +217,24 @@ void testConvertTaskArtifactUpdateEvent_ToProto() { @Test void testConvertTaskArtifactUpdateEvent_FromProto() { // Arrange - org.a2aproject.sdk.grpc.StreamResponse proto = org.a2aproject.sdk.grpc.StreamResponse.newBuilder() - .setArtifactUpdate(org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent.newBuilder() - .setTaskId("task-123") - .setContextId("context-456") - .setArtifact(org.a2aproject.sdk.grpc.Artifact.newBuilder() - .setArtifactId("artifact-1") - .setName("result") - .addParts(org.a2aproject.sdk.grpc.Part.newBuilder() - .setText("Result text") + org.a2aproject.sdk.grpc.StreamResponse proto = + org.a2aproject.sdk.grpc.StreamResponse.newBuilder() + .setArtifactUpdate( + org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent.newBuilder() + .setTaskId("task-123") + .setContextId("context-456") + .setArtifact( + org.a2aproject.sdk.grpc.Artifact.newBuilder() + .setArtifactId("artifact-1") + .setName("result") + .addParts( + org.a2aproject.sdk.grpc.Part + .newBuilder() + .setText("Result text") + .build()) + .build()) .build()) - .build()) - .build()) - .build(); + .build(); // Act StreamingEventKind result = StreamResponseMapper.INSTANCE.fromProto(proto); @@ -216,26 +252,32 @@ void testConvertTaskArtifactUpdateEvent_FromProto() { @Test void testConvertStreamResponse_FromProto_PayloadNotSet_ThrowsException() { // Arrange - org.a2aproject.sdk.grpc.StreamResponse proto = org.a2aproject.sdk.grpc.StreamResponse.newBuilder().build(); + org.a2aproject.sdk.grpc.StreamResponse proto = + org.a2aproject.sdk.grpc.StreamResponse.newBuilder().build(); // Act & Assert - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { - StreamResponseMapper.INSTANCE.fromProto(proto); - }); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + StreamResponseMapper.INSTANCE.fromProto(proto); + }); assertEquals("StreamResponse payload oneof field not set", exception.getMessage()); } @Test void testConvertStreamResponse_Roundtrip_Task() { // Arrange - Task originalTask = Task.builder() - .id("task-123") - .contextId("context-456") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + Task originalTask = + Task.builder() + .id("task-123") + .contextId("context-456") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); // Act - org.a2aproject.sdk.grpc.StreamResponse proto = StreamResponseMapper.INSTANCE.toProto(originalTask); + org.a2aproject.sdk.grpc.StreamResponse proto = + StreamResponseMapper.INSTANCE.toProto(originalTask); StreamingEventKind result = StreamResponseMapper.INSTANCE.fromProto(proto); // Assert @@ -250,15 +292,17 @@ void testConvertStreamResponse_Roundtrip_Task() { @Test void testConvertStreamResponse_Roundtrip_Message() { // Arrange - Message originalMessage = Message.builder() - .messageId("msg-123") - .contextId("context-456") - .role(Message.Role.ROLE_AGENT) - .parts(Collections.singletonList(new TextPart("Response"))) - .build(); + Message originalMessage = + Message.builder() + .messageId("msg-123") + .contextId("context-456") + .role(Message.Role.ROLE_AGENT) + .parts(Collections.singletonList(new TextPart("Response"))) + .build(); // Act - org.a2aproject.sdk.grpc.StreamResponse proto = StreamResponseMapper.INSTANCE.toProto(originalMessage); + org.a2aproject.sdk.grpc.StreamResponse proto = + StreamResponseMapper.INSTANCE.toProto(originalMessage); StreamingEventKind result = StreamResponseMapper.INSTANCE.fromProto(proto); // Assert diff --git a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtilsTest.java b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtilsTest.java index 300a2802a..c17c5de87 100644 --- a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtilsTest.java +++ b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/JSONRPCUtilsTest.java @@ -20,13 +20,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import java.util.Collections; -import java.util.Map; - import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; +import java.util.Collections; +import java.util.Map; import org.a2aproject.sdk.grpc.SendMessageResponse; import org.a2aproject.sdk.jsonrpc.common.json.InvalidParamsJsonMappingException; import org.a2aproject.sdk.jsonrpc.common.json.JsonMappingException; @@ -58,7 +57,8 @@ public class JSONRPCUtilsTest { @Test public void testSendMessage_WithExplicitNulls_RoundTrips() throws JsonProcessingException { - String json = """ + String json = + """ { "jsonrpc": "2.0", "id": "null-message", @@ -78,7 +78,9 @@ public void testSendMessage_WithExplicitNulls_RoundTrips() throws JsonProcessing } """; - SendMessageRequest request = assertInstanceOf(SendMessageRequest.class, JSONRPCUtils.parseRequestBody(json, null)); + SendMessageRequest request = + assertInstanceOf( + SendMessageRequest.class, JSONRPCUtils.parseRequestBody(json, null)); MessageSendParams params = request.getParams(); DataPart part = assertInstanceOf(DataPart.class, params.message().parts().get(0)); Map data = assertInstanceOf(Map.class, part.data()); @@ -86,19 +88,25 @@ public void testSendMessage_WithExplicitNulls_RoundTrips() throws JsonProcessing assertNull(data.get("attributeId")); assertEquals("ID", data.get("source")); assertEquals(Collections.singletonMap("partOptional", null), part.metadata()); - assertEquals(Collections.singletonMap("messageOptional", null), params.message().metadata()); + assertEquals( + Collections.singletonMap("messageOptional", null), params.message().metadata()); assertEquals(Collections.singletonMap("requestOptional", null), params.metadata()); - String serialized = JSONRPCUtils.toJsonRPCRequest(assertInstanceOf(String.class, request.getId()), SEND_MESSAGE_METHOD, - ProtoUtils.ToProto.sendMessageRequest(params)); - SendMessageRequest roundTripped = assertInstanceOf(SendMessageRequest.class, - JSONRPCUtils.parseRequestBody(serialized, null)); + String serialized = + JSONRPCUtils.toJsonRPCRequest( + assertInstanceOf(String.class, request.getId()), + SEND_MESSAGE_METHOD, + ProtoUtils.ToProto.sendMessageRequest(params)); + SendMessageRequest roundTripped = + assertInstanceOf( + SendMessageRequest.class, JSONRPCUtils.parseRequestBody(serialized, null)); assertEquals(params, roundTripped.getParams()); } @Test public void testGetTask_WithNestedNulls_RoundTrips() throws JsonProcessingException { - String json = """ + String json = + """ { "jsonrpc": "2.0", "id": "null-task", @@ -126,7 +134,10 @@ public void testGetTask_WithNestedNulls_RoundTrips() throws JsonProcessingExcept } """; - GetTaskResponse response = assertInstanceOf(GetTaskResponse.class, JSONRPCUtils.parseResponseBody(json, GET_TASK_METHOD)); + GetTaskResponse response = + assertInstanceOf( + GetTaskResponse.class, + JSONRPCUtils.parseResponseBody(json, GET_TASK_METHOD)); Task task = response.getResult(); assertNotNull(task); assertEquals(Collections.singletonMap("taskOptional", null), task.metadata()); @@ -134,38 +145,47 @@ public void testGetTask_WithNestedNulls_RoundTrips() throws JsonProcessingExcept Map data = assertInstanceOf(Map.class, part.data()); assertEquals(Collections.singletonMap("optional", null), data.get("nested")); assertEquals(Collections.singletonMap("partOptional", null), part.metadata()); - assertEquals(Collections.singletonMap("messageOptional", null), task.history().get(0).metadata()); - - String serialized = JSONRPCUtils.toJsonRPCResultResponse(response.getId(), ProtoUtils.ToProto.task(task)); - GetTaskResponse roundTripped = assertInstanceOf(GetTaskResponse.class, - JSONRPCUtils.parseResponseBody(serialized, GET_TASK_METHOD)); + assertEquals( + Collections.singletonMap("messageOptional", null), + task.history().get(0).metadata()); + + String serialized = + JSONRPCUtils.toJsonRPCResultResponse( + response.getId(), ProtoUtils.ToProto.task(task)); + GetTaskResponse roundTripped = + assertInstanceOf( + GetTaskResponse.class, + JSONRPCUtils.parseResponseBody(serialized, GET_TASK_METHOD)); assertEquals(task, roundTripped.getResult()); } @Test - public void testParseCreateTaskPushNotificationConfigRequest_ValidProtoFormat() throws JsonProcessingException { - String validRequest = """ - { - "jsonrpc": "2.0", - "method": "CreateTaskPushNotificationConfig", - "id": "1", - "params": { - "taskId": "task-123", - "tenant": "", - "id": "config-456", - "url": "https://example.com/callback", - "authentication": { - "scheme": "jwt" + public void testParseCreateTaskPushNotificationConfigRequest_ValidProtoFormat() + throws JsonProcessingException { + String validRequest = + """ + { + "jsonrpc": "2.0", + "method": "CreateTaskPushNotificationConfig", + "id": "1", + "params": { + "taskId": "task-123", + "tenant": "", + "id": "config-456", + "url": "https://example.com/callback", + "authentication": { + "scheme": "jwt" + } + } } - } - } - """; + """; A2ARequest request = JSONRPCUtils.parseRequestBody(validRequest, null); assertNotNull(request); assertInstanceOf(CreateTaskPushNotificationConfigRequest.class, request); - CreateTaskPushNotificationConfigRequest setRequest = (CreateTaskPushNotificationConfigRequest) request; + CreateTaskPushNotificationConfigRequest setRequest = + (CreateTaskPushNotificationConfigRequest) request; assertEquals("2.0", setRequest.getJsonrpc()); assertEquals("1", setRequest.getId()); assertEquals("CreateTaskPushNotificationConfig", setRequest.getMethod()); @@ -177,24 +197,27 @@ public void testParseCreateTaskPushNotificationConfigRequest_ValidProtoFormat() } @Test - public void testParseGetTaskPushNotificationConfigRequest_ValidProtoFormat() throws JsonProcessingException { - String validRequest = """ - { - "jsonrpc": "2.0", - "method": "GetTaskPushNotificationConfig", - "id": "2", - "params": { - "taskId": "task-123", - "id": "config-456" - } - } - """; + public void testParseGetTaskPushNotificationConfigRequest_ValidProtoFormat() + throws JsonProcessingException { + String validRequest = + """ + { + "jsonrpc": "2.0", + "method": "GetTaskPushNotificationConfig", + "id": "2", + "params": { + "taskId": "task-123", + "id": "config-456" + } + } + """; A2ARequest request = JSONRPCUtils.parseRequestBody(validRequest, null); assertNotNull(request); assertInstanceOf(GetTaskPushNotificationConfigRequest.class, request); - GetTaskPushNotificationConfigRequest getRequest = (GetTaskPushNotificationConfigRequest) request; + GetTaskPushNotificationConfigRequest getRequest = + (GetTaskPushNotificationConfigRequest) request; assertEquals("2.0", getRequest.getJsonrpc()); assertEquals("2", getRequest.getId()); assertEquals("GetTaskPushNotificationConfig", getRequest.getMethod()); @@ -204,53 +227,73 @@ public void testParseGetTaskPushNotificationConfigRequest_ValidProtoFormat() thr @Test public void testParseMalformedJSON_ThrowsJsonSyntaxException() { - String malformedRequest = """ - { - "jsonrpc": "2.0", - "method": "CreateTaskPushNotificationConfig", - "params": { - "parent": "tasks/task-123" - """; // Missing closing braces + String malformedRequest = + """ + { + "jsonrpc": "2.0", + "method": "CreateTaskPushNotificationConfig", + "params": { + "parent": "tasks/task-123" + """; // Missing closing braces - JsonSyntaxException exception = assertThrows(JsonSyntaxException.class, () -> { - JSONRPCUtils.parseRequestBody(malformedRequest, null); - }); - assertEquals("java.io.EOFException: End of input at line 6 column 1 path $.params.parent", exception.getMessage()); + JsonSyntaxException exception = + assertThrows( + JsonSyntaxException.class, + () -> { + JSONRPCUtils.parseRequestBody(malformedRequest, null); + }); + assertEquals( + "java.io.EOFException: End of input at line 6 column 1 path $.params.parent", + exception.getMessage()); } @Test public void testParseInvalidParams_ThrowsInvalidParamsJsonMappingException() { - String invalidParamsRequest = """ - { - "jsonrpc": "2.0", - "method": "CreateTaskPushNotificationConfig", - "id": "3", - "params": "not_a_dict" - } - """; - - InvalidParamsJsonMappingException exception = assertThrows( - InvalidParamsJsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(invalidParamsRequest, null) - ); + String invalidParamsRequest = + """ + { + "jsonrpc": "2.0", + "method": "CreateTaskPushNotificationConfig", + "id": "3", + "params": "not_a_dict" + } + """; + + InvalidParamsJsonMappingException exception = + assertThrows( + InvalidParamsJsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(invalidParamsRequest, null)); assertEquals("3", exception.getId()); } @ParameterizedTest - @ValueSource(strings = { - GET_TASK_METHOD, CANCEL_TASK_METHOD, LIST_TASK_METHOD, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, - GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, SEND_MESSAGE_METHOD, LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, - DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, SEND_STREAMING_MESSAGE_METHOD, SUBSCRIBE_TO_TASK_METHOD - }) + @ValueSource( + strings = { + GET_TASK_METHOD, + CANCEL_TASK_METHOD, + LIST_TASK_METHOD, + SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + SEND_MESSAGE_METHOD, + LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + SEND_STREAMING_MESSAGE_METHOD, + SUBSCRIBE_TO_TASK_METHOD + }) public void testParseOmittedParams_SameAsEmptyParams(String method) { // JSON-RPC 2.0 section 4: "params" MAY be omitted. An omitted "params" must be parsed - // like an empty params object instead of escaping parseRequestBody as a NullPointerException. - String omittedParamsRequest = """ - {"jsonrpc": "2.0", "method": "%s", "id": 7} - """.formatted(method); - String emptyParamsRequest = """ - {"jsonrpc": "2.0", "method": "%s", "id": 7, "params": {}} - """.formatted(method); + // like an empty params object instead of escaping parseRequestBody as a + // NullPointerException. + String omittedParamsRequest = + """ + {"jsonrpc": "2.0", "method": "%s", "id": 7} + """ + .formatted(method); + String emptyParamsRequest = + """ + {"jsonrpc": "2.0", "method": "%s", "id": 7, "params": {}} + """ + .formatted(method); assertEquals(parseOutcome(emptyParamsRequest), parseOutcome(omittedParamsRequest)); } @@ -266,16 +309,19 @@ private static Object parseOutcome(String body) { @Test public void testParseOmittedParams_ListTasksWithoutParams() throws Exception { // All ListTasks fields are optional, so a request without "params" is valid. - String omittedParamsRequest = """ - { - "jsonrpc": "2.0", - "method": "ListTasks", - "id": 8 - } - """; - - ListTasksRequest request = assertInstanceOf(ListTasksRequest.class, - JSONRPCUtils.parseRequestBody(omittedParamsRequest, null)); + String omittedParamsRequest = + """ + { + "jsonrpc": "2.0", + "method": "ListTasks", + "id": 8 + } + """; + + ListTasksRequest request = + assertInstanceOf( + ListTasksRequest.class, + JSONRPCUtils.parseRequestBody(omittedParamsRequest, null)); assertEquals(8, request.getId()); assertNotNull(request.getParams()); } @@ -284,108 +330,123 @@ public void testParseOmittedParams_ListTasksWithoutParams() throws Exception { public void testParseExplicitNullParams_IsNotTreatedAsOmitted() { // Only an omitted "params" is normalized to {}. An explicit "params": null keeps its // existing handling, so ListTasks (valid without params) still rejects it. - String explicitNullParamsRequest = """ - {"jsonrpc": "2.0", "method": "ListTasks", "id": 9, "params": null} - """; + String explicitNullParamsRequest = + """ + {"jsonrpc": "2.0", "method": "ListTasks", "id": 9, "params": null} + """; assertThrows( - JsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(explicitNullParamsRequest, null) - ); + JsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(explicitNullParamsRequest, null)); } @ParameterizedTest - @ValueSource(strings = { - "{\"jsonrpc\": \"2.0\", \"method\": \"GetExtendedAgentCard\", \"id\": 10}", - "{\"jsonrpc\": \"2.0\", \"method\": \"GetExtendedAgentCard\", \"id\": 10, \"params\": null}" - }) + @ValueSource( + strings = { + "{\"jsonrpc\": \"2.0\", \"method\": \"GetExtendedAgentCard\", \"id\": 10}", + "{\"jsonrpc\": \"2.0\", \"method\": \"GetExtendedAgentCard\", \"id\": 10," + + " \"params\": null}" + }) public void testParseGetExtendedAgentCard_AbsentParamsUnchanged(String body) throws Exception { // GetExtendedAgentCard keeps its own handling of absent params. - GetExtendedAgentCardRequest request = assertInstanceOf(GetExtendedAgentCardRequest.class, - JSONRPCUtils.parseRequestBody(body, null)); + GetExtendedAgentCardRequest request = + assertInstanceOf( + GetExtendedAgentCardRequest.class, + JSONRPCUtils.parseRequestBody(body, null)); assertEquals(10, request.getId()); } @ParameterizedTest @ValueSource(strings = {SEND_MESSAGE_METHOD, SEND_STREAMING_MESSAGE_METHOD}) public void testParseOmittedParams_RequiredFieldsMissing_ThrowsInvalidParams(String method) { - // "message" is required, and the existing validation rejects "params": {} with Invalid params + // "message" is required, and the existing validation rejects "params": {} with Invalid + // params // (-32602). An omitted "params" must hit the same validation and keep the request id, // instead of failing with an internal error. - String omittedParamsRequest = """ - {"jsonrpc": "2.0", "method": "%s", "id": 7} - """.formatted(method); - - InvalidParamsJsonMappingException exception = assertThrows( - InvalidParamsJsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(omittedParamsRequest, null) - ); + String omittedParamsRequest = + """ + {"jsonrpc": "2.0", "method": "%s", "id": 7} + """ + .formatted(method); + + InvalidParamsJsonMappingException exception = + assertThrows( + InvalidParamsJsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(omittedParamsRequest, null)); assertEquals(7, exception.getId()); } @ParameterizedTest - @ValueSource(strings = { - "[]", - "[{\"jsonrpc\": \"2.0\", \"method\": \"GetTask\", \"id\": 1, \"params\": {\"id\": \"task-1\"}}]", - "\"GetTask\"", - "42", - "true", - "null" - }) + @ValueSource( + strings = { + "[]", + "[{\"jsonrpc\": \"2.0\", \"method\": \"GetTask\", \"id\": 1, \"params\": {\"id\":" + + " \"task-1\"}}]", + "\"GetTask\"", + "42", + "true", + "null" + }) public void testParseNonObjectBody_ThrowsJsonMappingException(String body) { // JSON-RPC 2.0 section 5.1: a body that is valid JSON but not a Request object // (an array, including a batch, or a primitive) must yield InvalidRequest (-32600), // which the server routes derive from a plain JsonMappingException. - JsonMappingException exception = assertThrows( - JsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(body, null) - ); + JsonMappingException exception = + assertThrows( + JsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(body, null)); assertEquals(JsonMappingException.class, exception.getClass()); } @Test public void testParseInvalidProtoStructure_ThrowsInvalidParamsJsonMappingException() { - String invalidStructure = """ - { - "jsonrpc": "2.0", - "method": "CreateTaskPushNotificationConfig", - "id": "4", - "params": { - "invalid_field": "value" - } - } - """; - - InvalidParamsJsonMappingException exception = assertThrows( - InvalidParamsJsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(invalidStructure, null) - ); + String invalidStructure = + """ + { + "jsonrpc": "2.0", + "method": "CreateTaskPushNotificationConfig", + "id": "4", + "params": { + "invalid_field": "value" + } + } + """; + + InvalidParamsJsonMappingException exception = + assertThrows( + InvalidParamsJsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(invalidStructure, null)); assertEquals("4", exception.getId()); - assertEquals(ERROR_MESSAGE.formatted("invalid_field in message lf.a2a.v1.TaskPushNotificationConfig"), exception.getMessage()); + assertEquals( + ERROR_MESSAGE.formatted( + "invalid_field in message lf.a2a.v1.TaskPushNotificationConfig"), + exception.getMessage()); } @Test public void testParseNumericalTimestampThrowsInvalidParamsJsonMappingException() { - String validRequest = """ - { - "jsonrpc": "2.0", - "method": "ListTasks", - "id": "1", - "params": { - "statusTimestampAfter": "2023-10-27T10:00:00Z" - } - } - """; - String invalidRequest = """ - { - "jsonrpc": "2.0", - "method": "ListTasks", - "id": "2", - "params": { - "statusTimestampAfter": "1" - } - } - """; + String validRequest = + """ + { + "jsonrpc": "2.0", + "method": "ListTasks", + "id": "1", + "params": { + "statusTimestampAfter": "2023-10-27T10:00:00Z" + } + } + """; + String invalidRequest = + """ + { + "jsonrpc": "2.0", + "method": "ListTasks", + "id": "2", + "params": { + "statusTimestampAfter": "1" + } + } + """; try { A2ARequest request = JSONRPCUtils.parseRequestBody(validRequest, null); @@ -393,71 +454,77 @@ public void testParseNumericalTimestampThrowsInvalidParamsJsonMappingException() } catch (JsonProcessingException e) { fail(e); } - InvalidParamsJsonMappingException exception = assertThrows( - InvalidParamsJsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(invalidRequest, null) - ); + InvalidParamsJsonMappingException exception = + assertThrows( + InvalidParamsJsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(invalidRequest, null)); assertEquals("2", exception.getId()); } @Test public void testParseMissingField_ThrowsInvalidParamsError() throws JsonMappingException { - String missingRoleMessage= """ - { - "jsonrpc":"2.0", - "method":"SendMessage", - "id": "18", - "params":{ - "message":{ - "messageId":"message-1234", - "contextId":"context-1234", - "parts":[ - { - "text":"tell me a joke" - } - ], - "metadata":{ - - } - } - } - }"""; - InvalidParamsJsonMappingException exception = assertThrows( - InvalidParamsJsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(missingRoleMessage, null) - ); + String missingRoleMessage = + """ + { + "jsonrpc":"2.0", + "method":"SendMessage", + "id": "18", + "params":{ + "message":{ + "messageId":"message-1234", + "contextId":"context-1234", + "parts":[ + { + "text":"tell me a joke" + } + ], + "metadata":{ + + } + } + } + }\ + """; + InvalidParamsJsonMappingException exception = + assertThrows( + InvalidParamsJsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(missingRoleMessage, null)); assertEquals("18", exception.getId()); } @Test public void testParseUnknownField_ThrowsJsonMappingException() throws JsonMappingException { - String unknownFieldMessage= """ - { - "jsonrpc":"2.0", - "method":"SendMessage", - "id": "18", - "params":{ - "message":{ - "role": "ROLE_AGENT", - "unknown":"field", - "messageId":"message-1234", - "contextId":"context-1234", - "parts":[ - { - "text":"tell me a joke" - } - ], - "metadata":{ - - } - } - } - }"""; - JsonMappingException exception = assertThrows( - JsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(unknownFieldMessage, null) - ); - assertEquals(ERROR_MESSAGE.formatted("unknown in message lf.a2a.v1.Message"), exception.getMessage()); + String unknownFieldMessage = + """ + { + "jsonrpc":"2.0", + "method":"SendMessage", + "id": "18", + "params":{ + "message":{ + "role": "ROLE_AGENT", + "unknown":"field", + "messageId":"message-1234", + "contextId":"context-1234", + "parts":[ + { + "text":"tell me a joke" + } + ], + "metadata":{ + + } + } + } + }\ + """; + JsonMappingException exception = + assertThrows( + JsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(unknownFieldMessage, null)); + assertEquals( + ERROR_MESSAGE.formatted("unknown in message lf.a2a.v1.Message"), + exception.getMessage()); } @Test @@ -465,75 +532,90 @@ public void testParseInvalidTypeWithNullId_UsesEmptyStringSentinel() throws Exce // Test the low-level convertProtoBufExceptionToJsonProcessingException with null ID // This tests the sentinel value logic directly com.google.protobuf.InvalidProtocolBufferException protoException = - new com.google.protobuf.InvalidProtocolBufferException("Expected ENUM but found \"INVALID_VALUE\""); + new com.google.protobuf.InvalidProtocolBufferException( + "Expected ENUM but found \"INVALID_VALUE\""); // Use reflection to call the private method for testing - java.lang.reflect.Method method = JSONRPCUtils.class.getDeclaredMethod( - "convertProtoBufExceptionToJsonProcessingException", - com.google.protobuf.InvalidProtocolBufferException.class, - Object.class - ); + java.lang.reflect.Method method = + JSONRPCUtils.class.getDeclaredMethod( + "convertProtoBufExceptionToJsonProcessingException", + com.google.protobuf.InvalidProtocolBufferException.class, + Object.class); method.setAccessible(true); - JsonProcessingException exception = (JsonProcessingException) method.invoke( - null, - protoException, - null // null ID - ); + JsonProcessingException exception = + (JsonProcessingException) + method.invoke( + null, protoException, null // null ID + ); // Should be InvalidParamsJsonMappingException with empty string sentinel - assertInstanceOf(InvalidParamsJsonMappingException.class, exception, - "Expected InvalidParamsJsonMappingException for proto error"); - InvalidParamsJsonMappingException invalidParamsException = (InvalidParamsJsonMappingException) exception; - assertEquals("", invalidParamsException.getId(), - "Expected empty string sentinel when ID is null"); + assertInstanceOf( + InvalidParamsJsonMappingException.class, + exception, + "Expected InvalidParamsJsonMappingException for proto error"); + InvalidParamsJsonMappingException invalidParamsException = + (InvalidParamsJsonMappingException) exception; + assertEquals( + "", + invalidParamsException.getId(), + "Expected empty string sentinel when ID is null"); } @Test public void testParseInvalidTypeWithValidId_PreservesId() throws Exception { // Test the low-level convertProtoBufExceptionToJsonProcessingException with valid ID com.google.protobuf.InvalidProtocolBufferException protoException = - new com.google.protobuf.InvalidProtocolBufferException("Expected ENUM but found \"INVALID_VALUE\""); + new com.google.protobuf.InvalidProtocolBufferException( + "Expected ENUM but found \"INVALID_VALUE\""); // Use reflection to call the private method for testing - java.lang.reflect.Method method = JSONRPCUtils.class.getDeclaredMethod( - "convertProtoBufExceptionToJsonProcessingException", - com.google.protobuf.InvalidProtocolBufferException.class, - Object.class - ); + java.lang.reflect.Method method = + JSONRPCUtils.class.getDeclaredMethod( + "convertProtoBufExceptionToJsonProcessingException", + com.google.protobuf.InvalidProtocolBufferException.class, + Object.class); method.setAccessible(true); - JsonProcessingException exception = (JsonProcessingException) method.invoke( - null, - protoException, - 42 // Valid ID - ); + JsonProcessingException exception = + (JsonProcessingException) + method.invoke( + null, protoException, 42 // Valid ID + ); // Should be InvalidParamsJsonMappingException with preserved ID - assertInstanceOf(InvalidParamsJsonMappingException.class, exception, - "Expected InvalidParamsJsonMappingException for proto error"); - InvalidParamsJsonMappingException invalidParamsException = (InvalidParamsJsonMappingException) exception; - assertEquals(42, invalidParamsException.getId(), - "Expected actual ID to be preserved when present"); + assertInstanceOf( + InvalidParamsJsonMappingException.class, + exception, + "Expected InvalidParamsJsonMappingException for proto error"); + InvalidParamsJsonMappingException invalidParamsException = + (InvalidParamsJsonMappingException) exception; + assertEquals( + 42, + invalidParamsException.getId(), + "Expected actual ID to be preserved when present"); } @Test public void testGenerateCreateTaskPushNotificationConfigResponse_Success() throws Exception { - String responseJson = """ - { - "jsonrpc": "2.0", - "id": 1, - "result": { - "tenant": "tenant", - "taskId": "task-123", - "id": "config-456", - "url": "https://example.com/callback" - } - } - """; + String responseJson = + """ + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "tenant": "tenant", + "taskId": "task-123", + "id": "config-456", + "url": "https://example.com/callback" + } + } + """; CreateTaskPushNotificationConfigResponse response = - (CreateTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(responseJson, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + (CreateTaskPushNotificationConfigResponse) + JSONRPCUtils.parseResponseBody( + responseJson, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); assertNotNull(response); assertEquals(1, response.getId()); @@ -544,21 +626,24 @@ public void testGenerateCreateTaskPushNotificationConfigResponse_Success() throw @Test public void testGenerateGetTaskPushNotificationConfigResponse_Success() throws Exception { - String responseJson = """ - { - "jsonrpc": "2.0", - "id": 2, - "result": { - "tenant": "tenant", - "taskId": "task-123", - "id": "config-456", - "url": "https://example.com/callback" - } - } - """; + String responseJson = + """ + { + "jsonrpc": "2.0", + "id": 2, + "result": { + "tenant": "tenant", + "taskId": "task-123", + "id": "config-456", + "url": "https://example.com/callback" + } + } + """; GetTaskPushNotificationConfigResponse response = - (GetTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(responseJson, GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + (GetTaskPushNotificationConfigResponse) + JSONRPCUtils.parseResponseBody( + responseJson, GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); assertNotNull(response); assertEquals(2, response.getId()); @@ -569,19 +654,22 @@ public void testGenerateGetTaskPushNotificationConfigResponse_Success() throws E @Test public void testParseErrorResponse_InvalidParams() throws Exception { - String errorResponse = """ - { - "jsonrpc": "2.0", - "id": "5", - "error": { - "code": -32602, - "message": "Invalid params" - } - } - """; + String errorResponse = + """ + { + "jsonrpc": "2.0", + "id": "5", + "error": { + "code": -32602, + "message": "Invalid params" + } + } + """; CreateTaskPushNotificationConfigResponse response = - (CreateTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(errorResponse, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + (CreateTaskPushNotificationConfigResponse) + JSONRPCUtils.parseResponseBody( + errorResponse, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); assertNotNull(response); assertEquals("5", response.getId()); @@ -593,19 +681,22 @@ public void testParseErrorResponse_InvalidParams() throws Exception { @Test public void testParseErrorResponse_ParseError() throws Exception { - String errorResponse = """ - { - "jsonrpc": "2.0", - "id": 6, - "error": { - "code": -32700, - "message": "Parse error" - } - } - """; + String errorResponse = + """ + { + "jsonrpc": "2.0", + "id": 6, + "error": { + "code": -32700, + "message": "Parse error" + } + } + """; CreateTaskPushNotificationConfigResponse response = - (CreateTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(errorResponse, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + (CreateTaskPushNotificationConfigResponse) + JSONRPCUtils.parseResponseBody( + errorResponse, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); assertNotNull(response); assertEquals(6, response.getId()); @@ -635,27 +726,30 @@ public void testToJsonRPCErrorResponse_KnownErrorCode_ProducesDataArray() { @Test public void testProcessError_ArrayFormData_ExtractsFirstElement() throws Exception { - String errorResponse = """ - { - "jsonrpc": "2.0", - "id": "8", - "error": { - "code": -32001, - "message": "Task not found", - "data": [ - { - "@type": "type.googleapis.com/google.rpc.ErrorInfo", - "reason": "TASK_NOT_FOUND", - "domain": "a2a-protocol.org", - "metadata": {} + String errorResponse = + """ + { + "jsonrpc": "2.0", + "id": "8", + "error": { + "code": -32001, + "message": "Task not found", + "data": [ + { + "@type": "type.googleapis.com/google.rpc.ErrorInfo", + "reason": "TASK_NOT_FOUND", + "domain": "a2a-protocol.org", + "metadata": {} + } + ] } - ] - } - } - """; + } + """; CreateTaskPushNotificationConfigResponse response = - (CreateTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(errorResponse, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + (CreateTaskPushNotificationConfigResponse) + JSONRPCUtils.parseResponseBody( + errorResponse, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); assertNotNull(response); assertInstanceOf(TaskNotFoundError.class, response.getError()); @@ -666,20 +760,23 @@ public void testProcessError_ArrayFormData_ExtractsFirstElement() throws Excepti @Test public void testProcessError_ArrayFormData_NonObjectElement_DoesNotThrow() throws Exception { // Verifies that a non-object first array element does not cause a ClassCastException - String errorResponse = """ - { - "jsonrpc": "2.0", - "id": "9", - "error": { - "code": -32001, - "message": "Task not found", - "data": ["unexpected-string-element"] - } - } - """; + String errorResponse = + """ + { + "jsonrpc": "2.0", + "id": "9", + "error": { + "code": -32001, + "message": "Task not found", + "data": ["unexpected-string-element"] + } + } + """; CreateTaskPushNotificationConfigResponse response = - (CreateTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody(errorResponse, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + (CreateTaskPushNotificationConfigResponse) + JSONRPCUtils.parseResponseBody( + errorResponse, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); assertNotNull(response); assertInstanceOf(TaskNotFoundError.class, response.getError()); @@ -693,9 +790,9 @@ public void testToJsonRPCErrorResponse_RoundTrip() throws Exception { String json = JSONRPCUtils.toJsonRPCErrorResponse("req-rt", original); CreateTaskPushNotificationConfigResponse response = - (CreateTaskPushNotificationConfigResponse) JSONRPCUtils.parseResponseBody( - json, - SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + (CreateTaskPushNotificationConfigResponse) + JSONRPCUtils.parseResponseBody( + json, SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); assertNotNull(response); assertInstanceOf(TaskNotFoundError.class, response.getError()); @@ -707,52 +804,61 @@ public void testToJsonRPCErrorResponse_RoundTrip() throws Exception { public void testToJsonRPCRequest_doesNotHtmlEscapeAngleBrackets() { // Reproduce issue #892: HTML special characters in message text were escaped // to Unicode sequences (< for <, > for >) by JsonFormat.printer(). - MessageSendParams params = new MessageSendParams( - Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart(""))) - .contextId("context-1234") - .messageId("message-1234") - .build(), - null, null - ); + MessageSendParams params = + new MessageSendParams( + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart(""))) + .contextId("context-1234") + .messageId("message-1234") + .build(), + null, + null); org.a2aproject.sdk.grpc.SendMessageRequest protoRequest = - ProtoUtils.ToProto.sendMessageRequest(params); + ProtoUtils.ToProto.sendMessageRequest(params); String json = JSONRPCUtils.toJsonRPCRequest("test-id", SEND_MESSAGE_METHOD, protoRequest); - assertTrue(json.contains(""), - "JSON should preserve literal '' but got: " + json); - assertFalse(json.contains("\\u003c"), - "JSON must not contain HTML-escaped '<' (\\u003c) but got: " + json); - assertFalse(json.contains("\\u003e"), - "JSON must not contain HTML-escaped '>' (\\u003e) but got: " + json); + assertTrue( + json.contains(""), + "JSON should preserve literal '' but got: " + json); + assertFalse( + json.contains("\\u003c"), + "JSON must not contain HTML-escaped '<' (\\u003c) but got: " + json); + assertFalse( + json.contains("\\u003e"), + "JSON must not contain HTML-escaped '>' (\\u003e) but got: " + json); } @Test public void testToJsonRPCResultResponse_doesNotHtmlEscapeAngleBrackets() { // Reproduce issue #892: server responses must also send literal angle brackets. - Message message = Message.builder() - .role(Message.Role.ROLE_AGENT) - .parts(Collections.singletonList(new TextPart(""))) - .contextId("context-1234") - .messageId("message-5678") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_AGENT) + .parts(Collections.singletonList(new TextPart(""))) + .contextId("context-1234") + .messageId("message-5678") + .build(); SendMessageResponse protoResponse = ProtoUtils.ToProto.taskOrMessage(message); String json = JSONRPCUtils.toJsonRPCResultResponse("test-id", protoResponse); - assertTrue(json.contains(""), - "JSON should preserve literal '' but got: " + json); - assertFalse(json.contains("\\u003c"), - "JSON must not contain HTML-escaped '<' (\\u003c) but got: " + json); - assertFalse(json.contains("\\u003e"), - "JSON must not contain HTML-escaped '>' (\\u003e) but got: " + json); + assertTrue( + json.contains(""), + "JSON should preserve literal '' but got: " + json); + assertFalse( + json.contains("\\u003c"), + "JSON must not contain HTML-escaped '<' (\\u003c) but got: " + json); + assertFalse( + json.contains("\\u003e"), + "JSON must not contain HTML-escaped '>' (\\u003e) but got: " + json); } @Test void parseGetExtendedAgentCardWithTenantParams() throws Exception { - String json = """ + String json = + """ {"jsonrpc": "2.0", "id": "1", "method": "GetExtendedAgentCard", "params": {"tenant": "acme"}} """; A2ARequest request = JSONRPCUtils.parseRequestBody(json, null); @@ -764,7 +870,8 @@ void parseGetExtendedAgentCardWithTenantParams() throws Exception { @Test void parseGetExtendedAgentCardWithNoParams() throws Exception { - String json = """ + String json = + """ {"jsonrpc": "2.0", "id": "2", "method": "GetExtendedAgentCard"} """; A2ARequest request = JSONRPCUtils.parseRequestBody(json, null); @@ -773,7 +880,8 @@ void parseGetExtendedAgentCardWithNoParams() throws Exception { @Test void parseGetExtendedAgentCardWithNullParams() throws Exception { - String json = """ + String json = + """ {"jsonrpc": "2.0", "id": "3", "method": "GetExtendedAgentCard", "params": null} """; A2ARequest request = JSONRPCUtils.parseRequestBody(json, null); @@ -782,7 +890,8 @@ void parseGetExtendedAgentCardWithNullParams() throws Exception { @Test void parseGetExtendedAgentCardWithUrlTenant() throws Exception { - String json = """ + String json = + """ {"jsonrpc": "2.0", "id": "4", "method": "GetExtendedAgentCard"} """; A2ARequest request = JSONRPCUtils.parseRequestBody(json, "url-tenant"); @@ -794,7 +903,8 @@ void parseGetExtendedAgentCardWithUrlTenant() throws Exception { @Test void parseGetExtendedAgentCardBodyTenantTakesPrecedence() throws Exception { - String json = """ + String json = + """ {"jsonrpc": "2.0", "id": "5", "method": "GetExtendedAgentCard", "params": {"tenant": "body-tenant"}} """; A2ARequest request = JSONRPCUtils.parseRequestBody(json, "url-tenant"); @@ -806,16 +916,23 @@ void parseGetExtendedAgentCardBodyTenantTakesPrecedence() throws Exception { @Test public void testRequestAndResponseEmptyMessageIdsKeepDistinctErrors() { - String body = """ + String body = + """ {"jsonrpc":"2.0","id":"empty-message","method":"SendMessage","params":{ "message":{"messageId":"","role":"ROLE_USER","parts":[{"text":"hello"}]}}} """; - InvalidParamsJsonMappingException requestError = assertThrows(InvalidParamsJsonMappingException.class, - () -> JSONRPCUtils.parseRequestBody(body, null)); + InvalidParamsJsonMappingException requestError = + assertThrows( + InvalidParamsJsonMappingException.class, + () -> JSONRPCUtils.parseRequestBody(body, null)); assertEquals("Required field cannot be null or empty", requestError.getMessage()); assertEquals("empty-message", requestError.getId()); - InvalidParamsError responseError = assertThrows(InvalidParamsError.class, - () -> ProtoUtils.FromProto.message(org.a2aproject.sdk.grpc.Message.getDefaultInstance())); + InvalidParamsError responseError = + assertThrows( + InvalidParamsError.class, + () -> + ProtoUtils.FromProto.message( + org.a2aproject.sdk.grpc.Message.getDefaultInstance())); assertEquals("Invalid parameters", responseError.getMessage()); } @@ -844,12 +961,14 @@ public void testGetAndValidateId_NumericStringId_ReturnsString() throws Exceptio } @Test - public void testGetAndValidateId_NumericStringIdOutsideIntRange_ReturnsString() throws Exception { + public void testGetAndValidateId_NumericStringIdOutsideIntRange_ReturnsString() + throws Exception { // Large numeric strings previously caused an uncaught NumberFormatException // due to integer overflow after the pattern match. Now returned safely as String. JsonObject json = JsonParser.parseString("{\"id\": \"2147483648\"}").getAsJsonObject(); Object id = JSONRPCUtils.getAndValidateId(json); - assertInstanceOf(String.class, id, "Out-of-range numeric string ID must be returned as String"); + assertInstanceOf( + String.class, id, "Out-of-range numeric string ID must be returned as String"); assertEquals("2147483648", id); } @@ -861,5 +980,4 @@ public void testGetAndValidateId_NumericId_ReturnsInteger() throws Exception { assertInstanceOf(Integer.class, id, "JSON number ID must be returned as Integer"); assertEquals(42, id); } - } diff --git a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/PartTypeAdapterTest.java b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/PartTypeAdapterTest.java index 26a1463da..701a306bb 100644 --- a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/PartTypeAdapterTest.java +++ b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/PartTypeAdapterTest.java @@ -1,8 +1,5 @@ package org.a2aproject.sdk.grpc.utils; -import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; -import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; - import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -12,28 +9,27 @@ import com.google.protobuf.Value; import com.google.protobuf.util.JsonFormat; import com.google.protobuf.util.Structs; -import org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; import java.util.List; import java.util.Map; - +import org.a2aproject.sdk.grpc.mapper.A2ACommonFieldMapper; +import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; +import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.spec.DataPart; import org.a2aproject.sdk.spec.FilePart; import org.a2aproject.sdk.spec.FileWithBytes; import org.a2aproject.sdk.spec.FileWithUri; import org.a2aproject.sdk.spec.Part; import org.a2aproject.sdk.spec.TextPart; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Base64; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; - public class PartTypeAdapterTest { - @TempDir - Path tempDir; + @TempDir Path tempDir; // ------------------------------------------------------------------------- // TextPart @@ -59,11 +55,16 @@ public void shouldSerializeTextPartWithMetadata() throws JsonProcessingException } @Test - public void shouldDeserializeTextPart() throws JsonProcessingException, InvalidProtocolBufferException { + public void shouldDeserializeTextPart() + throws JsonProcessingException, InvalidProtocolBufferException { org.a2aproject.sdk.grpc.Part.Builder builder = org.a2aproject.sdk.grpc.Part.newBuilder(); builder.setText("Hello, world!"); - String json = JsonFormat.printer().alwaysPrintFieldsWithNoPresence().omittingInsignificantWhitespace().print(builder); - Partpart = JsonUtil.fromJson(json, Part.class); + String json = + JsonFormat.printer() + .alwaysPrintFieldsWithNoPresence() + .omittingInsignificantWhitespace() + .print(builder); + Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(TextPart.class, part); TextPart textPart = (TextPart) part; assertEquals("Hello, world!", textPart.text()); @@ -72,11 +73,16 @@ public void shouldDeserializeTextPart() throws JsonProcessingException, InvalidP } @Test - public void shouldDeserializeTextPartWithMetadata() throws JsonProcessingException, InvalidProtocolBufferException { + public void shouldDeserializeTextPartWithMetadata() + throws JsonProcessingException, InvalidProtocolBufferException { org.a2aproject.sdk.grpc.Part.Builder builder = org.a2aproject.sdk.grpc.Part.newBuilder(); builder.setText("Hi"); builder.setMetadata(A2ACommonFieldMapper.INSTANCE.metadataToProto(Map.of("key", "value"))); - String json = JsonFormat.printer().alwaysPrintFieldsWithNoPresence().omittingInsignificantWhitespace().print(builder); + String json = + JsonFormat.printer() + .alwaysPrintFieldsWithNoPresence() + .omittingInsignificantWhitespace() + .print(builder); Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(TextPart.class, part); TextPart textPart = (TextPart) part; @@ -110,11 +116,18 @@ public void shouldSerializeFilePartWithBytes() throws JsonProcessingException { } @Test - public void shouldDeserializeFilePartWithBytes() throws JsonProcessingException, InvalidProtocolBufferException { + public void shouldDeserializeFilePartWithBytes() + throws JsonProcessingException, InvalidProtocolBufferException { org.a2aproject.sdk.grpc.Part.Builder builder = org.a2aproject.sdk.grpc.Part.newBuilder(); - builder.setFilename("diagram.png").setMediaType("image/png").setRaw(ByteString.copyFrom(Base64.getDecoder().decode("abc12w=="))); + builder.setFilename("diagram.png") + .setMediaType("image/png") + .setRaw(ByteString.copyFrom(Base64.getDecoder().decode("abc12w=="))); builder.setMetadata(A2ACommonFieldMapper.INSTANCE.metadataToProto(Map.of("key", "value"))); - String json = JsonFormat.printer().alwaysPrintFieldsWithNoPresence().omittingInsignificantWhitespace().print(builder); + String json = + JsonFormat.printer() + .alwaysPrintFieldsWithNoPresence() + .omittingInsignificantWhitespace() + .print(builder); Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(FilePart.class, part); FilePart filePart = (FilePart) part; @@ -124,12 +137,12 @@ public void shouldDeserializeFilePartWithBytes() throws JsonProcessingException, assertEquals("diagram.png", fileWithBytes.name()); assertEquals("abc12w==", fileWithBytes.bytes()); assertEquals("value", filePart.metadata().get("key")); - } @Test public void shouldRoundTripFilePartWithBytes() throws JsonProcessingException { - FilePart original = new FilePart(new FileWithBytes("application/pdf", "report.pdf", "AAEC")); + FilePart original = + new FilePart(new FileWithBytes("application/pdf", "report.pdf", "AAEC")); String json = JsonUtil.toJson(original); Part deserialized = JsonUtil.fromJson(json, Part.class); assertInstanceOf(FilePart.class, deserialized); @@ -142,29 +155,30 @@ public void shouldRoundTripFilePartWithBytes() throws JsonProcessingException { } @Test - public void shouldRoundTripFilePartWithBytesFromRealFile() throws JsonProcessingException, IOException { + public void shouldRoundTripFilePartWithBytesFromRealFile() + throws JsonProcessingException, IOException { // Create a temporary file with some content Path testFile = tempDir.resolve("test-file.txt"); String fileContent = "This is test content for lazy loading verification"; Files.writeString(testFile, fileContent); - + // Create FileWithBytes from the file path (lazy loading) FileWithBytes fileWithBytes = new FileWithBytes("text/plain", testFile); FilePart original = new FilePart(fileWithBytes); - + // Serialize to JSON (this triggers lazy loading) String json = JsonUtil.toJson(original); - + // Deserialize and verify Part deserialized = JsonUtil.fromJson(json, Part.class); assertInstanceOf(FilePart.class, deserialized); FilePart result = (FilePart) deserialized; assertInstanceOf(FileWithBytes.class, result.file()); FileWithBytes bytes = (FileWithBytes) result.file(); - + assertEquals("text/plain", bytes.mimeType()); assertEquals("test-file.txt", bytes.name()); - + // Verify the content by decoding the base64 byte[] decodedBytes = Base64.getDecoder().decode(bytes.bytes()); String decodedContent = new String(decodedBytes); @@ -177,7 +191,9 @@ public void shouldRoundTripFilePartWithBytesFromRealFile() throws JsonProcessing @Test public void shouldSerializeFilePartWithUri() throws JsonProcessingException { - FilePart part = new FilePart(new FileWithUri("image/png", "photo.png", "https://example.com/photo.png")); + FilePart part = + new FilePart( + new FileWithUri("image/png", "photo.png", "https://example.com/photo.png")); String json = JsonUtil.toJson(part); // Verify the serialized JSON can be deserialized correctly (round-trip) Part deserialized = JsonUtil.fromJson(json, Part.class); @@ -189,11 +205,18 @@ public void shouldSerializeFilePartWithUri() throws JsonProcessingException { } @Test - public void shouldDeserializeFilePartWithUri() throws JsonProcessingException, InvalidProtocolBufferException { + public void shouldDeserializeFilePartWithUri() + throws JsonProcessingException, InvalidProtocolBufferException { org.a2aproject.sdk.grpc.Part.Builder builder = org.a2aproject.sdk.grpc.Part.newBuilder(); - builder.setFilename("photo.png").setMediaType("image/png").setUrl("https://example.com/photo.png"); + builder.setFilename("photo.png") + .setMediaType("image/png") + .setUrl("https://example.com/photo.png"); builder.setMetadata(A2ACommonFieldMapper.INSTANCE.metadataToProto(Map.of("key", "value"))); - String json = JsonFormat.printer().alwaysPrintFieldsWithNoPresence().omittingInsignificantWhitespace().print(builder); + String json = + JsonFormat.printer() + .alwaysPrintFieldsWithNoPresence() + .omittingInsignificantWhitespace() + .print(builder); Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(FilePart.class, part); FilePart filePart = (FilePart) part; @@ -207,7 +230,10 @@ public void shouldDeserializeFilePartWithUri() throws JsonProcessingException, I @Test public void shouldRoundTripFilePartWithUri() throws JsonProcessingException { - FilePart original = new FilePart(new FileWithUri("text/plain", "notes.txt", "https://example.com/notes.txt")); + FilePart original = + new FilePart( + new FileWithUri( + "text/plain", "notes.txt", "https://example.com/notes.txt")); String json = JsonUtil.toJson(original); Part deserialized = JsonUtil.fromJson(json, Part.class); assertInstanceOf(FilePart.class, deserialized); @@ -221,9 +247,10 @@ public void shouldRoundTripFilePartWithUri() throws JsonProcessingException { @Test public void shouldRoundTripFilePartWithMetadata() throws JsonProcessingException { - FilePart original = new FilePart( - new FileWithUri("image/jpeg", "pic.jpg", "https://example.com/pic.jpg"), - Map.of("source", "camera")); + FilePart original = + new FilePart( + new FileWithUri("image/jpeg", "pic.jpg", "https://example.com/pic.jpg"), + Map.of("source", "camera")); String json = JsonUtil.toJson(original); Part deserialized = JsonUtil.fromJson(json, Part.class); assertInstanceOf(FilePart.class, deserialized); @@ -248,11 +275,23 @@ public void shouldSerializeDataPartWithObject() throws JsonProcessingException { } @Test - public void shouldDeserializeDataPartWithObject() throws JsonProcessingException, InvalidProtocolBufferException { + public void shouldDeserializeDataPartWithObject() + throws JsonProcessingException, InvalidProtocolBufferException { org.a2aproject.sdk.grpc.Part.Builder builder = org.a2aproject.sdk.grpc.Part.newBuilder(); - builder.setData(Value.newBuilder().setStructValue(Structs.of("count", Value.newBuilder().setNumberValue(42).build(), "label", Value.newBuilder().setStringValue("items").build()))); + builder.setData( + Value.newBuilder() + .setStructValue( + Structs.of( + "count", + Value.newBuilder().setNumberValue(42).build(), + "label", + Value.newBuilder().setStringValue("items").build()))); builder.setMetadata(A2ACommonFieldMapper.INSTANCE.metadataToProto(Map.of("key", "value"))); - String json = JsonFormat.printer().alwaysPrintFieldsWithNoPresence().omittingInsignificantWhitespace().print(builder); + String json = + JsonFormat.printer() + .alwaysPrintFieldsWithNoPresence() + .omittingInsignificantWhitespace() + .print(builder); Part part = JsonUtil.fromJson(json, Part.class); assertInstanceOf(DataPart.class, part); @SuppressWarnings("unchecked") diff --git a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/ProtoJsonUtilsTest.java b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/ProtoJsonUtilsTest.java index 961af246d..981946955 100644 --- a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/ProtoJsonUtilsTest.java +++ b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/ProtoJsonUtilsTest.java @@ -4,10 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.util.Collections; - import com.google.protobuf.util.JsonFormat; - +import java.util.Collections; import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.MessageSendParams; import org.a2aproject.sdk.spec.TextPart; @@ -17,67 +15,78 @@ public class ProtoJsonUtilsTest { @Test public void toJson_doesNotHtmlEscapeAngleBrackets() throws Exception { - MessageSendParams params = new MessageSendParams( - Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart(""))) - .contextId("context-1234") - .messageId("message-1234") - .build(), - null, null - ); + MessageSendParams params = + new MessageSendParams( + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart(""))) + .contextId("context-1234") + .messageId("message-1234") + .build(), + null, + null); var proto = ProtoUtils.ToProto.sendMessageRequest(params); String json = ProtoJsonUtils.toJson(JsonFormat.printer(), proto); - assertTrue(json.contains(""), - "JSON should preserve literal '' but got: " + json); - assertFalse(json.contains("\\u003c"), - "JSON must not contain HTML-escaped '<' (\\u003c) but got: " + json); - assertFalse(json.contains("\\u003e"), - "JSON must not contain HTML-escaped '>' (\\u003e) but got: " + json); + assertTrue( + json.contains(""), + "JSON should preserve literal '' but got: " + json); + assertFalse( + json.contains("\\u003c"), + "JSON must not contain HTML-escaped '<' (\\u003c) but got: " + json); + assertFalse( + json.contains("\\u003e"), + "JSON must not contain HTML-escaped '>' (\\u003e) but got: " + json); } @Test public void toJson_doesNotHtmlEscapeAmpersand() throws Exception { - MessageSendParams params = new MessageSendParams( - Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("foo&bar"))) - .contextId("context-1234") - .messageId("message-1234") - .build(), - null, null - ); + MessageSendParams params = + new MessageSendParams( + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("foo&bar"))) + .contextId("context-1234") + .messageId("message-1234") + .build(), + null, + null); var proto = ProtoUtils.ToProto.sendMessageRequest(params); String json = ProtoJsonUtils.toJson(JsonFormat.printer(), proto); - assertTrue(json.contains("foo&bar"), - "JSON should preserve literal '&' but got: " + json); - assertFalse(json.contains("\\u0026"), - "JSON must not contain HTML-escaped '&' (\\u0026) but got: " + json); + assertTrue(json.contains("foo&bar"), "JSON should preserve literal '&' but got: " + json); + assertFalse( + json.contains("\\u0026"), + "JSON must not contain HTML-escaped '&' (\\u0026) but got: " + json); } @Test public void toJson_respectsAlwaysPrintFieldsWithNoPresence() throws Exception { - MessageSendParams params = new MessageSendParams( - Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("hello"))) - .contextId("context-1234") - .messageId("message-1234") - .build(), - null, null - ); + MessageSendParams params = + new MessageSendParams( + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("hello"))) + .contextId("context-1234") + .messageId("message-1234") + .build(), + null, + null); var proto = ProtoUtils.ToProto.sendMessageRequest(params); - String withDefaults = ProtoJsonUtils.toJson( - JsonFormat.printer().alwaysPrintFieldsWithNoPresence(), proto); + String withDefaults = + ProtoJsonUtils.toJson( + JsonFormat.printer().alwaysPrintFieldsWithNoPresence(), proto); String withoutDefaults = ProtoJsonUtils.toJson(JsonFormat.printer(), proto); - assertTrue(withDefaults.length() > withoutDefaults.length(), - "alwaysPrintFieldsWithNoPresence should produce more fields, got:\n with: " + withDefaults + "\n without: " + withoutDefaults); + assertTrue( + withDefaults.length() > withoutDefaults.length(), + "alwaysPrintFieldsWithNoPresence should produce more fields, got:\n with: " + + withDefaults + + "\n without: " + + withoutDefaults); } @Test diff --git a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/ToProtoTest.java b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/ToProtoTest.java index 8c3130b52..c19e366c4 100644 --- a/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/ToProtoTest.java +++ b/spec-grpc/src/test/java/org/a2aproject/sdk/grpc/utils/ToProtoTest.java @@ -8,22 +8,19 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import com.google.protobuf.Struct; +import com.google.protobuf.Value; import java.time.OffsetDateTime; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; - -import com.google.protobuf.Struct; -import com.google.protobuf.Value; - import org.a2aproject.sdk.grpc.SendMessageConfiguration; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.AgentSkill; -import org.a2aproject.sdk.spec.SecurityRequirement; import org.a2aproject.sdk.spec.Artifact; import org.a2aproject.sdk.spec.AuthenticationInfo; import org.a2aproject.sdk.spec.DataPart; @@ -32,6 +29,7 @@ import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams; import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.MessageSendConfiguration; +import org.a2aproject.sdk.spec.SecurityRequirement; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskArtifactUpdateEvent; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; @@ -43,35 +41,43 @@ public class ToProtoTest { - private static final Message SIMPLE_MESSAGE = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(Collections.singletonList(new TextPart("tell me a joke"))) - .contextId("context-1234") - .messageId("message-1234") - .build(); + private static final Message SIMPLE_MESSAGE = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("tell me a joke"))) + .contextId("context-1234") + .messageId("message-1234") + .build(); @Test public void convertAgentCard() { - AgentCard agentCard = AgentCard.builder() - .name("Hello World Agent") - .description("Just a hello world agent") - .supportedInterfaces(Collections.singletonList(new AgentInterface("jsonrpc", "http://localhost:9999", "", "123"))) - .version("1.0.0") - .documentationUrl("http://example.com/docs") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(AgentSkill.builder() - .id("hello_world") - .name("Returns hello world") - .description("just returns hello world") - .tags(Collections.singletonList("hello world")) - .examples(List.of("hi", "hello world")) - .build())) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("Hello World Agent") + .description("Just a hello world agent") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface( + "jsonrpc", "http://localhost:9999", "", "123"))) + .version("1.0.0") + .documentationUrl("http://example.com/docs") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills( + Collections.singletonList( + AgentSkill.builder() + .id("hello_world") + .name("Returns hello world") + .description("just returns hello world") + .tags(Collections.singletonList("hello world")) + .examples(List.of("hi", "hello world")) + .build())) + .build(); org.a2aproject.sdk.grpc.AgentCard result = ProtoUtils.ToProto.agentCard(agentCard); assertEquals("Hello World Agent", result.getName()); assertEquals("Just a hello world agent", result.getDescription()); @@ -86,39 +92,54 @@ public void convertAgentCard() { assertEquals(1, result.getDefaultOutputModesCount()); assertEquals("text", result.getDefaultOutputModes(0)); // protocolVersion is now in AgentInterface - agentCard = AgentCard.builder() - .name("Hello World Agent") - .description("Just a hello world agent") - .supportedInterfaces(Collections.singletonList(new AgentInterface("jsonrpc", "http://localhost:9999"))) - .version("1.0.0") - .documentationUrl("http://example.com/docs") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(Collections.singletonList(AgentSkill.builder() - .id("hello_world") - .name("Returns hello world") - .description("just returns hello world") - .tags(Collections.singletonList("hello world")) - .examples(List.of("hi", "hello world")) - .build())) - // .iconUrl("http://example.com/icon.svg") - .securitySchemes(Map.of("basic", HTTPAuthSecurityScheme.builder().scheme("basic").description("Basic Auth").build())) - .securityRequirements(List.of(SecurityRequirement.builder() - .scheme("oauth", - List.of("read")) - .build())) - .build(); + agentCard = + AgentCard.builder() + .name("Hello World Agent") + .description("Just a hello world agent") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("jsonrpc", "http://localhost:9999"))) + .version("1.0.0") + .documentationUrl("http://example.com/docs") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills( + Collections.singletonList( + AgentSkill.builder() + .id("hello_world") + .name("Returns hello world") + .description("just returns hello world") + .tags(Collections.singletonList("hello world")) + .examples(List.of("hi", "hello world")) + .build())) + // .iconUrl("http://example.com/icon.svg") + .securitySchemes( + Map.of( + "basic", + HTTPAuthSecurityScheme.builder() + .scheme("basic") + .description("Basic Auth") + .build())) + .securityRequirements( + List.of( + SecurityRequirement.builder() + .scheme("oauth", List.of("read")) + .build())) + .build(); result = ProtoUtils.ToProto.agentCard(agentCard); assertEquals("Hello World Agent", result.getName()); assertEquals("Just a hello world agent", result.getDescription()); assertEquals(1, result.getSupportedInterfacesList().size()); assertEquals("http://localhost:9999", result.getSupportedInterfacesList().get(0).getUrl()); assertEquals("jsonrpc", result.getSupportedInterfacesList().get(0).getProtocolBinding()); - assertEquals(CURRENT_PROTOCOL_VERSION, result.getSupportedInterfacesList().get(0).getProtocolVersion()); + assertEquals( + CURRENT_PROTOCOL_VERSION, + result.getSupportedInterfacesList().get(0).getProtocolVersion()); assertEquals("1.0.0", result.getVersion()); assertEquals("http://example.com/docs", result.getDocumentationUrl()); assertEquals(1, result.getDefaultInputModesCount()); @@ -129,43 +150,75 @@ public void convertAgentCard() { assertEquals(1, result.getSecurityRequirementsCount()); assertEquals(1, result.getSecurityRequirements(0).getSchemesMap().size()); assertEquals(true, result.getSecurityRequirements(0).getSchemesMap().containsKey("oauth")); - assertEquals(1, result.getSecurityRequirements(0).getSchemesMap().get("oauth").getListCount()); - assertEquals("read", result.getSecurityRequirements(0).getSchemesMap().get("oauth").getList(0)); + assertEquals( + 1, result.getSecurityRequirements(0).getSchemesMap().get("oauth").getListCount()); + assertEquals( + "read", result.getSecurityRequirements(0).getSchemesMap().get("oauth").getList(0)); assertEquals(1, result.getSecuritySchemesMap().size()); assertEquals(true, result.getSecuritySchemesMap().containsKey("basic")); - assertEquals(result.getSecuritySchemesMap().get("basic").getApiKeySecurityScheme().getDefaultInstanceForType(), result.getSecuritySchemesMap().get("basic").getApiKeySecurityScheme()); - assertEquals(result.getSecuritySchemesMap().get("basic").getOauth2SecurityScheme().getDefaultInstanceForType(), result.getSecuritySchemesMap().get("basic").getOauth2SecurityScheme()); - assertEquals("basic", result.getSecuritySchemesMap().get("basic").getHttpAuthSecurityScheme().getScheme()); - assertEquals("Basic Auth", result.getSecuritySchemesMap().get("basic").getHttpAuthSecurityScheme().getDescription()); + assertEquals( + result.getSecuritySchemesMap() + .get("basic") + .getApiKeySecurityScheme() + .getDefaultInstanceForType(), + result.getSecuritySchemesMap().get("basic").getApiKeySecurityScheme()); + assertEquals( + result.getSecuritySchemesMap() + .get("basic") + .getOauth2SecurityScheme() + .getDefaultInstanceForType(), + result.getSecuritySchemesMap().get("basic").getOauth2SecurityScheme()); + assertEquals( + "basic", + result.getSecuritySchemesMap() + .get("basic") + .getHttpAuthSecurityScheme() + .getScheme()); + assertEquals( + "Basic Auth", + result.getSecuritySchemesMap() + .get("basic") + .getHttpAuthSecurityScheme() + .getDescription()); } @Test public void convertTask() { - Task task = Task.builder().id("cancel-task-123") - .contextId("session-xyz") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + Task task = + Task.builder() + .id("cancel-task-123") + .contextId("session-xyz") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); org.a2aproject.sdk.grpc.Task result = ProtoUtils.ToProto.task(task); assertEquals("session-xyz", result.getContextId()); assertEquals("cancel-task-123", result.getId()); - assertEquals(org.a2aproject.sdk.grpc.TaskState.TASK_STATE_SUBMITTED, result.getStatus().getState()); + assertEquals( + org.a2aproject.sdk.grpc.TaskState.TASK_STATE_SUBMITTED, + result.getStatus().getState()); assertEquals(0, result.getArtifactsCount()); assertEquals(0, result.getHistoryCount()); - task = Task.builder().id("cancel-task-123") - .contextId("session-xyz") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .artifacts(List.of(Artifact.builder() - .artifactId("11") - .name("artefact") - .parts(new TextPart("text")) - .build())) - .history(List.of(SIMPLE_MESSAGE)) - .metadata(Collections.emptyMap()) - .build(); + task = + Task.builder() + .id("cancel-task-123") + .contextId("session-xyz") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .artifacts( + List.of( + Artifact.builder() + .artifactId("11") + .name("artefact") + .parts(new TextPart("text")) + .build())) + .history(List.of(SIMPLE_MESSAGE)) + .metadata(Collections.emptyMap()) + .build(); result = ProtoUtils.ToProto.task(task); assertEquals("session-xyz", result.getContextId()); assertEquals("cancel-task-123", result.getId()); - assertEquals(org.a2aproject.sdk.grpc.TaskState.TASK_STATE_SUBMITTED, result.getStatus().getState()); + assertEquals( + org.a2aproject.sdk.grpc.TaskState.TASK_STATE_SUBMITTED, + result.getStatus().getState()); assertEquals(1, result.getArtifactsCount()); assertEquals("11", result.getArtifacts(0).getArtifactId()); assertEquals("artefact", result.getArtifacts(0).getName()); @@ -197,11 +250,12 @@ public void convertMessage() { assertEquals(false, result.getParts(0).hasRaw()); assertEquals(false, result.getParts(0).hasUrl()); assertEquals(false, result.getParts(0).hasData()); - Message message = Message.builder() - .role(Message.Role.ROLE_AGENT) - .parts(Collections.singletonList(new TextPart("tell me a joke"))) - .messageId("message-1234") - .build(); + Message message = + Message.builder() + .role(Message.Role.ROLE_AGENT) + .parts(Collections.singletonList(new TextPart("tell me a joke"))) + .messageId("message-1234") + .build(); result = ProtoUtils.ToProto.message(message); assertEquals("", result.getContextId()); assertEquals("message-1234", result.getMessageId()); @@ -219,20 +273,26 @@ public void convertMessageWithExplicitNulls() { org.a2aproject.sdk.grpc.Message result = ProtoUtils.ToProto.message(message); - assertEquals(Value.KindCase.NULL_VALUE, - result.getParts(0).getData().getStructValue().getFieldsOrThrow("attributeId").getKindCase()); - assertEquals(Value.KindCase.NULL_VALUE, + assertEquals( + Value.KindCase.NULL_VALUE, + result.getParts(0) + .getData() + .getStructValue() + .getFieldsOrThrow("attributeId") + .getKindCase()); + assertEquals( + Value.KindCase.NULL_VALUE, result.getParts(0).getMetadata().getFieldsOrThrow("optional").getKindCase()); - assertEquals(Value.KindCase.NULL_VALUE, + assertEquals( + Value.KindCase.NULL_VALUE, result.getMetadata().getFieldsOrThrow("optional").getKindCase()); assertEquals(message, ProtoUtils.FromProto.message(result)); } @Test public void convertMessageWithAbsentOrEmptyMetadata() { - org.a2aproject.sdk.grpc.Message.Builder builder = ProtoUtils.ToProto.message(SIMPLE_MESSAGE) - .toBuilder() - .clearMetadata(); + org.a2aproject.sdk.grpc.Message.Builder builder = + ProtoUtils.ToProto.message(SIMPLE_MESSAGE).toBuilder().clearMetadata(); assertNull(ProtoUtils.FromProto.message(builder).metadata()); builder.setMetadata(Struct.getDefaultInstance()); @@ -242,25 +302,33 @@ public void convertMessageWithAbsentOrEmptyMetadata() { @Test public void convertTaskWithExplicitNulls() { Message message = messageWithExplicitNulls(); - Task task = Task.builder() - .id("task-null") - .contextId("context-null") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED, message, - OffsetDateTime.parse("2026-09-21T00:00:00Z"))) - .history(List.of(message)) - .artifacts(List.of(Artifact.builder() - .artifactId("artifact-null") - .parts(message.parts()) + Task task = + Task.builder() + .id("task-null") + .contextId("context-null") + .status( + new TaskStatus( + TaskState.TASK_STATE_COMPLETED, + message, + OffsetDateTime.parse("2026-09-21T00:00:00Z"))) + .history(List.of(message)) + .artifacts( + List.of( + Artifact.builder() + .artifactId("artifact-null") + .parts(message.parts()) + .metadata(message.metadata()) + .build())) .metadata(message.metadata()) - .build())) - .metadata(message.metadata()) - .build(); + .build(); org.a2aproject.sdk.grpc.Task result = ProtoUtils.ToProto.task(task); - assertEquals(Value.KindCase.NULL_VALUE, + assertEquals( + Value.KindCase.NULL_VALUE, result.getMetadata().getFieldsOrThrow("optional").getKindCase()); - assertEquals(Value.KindCase.NULL_VALUE, + assertEquals( + Value.KindCase.NULL_VALUE, result.getArtifacts(0).getMetadata().getFieldsOrThrow("optional").getKindCase()); assertEquals(task, ProtoUtils.FromProto.task(result)); } @@ -268,7 +336,10 @@ public void convertTaskWithExplicitNulls() { private Message messageWithExplicitNulls() { Map data = new HashMap<>(); data.put("attributeId", null); - data.put("items", Arrays.asList(null, Collections.singletonMap("optional", null), Map.of(), List.of())); + data.put( + "items", + Arrays.asList( + null, Collections.singletonMap("optional", null), Map.of(), List.of())); Map metadata = Collections.singletonMap("optional", null); return Message.builder() .messageId("message-null") @@ -281,23 +352,26 @@ private Message messageWithExplicitNulls() { @Test public void convertTaskPushNotificationConfig() { - TaskPushNotificationConfig taskPushConfig = TaskPushNotificationConfig.builder() - .id("xyz") - .taskId("push-task-123") - .url("http://example.com") - .build(); - org.a2aproject.sdk.grpc.TaskPushNotificationConfig result = ProtoUtils.ToProto.taskPushNotificationConfig(taskPushConfig); + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() + .id("xyz") + .taskId("push-task-123") + .url("http://example.com") + .build(); + org.a2aproject.sdk.grpc.TaskPushNotificationConfig result = + ProtoUtils.ToProto.taskPushNotificationConfig(taskPushConfig); assertEquals("push-task-123", result.getTaskId()); assertEquals("xyz", result.getId()); assertEquals("http://example.com", result.getUrl()); assertEquals(false, result.hasAuthentication()); - taskPushConfig = TaskPushNotificationConfig.builder() - .id("xyz") - .taskId("push-task-123") - .url("http://example.com") - .token("AAAAAA") - .authentication(new AuthenticationInfo("jwt", "credentials")) - .build(); + taskPushConfig = + TaskPushNotificationConfig.builder() + .id("xyz") + .taskId("push-task-123") + .url("http://example.com") + .token("AAAAAA") + .authentication(new AuthenticationInfo("jwt", "credentials")) + .build(); result = ProtoUtils.ToProto.taskPushNotificationConfig(taskPushConfig); assertEquals("push-task-123", result.getTaskId()); assertEquals("xyz", result.getId()); @@ -310,14 +384,18 @@ public void convertTaskPushNotificationConfig() { @Test public void convertTaskArtifactUpdateEvent() { - TaskArtifactUpdateEvent task = TaskArtifactUpdateEvent.builder() - .taskId("task-123") - .contextId("session-123") - .artifact(Artifact.builder() - .artifactId("11") - .parts(new TextPart("text")) - .build()).build(); - org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent result = ProtoUtils.ToProto.taskArtifactUpdateEvent(task); + TaskArtifactUpdateEvent task = + TaskArtifactUpdateEvent.builder() + .taskId("task-123") + .contextId("session-123") + .artifact( + Artifact.builder() + .artifactId("11") + .parts(new TextPart("text")) + .build()) + .build(); + org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent result = + ProtoUtils.ToProto.taskArtifactUpdateEvent(task); assertEquals("task-123", result.getTaskId()); assertEquals("session-123", result.getContextId()); assertNotNull(result.getArtifact()); @@ -329,26 +407,25 @@ public void convertTaskArtifactUpdateEvent() { @Test public void convertTaskStatusUpdateEvent() { TaskStatus completedStatus = new TaskStatus(TaskState.TASK_STATE_COMPLETED); - TaskStatusUpdateEvent tsue = new TaskStatusUpdateEvent( - "1234", - completedStatus, - "xyz", - null - ); - org.a2aproject.sdk.grpc.TaskStatusUpdateEvent result = ProtoUtils.ToProto.taskStatusUpdateEvent(tsue); + TaskStatusUpdateEvent tsue = + new TaskStatusUpdateEvent("1234", completedStatus, "xyz", null); + org.a2aproject.sdk.grpc.TaskStatusUpdateEvent result = + ProtoUtils.ToProto.taskStatusUpdateEvent(tsue); assertEquals("1234", result.getTaskId()); assertEquals("xyz", result.getContextId()); // Note: isFinal field has been removed from the proto (field 4 is reserved) // It is now derived from status.state().isFinal() - assertEquals(org.a2aproject.sdk.grpc.TaskState.TASK_STATE_COMPLETED, result.getStatus().getState()); + assertEquals( + org.a2aproject.sdk.grpc.TaskState.TASK_STATE_COMPLETED, + result.getStatus().getState()); } @Test public void convertSendMessageConfiguration() { - MessageSendConfiguration configuration = MessageSendConfiguration.builder() - .acceptedOutputModes(List.of("text")) - .build(); - SendMessageConfiguration result = ProtoUtils.ToProto.messageSendConfiguration(configuration); + MessageSendConfiguration configuration = + MessageSendConfiguration.builder().acceptedOutputModes(List.of("text")).build(); + SendMessageConfiguration result = + ProtoUtils.ToProto.messageSendConfiguration(configuration); assertFalse(result.getReturnImmediately()); assertEquals(1, result.getAcceptedOutputModesCount()); assertEquals("text", result.getAcceptedOutputModesBytes(0).toStringUtf8()); @@ -357,12 +434,10 @@ public void convertSendMessageConfiguration() { @Test public void convertTaskTimestampStatus() { OffsetDateTime expectedTimestamp = OffsetDateTime.parse("2024-10-05T12:34:56Z"); - TaskStatus testStatus = new TaskStatus(TaskState.TASK_STATE_COMPLETED, null, expectedTimestamp); - Task task = Task.builder() - .id("task-123") - .contextId("context-456") - .status(testStatus) - .build(); + TaskStatus testStatus = + new TaskStatus(TaskState.TASK_STATE_COMPLETED, null, expectedTimestamp); + Task task = + Task.builder().id("task-123").contextId("context-456").status(testStatus).build(); org.a2aproject.sdk.grpc.Task grpcTask = ProtoUtils.ToProto.task(task); task = ProtoUtils.FromProto.task(grpcTask); @@ -374,10 +449,8 @@ public void convertTaskTimestampStatus() { @Test public void convertDeleteTaskPushNotificationConfigRequest() { - DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams( - "task-123", - "config-456" - ); + DeleteTaskPushNotificationConfigParams params = + new DeleteTaskPushNotificationConfigParams("task-123", "config-456"); org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest result = ProtoUtils.ToProto.deleteTaskPushNotificationConfigRequest(params); @@ -394,7 +467,8 @@ public void convertDeleteTaskPushNotificationConfigRequest() { @Test public void convertListTaskPushNotificationConfigsRequest() { - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams("task-789"); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams("task-789"); org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest result = ProtoUtils.ToProto.listTaskPushNotificationConfigsRequest(params); diff --git a/spec/pom.xml b/spec/pom.xml index def446263..d657ee06a 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -38,4 +36,4 @@ - \ No newline at end of file + diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientError.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientError.java index fd8dd980c..a45f14bc6 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientError.java @@ -25,8 +25,7 @@ public class A2AClientError extends RuntimeException { /** * Constructs a new A2AClientError with no detail message. */ - public A2AClientError() { - } + public A2AClientError() {} /** * Constructs a new A2AClientError with the specified detail message. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientHTTPError.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientHTTPError.java index 2e396cc95..81a1ef1c1 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientHTTPError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientHTTPError.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Map; - import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.util.HttpHeaderUtils; import org.jspecify.annotations.Nullable; @@ -51,8 +50,7 @@ public class A2AClientHTTPError extends A2AClientError { /** * The raw HTTP response body, may be {@code null}. */ - @Nullable - private final String responseBody; + @Nullable private final String responseBody; /** * The HTTP response headers. @@ -67,8 +65,11 @@ public class A2AClientHTTPError extends A2AClientError { * @param responseBody the raw HTTP response body, may be {@code null} * @param responseHeaders the HTTP response headers */ - public A2AClientHTTPError(int code, String message, @Nullable String responseBody, - Map> responseHeaders) { + public A2AClientHTTPError( + int code, + String message, + @Nullable String responseBody, + Map> responseHeaders) { Assert.checkNotNullParam("message", message); Assert.checkNotNullParam("responseHeaders", responseHeaders); this.code = code; diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientInvalidArgsError.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientInvalidArgsError.java index ff51b6f39..f961d6d81 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientInvalidArgsError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientInvalidArgsError.java @@ -30,8 +30,7 @@ public class A2AClientInvalidArgsError extends A2AClientError { /** * Creates a new invalid arguments error with no message. */ - public A2AClientInvalidArgsError() { - } + public A2AClientInvalidArgsError() {} /** * Creates a new invalid arguments error with the specified message. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientInvalidStateError.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientInvalidStateError.java index eebd5a2e0..174a85e3b 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientInvalidStateError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientInvalidStateError.java @@ -29,8 +29,7 @@ public class A2AClientInvalidStateError extends A2AClientError { /** * Creates a new invalid state error with no message. */ - public A2AClientInvalidStateError() { - } + public A2AClientInvalidStateError() {} /** * Creates a new invalid state error with the specified message. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientJSONError.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientJSONError.java index 4620bac89..5252cea13 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientJSONError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientJSONError.java @@ -30,8 +30,7 @@ public class A2AClientJSONError extends A2AClientError { /** * Creates a new JSON error with no message. */ - public A2AClientJSONError() { - } + public A2AClientJSONError() {} /** * Creates a new JSON error with the specified message. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AError.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AError.java index d7f4b463d..af1ff316c 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AError.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.spec; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -55,7 +54,8 @@ public class A2AError extends RuntimeException implements Event { public A2AError(Integer code, String message, @Nullable Map details) { super(Assert.checkNotNullParam("message", message)); this.code = Assert.checkNotNullParam("code", code); - this.details = details == null ? Map.of() : CollectionCopies.unmodifiableShallowMap(details); + this.details = + details == null ? Map.of() : CollectionCopies.unmodifiableShallowMap(details); } /** diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AErrorCodes.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AErrorCodes.java index 79129944c..a1d3d99f3 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AErrorCodes.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AErrorCodes.java @@ -111,5 +111,4 @@ public int httpCode() { } return null; } - } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AException.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AException.java index 0283c318e..adcd24f25 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AException.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AException.java @@ -23,8 +23,7 @@ public class A2AException extends RuntimeException { * Constructs a new {@code A2AException} instance. The message is left blank ({@code null}), and no * cause is specified. */ - public A2AException() { - } + public A2AException() {} /** * Constructs a new {@code A2AException} instance with an initial message. No cause is specified. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AMethods.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AMethods.java index 65eec783b..d4fa5bcb8 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AMethods.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AMethods.java @@ -1,4 +1,3 @@ - package org.a2aproject.sdk.spec; /** @@ -7,26 +6,34 @@ public interface A2AMethods { /** Method name for canceling a task. */ String CANCEL_TASK_METHOD = "CancelTask"; + /** Method name for deleting task push notification configuration. */ String DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD = "DeleteTaskPushNotificationConfig"; + /** Method name for getting extended agent card information. */ String GET_EXTENDED_AGENT_CARD_METHOD = "GetExtendedAgentCard"; + /** Method name for getting a task. */ String GET_TASK_METHOD = "GetTask"; + /** Method name for getting task push notification configuration. */ String GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD = "GetTaskPushNotificationConfig"; + /** Method name for listing tasks. */ String LIST_TASK_METHOD = "ListTasks"; + /** Method name for listing task push notification configurations. */ String LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD = "ListTaskPushNotificationConfigs"; + /** Method name for sending a message. */ String SEND_MESSAGE_METHOD = "SendMessage"; + /** Method name for sending a streaming message. */ String SEND_STREAMING_MESSAGE_METHOD = "SendStreamingMessage"; + /** Method name for setting task push notification configuration. */ String SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD = "CreateTaskPushNotificationConfig"; + /** Method name for subscribing to task events. */ String SUBSCRIBE_TO_TASK_METHOD = "SubscribeToTask"; - - } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AProtocolError.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AProtocolError.java index e17df3733..245dd0ddd 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AProtocolError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AProtocolError.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.spec; import java.util.Map; - import org.jspecify.annotations.Nullable; /** diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/APIKeySecurityScheme.java b/spec/src/main/java/org/a2aproject/sdk/spec/APIKeySecurityScheme.java index 88cff4596..67043dc12 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/APIKeySecurityScheme.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/APIKeySecurityScheme.java @@ -18,8 +18,8 @@ * @see OpenAPI Security Scheme * @see A2A Protocol Specification */ -public record APIKeySecurityScheme(Location location, String name, @Nullable - String description) implements SecurityScheme { +public record APIKeySecurityScheme(Location location, String name, @Nullable String description) + implements SecurityScheme { /** * The security scheme type identifier for API key authentication. @@ -95,7 +95,7 @@ public static Location fromString(String location) { return QUERY; } default -> - throw new IllegalArgumentException("Invalid API key location: " + location); + throw new IllegalArgumentException("Invalid API key location: " + location); } } } @@ -130,8 +130,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the location where the API key should be sent. @@ -173,8 +172,10 @@ public Builder description(String description) { * @throws IllegalArgumentException if location or name is null */ public APIKeySecurityScheme build() { - return new APIKeySecurityScheme(Assert.checkNotNullParam("location", location), - Assert.checkNotNullParam("name", name), description); + return new APIKeySecurityScheme( + Assert.checkNotNullParam("location", location), + Assert.checkNotNullParam("name", name), + description); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/AgentCapabilities.java b/spec/src/main/java/org/a2aproject/sdk/spec/AgentCapabilities.java index 96bed4b90..589c5897b 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/AgentCapabilities.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/AgentCapabilities.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.spec; +import java.util.List; import org.a2aproject.sdk.spec.util.CollectionCopies; import org.jspecify.annotations.Nullable; -import java.util.List; - /** * Defines optional capabilities supported by an agent in the A2A Protocol. *

    @@ -32,10 +31,11 @@ * @see AgentExtension * @see A2A Protocol Specification */ -public record AgentCapabilities(boolean streaming, - boolean pushNotifications, - boolean extendedAgentCard, - @Nullable List extensions) { +public record AgentCapabilities( + boolean streaming, + boolean pushNotifications, + boolean extendedAgentCard, + @Nullable List extensions) { public AgentCapabilities { extensions = CollectionCopies.immutableNullableList(extensions); @@ -49,6 +49,7 @@ public record AgentCapabilities(boolean streaming, public static Builder builder() { return new Builder(); } + /** * Builder for constructing immutable {@link AgentCapabilities} instances. *

    @@ -73,8 +74,7 @@ public static class Builder { /** * Creates a new Builder with all capabilities set to false by default. */ - private Builder() { - } + private Builder() {} /** * Sets whether the agent supports streaming responses. @@ -137,7 +137,8 @@ public Builder extensions(List extensions) { * @return a new AgentCapabilities instance */ public AgentCapabilities build() { - return new AgentCapabilities(streaming, pushNotifications, extendedAgentCard, extensions); + return new AgentCapabilities( + streaming, pushNotifications, extendedAgentCard, extensions); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java b/spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java index 5e12aa941..06cb864bd 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java @@ -1,11 +1,10 @@ package org.a2aproject.sdk.spec; -import org.a2aproject.sdk.util.Assert; -import org.a2aproject.sdk.spec.util.CollectionCopies; -import org.jspecify.annotations.Nullable; - import java.util.List; import java.util.Map; +import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; +import org.jspecify.annotations.Nullable; /** * The AgentCard is a self-describing manifest for an agent in the A2A Protocol. @@ -173,8 +172,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Creates a new Builder initialized with values from an existing AgentCard. @@ -191,17 +189,22 @@ private Builder(AgentCard card) { this.version = card.version(); this.documentationUrl = card.documentationUrl(); this.capabilities = card.capabilities(); - this.defaultInputModes = CollectionCopies.mutableListCopyOrEmpty(card.defaultInputModes()); - this.defaultOutputModes = CollectionCopies.mutableListCopyOrEmpty(card.defaultOutputModes()); + this.defaultInputModes = + CollectionCopies.mutableListCopyOrEmpty(card.defaultInputModes()); + this.defaultOutputModes = + CollectionCopies.mutableListCopyOrEmpty(card.defaultOutputModes()); this.skills = CollectionCopies.mutableListCopyOrEmpty(card.skills()); this.securitySchemes = CollectionCopies.mutableMapCopyOrEmpty(card.securitySchemes()); - this.securityRequirements = CollectionCopies.mutableListCopyOrEmpty(card.securityRequirements()); + this.securityRequirements = + CollectionCopies.mutableListCopyOrEmpty(card.securityRequirements()); this.iconUrl = card.iconUrl(); - this.supportedInterfaces = CollectionCopies.mutableListCopyOrEmpty(card.supportedInterfaces()); + this.supportedInterfaces = + CollectionCopies.mutableListCopyOrEmpty(card.supportedInterfaces()); this.signatures = CollectionCopies.immutableNullableList(card.signatures()); this.url = card.url(); this.preferredTransport = card.preferredTransport(); - this.additionalInterfaces = CollectionCopies.immutableNullableList(card.additionalInterfaces()); + this.additionalInterfaces = + CollectionCopies.immutableNullableList(card.additionalInterfaces()); } /** @@ -432,7 +435,7 @@ public AgentCard build() { Assert.checkNotNullParam("supportedInterfaces", supportedInterfaces), signatures, url, - preferredTransport == null ? "JSONRPC" : preferredTransport, + preferredTransport == null ? "JSONRPC" : preferredTransport, additionalInterfaces); } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/AgentCardSignature.java b/spec/src/main/java/org/a2aproject/sdk/spec/AgentCardSignature.java index 8c325b3b3..98621e672 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/AgentCardSignature.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/AgentCardSignature.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.spec; -import java.util.Map; - import com.google.gson.annotations.SerializedName; -import org.a2aproject.sdk.util.Assert; +import java.util.Map; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -33,8 +32,10 @@ * @see RFC 7515 - JSON Web Signature * @see A2A Protocol Specification */ -public record AgentCardSignature(@Nullable Map header, @SerializedName("protected")String protectedHeader, - String signature) { +public record AgentCardSignature( + @Nullable Map header, + @SerializedName("protected") String protectedHeader, + String signature) { /** * Compact constructor that validates required fields. @@ -79,8 +80,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the optional unprotected header with additional metadata. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/AgentExtension.java b/spec/src/main/java/org/a2aproject/sdk/spec/AgentExtension.java index 299a081bd..cd9815fad 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/AgentExtension.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/AgentExtension.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.spec; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -25,7 +24,11 @@ * @see AgentCard * @see A2A Protocol Specification */ -public record AgentExtension (@Nullable String description, @Nullable Map params, boolean required, String uri) { +public record AgentExtension( + @Nullable String description, + @Nullable Map params, + boolean required, + String uri) { /** * Compact constructor that validates required fields. @@ -64,7 +67,7 @@ public static Builder builder() { * } */ public static class Builder { - @Nullable String description; + @Nullable String description; @Nullable Map params; boolean required; @Nullable String uri; @@ -72,8 +75,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets a human-readable description of the extension's purpose. @@ -126,8 +128,8 @@ public Builder uri(String uri) { * @throws IllegalArgumentException if uri is null */ public AgentExtension build() { - return new AgentExtension(description, params, required, Assert.checkNotNullParam("uri", uri)); + return new AgentExtension( + description, params, required, Assert.checkNotNullParam("uri", uri)); } } - } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/AgentInterface.java b/spec/src/main/java/org/a2aproject/sdk/spec/AgentInterface.java index 3ad2d4717..22347f6e0 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/AgentInterface.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/AgentInterface.java @@ -26,7 +26,8 @@ * @see TransportProtocol * @see A2A Protocol Specification */ -public record AgentInterface(String protocolBinding, String url, @Nullable String tenant, String protocolVersion) { +public record AgentInterface( + String protocolBinding, String url, @Nullable String tenant, String protocolVersion) { /** The default A2A Protocol version used when not explicitly specified. */ public static final String CURRENT_PROTOCOL_VERSION = "1.0"; @@ -40,7 +41,7 @@ public record AgentInterface(String protocolBinding, String url, @Nullable Strin * @param protocolVersion the protocolVersion parameter (see class-level JavaDoc) * @throws IllegalArgumentException if protocolBinding, url, or protocolVersion is null */ - public AgentInterface { + public AgentInterface { Assert.checkNotNullParam("protocolBinding", protocolBinding); Assert.checkNotNullParam("url", url); diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/AgentSkill.java b/spec/src/main/java/org/a2aproject/sdk/spec/AgentSkill.java index a070c0813..95d3ed4b9 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/AgentSkill.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/AgentSkill.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.spec; import java.util.List; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -40,9 +39,15 @@ * @see AgentCard * @see A2A Protocol Specification */ -public record AgentSkill(String id, String name, String description, List tags, - @Nullable List examples, @Nullable List inputModes, @Nullable List outputModes, - @Nullable List securityRequirements) { +public record AgentSkill( + String id, + String name, + String description, + List tags, + @Nullable List examples, + @Nullable List inputModes, + @Nullable List outputModes, + @Nullable List securityRequirements) { /** * Compact constructor that validates required fields. @@ -115,8 +120,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the unique identifier for the skill. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/Artifact.java b/spec/src/main/java/org/a2aproject/sdk/spec/Artifact.java index e98ed49df..62a0f0679 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/Artifact.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/Artifact.java @@ -2,9 +2,8 @@ import java.util.List; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -31,8 +30,13 @@ * @param extensions protocol extensions used in this artifact (optional) * @see A2A Protocol Specification */ -public record Artifact(String artifactId, @Nullable String name, @Nullable String description, List> parts, @Nullable Map metadata, - @Nullable List extensions) { +public record Artifact( + String artifactId, + @Nullable String name, + @Nullable String description, + List> parts, + @Nullable Map metadata, + @Nullable List extensions) { /** * Compact constructor that validates required fields. @@ -105,8 +109,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder(){ - } + private Builder() {} /** * Creates a new Builder initialized with values from an existing Artifact. @@ -206,7 +209,13 @@ public Builder extensions(@Nullable List extensions) { * @throws IllegalArgumentException if required fields are missing or parts is empty */ public Artifact build() { - return new Artifact(Assert.checkNotNullParam("artifactId", artifactId), name, description, Assert.checkNotNullParam("parts", parts), metadata, extensions); + return new Artifact( + Assert.checkNotNullParam("artifactId", artifactId), + name, + description, + Assert.checkNotNullParam("parts", parts), + metadata, + extensions); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/AuthenticationInfo.java b/spec/src/main/java/org/a2aproject/sdk/spec/AuthenticationInfo.java index bfcf7a67d..58fb93918 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/AuthenticationInfo.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/AuthenticationInfo.java @@ -1,6 +1,5 @@ package org.a2aproject.sdk.spec; - import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/AuthorizationCodeOAuthFlow.java b/spec/src/main/java/org/a2aproject/sdk/spec/AuthorizationCodeOAuthFlow.java index b632c6bfd..8c5462702 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/AuthorizationCodeOAuthFlow.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/AuthorizationCodeOAuthFlow.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.spec; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; /** * Configuration for the OAuth 2.0 Authorization Code flow. @@ -26,8 +25,12 @@ * @see RFC 6749 - Authorization Code Grant * @see A2A Protocol Specification */ -public record AuthorizationCodeOAuthFlow(String authorizationUrl, String refreshUrl, Map scopes, - String tokenUrl, boolean pkceRequired) { +public record AuthorizationCodeOAuthFlow( + String authorizationUrl, + String refreshUrl, + Map scopes, + String tokenUrl, + boolean pkceRequired) { /** * Compact constructor that validates required fields. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java b/spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java index 6a841e92e..91211b4ef 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.spec; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; import org.a2aproject.sdk.spec.util.Utils; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -62,8 +61,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the id. @@ -105,11 +103,7 @@ public Builder metadata(Map metadata) { * @throws IllegalArgumentException if id is null */ public CancelTaskParams build() { - return new CancelTaskParams( - Assert.checkNotNullParam("id", id), - tenant, - metadata - ); + return new CancelTaskParams(Assert.checkNotNullParam("id", id), tenant, metadata); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/ClientCredentialsOAuthFlow.java b/spec/src/main/java/org/a2aproject/sdk/spec/ClientCredentialsOAuthFlow.java index 6fccfed51..f49d254c6 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/ClientCredentialsOAuthFlow.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/ClientCredentialsOAuthFlow.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.spec; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; /** * Configuration for the OAuth 2.0 Client Credentials flow. @@ -25,7 +24,8 @@ * @see RFC 6749 - Client Credentials Grant * @see A2A Protocol Specification */ -public record ClientCredentialsOAuthFlow(String refreshUrl, Map scopes, String tokenUrl) { +public record ClientCredentialsOAuthFlow( + String refreshUrl, Map scopes, String tokenUrl) { /** * Compact constructor that validates required fields. @@ -40,5 +40,4 @@ public record ClientCredentialsOAuthFlow(String refreshUrl, Map Assert.checkNotNullParam("tokenUrl", tokenUrl); scopes = CollectionCopies.immutableMap(scopes); } - } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/Compat03Fields.java b/spec/src/main/java/org/a2aproject/sdk/spec/Compat03Fields.java index 20f49d0fc..2d87fe8e4 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/Compat03Fields.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/Compat03Fields.java @@ -17,11 +17,13 @@ public final class Compat03Fields { COMPAT_03_AVAILABLE = available; } - private Compat03Fields() { - } + private Compat03Fields() {} - public static void addCompat03FieldsIfAvailable(AgentCard.Builder builder, - List supportedInterfaces, String url, String preferredTransport) { + public static void addCompat03FieldsIfAvailable( + AgentCard.Builder builder, + List supportedInterfaces, + String url, + String preferredTransport) { if (!COMPAT_03_AVAILABLE) { return; } @@ -29,7 +31,10 @@ public static void addCompat03FieldsIfAvailable(AgentCard.Builder builder, .preferredTransport(preferredTransport) .additionalInterfaces( supportedInterfaces.stream() - .map(iface -> new Legacy_0_3_AgentInterface(iface.protocolBinding(), iface.url())) + .map( + iface -> + new Legacy_0_3_AgentInterface( + iface.protocolBinding(), iface.url())) .toList()); } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/ContentTypeNotSupportedError.java b/spec/src/main/java/org/a2aproject/sdk/spec/ContentTypeNotSupportedError.java index 5ccd66944..48ee9a00f 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/ContentTypeNotSupportedError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/ContentTypeNotSupportedError.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; /** @@ -44,6 +43,7 @@ public class ContentTypeNotSupportedError extends A2AProtocolError { public ContentTypeNotSupportedError() { this(null, null, null); } + /** * Constructs a content type not supported error. * @@ -51,8 +51,12 @@ public ContentTypeNotSupportedError() { * @param message the error message * @param details additional error details */ - public ContentTypeNotSupportedError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { - super(defaultIfNull(code, A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED.code()), + public ContentTypeNotSupportedError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { + super( + defaultIfNull(code, A2AErrorCodes.CONTENT_TYPE_NOT_SUPPORTED.code()), defaultIfNull(message, "Incompatible content types"), details); } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/DataPart.java b/spec/src/main/java/org/a2aproject/sdk/spec/DataPart.java index 06acba3ad..ce5e7022a 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/DataPart.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/DataPart.java @@ -1,17 +1,15 @@ package org.a2aproject.sdk.spec; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; import com.google.gson.ToNumberPolicy; -import org.a2aproject.sdk.util.Assert; -import org.a2aproject.sdk.spec.util.CollectionCopies; import java.util.List; import java.util.Map; +import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; - /** * Represents a structured data content part within a {@link Message} or {@link Artifact}. *

    @@ -48,7 +46,8 @@ * @see Message * @see Artifact */ -public record DataPart(Object data, @Nullable Map metadata) implements Part { +public record DataPart(Object data, @Nullable Map metadata) + implements Part { /** * The JSON member name discriminator for data parts: "data". @@ -68,7 +67,7 @@ public record DataPart(Object data, @Nullable Map metadata) impl * @param metadata additional metadata for the part * @throws IllegalArgumentException if data is null */ - public DataPart (Object data, @Nullable Map metadata) { + public DataPart(Object data, @Nullable Map metadata) { Assert.checkNotNullParam("data", data); this.metadata = CollectionCopies.unmodifiableNullableShallowMap(metadata); this.data = defensivelyCopy(data); @@ -135,9 +134,8 @@ public static DataPart fromJson(String json, @Nullable Map metad } } - private static final Gson JSON_PARSER = new GsonBuilder() - .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) - .create(); + private static final Gson JSON_PARSER = + new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create(); /** * Creates a defensive copy of mutable collection types to ensure immutability. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/DeleteTaskPushNotificationConfigParams.java b/spec/src/main/java/org/a2aproject/sdk/spec/DeleteTaskPushNotificationConfigParams.java index 4b4e1e282..65fa5adb7 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/DeleteTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/DeleteTaskPushNotificationConfigParams.java @@ -1,9 +1,7 @@ package org.a2aproject.sdk.spec; - - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.Utils; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -17,7 +15,8 @@ * @param tenant optional tenant, provided as a path parameter. * @see A2A Protocol Specification */ -public record DeleteTaskPushNotificationConfigParams(String taskId, String id, @Nullable String tenant) { +public record DeleteTaskPushNotificationConfigParams( + String taskId, String id, @Nullable String tenant) { /** * Compact constructor that validates required fields. @@ -66,8 +65,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the task identifier. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/DeviceCodeOAuthFlow.java b/spec/src/main/java/org/a2aproject/sdk/spec/DeviceCodeOAuthFlow.java index ceb7122a1..a284afc96 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/DeviceCodeOAuthFlow.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/DeviceCodeOAuthFlow.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.spec; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; /** * Configuration details for the OAuth 2.0 Device Code flow (RFC 8628). @@ -19,7 +18,11 @@ * @see OAuth2SecurityScheme for the security scheme using these flows * @see A2A Protocol Specification */ -public record DeviceCodeOAuthFlow(String deviceAuthorizationUrl, String tokenUrl, String refreshUrl, Map scopes) { +public record DeviceCodeOAuthFlow( + String deviceAuthorizationUrl, + String tokenUrl, + String refreshUrl, + Map scopes) { /** * Compact constructor that validates required fields. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/Event.java b/spec/src/main/java/org/a2aproject/sdk/spec/Event.java index ebd01998b..91f73e449 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/Event.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/Event.java @@ -25,5 +25,4 @@ * @see StreamingEventKind * @see UpdateEvent */ -public interface Event { -} +public interface Event {} diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/ExtendedAgentCardNotConfiguredError.java b/spec/src/main/java/org/a2aproject/sdk/spec/ExtendedAgentCardNotConfiguredError.java index 394207529..f769c607c 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/ExtendedAgentCardNotConfiguredError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/ExtendedAgentCardNotConfiguredError.java @@ -3,10 +3,8 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; - /** * A2A Protocol error indicating that the agent does not have an authenticated extended card configured. *

    @@ -39,7 +37,10 @@ public class ExtendedAgentCardNotConfiguredError extends A2AProtocolError { * @param message the error message * @param details additional error details */ - public ExtendedAgentCardNotConfiguredError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public ExtendedAgentCardNotConfiguredError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.EXTENDED_AGENT_CARD_NOT_CONFIGURED.code()), defaultIfNull(message, "Extended Card not configured"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/ExtensionSupportRequiredError.java b/spec/src/main/java/org/a2aproject/sdk/spec/ExtensionSupportRequiredError.java index edc7380ab..fce53d114 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/ExtensionSupportRequiredError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/ExtensionSupportRequiredError.java @@ -3,10 +3,8 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; - /** * A2A Protocol error indicating that a client requested use of an extension marked as required * but did not declare support for it. @@ -39,7 +37,10 @@ public class ExtensionSupportRequiredError extends A2AProtocolError { * @param message the error message (defaults to standard message if null) * @param details additional error details (optional) */ - public ExtensionSupportRequiredError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public ExtensionSupportRequiredError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.EXTENSION_SUPPORT_REQUIRED.code()), defaultIfNull(message, "Extension support required but not declared"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/FilePart.java b/spec/src/main/java/org/a2aproject/sdk/spec/FilePart.java index 335f5ef08..f38f204dd 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/FilePart.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/FilePart.java @@ -1,12 +1,10 @@ package org.a2aproject.sdk.spec; - -import org.a2aproject.sdk.util.Assert; -import org.a2aproject.sdk.spec.util.CollectionCopies; import java.util.Map; +import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; - /** * Represents a file content part within a {@link Message} or {@link Artifact}. *

    @@ -40,7 +38,8 @@ * @see FileWithBytes * @see FileWithUri */ -public record FilePart(FileContent file, @Nullable Map metadata) implements Part { +public record FilePart(FileContent file, @Nullable Map metadata) + implements Part { /** * The JSON member name discriminator for file parts: "file". @@ -56,7 +55,7 @@ public record FilePart(FileContent file, @Nullable Map metadata) * @param file the file content (required, either FileWithBytes or FileWithUri) * @throws IllegalArgumentException if file is null */ - public FilePart (FileContent file, @Nullable Map metadata) { + public FilePart(FileContent file, @Nullable Map metadata) { Assert.checkNotNullParam("file", file); this.metadata = CollectionCopies.unmodifiableNullableShallowMap(metadata); this.file = file; @@ -68,7 +67,7 @@ public FilePart (FileContent file, @Nullable Map metadata) { * @param file the file content (required, either FileWithBytes or FileWithUri) * @throws IllegalArgumentException if file is null */ - public FilePart (FileContent file) { + public FilePart(FileContent file) { this(file, null); } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/FileWithBytes.java b/spec/src/main/java/org/a2aproject/sdk/spec/FileWithBytes.java index 15e26b566..ca17858fc 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/FileWithBytes.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/FileWithBytes.java @@ -1,6 +1,5 @@ package org.a2aproject.sdk.spec; -import org.a2aproject.sdk.util.Assert; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; @@ -13,7 +12,7 @@ import java.nio.file.Path; import java.util.Base64; import java.util.Objects; - +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -54,8 +53,7 @@ public final class FileWithBytes implements FileContent { private final ByteSource source; // Soft-reference cache: held in memory but reclaimable by GC under memory pressure - @Nullable - private volatile SoftReference cachedBytes; + @Nullable private volatile SoftReference cachedBytes; /** * Creates a {@code FileWithBytes} with pre-encoded base64 content. @@ -254,8 +252,9 @@ private static void validateFile(Path file) { long size = Files.size(file); if (size > MAX_FILE_SIZE) { throw new IllegalArgumentException( - String.format("File too large: %d bytes (maximum: %d bytes)", size, MAX_FILE_SIZE) - ); + String.format( + "File too large: %d bytes (maximum: %d bytes)", + size, MAX_FILE_SIZE)); } } catch (IOException e) { throw new RuntimeException("Failed to check file size: " + file, e); @@ -329,7 +328,7 @@ public String getBase64() { private static String encodeFileToBase64(Path path) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(path)); - OutputStream base64OutputStream = Base64.getEncoder().wrap(outputStream)) { + OutputStream base64OutputStream = Base64.getEncoder().wrap(outputStream)) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/FileWithUri.java b/spec/src/main/java/org/a2aproject/sdk/spec/FileWithUri.java index 338420cbd..bf2d9dc96 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/FileWithUri.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/FileWithUri.java @@ -28,6 +28,4 @@ * @see FilePart * @see FileWithBytes */ -public record FileWithUri(String mimeType, String name, String uri) implements FileContent { -} - +public record FileWithUri(String mimeType, String name, String uri) implements FileContent {} diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java b/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java index 584b7de23..0c87fd2e0 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java @@ -1,9 +1,7 @@ package org.a2aproject.sdk.spec; - - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.Utils; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -18,7 +16,8 @@ * @see TaskPushNotificationConfig for the returned configuration structure * @see A2A Protocol Specification */ -public record GetTaskPushNotificationConfigParams(String taskId, @Nullable String id, @Nullable String tenant) { +public record GetTaskPushNotificationConfigParams( + String taskId, @Nullable String id, @Nullable String tenant) { /** * Compact constructor that validates required fields. @@ -72,8 +71,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the task ID. @@ -115,9 +113,7 @@ public Builder tenant(@Nullable String tenant) { */ public GetTaskPushNotificationConfigParams build() { return new GetTaskPushNotificationConfigParams( - Assert.checkNotNullParam("taskId", taskId), - id, - tenant); + Assert.checkNotNullParam("taskId", taskId), id, tenant); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/HTTPAuthSecurityScheme.java b/spec/src/main/java/org/a2aproject/sdk/spec/HTTPAuthSecurityScheme.java index be0c4f13c..68ffe1caa 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/HTTPAuthSecurityScheme.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/HTTPAuthSecurityScheme.java @@ -36,7 +36,9 @@ * @see RFC 7235 - HTTP Authentication * @see A2A Protocol Specification */ -public record HTTPAuthSecurityScheme(@Nullable String bearerFormat, String scheme, @Nullable String description) implements SecurityScheme { +public record HTTPAuthSecurityScheme( + @Nullable String bearerFormat, String scheme, @Nullable String description) + implements SecurityScheme { /** The HTTP security scheme type identifier. */ public static final String TYPE = "httpAuthSecurityScheme"; @@ -81,8 +83,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the bearer token format hint. @@ -124,7 +125,8 @@ public Builder description(String description) { * @throws IllegalArgumentException if required fields are missing */ public HTTPAuthSecurityScheme build() { - return new HTTPAuthSecurityScheme(bearerFormat, Assert.checkNotNullParam("scheme", scheme), description); + return new HTTPAuthSecurityScheme( + bearerFormat, Assert.checkNotNullParam("scheme", scheme), description); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/InternalError.java b/spec/src/main/java/org/a2aproject/sdk/spec/InternalError.java index d31532878..cfc15e249 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/InternalError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/InternalError.java @@ -3,10 +3,8 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; - /** * JSON-RPC error indicating an internal error occurred on the server. *

    @@ -37,7 +35,10 @@ public class InternalError extends A2AError { * @param message the error message * @param details additional error details */ - public InternalError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public InternalError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.INTERNAL.code()), defaultIfNull(message, "Internal Error"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/InvalidAgentResponseError.java b/spec/src/main/java/org/a2aproject/sdk/spec/InvalidAgentResponseError.java index 8f22d5bcc..4e0552102 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/InvalidAgentResponseError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/InvalidAgentResponseError.java @@ -3,10 +3,8 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; - /** * A2A Protocol error indicating that an agent returned a response not conforming to protocol specifications. *

    @@ -47,7 +45,10 @@ public class InvalidAgentResponseError extends A2AProtocolError { * @param message the error message * @param details additional error details */ - public InvalidAgentResponseError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public InvalidAgentResponseError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.INVALID_AGENT_RESPONSE.code()), defaultIfNull(message, "Invalid agent response"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/InvalidParamsError.java b/spec/src/main/java/org/a2aproject/sdk/spec/InvalidParamsError.java index 6c12677ec..cfd4606f3 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/InvalidParamsError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/InvalidParamsError.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; /** @@ -40,7 +39,10 @@ public class InvalidParamsError extends A2AError { * @param message the error message * @param details additional error details */ - public InvalidParamsError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public InvalidParamsError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.INVALID_PARAMS.code()), defaultIfNull(message, "Invalid parameters"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/InvalidRequestError.java b/spec/src/main/java/org/a2aproject/sdk/spec/InvalidRequestError.java index d3c3f7b4a..7166e1481 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/InvalidRequestError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/InvalidRequestError.java @@ -3,10 +3,8 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; - /** * JSON-RPC error indicating that the request payload is not a valid JSON-RPC Request object. *

    @@ -48,7 +46,10 @@ public InvalidRequestError() { * @param message the error message * @param details additional error details */ - public InvalidRequestError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public InvalidRequestError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.INVALID_REQUEST.code()), defaultIfNull(message, "Request payload validation error"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/JSONParseError.java b/spec/src/main/java/org/a2aproject/sdk/spec/JSONParseError.java index fd3fe66cd..7184c4628 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/JSONParseError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/JSONParseError.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; /** @@ -54,7 +53,10 @@ public JSONParseError(String message) { * @param message the error message * @param details additional error details */ - public JSONParseError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public JSONParseError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.JSON_PARSE.code()), defaultIfNull(message, "Invalid JSON payload"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/ListTaskPushNotificationConfigsParams.java b/spec/src/main/java/org/a2aproject/sdk/spec/ListTaskPushNotificationConfigsParams.java index 81c0126e9..405ccc652 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/ListTaskPushNotificationConfigsParams.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/ListTaskPushNotificationConfigsParams.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.spec; -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.Utils; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -17,7 +17,8 @@ * @see TaskPushNotificationConfig for the configuration structure * @see A2A Protocol Specification */ -public record ListTaskPushNotificationConfigsParams(String id, int pageSize, String pageToken, @Nullable String tenant) { +public record ListTaskPushNotificationConfigsParams( + String id, int pageSize, String pageToken, @Nullable String tenant) { /** * Compact constructor for validation. @@ -48,10 +49,10 @@ public ListTaskPushNotificationConfigsParams(String id) { * @return the effective page size */ public int getEffectivePageSize() { - if (pageSize <= 0 || pageSize > 100) { - return 100; - } - return pageSize; + if (pageSize <= 0 || pageSize > 100) { + return 100; + } + return pageSize; } /** @@ -75,8 +76,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the id. @@ -130,11 +130,10 @@ public Builder tenant(@Nullable String tenant) { */ public ListTaskPushNotificationConfigsParams build() { return new ListTaskPushNotificationConfigsParams( - Assert.checkNotNullParam("id", id), - pageSize != null ? pageSize : 0, - pageToken != null ? pageToken : "", - tenant - ); + Assert.checkNotNullParam("id", id), + pageSize != null ? pageSize : 0, + pageToken != null ? pageToken : "", + tenant); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/ListTaskPushNotificationConfigsResult.java b/spec/src/main/java/org/a2aproject/sdk/spec/ListTaskPushNotificationConfigsResult.java index d80f625a6..9ed60ea40 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/ListTaskPushNotificationConfigsResult.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/ListTaskPushNotificationConfigsResult.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.spec; import java.util.List; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -12,8 +11,8 @@ * @param configs List of push notification configurations for the task * @param nextPageToken Token for retrieving the next page of results (null if no more results) */ -public record ListTaskPushNotificationConfigsResult(List configs, - @Nullable String nextPageToken) { +public record ListTaskPushNotificationConfigsResult( + List configs, @Nullable String nextPageToken) { /** * Compact constructor for validation. * Validates parameters and creates a defensive copy of the configs list. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/ListTasksParams.java b/spec/src/main/java/org/a2aproject/sdk/spec/ListTasksParams.java index 9cb64bb37..5ca1f5142 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/ListTasksParams.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/ListTasksParams.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.spec; import java.time.Instant; - import org.a2aproject.sdk.spec.util.Utils; import org.jspecify.annotations.Nullable; @@ -25,11 +24,11 @@ public record ListTasksParams( @Nullable Integer historyLength, @Nullable Instant statusTimestampAfter, @Nullable Boolean includeArtifacts, - @Nullable String tenant -) { + @Nullable String tenant) { private static final int MIN_PAGE_SIZE = 1; private static final int MAX_PAGE_SIZE = 100; private static final int DEFAULT_PAGE_SIZE = 50; + /** * Compact constructor for validation. * Validates that the tenant parameter is not null and parameters are within valid ranges. @@ -48,16 +47,24 @@ public record ListTasksParams( Utils.validateTenant(tenant); // Validate pageSize (1-100) if (pageSize != null && (pageSize < MIN_PAGE_SIZE || pageSize > MAX_PAGE_SIZE)) { - throw new InvalidParamsError(null, - "pageSize must be between " + MIN_PAGE_SIZE + " and " + MAX_PAGE_SIZE + ", got: " + pageSize, null); + throw new InvalidParamsError( + null, + "pageSize must be between " + + MIN_PAGE_SIZE + + " and " + + MAX_PAGE_SIZE + + ", got: " + + pageSize, + null); } // Validate historyLength (>= 0) if (historyLength != null && historyLength < 0) { - throw new InvalidParamsError(null, - "historyLength must be non-negative, got: " + historyLength, null); + throw new InvalidParamsError( + null, "historyLength must be non-negative, got: " + historyLength, null); } } + /** * Default constructor for listing all tasks. */ @@ -129,8 +136,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the contextId. @@ -226,8 +232,15 @@ public Builder tenant(@Nullable String tenant) { * @return a new ListTasksParams instance */ public ListTasksParams build() { - return new ListTasksParams(contextId, status, pageSize, pageToken, historyLength, - statusTimestampAfter, includeArtifacts, tenant); + return new ListTasksParams( + contextId, + status, + pageSize, + pageToken, + historyLength, + statusTimestampAfter, + includeArtifacts, + tenant); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/Message.java b/spec/src/main/java/org/a2aproject/sdk/spec/Message.java index 2ee068d57..7b347213c 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/Message.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/Message.java @@ -3,9 +3,8 @@ import java.util.List; import java.util.Map; import java.util.UUID; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -35,14 +34,16 @@ * @param extensions list of protocol extensions used in this message * @see A2A Protocol Specification */ -public record Message(Role role, List> parts, +public record Message( + Role role, + List> parts, String messageId, @Nullable String contextId, @Nullable String taskId, @Nullable List referenceTaskIds, @Nullable Map metadata, - @Nullable List extensions - ) implements EventKind, StreamingEventKind { + @Nullable List extensions) + implements EventKind, StreamingEventKind { /** * The identifier when used in streaming responses @@ -137,28 +138,19 @@ public enum Role { */ public static class Builder { - private @Nullable - Role role; - private @Nullable - List> parts; - private @Nullable - String messageId; - private @Nullable - String contextId; - private @Nullable - String taskId; - private @Nullable - List referenceTaskIds; - private @Nullable - Map metadata; - private @Nullable - List extensions; + private @Nullable Role role; + private @Nullable List> parts; + private @Nullable String messageId; + private @Nullable String contextId; + private @Nullable String taskId; + private @Nullable List referenceTaskIds; + private @Nullable Map metadata; + private @Nullable List extensions; /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Creates a new Builder initialized with values from an existing Message. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/MessageSendConfiguration.java b/spec/src/main/java/org/a2aproject/sdk/spec/MessageSendConfiguration.java index 1c5b59abd..46ee59a16 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/MessageSendConfiguration.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/MessageSendConfiguration.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.spec; import java.util.List; - import org.a2aproject.sdk.spec.util.CollectionCopies; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -23,8 +22,11 @@ * @see TaskPushNotificationConfig for push notification options * @see A2A Protocol Specification */ -public record MessageSendConfiguration(@Nullable List acceptedOutputModes, @Nullable Integer historyLength, - @Nullable TaskPushNotificationConfig taskPushNotificationConfig, Boolean returnImmediately) { +public record MessageSendConfiguration( + @Nullable List acceptedOutputModes, + @Nullable Integer historyLength, + @Nullable TaskPushNotificationConfig taskPushNotificationConfig, + Boolean returnImmediately) { /** * Compact constructor for validation. @@ -67,8 +69,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the accepted output modes. @@ -88,7 +89,8 @@ public Builder acceptedOutputModes(List acceptedOutputModes) { * @param taskPushNotificationConfig configuration for push notifications * @return this builder */ - public Builder taskPushNotificationConfig(@Nullable TaskPushNotificationConfig taskPushNotificationConfig) { + public Builder taskPushNotificationConfig( + @Nullable TaskPushNotificationConfig taskPushNotificationConfig) { this.taskPushNotificationConfig = taskPushNotificationConfig; return this; } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/MessageSendParams.java b/spec/src/main/java/org/a2aproject/sdk/spec/MessageSendParams.java index efa47335e..d7ce7b368 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/MessageSendParams.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/MessageSendParams.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.spec; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; import org.a2aproject.sdk.spec.util.Utils; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -23,8 +22,11 @@ * @see MessageSendConfiguration for available configuration options * @see A2A Protocol Specification */ -public record MessageSendParams(Message message, @Nullable MessageSendConfiguration configuration, - @Nullable Map metadata, @Nullable String tenant) { +public record MessageSendParams( + Message message, + @Nullable MessageSendConfiguration configuration, + @Nullable Map metadata, + @Nullable String tenant) { /** * Compact constructor for validation. @@ -48,7 +50,10 @@ public record MessageSendParams(Message message, @Nullable MessageSendConfigurat * @param configuration optional configuration for message processing * @param metadata optional metadata */ - public MessageSendParams(Message message, @Nullable MessageSendConfiguration configuration, @Nullable Map metadata) { + public MessageSendParams( + Message message, + @Nullable MessageSendConfiguration configuration, + @Nullable Map metadata) { this(message, configuration, metadata, null); } @@ -76,8 +81,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the message to send to the agent. @@ -131,10 +135,7 @@ public Builder tenant(@Nullable String tenant) { */ public MessageSendParams build() { return new MessageSendParams( - Assert.checkNotNullParam("message", message), - configuration, - metadata, - tenant); + Assert.checkNotNullParam("message", message), configuration, metadata, tenant); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/MethodNotFoundError.java b/spec/src/main/java/org/a2aproject/sdk/spec/MethodNotFoundError.java index ec007ab72..74ad55b29 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/MethodNotFoundError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/MethodNotFoundError.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; /** @@ -32,7 +31,10 @@ public class MethodNotFoundError extends A2AError { * @param message the error message (defaults to "Method not found" if null) * @param details additional error details (optional) */ - public MethodNotFoundError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public MethodNotFoundError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.METHOD_NOT_FOUND.code()), defaultIfNull(message, "Method not found"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/OAuth2SecurityScheme.java b/spec/src/main/java/org/a2aproject/sdk/spec/OAuth2SecurityScheme.java index 322cdebdb..4e9fca9eb 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/OAuth2SecurityScheme.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/OAuth2SecurityScheme.java @@ -20,7 +20,8 @@ * @see OpenAPI Security Scheme * @see A2A Protocol Specification */ -public record OAuth2SecurityScheme(OAuthFlows flows, @Nullable String description, @Nullable String oauth2MetadataUrl) +public record OAuth2SecurityScheme( + OAuthFlows flows, @Nullable String description, @Nullable String oauth2MetadataUrl) implements SecurityScheme { /** @@ -75,8 +76,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the OAuth flows configuration. @@ -118,7 +118,8 @@ public Builder oauth2MetadataUrl(String oauth2MetadataUrl) { * @throws IllegalArgumentException if flows is null */ public OAuth2SecurityScheme build() { - return new OAuth2SecurityScheme(Assert.checkNotNullParam("flows", flows), description, oauth2MetadataUrl); + return new OAuth2SecurityScheme( + Assert.checkNotNullParam("flows", flows), description, oauth2MetadataUrl); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/OAuthFlows.java b/spec/src/main/java/org/a2aproject/sdk/spec/OAuthFlows.java index 805973fbf..a844518cd 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/OAuthFlows.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/OAuthFlows.java @@ -17,8 +17,10 @@ * @see OpenAPI OAuth Flows Object * @see A2A Protocol Specification */ -public record OAuthFlows(@Nullable AuthorizationCodeOAuthFlow authorizationCode, @Nullable ClientCredentialsOAuthFlow clientCredentials, - @Nullable DeviceCodeOAuthFlow deviceCode) { +public record OAuthFlows( + @Nullable AuthorizationCodeOAuthFlow authorizationCode, + @Nullable ClientCredentialsOAuthFlow clientCredentials, + @Nullable DeviceCodeOAuthFlow deviceCode) { /** * Create a new Builder @@ -40,8 +42,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the authorization code flow configuration. @@ -76,7 +77,6 @@ public Builder deviceCode(DeviceCodeOAuthFlow deviceCode) { return this; } - /** * Builds a new immutable OAuthFlows instance. * diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/OpenIdConnectSecurityScheme.java b/spec/src/main/java/org/a2aproject/sdk/spec/OpenIdConnectSecurityScheme.java index 532c9fd25..533544a03 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/OpenIdConnectSecurityScheme.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/OpenIdConnectSecurityScheme.java @@ -29,7 +29,8 @@ * @see OpenID Connect Discovery * @see A2A Protocol Specification */ -public record OpenIdConnectSecurityScheme(String openIdConnectUrl, @Nullable String description) implements SecurityScheme { +public record OpenIdConnectSecurityScheme(String openIdConnectUrl, @Nullable String description) + implements SecurityScheme { /** * The type identifier for OpenID Connect security schemes: "openIdConnect". @@ -74,8 +75,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the OpenID Connect Discovery URL. @@ -106,7 +106,8 @@ public Builder description(String description) { * @throws IllegalArgumentException if openIdConnectUrl is null */ public OpenIdConnectSecurityScheme build() { - return new OpenIdConnectSecurityScheme(Assert.checkNotNullParam("openIdConnectUrl", openIdConnectUrl), description); + return new OpenIdConnectSecurityScheme( + Assert.checkNotNullParam("openIdConnectUrl", openIdConnectUrl), description); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/Part.java b/spec/src/main/java/org/a2aproject/sdk/spec/Part.java index f9678e838..aac073222 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/Part.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/Part.java @@ -31,4 +31,4 @@ */ public interface Part { // No methods - use instanceof for type discrimination -} \ No newline at end of file +} diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/PushNotificationNotSupportedError.java b/spec/src/main/java/org/a2aproject/sdk/spec/PushNotificationNotSupportedError.java index cfd061e81..05609429d 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/PushNotificationNotSupportedError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/PushNotificationNotSupportedError.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; /** @@ -45,7 +44,10 @@ public PushNotificationNotSupportedError() { * @param message the error message (defaults to "Push Notification is not supported" if null) * @param details additional error details (optional) */ - public PushNotificationNotSupportedError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public PushNotificationNotSupportedError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.PUSH_NOTIFICATION_NOT_SUPPORTED.code()), defaultIfNull(message, "Push Notification is not supported"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/SecurityRequirement.java b/spec/src/main/java/org/a2aproject/sdk/spec/SecurityRequirement.java index 049cef01d..042d515a5 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/SecurityRequirement.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/SecurityRequirement.java @@ -5,9 +5,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; /** * Represents a security requirement in the A2A Protocol. @@ -88,8 +87,7 @@ public static class Builder { /** * Creates a new Builder with an empty schemes map. */ - private Builder() { - } + private Builder() {} /** * Adds a security scheme with its required scopes. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/SecurityScheme.java b/spec/src/main/java/org/a2aproject/sdk/spec/SecurityScheme.java index 0f47b2b0e..767b5a912 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/SecurityScheme.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/SecurityScheme.java @@ -20,8 +20,12 @@ * @see OpenAPI Security Scheme Object * @see A2A Protocol Specification */ -public sealed interface SecurityScheme permits APIKeySecurityScheme, HTTPAuthSecurityScheme, OAuth2SecurityScheme, - OpenIdConnectSecurityScheme, MutualTLSSecurityScheme { +public sealed interface SecurityScheme + permits APIKeySecurityScheme, + HTTPAuthSecurityScheme, + OAuth2SecurityScheme, + OpenIdConnectSecurityScheme, + MutualTLSSecurityScheme { /** * Returns the human-readable description of this security scheme. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/StreamingEventKind.java b/spec/src/main/java/org/a2aproject/sdk/spec/StreamingEventKind.java index 0cfe02d30..7c9455651 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/StreamingEventKind.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/StreamingEventKind.java @@ -30,7 +30,8 @@ * @see EventKind * @see UpdateEvent */ -public sealed interface StreamingEventKind extends Event permits Task, Message, TaskStatusUpdateEvent, TaskArtifactUpdateEvent { +public sealed interface StreamingEventKind extends Event + permits Task, Message, TaskStatusUpdateEvent, TaskArtifactUpdateEvent { /** * Returns the type identifier for this streaming event. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/Task.java b/spec/src/main/java/org/a2aproject/sdk/spec/Task.java index b2523fbc5..6f7be20fa 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/Task.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/Task.java @@ -2,9 +2,8 @@ import java.util.List; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -44,8 +43,14 @@ * @see Message * @see A2A Protocol Specification */ -public record Task(String id, String contextId, TaskStatus status, @Nullable List artifacts, - @Nullable List history, @Nullable Map metadata) implements EventKind, StreamingEventKind { +public record Task( + String id, + String contextId, + TaskStatus status, + @Nullable List artifacts, + @Nullable List history, + @Nullable Map metadata) + implements EventKind, StreamingEventKind { /** * The identifier when used in streaming responses @@ -130,8 +135,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Creates a new Builder initialized with values from an existing Task. @@ -145,7 +149,6 @@ private Builder(Task task) { artifacts = task.artifacts(); history = task.history(); metadata = task.metadata(); - } /** diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TaskArtifactUpdateEvent.java b/spec/src/main/java/org/a2aproject/sdk/spec/TaskArtifactUpdateEvent.java index 342c60fe9..88a70b668 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskArtifactUpdateEvent.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskArtifactUpdateEvent.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.spec; import java.util.Map; - -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -40,8 +39,14 @@ * @see Artifact * @see Task */ -public record TaskArtifactUpdateEvent(String taskId, Artifact artifact, String contextId, @Nullable Boolean append, - @Nullable Boolean lastChunk, @Nullable Map metadata) implements EventKind, StreamingEventKind, UpdateEvent { +public record TaskArtifactUpdateEvent( + String taskId, + Artifact artifact, + String contextId, + @Nullable Boolean append, + @Nullable Boolean lastChunk, + @Nullable Map metadata) + implements EventKind, StreamingEventKind, UpdateEvent { /** * The identifier when used in streaming responses @@ -128,8 +133,7 @@ public static class Builder { private @Nullable Boolean lastChunk; private @Nullable Map metadata; - private Builder() { - } + private Builder() {} private Builder(TaskArtifactUpdateEvent existingTaskArtifactUpdateEvent) { this.taskId = existingTaskArtifactUpdateEvent.taskId; @@ -191,7 +195,7 @@ public Builder append(@Nullable Boolean append) { * @return this builder for method chaining */ public Builder lastChunk(@Nullable Boolean lastChunk) { - this.lastChunk = lastChunk; + this.lastChunk = lastChunk; return this; } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TaskIdParams.java b/spec/src/main/java/org/a2aproject/sdk/spec/TaskIdParams.java index b6aed6ef3..dd9c13400 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskIdParams.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskIdParams.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.spec; -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.Utils; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -23,7 +23,7 @@ public record TaskIdParams(String id, @Nullable String tenant) { * @param id the task identifier * @param tenant the tenant identifier */ - public TaskIdParams { + public TaskIdParams { Assert.checkNotNullParam("id", id); Utils.validateTenant(tenant); } @@ -56,8 +56,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the id. @@ -81,7 +80,6 @@ public Builder tenant(@Nullable String tenant) { return this; } - /** * Builds the TaskIdParams. * @@ -89,10 +87,7 @@ public Builder tenant(@Nullable String tenant) { * @throws IllegalArgumentException if id is null */ public TaskIdParams build() { - return new TaskIdParams( - Assert.checkNotNullParam("id", id), - tenant - ); + return new TaskIdParams(Assert.checkNotNullParam("id", id), tenant); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TaskNotCancelableError.java b/spec/src/main/java/org/a2aproject/sdk/spec/TaskNotCancelableError.java index a5cacfeaa..8fb54121b 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskNotCancelableError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskNotCancelableError.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; /** @@ -48,7 +47,10 @@ public TaskNotCancelableError() { * @param message the error message (defaults to "Task cannot be canceled" if null) * @param details additional error details (optional) */ - public TaskNotCancelableError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public TaskNotCancelableError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.TASK_NOT_CANCELABLE.code()), defaultIfNull(message, "Task cannot be canceled"), @@ -63,5 +65,4 @@ public TaskNotCancelableError(@Nullable Integer code, @Nullable String message, public TaskNotCancelableError(String message) { this(null, message, null); } - } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TaskNotFoundError.java b/spec/src/main/java/org/a2aproject/sdk/spec/TaskNotFoundError.java index 11cb0683a..4a039464f 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskNotFoundError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskNotFoundError.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; /** diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TaskPushNotificationConfig.java b/spec/src/main/java/org/a2aproject/sdk/spec/TaskPushNotificationConfig.java index e98924bee..7a44de25d 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskPushNotificationConfig.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskPushNotificationConfig.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.spec; -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.Utils; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -32,8 +32,13 @@ * @see MessageSendConfiguration for configuring push notifications on message send * @see A2A Protocol Specification */ -public record TaskPushNotificationConfig(@Nullable String id, @Nullable String taskId, String url, @Nullable String token, - @Nullable AuthenticationInfo authentication, @Nullable String tenant) { +public record TaskPushNotificationConfig( + @Nullable String id, + @Nullable String taskId, + String url, + @Nullable String token, + @Nullable AuthenticationInfo authentication, + @Nullable String tenant) { /** * Compact constructor for validation. @@ -85,8 +90,7 @@ public static class Builder { private @Nullable String tenant; /** Creates an empty builder. */ - private Builder() { - } + private Builder() {} /** * Creates a builder initialized from an existing configuration. diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TaskQueryParams.java b/spec/src/main/java/org/a2aproject/sdk/spec/TaskQueryParams.java index da483bf4d..56d00d211 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskQueryParams.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskQueryParams.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.spec; -import org.a2aproject.sdk.util.Assert; import org.a2aproject.sdk.spec.util.Utils; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -69,8 +69,7 @@ public static class Builder { /** * Creates a new Builder with all fields unset. */ - private Builder() { - } + private Builder() {} /** * Sets the id. @@ -112,11 +111,7 @@ public Builder tenant(@Nullable String tenant) { * @throws IllegalArgumentException if id is null or historyLength is negative */ public TaskQueryParams build() { - return new TaskQueryParams( - Assert.checkNotNullParam("id", id), - historyLength, - tenant - ); + return new TaskQueryParams(Assert.checkNotNullParam("id", id), historyLength, tenant); } } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TaskState.java b/spec/src/main/java/org/a2aproject/sdk/spec/TaskState.java index a9a243464..2fcc57aab 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskState.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskState.java @@ -85,7 +85,7 @@ public enum TaskState { * * @return {@code true} if this is a terminal state, {@code false} else. */ - public boolean isFinal(){ + public boolean isFinal() { return isFinal; } @@ -108,4 +108,4 @@ public boolean isFinal(){ public boolean isInterrupted() { return isInterrupted; } -} \ No newline at end of file +} diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TaskStatus.java b/spec/src/main/java/org/a2aproject/sdk/spec/TaskStatus.java index 5f8cb1466..d5125ab92 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskStatus.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskStatus.java @@ -1,9 +1,8 @@ package org.a2aproject.sdk.spec; import java.time.OffsetDateTime; - -import org.a2aproject.sdk.util.Assert; import java.time.ZoneOffset; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -48,7 +47,8 @@ public record TaskStatus(TaskState state, @Nullable Message message, OffsetDateT * @param message optional status message * @param timestamp the status timestamp */ - public TaskStatus(TaskState state, @Nullable Message message, @Nullable OffsetDateTime timestamp){ + public TaskStatus( + TaskState state, @Nullable Message message, @Nullable OffsetDateTime timestamp) { this.state = Assert.checkNotNullParam("state", state); this.timestamp = timestamp == null ? OffsetDateTime.now(ZoneOffset.UTC) : timestamp; this.message = message; @@ -62,6 +62,7 @@ public TaskStatus(TaskState state, @Nullable Message message, @Nullable OffsetDa public OffsetDateTime timestamp() { return timestamp; } + /** * Creates a TaskStatus with only a state, using the current UTC time as the timestamp. *

    diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TaskStatusUpdateEvent.java b/spec/src/main/java/org/a2aproject/sdk/spec/TaskStatusUpdateEvent.java index a2ec7fe82..6897f06ab 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskStatusUpdateEvent.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskStatusUpdateEvent.java @@ -1,11 +1,10 @@ package org.a2aproject.sdk.spec; -import org.a2aproject.sdk.util.Assert; +import java.util.Map; import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; -import java.util.Map; - /** * An event sent by the agent to notify the client of a change in a task's status. * This is typically used in streaming or subscription models. @@ -15,8 +14,9 @@ * @param contextId the context identifier (required) * @param metadata additional metadata (optional) */ -public record TaskStatusUpdateEvent(String taskId, TaskStatus status, String contextId, - @Nullable Map metadata) implements EventKind, StreamingEventKind, UpdateEvent { +public record TaskStatusUpdateEvent( + String taskId, TaskStatus status, String contextId, @Nullable Map metadata) + implements EventKind, StreamingEventKind, UpdateEvent { /** * The identifier when used in streaming responses @@ -89,8 +89,7 @@ public static class Builder { private @Nullable String contextId; private @Nullable Map metadata; - private Builder() { - } + private Builder() {} private Builder(TaskStatusUpdateEvent existingTaskStatusUpdateEvent) { this.taskId = existingTaskStatusUpdateEvent.taskId; diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/TextPart.java b/spec/src/main/java/org/a2aproject/sdk/spec/TextPart.java index 075f93163..4a8e2afd6 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/TextPart.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/TextPart.java @@ -1,12 +1,10 @@ package org.a2aproject.sdk.spec; - -import org.a2aproject.sdk.util.Assert; -import org.a2aproject.sdk.spec.util.CollectionCopies; import java.util.Map; +import org.a2aproject.sdk.spec.util.CollectionCopies; +import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; - /** * Represents a plain text content part within a {@link Message} or {@link Artifact}. *

    @@ -28,7 +26,8 @@ * @see Message * @see Artifact */ -public record TextPart(String text, @Nullable Map metadata) implements Part { +public record TextPart(String text, @Nullable Map metadata) + implements Part { /** * The JSON member name discriminator for text parts: "text". @@ -44,7 +43,7 @@ public record TextPart(String text, @Nullable Map metadata) impl * @param text the text content (required, must not be null) * @throws IllegalArgumentException if text is null */ - public TextPart (String text, @Nullable Map metadata) { + public TextPart(String text, @Nullable Map metadata) { Assert.checkNotNullParam("text", text); this.metadata = CollectionCopies.unmodifiableNullableShallowMap(metadata); this.text = text; @@ -56,7 +55,7 @@ public TextPart (String text, @Nullable Map metadata) { * @param text the text content (required, must not be null) * @throws IllegalArgumentException if data is null */ - public TextPart (String text){ + public TextPart(String text) { this(text, null); } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/UnsupportedOperationError.java b/spec/src/main/java/org/a2aproject/sdk/spec/UnsupportedOperationError.java index 345c86695..d102e49f5 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/UnsupportedOperationError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/UnsupportedOperationError.java @@ -3,7 +3,6 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; /** @@ -43,7 +42,10 @@ public class UnsupportedOperationError extends A2AProtocolError { * @param message the error message (defaults to "This operation is not supported" if null) * @param details additional error details (optional) */ - public UnsupportedOperationError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public UnsupportedOperationError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.UNSUPPORTED_OPERATION.code()), defaultIfNull(message, "This operation is not supported"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/UpdateEvent.java b/spec/src/main/java/org/a2aproject/sdk/spec/UpdateEvent.java index 386e3173a..e94101e49 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/UpdateEvent.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/UpdateEvent.java @@ -20,5 +20,4 @@ * @see TaskStatusUpdateEvent * @see TaskArtifactUpdateEvent */ -public sealed interface UpdateEvent permits TaskStatusUpdateEvent, TaskArtifactUpdateEvent { -} +public sealed interface UpdateEvent permits TaskStatusUpdateEvent, TaskArtifactUpdateEvent {} diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/VersionNotSupportedError.java b/spec/src/main/java/org/a2aproject/sdk/spec/VersionNotSupportedError.java index 75f37e9de..6d9e85cb9 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/VersionNotSupportedError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/VersionNotSupportedError.java @@ -3,10 +3,8 @@ import static org.a2aproject.sdk.spec.util.Utils.defaultIfNull; import java.util.Map; - import org.jspecify.annotations.Nullable; - /** * A2A Protocol error indicating that the A2A protocol version specified in the request * is not supported by the agent. @@ -38,7 +36,10 @@ public class VersionNotSupportedError extends A2AProtocolError { * @param message the error message (defaults to standard message if null) * @param details additional error details (optional) */ - public VersionNotSupportedError(@Nullable Integer code, @Nullable String message, @Nullable Map details) { + public VersionNotSupportedError( + @Nullable Integer code, + @Nullable String message, + @Nullable Map details) { super( defaultIfNull(code, A2AErrorCodes.VERSION_NOT_SUPPORTED.code()), defaultIfNull(message, "Protocol version not supported"), diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/util/CollectionCopies.java b/spec/src/main/java/org/a2aproject/sdk/spec/util/CollectionCopies.java index 62986b0b1..27dce8d0c 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/util/CollectionCopies.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/util/CollectionCopies.java @@ -1,12 +1,11 @@ package org.a2aproject.sdk.spec.util; -import org.jspecify.annotations.Nullable; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; /** * Utility methods for creating defensive copies of collection fields. @@ -22,8 +21,7 @@ */ public final class CollectionCopies { - private CollectionCopies() { - } + private CollectionCopies() {} /** * Creates an immutable defensive copy of a non-null list. @@ -131,7 +129,8 @@ public static Map unmodifiableShallowMap(Map map) { * @return {@code null} if the source is {@code null}, otherwise an unmodifiable shallow copy * preserving null values */ - public static @Nullable Map unmodifiableNullableShallowMap(@Nullable Map map) { + public static @Nullable Map unmodifiableNullableShallowMap( + @Nullable Map map) { return map == null ? null : unmodifiableShallowMap(map); } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/util/ErrorDetail.java b/spec/src/main/java/org/a2aproject/sdk/spec/util/ErrorDetail.java index 8fbf241bd..c91716beb 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/util/ErrorDetail.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/util/ErrorDetail.java @@ -1,8 +1,7 @@ package org.a2aproject.sdk.spec.util; -import java.util.Map; - import com.google.gson.annotations.SerializedName; +import java.util.Map; import org.a2aproject.sdk.util.Assert; import org.jspecify.annotations.Nullable; diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/util/PageToken.java b/spec/src/main/java/org/a2aproject/sdk/spec/util/PageToken.java index f0c017115..d93a87abc 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/util/PageToken.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/util/PageToken.java @@ -1,7 +1,6 @@ package org.a2aproject.sdk.spec.util; import java.time.Instant; - import org.a2aproject.sdk.spec.InvalidParamsError; import org.jspecify.annotations.Nullable; @@ -37,20 +36,21 @@ public record PageToken(Instant timestamp, String id) { String[] tokenParts = tokenStr.split(":", 2); if (tokenParts.length != 2) { - throw new InvalidParamsError(null, - "Invalid pageToken format: expected 'timestamp:id'", null); + throw new InvalidParamsError( + null, "Invalid pageToken format: expected 'timestamp:id'", null); } try { long timestampMillis = Long.parseLong(tokenParts[0]); String id = tokenParts[1]; if (id.isEmpty()) { - throw new InvalidParamsError(null, "Invalid pageToken format: id part cannot be empty", null); + throw new InvalidParamsError( + null, "Invalid pageToken format: id part cannot be empty", null); } return new PageToken(Instant.ofEpochMilli(timestampMillis), id); } catch (NumberFormatException e) { - throw new InvalidParamsError(null, - "Invalid pageToken format: timestamp must be numeric milliseconds", null); + throw new InvalidParamsError( + null, "Invalid pageToken format: timestamp must be numeric milliseconds", null); } } diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/util/Utils.java b/spec/src/main/java/org/a2aproject/sdk/spec/util/Utils.java index d05e387a2..28a15aff9 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/util/Utils.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/util/Utils.java @@ -2,6 +2,7 @@ import static org.a2aproject.sdk.util.Assert.checkNotNullParam; +import com.google.gson.Gson; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -10,8 +11,6 @@ import java.util.Map; import java.util.logging.Logger; import java.util.regex.Pattern; - -import com.google.gson.Gson; import org.a2aproject.sdk.spec.A2AClientException; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; @@ -46,7 +45,8 @@ public class Utils { /** Matches both {@code /.well-known/agent-card.json} and {@code /.well-known/{tenant}/agent-card.json} at end of URL. * Tenant characters are constrained to the same set enforced by {@link #validateTenant}: {@code [a-zA-Z0-9_.-]}. */ - private static final Pattern WELL_KNOWN_SUFFIX = Pattern.compile("/\\.well-known/([a-zA-Z0-9_.\\-]+/)?agent-card\\.json$"); + private static final Pattern WELL_KNOWN_SUFFIX = + Pattern.compile("/\\.well-known/([a-zA-Z0-9_.\\-]+/)?agent-card\\.json$"); /** Validates that a (normalized) tenant identifier contains only safe characters: {@code [a-zA-Z0-9_.-]}. */ private static final Pattern VALID_TENANT_CHARS = Pattern.compile("^[a-zA-Z0-9_.\\-]+$"); @@ -113,9 +113,11 @@ public static void rethrow(Throwable t) throws T { * @see TaskArtifactUpdateEvent for streaming artifact updates * @see Artifact for artifact structure */ - public static Task appendArtifactToTask(Task task, TaskArtifactUpdateEvent event, String taskId) { + public static Task appendArtifactToTask( + Task task, TaskArtifactUpdateEvent event, String taskId) { // Append artifacts - List artifacts = task.artifacts() == null ? new ArrayList<>() : new ArrayList<>(task.artifacts()); + List artifacts = + task.artifacts() == null ? new ArrayList<>() : new ArrayList<>(task.artifacts()); Artifact newArtifact = event.artifact(); String artifactId = newArtifact.artifactId(); @@ -137,18 +139,23 @@ public static Task appendArtifactToTask(Task task, TaskArtifactUpdateEvent event // This represents the first chunk for this artifact index if (existingArtifactIndex >= 0) { // Replace the existing artifact entirely with the new artifact - LOGGER.fine(String.format("Replacing artifact at id %s for task %s", artifactId, taskId)); + LOGGER.fine( + String.format( + "Replacing artifact at id %s for task %s", artifactId, taskId)); artifacts.set(existingArtifactIndex, newArtifact); } else { // Append the new artifact since no artifact with this id/index exists yet - LOGGER.fine(String.format("Adding artifact at id %s for task %s", artifactId, taskId)); + LOGGER.fine( + String.format("Adding artifact at id %s for task %s", artifactId, taskId)); artifacts.add(newArtifact); } } else if (existingArtifact != null) { // Append new parts to the existing artifact's parts list // Do this to a copy - LOGGER.fine(String.format("Appending parts to artifact id %s for task %s", artifactId, taskId)); + LOGGER.fine( + String.format( + "Appending parts to artifact id %s for task %s", artifactId, taskId)); List> parts = new ArrayList<>(existingArtifact.parts()); parts.addAll(newArtifact.parts()); @@ -163,23 +170,23 @@ public static Task appendArtifactToTask(Task task, TaskArtifactUpdateEvent event } } - Artifact updated = Artifact.builder(existingArtifact) - .parts(parts) - .metadata(mergedMetadata) - .build(); + Artifact updated = + Artifact.builder(existingArtifact) + .parts(parts) + .metadata(mergedMetadata) + .build(); artifacts.set(existingArtifactIndex, updated); } else { // We received a chunk to append, but we don't have an existing artifact. // We will ignore this chunk LOGGER.warning( - String.format("Received append=true for nonexistent artifact index for artifact %s in task %s. Ignoring chunk.", + String.format( + "Received append=true for nonexistent artifact index for artifact %s in" + + " task %s. Ignoring chunk.", artifactId, taskId)); } - return Task.builder(task) - .artifacts(artifacts) - .build(); - + return Task.builder(task).artifacts(artifacts).build(); } /** @@ -259,7 +266,8 @@ private static String stripTrailingSlash(String s) { * @return the first defined URL in the supported interaces of the agent card. * @throws A2AClientException if no server interface is available in the AgentCard */ - public static AgentInterface getFavoriteInterface(AgentCard agentCard) throws A2AClientException { + public static AgentInterface getFavoriteInterface(AgentCard agentCard) + throws A2AClientException { if (agentCard.supportedInterfaces() == null || agentCard.supportedInterfaces().isEmpty()) { throw new A2AClientException("No server interface available in the AgentCard"); } @@ -291,7 +299,8 @@ public static void validateTenant(@Nullable String tenant) { } if (stripped.length() > MAX_TENANT_LENGTH) { - throw new IllegalArgumentException("Tenant exceeds maximum length of " + MAX_TENANT_LENGTH + " characters"); + throw new IllegalArgumentException( + "Tenant exceeds maximum length of " + MAX_TENANT_LENGTH + " characters"); } if (!VALID_TENANT_CHARS.matcher(stripped).matches()) { diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/util/package-info.java b/spec/src/main/java/org/a2aproject/sdk/spec/util/package-info.java index c5023070e..384674085 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/util/package-info.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/util/package-info.java @@ -5,4 +5,3 @@ package org.a2aproject.sdk.spec.util; import org.jspecify.annotations.NullMarked; - diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/AgentCapabilitiesTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/AgentCapabilitiesTest.java index 938bb4b75..476c1b91e 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/AgentCapabilitiesTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/AgentCapabilitiesTest.java @@ -6,7 +6,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; - import org.junit.jupiter.api.Test; class AgentCapabilitiesTest { @@ -14,13 +13,28 @@ class AgentCapabilitiesTest { @Test void testConstruction_defensivelyCopiesExtensions() { List extensions = new ArrayList<>(); - extensions.add(new AgentExtension("Custom auth", Map.of("authType", "bearer"), true, "https://example.com/ext")); + extensions.add( + new AgentExtension( + "Custom auth", + Map.of("authType", "bearer"), + true, + "https://example.com/ext")); AgentCapabilities capabilities = new AgentCapabilities(true, false, true, extensions); - extensions.add(new AgentExtension("Another", Map.of("x", "y"), false, "https://example.com/ext2")); + extensions.add( + new AgentExtension("Another", Map.of("x", "y"), false, "https://example.com/ext2")); assertEquals(1, capabilities.extensions().size()); - assertThrows(UnsupportedOperationException.class, () -> capabilities.extensions().add( - new AgentExtension("Mutated", Map.of(), false, "https://example.com/ext3"))); + assertThrows( + UnsupportedOperationException.class, + () -> + capabilities + .extensions() + .add( + new AgentExtension( + "Mutated", + Map.of(), + false, + "https://example.com/ext3"))); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/AgentCardSignatureTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/AgentCardSignatureTest.java index 186dcc884..02dc0ba4f 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/AgentCardSignatureTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/AgentCardSignatureTest.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.Map; - import org.junit.jupiter.api.Test; class AgentCardSignatureTest { @@ -34,6 +33,7 @@ void testConstruction_preservesNullHeaderValues() { assertEquals(1, signature.header().size()); assertTrue(signature.header().containsKey("foo")); assertNull(signature.header().get("foo")); - assertThrows(UnsupportedOperationException.class, () -> signature.header().put("bar", "baz")); + assertThrows( + UnsupportedOperationException.class, () -> signature.header().put("bar", "baz")); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/AgentCardTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/AgentCardTest.java index 3dc27aa19..3c5651bf1 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/AgentCardTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/AgentCardTest.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.List; - import org.junit.jupiter.api.Test; class AgentCardTest { @@ -17,48 +16,53 @@ void testConstruction_defensivelyCopiesCollections() { List defaultOutputModes = new ArrayList<>(); defaultOutputModes.add("text"); List skills = new ArrayList<>(); - skills.add(AgentSkill.builder() - .id("weather_query") - .name("Weather Queries") - .description("Get weather information") - .tags(List.of("weather")) - .build()); + skills.add( + AgentSkill.builder() + .id("weather_query") + .name("Weather Queries") + .description("Get weather information") + .tags(List.of("weather")) + .build()); List supportedInterfaces = new ArrayList<>(); - supportedInterfaces.add(new AgentInterface("JSONRPC", "http://localhost:9999", null, "1.0")); + supportedInterfaces.add( + new AgentInterface("JSONRPC", "http://localhost:9999", null, "1.0")); - AgentCard card = new AgentCard( - "Weather Agent", - "Provides weather information", - null, - "1.0.0", - null, - AgentCapabilities.builder().streaming(true).build(), - defaultInputModes, - defaultOutputModes, - skills, - null, - null, - null, - supportedInterfaces, - null, - null, - null, - null); + AgentCard card = + new AgentCard( + "Weather Agent", + "Provides weather information", + null, + "1.0.0", + null, + AgentCapabilities.builder().streaming(true).build(), + defaultInputModes, + defaultOutputModes, + skills, + null, + null, + null, + supportedInterfaces, + null, + null, + null, + null); defaultInputModes.add("audio"); defaultOutputModes.add("audio"); - skills.add(AgentSkill.builder() - .id("translate") - .name("Translation") - .description("Translate text") - .tags(List.of("translate")) - .build()); + skills.add( + AgentSkill.builder() + .id("translate") + .name("Translation") + .description("Translate text") + .tags(List.of("translate")) + .build()); supportedInterfaces.add(new AgentInterface("GRPC", "grpc://localhost:9090", null, "1.0")); assertEquals(List.of("text"), card.defaultInputModes()); assertEquals(List.of("text"), card.defaultOutputModes()); assertEquals(1, card.skills().size()); assertEquals(1, card.supportedInterfaces().size()); - assertThrows(UnsupportedOperationException.class, () -> card.defaultInputModes().add("video")); + assertThrows( + UnsupportedOperationException.class, () -> card.defaultInputModes().add("video")); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/AgentExtensionTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/AgentExtensionTest.java index 280d6d876..bdf65dccb 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/AgentExtensionTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/AgentExtensionTest.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.Map; - import org.junit.jupiter.api.Test; class AgentExtensionTest { @@ -17,7 +16,8 @@ void testConstruction_defensivelyCopiesParams() { Map params = new HashMap<>(); params.put("authType", "bearer"); - AgentExtension extension = new AgentExtension("Custom auth", params, true, "https://example.com/ext"); + AgentExtension extension = + new AgentExtension("Custom auth", params, true, "https://example.com/ext"); params.put("mutated", true); assertEquals(Map.of("authType", "bearer"), extension.params()); @@ -29,11 +29,13 @@ void testConstruction_preservesNullParamsValues() { Map params = new HashMap<>(); params.put("foo", null); - AgentExtension extension = new AgentExtension("Custom auth", params, true, "https://example.com/ext"); + AgentExtension extension = + new AgentExtension("Custom auth", params, true, "https://example.com/ext"); assertEquals(1, extension.params().size()); assertTrue(extension.params().containsKey("foo")); assertNull(extension.params().get("foo")); - assertThrows(UnsupportedOperationException.class, () -> extension.params().put("bar", "baz")); + assertThrows( + UnsupportedOperationException.class, () -> extension.params().put("bar", "baz")); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/AgentSkillTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/AgentSkillTest.java index 39e8c2c22..02a49a9d2 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/AgentSkillTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/AgentSkillTest.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.List; - import org.junit.jupiter.api.Test; class AgentSkillTest { @@ -21,15 +20,16 @@ void testConstruction_defensivelyCopiesCollections() { List outputModes = new ArrayList<>(); outputModes.add("text"); - AgentSkill skill = new AgentSkill( - "weather_query", - "Weather Queries", - "Get weather information", - tags, - examples, - inputModes, - outputModes, - null); + AgentSkill skill = + new AgentSkill( + "weather_query", + "Weather Queries", + "Get weather information", + tags, + examples, + inputModes, + outputModes, + null); tags.add("mutated"); examples.add("mutated"); diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/ArtifactTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/ArtifactTest.java index a2fc45ae1..286839302 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/ArtifactTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/ArtifactTest.java @@ -9,7 +9,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.junit.jupiter.api.Test; class ArtifactTest { @@ -31,7 +30,8 @@ void testConstruction_defensivelyCopiesCollections() { assertEquals(1, artifact.parts().size()); assertEquals(Map.of("source", "web"), artifact.metadata()); assertEquals(List.of("ext-1"), artifact.extensions()); - assertThrows(UnsupportedOperationException.class, () -> artifact.parts().add(new TextPart("x"))); + assertThrows( + UnsupportedOperationException.class, () -> artifact.parts().add(new TextPart("x"))); } @Test @@ -45,6 +45,7 @@ void testConstruction_preservesNullMetadataValues() { assertEquals(1, artifact.metadata().size()); assertTrue(artifact.metadata().containsKey("foo")); assertNull(artifact.metadata().get("foo")); - assertThrows(UnsupportedOperationException.class, () -> artifact.metadata().put("bar", "baz")); + assertThrows( + UnsupportedOperationException.class, () -> artifact.metadata().put("bar", "baz")); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/AuthorizationCodeOAuthFlowTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/AuthorizationCodeOAuthFlowTest.java index 884138ef0..1d91252f1 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/AuthorizationCodeOAuthFlowTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/AuthorizationCodeOAuthFlowTest.java @@ -5,7 +5,6 @@ import java.util.HashMap; import java.util.Map; - import org.junit.jupiter.api.Test; class AuthorizationCodeOAuthFlowTest { @@ -15,12 +14,13 @@ void testConstruction_defensivelyCopiesScopes() { Map scopes = new HashMap<>(); scopes.put("read", "Read access"); - AuthorizationCodeOAuthFlow flow = new AuthorizationCodeOAuthFlow( - "https://auth.example.com/authorize", - "https://auth.example.com/refresh", - scopes, - "https://auth.example.com/token", - true); + AuthorizationCodeOAuthFlow flow = + new AuthorizationCodeOAuthFlow( + "https://auth.example.com/authorize", + "https://auth.example.com/refresh", + scopes, + "https://auth.example.com/token", + true); scopes.put("write", "Write access"); diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/CancelTaskParamsTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/CancelTaskParamsTest.java index d009dd1b3..59f8e2b58 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/CancelTaskParamsTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/CancelTaskParamsTest.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.Map; - import org.junit.jupiter.api.Test; class CancelTaskParamsTest { @@ -29,15 +28,17 @@ void testBuilderPreservesNullMetadataValues() { Map metadata = new HashMap<>(); metadata.put("foo", null); - CancelTaskParams params = CancelTaskParams.builder() - .id("task-1") - .tenant("tenant-1") - .metadata(metadata) - .build(); + CancelTaskParams params = + CancelTaskParams.builder() + .id("task-1") + .tenant("tenant-1") + .metadata(metadata) + .build(); assertEquals(1, params.metadata().size()); assertTrue(params.metadata().containsKey("foo")); assertNull(params.metadata().get("foo")); - assertThrows(UnsupportedOperationException.class, () -> params.metadata().put("bar", "baz")); + assertThrows( + UnsupportedOperationException.class, () -> params.metadata().put("bar", "baz")); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/ClientCredentialsOAuthFlowTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/ClientCredentialsOAuthFlowTest.java index 59db8b961..f63856e1b 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/ClientCredentialsOAuthFlowTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/ClientCredentialsOAuthFlowTest.java @@ -5,7 +5,6 @@ import java.util.HashMap; import java.util.Map; - import org.junit.jupiter.api.Test; class ClientCredentialsOAuthFlowTest { @@ -15,10 +14,11 @@ void testConstruction_defensivelyCopiesScopes() { Map scopes = new HashMap<>(); scopes.put("read", "Read access"); - ClientCredentialsOAuthFlow flow = new ClientCredentialsOAuthFlow( - "https://auth.example.com/refresh", - scopes, - "https://auth.example.com/token"); + ClientCredentialsOAuthFlow flow = + new ClientCredentialsOAuthFlow( + "https://auth.example.com/refresh", + scopes, + "https://auth.example.com/token"); scopes.put("write", "Write access"); diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/DataPartTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/DataPartTest.java index 1c75d4bfd..6bdce6830 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/DataPartTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/DataPartTest.java @@ -11,15 +11,17 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.junit.jupiter.api.Test; class DataPartTest { @Test void testFromJson_object() { - DataPart part = DataPart.fromJson(""" - {"temperature": 22.5, "humidity": 65}"""); + DataPart part = + DataPart.fromJson( + """ + {"temperature": 22.5, "humidity": 65}\ + """); Map data = assertInstanceOf(Map.class, part.data()); assertEquals(22.5, data.get("temperature")); @@ -29,8 +31,11 @@ void testFromJson_object() { @Test void testFromJson_array() { - DataPart part = DataPart.fromJson(""" - ["a", "b", "c"]"""); + DataPart part = + DataPart.fromJson( + """ + ["a", "b", "c"]\ + """); List data = assertInstanceOf(List.class, part.data()); assertEquals(List.of("a", "b", "c"), data); @@ -67,8 +72,12 @@ void testFromJson_boolean() { @Test void testFromJson_withMetadata() { Map metadata = Map.of("source", "sensor"); - DataPart part = DataPart.fromJson(""" - {"temperature": 22.5}""", metadata); + DataPart part = + DataPart.fromJson( + """ + {"temperature": 22.5}\ + """, + metadata); assertInstanceOf(Map.class, part.data()); assertEquals("sensor", part.metadata().get("source")); @@ -83,13 +92,17 @@ void testConstructor_withNullMetadataValuePreservesValueAndIsImmutable() { assertTrue(part.metadata().containsKey("source")); assertNull(part.metadata().get("source")); - assertThrows(UnsupportedOperationException.class, () -> part.metadata().put("another", "value")); + assertThrows( + UnsupportedOperationException.class, () -> part.metadata().put("another", "value")); } @Test void testFromJson_nestedObject() { - DataPart part = DataPart.fromJson(""" - {"outer": {"inner": [1, 2, 3]}}"""); + DataPart part = + DataPart.fromJson( + """ + {"outer": {"inner": [1, 2, 3]}}\ + """); Map data = assertInstanceOf(Map.class, part.data()); Map outer = assertInstanceOf(Map.class, data.get("outer")); @@ -119,7 +132,9 @@ void testDataMapIsDefensivelyCopiedAndImmutable() { DataPart part = new DataPart(data); - assertThrows(UnsupportedOperationException.class, () -> ((Map) part.data()).put("humidity", 65)); + assertThrows( + UnsupportedOperationException.class, + () -> ((Map) part.data()).put("humidity", 65)); data.put("humidity", 65); assertEquals(Map.of("temperature", 22.5), part.data()); } @@ -131,7 +146,8 @@ void testDataListIsDefensivelyCopiedAndImmutable() { DataPart part = new DataPart(data); - assertThrows(UnsupportedOperationException.class, () -> ((List) part.data()).add("b")); + assertThrows( + UnsupportedOperationException.class, () -> ((List) part.data()).add("b")); data.add("b"); assertEquals(List.of("a"), part.data()); } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/DeviceCodeOAuthFlowTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/DeviceCodeOAuthFlowTest.java index 3368aa2cb..ba902ca83 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/DeviceCodeOAuthFlowTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/DeviceCodeOAuthFlowTest.java @@ -6,7 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Map; - import org.junit.jupiter.api.Test; /** @@ -22,11 +21,13 @@ class DeviceCodeOAuthFlowTest { private static final String DEVICE_AUTH_URL = "https://auth.example.com/device/code"; private static final String TOKEN_URL = "https://auth.example.com/token"; private static final String REFRESH_URL = "https://auth.example.com/refresh"; - private static final Map SCOPES = Map.of("read", "Read access", "write", "Write access"); + private static final Map SCOPES = + Map.of("read", "Read access", "write", "Write access"); @Test void testConstruction_withAllFields() { - DeviceCodeOAuthFlow flow = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, SCOPES); + DeviceCodeOAuthFlow flow = + new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, SCOPES); assertEquals(DEVICE_AUTH_URL, flow.deviceAuthorizationUrl()); assertEquals(TOKEN_URL, flow.tokenUrl()); @@ -36,7 +37,8 @@ void testConstruction_withAllFields() { @Test void testConstruction_withNullRefreshUrl() { - DeviceCodeOAuthFlow flow = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, SCOPES); + DeviceCodeOAuthFlow flow = + new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, SCOPES); assertEquals(DEVICE_AUTH_URL, flow.deviceAuthorizationUrl()); assertEquals(TOKEN_URL, flow.tokenUrl()); @@ -47,7 +49,8 @@ void testConstruction_withNullRefreshUrl() { @Test void testConstruction_withEmptyScopes() { Map emptyScopes = Map.of(); - DeviceCodeOAuthFlow flow = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, emptyScopes); + DeviceCodeOAuthFlow flow = + new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, emptyScopes); assertEquals(DEVICE_AUTH_URL, flow.deviceAuthorizationUrl()); assertEquals(TOKEN_URL, flow.tokenUrl()); @@ -57,28 +60,35 @@ void testConstruction_withEmptyScopes() { @Test void testConstruction_nullDeviceAuthorizationUrl_throwsException() { - assertThrows(IllegalArgumentException.class, + assertThrows( + IllegalArgumentException.class, () -> new DeviceCodeOAuthFlow(null, TOKEN_URL, REFRESH_URL, SCOPES)); } @Test void testConstruction_nullTokenUrl_throwsException() { - assertThrows(IllegalArgumentException.class, + assertThrows( + IllegalArgumentException.class, () -> new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, null, REFRESH_URL, SCOPES)); } @Test void testConstruction_nullScopes_throwsException() { - assertThrows(IllegalArgumentException.class, + assertThrows( + IllegalArgumentException.class, () -> new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, null)); } @Test void testEqualityAndHashCode() { - DeviceCodeOAuthFlow flow1 = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, SCOPES); - DeviceCodeOAuthFlow flow2 = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, SCOPES); - DeviceCodeOAuthFlow flow3 = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, SCOPES); - DeviceCodeOAuthFlow flow4 = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, SCOPES); + DeviceCodeOAuthFlow flow1 = + new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, SCOPES); + DeviceCodeOAuthFlow flow2 = + new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, SCOPES); + DeviceCodeOAuthFlow flow3 = + new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, SCOPES); + DeviceCodeOAuthFlow flow4 = + new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, null, SCOPES); // Test for equality and hashCode consistency assertEquals(flow1, flow2); @@ -88,7 +98,9 @@ void testEqualityAndHashCode() { // Test for inequality with different field values assertNotEquals(flow1, flow3); - assertNotEquals(flow1, new DeviceCodeOAuthFlow("https://other.com", TOKEN_URL, REFRESH_URL, SCOPES)); + assertNotEquals( + flow1, + new DeviceCodeOAuthFlow("https://other.com", TOKEN_URL, REFRESH_URL, SCOPES)); assertNotEquals(flow1, null); assertNotEquals(flow1, "not a flow"); } @@ -97,12 +109,17 @@ void testEqualityAndHashCode() { void testScopesImmutability() { Map mutableScopes = new java.util.HashMap<>(); mutableScopes.put("read", "Read access"); - DeviceCodeOAuthFlow flow = new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, mutableScopes); + DeviceCodeOAuthFlow flow = + new DeviceCodeOAuthFlow(DEVICE_AUTH_URL, TOKEN_URL, REFRESH_URL, mutableScopes); // Modifying the original map should not affect the record mutableScopes.put("write", "Write access"); - assertNotEquals(mutableScopes.size(), flow.scopes().size(), + assertNotEquals( + mutableScopes.size(), + flow.scopes().size(), "Record should be immutable and perform a defensive copy of the scopes map"); - assertThrows(UnsupportedOperationException.class, () -> flow.scopes().put("admin", "Admin access")); + assertThrows( + UnsupportedOperationException.class, + () -> flow.scopes().put("admin", "Admin access")); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/FilePartTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/FilePartTest.java index d50f363ac..e0f2a78b1 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/FilePartTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/FilePartTest.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.Map; - import org.junit.jupiter.api.Test; class FilePartTest { @@ -17,12 +16,16 @@ void testConstructor_withNullMetadataValuePreservesValueAndIsImmutable() { Map metadata = new HashMap<>(); metadata.put("source", null); - FilePart part = new FilePart(new FileWithUri("text/plain", "example.txt", "https://example.com/example.txt"), - metadata); + FilePart part = + new FilePart( + new FileWithUri( + "text/plain", "example.txt", "https://example.com/example.txt"), + metadata); assertEquals("example.txt", part.file().name()); assertTrue(part.metadata().containsKey("source")); assertNull(part.metadata().get("source")); - assertThrows(UnsupportedOperationException.class, () -> part.metadata().put("another", "value")); + assertThrows( + UnsupportedOperationException.class, () -> part.metadata().put("another", "value")); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/FileWithBytesTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/FileWithBytesTest.java index 53ea9268b..2544c7ee7 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/FileWithBytesTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/FileWithBytesTest.java @@ -12,7 +12,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Base64; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -38,8 +37,7 @@ class FileWithBytesTest { private static final String SVG_MIME_TYPE = "image/svg+xml"; private static final String SVG_RESOURCE = "/a2a-logo-white.svg"; - @TempDir - Path tempDir; + @TempDir Path tempDir; private Path svgPath() throws URISyntaxException { return Path.of(getClass().getResource(SVG_RESOURCE).toURI()); @@ -151,7 +149,7 @@ void testByteArrayConstructor_emptyArray() throws IOException { @Test void testByteArrayConstructor_binaryContent() throws IOException { - byte[] content = new byte[]{0, 1, 2, (byte) 0xFF, (byte) 0xFE}; + byte[] content = new byte[] {0, 1, 2, (byte) 0xFF, (byte) 0xFE}; FileWithBytes fwb = new FileWithBytes("application/octet-stream", "bin.dat", content); @@ -204,8 +202,10 @@ void testFileConstructorMatchesCanonicalConstructor() throws IOException { void testPathConstructor_rejectsNonExistentFile() { Path nonExistent = tempDir.resolve("does-not-exist.txt"); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new FileWithBytes("text/plain", nonExistent)); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new FileWithBytes("text/plain", nonExistent)); assertTrue(exception.getMessage().contains("does not exist")); } @@ -215,8 +215,10 @@ void testPathConstructor_rejectsDirectory() throws IOException { Path directory = tempDir.resolve("subdir"); Files.createDirectory(directory); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new FileWithBytes("text/plain", directory)); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new FileWithBytes("text/plain", directory)); assertTrue(exception.getMessage().contains("Not a regular file")); } @@ -232,8 +234,10 @@ void testPathConstructor_rejectsTooLargeFile() throws IOException { } } - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new FileWithBytes("application/octet-stream", largeFile)); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> new FileWithBytes("application/octet-stream", largeFile)); assertTrue(exception.getMessage().contains("too large")); assertTrue(exception.getMessage().contains("maximum")); diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java index 030bf209e..d1dd96e90 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java @@ -9,7 +9,8 @@ class GetTaskPushNotificationConfigParamsTest { @Test void testConstructionAllowsOmittedConfigurationId() { - GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("task-1"); + GetTaskPushNotificationConfigParams params = + new GetTaskPushNotificationConfigParams("task-1"); assertEquals("task-1", params.taskId()); assertNull(params.id()); @@ -17,9 +18,8 @@ void testConstructionAllowsOmittedConfigurationId() { @Test void testBuilderAllowsOmittedConfigurationId() { - GetTaskPushNotificationConfigParams params = GetTaskPushNotificationConfigParams.builder() - .taskId("task-1") - .build(); + GetTaskPushNotificationConfigParams params = + GetTaskPushNotificationConfigParams.builder().taskId("task-1").build(); assertNull(params.id()); } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/MessageSendConfigurationTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/MessageSendConfigurationTest.java index 614f44a86..22eb285ac 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/MessageSendConfigurationTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/MessageSendConfigurationTest.java @@ -6,7 +6,6 @@ import java.util.ArrayList; import java.util.List; - import org.junit.jupiter.api.Test; class MessageSendConfigurationTest { @@ -14,13 +13,15 @@ class MessageSendConfigurationTest { @Test void testConstruction_withAllFields() { List acceptedOutputModes = List.of("text", "audio"); - TaskPushNotificationConfig config = TaskPushNotificationConfig.builder() - .id("id") - .taskId("task-123") - .url("https://example.com") - .build(); + TaskPushNotificationConfig config = + TaskPushNotificationConfig.builder() + .id("id") + .taskId("task-123") + .url("https://example.com") + .build(); - MessageSendConfiguration result = new MessageSendConfiguration(acceptedOutputModes, 3, config, Boolean.TRUE); + MessageSendConfiguration result = + new MessageSendConfiguration(acceptedOutputModes, 3, config, Boolean.TRUE); assertEquals(acceptedOutputModes, result.acceptedOutputModes()); assertEquals(Integer.valueOf(3), result.historyLength()); @@ -40,7 +41,8 @@ void testConstruction_withNullAcceptedOutputModes() { @Test void testConstruction_negativeHistoryLength_throwsException() { - assertThrows(IllegalArgumentException.class, + assertThrows( + IllegalArgumentException.class, () -> new MessageSendConfiguration(List.of("text"), -1, null, null)); } @@ -49,11 +51,14 @@ void testAcceptedOutputModes_areDefensivelyCopied() { List mutableModes = new ArrayList<>(); mutableModes.add("text"); - MessageSendConfiguration result = new MessageSendConfiguration(mutableModes, null, null, null); + MessageSendConfiguration result = + new MessageSendConfiguration(mutableModes, null, null, null); mutableModes.add("audio"); assertEquals(List.of("text"), result.acceptedOutputModes()); - assertThrows(UnsupportedOperationException.class, () -> result.acceptedOutputModes().add("video")); + assertThrows( + UnsupportedOperationException.class, + () -> result.acceptedOutputModes().add("video")); } @Test @@ -61,9 +66,8 @@ void testBuilderAcceptedOutputModes_areDefensivelyCopiedByRecord() { List mutableModes = new ArrayList<>(); mutableModes.add("text"); - MessageSendConfiguration result = MessageSendConfiguration.builder() - .acceptedOutputModes(mutableModes) - .build(); + MessageSendConfiguration result = + MessageSendConfiguration.builder().acceptedOutputModes(mutableModes).build(); mutableModes.add("audio"); assertEquals(List.of("text"), result.acceptedOutputModes()); diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/MessageSendParamsTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/MessageSendParamsTest.java index 81b0a782f..733bb6fbb 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/MessageSendParamsTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/MessageSendParamsTest.java @@ -1,25 +1,23 @@ package org.a2aproject.sdk.spec; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.HashMap; import java.util.List; import java.util.Map; - import org.junit.jupiter.api.Test; class MessageSendParamsTest { @Test void testConstruction_withAllFields() { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(new TextPart("hello")) - .build(); - MessageSendConfiguration configuration = new MessageSendConfiguration(List.of("text"), 2, null, Boolean.TRUE); + Message message = + Message.builder().role(Message.Role.ROLE_USER).parts(new TextPart("hello")).build(); + MessageSendConfiguration configuration = + new MessageSendConfiguration(List.of("text"), 2, null, Boolean.TRUE); Map metadata = Map.of("source", "web"); MessageSendParams params = new MessageSendParams(message, configuration, metadata); @@ -32,10 +30,8 @@ void testConstruction_withAllFields() { @Test void testConstruction_nullMetadata() { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(new TextPart("hello")) - .build(); + Message message = + Message.builder().role(Message.Role.ROLE_USER).parts(new TextPart("hello")).build(); MessageSendParams params = new MessageSendParams(message, null, null); @@ -49,10 +45,8 @@ void testConstruction_nullMessage_throwsException() { @Test void testMetadata_isDefensivelyCopied() { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(new TextPart("hello")) - .build(); + Message message = + Message.builder().role(Message.Role.ROLE_USER).parts(new TextPart("hello")).build(); Map mutableMetadata = new HashMap<>(); mutableMetadata.put("source", "web"); @@ -60,15 +54,15 @@ void testMetadata_isDefensivelyCopied() { mutableMetadata.put("write", "should-not-leak"); assertEquals(Map.of("source", "web"), params.metadata()); - assertThrows(UnsupportedOperationException.class, () -> params.metadata().put("another", "value")); + assertThrows( + UnsupportedOperationException.class, + () -> params.metadata().put("another", "value")); } @Test void testMetadata_preservesNullValues() { - Message message = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(new TextPart("hello")) - .build(); + Message message = + Message.builder().role(Message.Role.ROLE_USER).parts(new TextPart("hello")).build(); Map mutableMetadata = new HashMap<>(); mutableMetadata.put("foo", null); @@ -77,6 +71,8 @@ void testMetadata_preservesNullValues() { assertEquals(1, params.metadata().size()); assertTrue(params.metadata().containsKey("foo")); assertNull(params.metadata().get("foo")); - assertThrows(UnsupportedOperationException.class, () -> params.metadata().put("another", "value")); + assertThrows( + UnsupportedOperationException.class, + () -> params.metadata().put("another", "value")); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/SecurityRequirementTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/SecurityRequirementTest.java index f178414f5..2a15554de 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/SecurityRequirementTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/SecurityRequirementTest.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.junit.jupiter.api.Test; class SecurityRequirementTest { @@ -24,7 +23,11 @@ void testConstruction_defensivelyCopiesNestedScopes() { schemes.put("apiKey", List.of()); assertEquals(List.of("read"), requirement.schemes().get("oauth2")); - assertThrows(UnsupportedOperationException.class, () -> requirement.schemes().put("x", List.of())); - assertThrows(UnsupportedOperationException.class, () -> requirement.schemes().get("oauth2").add("x")); + assertThrows( + UnsupportedOperationException.class, + () -> requirement.schemes().put("x", List.of())); + assertThrows( + UnsupportedOperationException.class, + () -> requirement.schemes().get("oauth2").add("x")); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/TaskArtifactUpdateEventTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/TaskArtifactUpdateEventTest.java index d1734863d..417cd4bc1 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/TaskArtifactUpdateEventTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/TaskArtifactUpdateEventTest.java @@ -8,7 +8,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - import org.junit.jupiter.api.Test; class TaskArtifactUpdateEventTest { @@ -18,13 +17,20 @@ void testConstruction_defensivelyCopiesMetadata() { Map metadata = new HashMap<>(); metadata.put("source", "web"); - TaskArtifactUpdateEvent event = new TaskArtifactUpdateEvent( - "task-1", - new Artifact("artifact-1", null, null, List.of(new TextPart("hello")), null, null), - "context-1", - true, - false, - metadata); + TaskArtifactUpdateEvent event = + new TaskArtifactUpdateEvent( + "task-1", + new Artifact( + "artifact-1", + null, + null, + List.of(new TextPart("hello")), + null, + null), + "context-1", + true, + false, + metadata); metadata.put("write", "should-not-leak"); @@ -37,13 +43,20 @@ void testConstruction_preservesNullMetadataValues() { Map metadata = new HashMap<>(); metadata.put("foo", null); - TaskArtifactUpdateEvent event = new TaskArtifactUpdateEvent( - "task-1", - new Artifact("artifact-1", null, null, List.of(new TextPart("hello")), null, null), - "context-1", - true, - false, - metadata); + TaskArtifactUpdateEvent event = + new TaskArtifactUpdateEvent( + "task-1", + new Artifact( + "artifact-1", + null, + null, + List.of(new TextPart("hello")), + null, + null), + "context-1", + true, + false, + metadata); assertEquals(1, event.metadata().size()); assertTrue(event.metadata().containsKey("foo")); diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java index bf5216182..b22298e1a 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java @@ -9,10 +9,11 @@ class TaskPushNotificationConfigTest { @Test void builderAllowsAnOmittedConfigurationId() { - TaskPushNotificationConfig config = TaskPushNotificationConfig.builder() - .taskId("task-123") - .url("https://example.com/callback") - .build(); + TaskPushNotificationConfig config = + TaskPushNotificationConfig.builder() + .taskId("task-123") + .url("https://example.com/callback") + .build(); assertNull(config.id()); assertEquals("task-123", config.taskId()); diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/TaskStatusUpdateEventTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/TaskStatusUpdateEventTest.java index 364a14447..8f79e228d 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/TaskStatusUpdateEventTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/TaskStatusUpdateEventTest.java @@ -8,7 +8,6 @@ import java.util.HashMap; import java.util.Map; - import org.junit.jupiter.api.Test; class TaskStatusUpdateEventTest { @@ -21,7 +20,8 @@ void testConstruction_withAllFields() { Map metadata = Map.of("source", "sensor"); TaskStatus status = new TaskStatus(TaskState.TASK_STATE_WORKING); - TaskStatusUpdateEvent event = new TaskStatusUpdateEvent(TASK_ID, status, CONTEXT_ID, metadata); + TaskStatusUpdateEvent event = + new TaskStatusUpdateEvent(TASK_ID, status, CONTEXT_ID, metadata); assertEquals(TASK_ID, event.taskId()); assertEquals(status, event.status()); @@ -31,31 +31,42 @@ void testConstruction_withAllFields() { @Test void testConstruction_withNullMetadata() { - TaskStatusUpdateEvent event = new TaskStatusUpdateEvent( - TASK_ID, - new TaskStatus(TaskState.TASK_STATE_WORKING), - CONTEXT_ID, - null); + TaskStatusUpdateEvent event = + new TaskStatusUpdateEvent( + TASK_ID, new TaskStatus(TaskState.TASK_STATE_WORKING), CONTEXT_ID, null); assertNull(event.metadata()); } @Test void testConstruction_nullTaskId_throwsException() { - assertThrows(IllegalArgumentException.class, () -> - new TaskStatusUpdateEvent(null, new TaskStatus(TaskState.TASK_STATE_WORKING), CONTEXT_ID, Map.of())); + assertThrows( + IllegalArgumentException.class, + () -> + new TaskStatusUpdateEvent( + null, + new TaskStatus(TaskState.TASK_STATE_WORKING), + CONTEXT_ID, + Map.of())); } @Test void testConstruction_nullStatus_throwsException() { - assertThrows(IllegalArgumentException.class, () -> - new TaskStatusUpdateEvent(TASK_ID, null, CONTEXT_ID, Map.of())); + assertThrows( + IllegalArgumentException.class, + () -> new TaskStatusUpdateEvent(TASK_ID, null, CONTEXT_ID, Map.of())); } @Test void testConstruction_nullContextId_throwsException() { - assertThrows(IllegalArgumentException.class, () -> - new TaskStatusUpdateEvent(TASK_ID, new TaskStatus(TaskState.TASK_STATE_WORKING), null, Map.of())); + assertThrows( + IllegalArgumentException.class, + () -> + new TaskStatusUpdateEvent( + TASK_ID, + new TaskStatus(TaskState.TASK_STATE_WORKING), + null, + Map.of())); } @Test @@ -63,17 +74,20 @@ void testMetadataImmutability() { Map mutableMetadata = new HashMap<>(); mutableMetadata.put("source", "sensor"); - TaskStatusUpdateEvent event = TaskStatusUpdateEvent.builder() - .taskId(TASK_ID) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .contextId(CONTEXT_ID) - .metadata(mutableMetadata) - .build(); + TaskStatusUpdateEvent event = + TaskStatusUpdateEvent.builder() + .taskId(TASK_ID) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .contextId(CONTEXT_ID) + .metadata(mutableMetadata) + .build(); mutableMetadata.put("write", "should-not-leak"); assertEquals(Map.of("source", "sensor"), event.metadata()); - assertThrows(UnsupportedOperationException.class, () -> event.metadata().put("another", "value")); + assertThrows( + UnsupportedOperationException.class, + () -> event.metadata().put("another", "value")); } @Test @@ -81,11 +95,12 @@ void testConstruction_preservesNullMetadataValues() { Map metadata = new HashMap<>(); metadata.put("foo", null); - TaskStatusUpdateEvent event = new TaskStatusUpdateEvent( - TASK_ID, - new TaskStatus(TaskState.TASK_STATE_WORKING), - CONTEXT_ID, - metadata); + TaskStatusUpdateEvent event = + new TaskStatusUpdateEvent( + TASK_ID, + new TaskStatus(TaskState.TASK_STATE_WORKING), + CONTEXT_ID, + metadata); assertEquals(1, event.metadata().size()); assertTrue(event.metadata().containsKey("foo")); @@ -95,11 +110,12 @@ void testConstruction_preservesNullMetadataValues() { @Test void testBuilderFromExistingEvent_keepsEqualValues() { - TaskStatusUpdateEvent original = new TaskStatusUpdateEvent( - TASK_ID, - new TaskStatus(TaskState.TASK_STATE_WORKING), - CONTEXT_ID, - Map.of("source", "sensor")); + TaskStatusUpdateEvent original = + new TaskStatusUpdateEvent( + TASK_ID, + new TaskStatus(TaskState.TASK_STATE_WORKING), + CONTEXT_ID, + Map.of("source", "sensor")); TaskStatusUpdateEvent copied = TaskStatusUpdateEvent.builder(original).build(); diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/TextPartTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/TextPartTest.java index 3f1224674..a27037943 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/TextPartTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/TextPartTest.java @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.Map; - import org.junit.jupiter.api.Test; class TextPartTest { @@ -22,6 +21,7 @@ void testConstructor_withNullMetadataValuePreservesValueAndIsImmutable() { assertEquals("hello", part.text()); assertTrue(part.metadata().containsKey("language")); assertNull(part.metadata().get("language")); - assertThrows(UnsupportedOperationException.class, () -> part.metadata().put("another", "value")); + assertThrows( + UnsupportedOperationException.class, () -> part.metadata().put("another", "value")); } } diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/util/PageTokenTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/util/PageTokenTest.java index 90c968a2d..a395c241d 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/util/PageTokenTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/util/PageTokenTest.java @@ -7,7 +7,6 @@ import java.time.Instant; import java.util.stream.Stream; - import org.a2aproject.sdk.spec.InvalidParamsError; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -28,15 +27,17 @@ class PageTokenTest { */ static Stream validTokens() { return Stream.of( - Arguments.of("1640000000000:user123", 1640000000000L, "user123"), - Arguments.of("1640000000000:user:123:extra", 1640000000000L, "user:123:extra"), - Arguments.of("0:user123", 0L, "user123"), - Arguments.of("-1000:user123", -1000L, "user123"), - Arguments.of(Long.MAX_VALUE + ":user123", Long.MAX_VALUE, "user123"), - Arguments.of(Long.MIN_VALUE + ":user123", Long.MIN_VALUE, "user123"), - Arguments.of("1640000000000:" + "a".repeat(1000), 1640000000000L, "a".repeat(1000)), - Arguments.of("1640000000000:user-123_test@example.com", 1640000000000L, "user-123_test@example.com") - ); + Arguments.of("1640000000000:user123", 1640000000000L, "user123"), + Arguments.of("1640000000000:user:123:extra", 1640000000000L, "user:123:extra"), + Arguments.of("0:user123", 0L, "user123"), + Arguments.of("-1000:user123", -1000L, "user123"), + Arguments.of(Long.MAX_VALUE + ":user123", Long.MAX_VALUE, "user123"), + Arguments.of(Long.MIN_VALUE + ":user123", Long.MIN_VALUE, "user123"), + Arguments.of("1640000000000:" + "a".repeat(1000), 1640000000000L, "a".repeat(1000)), + Arguments.of( + "1640000000000:user-123_test@example.com", + 1640000000000L, + "user-123_test@example.com")); } /** @@ -44,7 +45,8 @@ static Stream validTokens() { */ @ParameterizedTest @MethodSource("validTokens") - void testFromString_validTokens_parseCorrectly(String tokenStr, long expectedTimestampMillis, String expectedId) { + void testFromString_validTokens_parseCorrectly( + String tokenStr, long expectedTimestampMillis, String expectedId) { PageToken token = PageToken.fromString(tokenStr); assertNotNull(token); @@ -72,14 +74,22 @@ void testFromString_nullOrEmpty_returnsNull(String tokenStr) { */ static Stream invalidTokenFormats() { return Stream.of( - Arguments.of("1640000000000user123", "Invalid pageToken format: expected 'timestamp:id'"), - Arguments.of("1640000000000", "Invalid pageToken format: expected 'timestamp:id'"), - Arguments.of("1640000000000:", "Invalid pageToken format: id part cannot be empty"), - Arguments.of(":", "Invalid pageToken format: timestamp must be numeric milliseconds"), - Arguments.of("notanumber:user123", "Invalid pageToken format: timestamp must be numeric milliseconds"), - Arguments.of("1640000000.123:user123", "Invalid pageToken format: timestamp must be numeric milliseconds"), - Arguments.of("1640 000 000:user123", "Invalid pageToken format: timestamp must be numeric milliseconds") - ); + Arguments.of( + "1640000000000user123", + "Invalid pageToken format: expected 'timestamp:id'"), + Arguments.of("1640000000000", "Invalid pageToken format: expected 'timestamp:id'"), + Arguments.of("1640000000000:", "Invalid pageToken format: id part cannot be empty"), + Arguments.of( + ":", "Invalid pageToken format: timestamp must be numeric milliseconds"), + Arguments.of( + "notanumber:user123", + "Invalid pageToken format: timestamp must be numeric milliseconds"), + Arguments.of( + "1640000000.123:user123", + "Invalid pageToken format: timestamp must be numeric milliseconds"), + Arguments.of( + "1640 000 000:user123", + "Invalid pageToken format: timestamp must be numeric milliseconds")); } /** @@ -87,10 +97,14 @@ static Stream invalidTokenFormats() { */ @ParameterizedTest @MethodSource("invalidTokenFormats") - void testFromString_invalidFormats_throwsInvalidParamsError(String tokenStr, String expectedErrorMessage) { - InvalidParamsError ex = assertThrows(InvalidParamsError.class, () -> { - PageToken.fromString(tokenStr); - }); + void testFromString_invalidFormats_throwsInvalidParamsError( + String tokenStr, String expectedErrorMessage) { + InvalidParamsError ex = + assertThrows( + InvalidParamsError.class, + () -> { + PageToken.fromString(tokenStr); + }); assertNotNull(ex.getMessage()); assertEquals(expectedErrorMessage, ex.getMessage()); } @@ -103,11 +117,10 @@ void testFromString_invalidFormats_throwsInvalidParamsError(String tokenStr, Str */ static Stream toStringTestData() { return Stream.of( - Arguments.of(1640000000000L, "user123", "1640000000000:user123"), - Arguments.of(1640000000000L, "user:123", "1640000000000:user:123"), - Arguments.of(0L, "user123", "0:user123"), - Arguments.of(-1000L, "user123", "-1000:user123") - ); + Arguments.of(1640000000000L, "user123", "1640000000000:user123"), + Arguments.of(1640000000000L, "user:123", "1640000000000:user:123"), + Arguments.of(0L, "user123", "0:user123"), + Arguments.of(-1000L, "user123", "-1000:user123")); } /** @@ -128,11 +141,10 @@ void testToString_formatsCorrectly(long timestampMillis, String id, String expec */ static Stream roundTripTokens() { return Stream.of( - "1640000000000:user123", - "1640000000000:user:123:extra", - "0:user123", - "-1000:user123" - ); + "1640000000000:user123", + "1640000000000:user:123:extra", + "0:user123", + "-1000:user123"); } /** diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/util/UtilsTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/util/UtilsTest.java index 2ae04ba14..6c7497d48 100644 --- a/spec/src/test/java/org/a2aproject/sdk/spec/util/UtilsTest.java +++ b/spec/src/test/java/org/a2aproject/sdk/spec/util/UtilsTest.java @@ -1,12 +1,11 @@ package org.a2aproject.sdk.spec.util; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import java.net.URISyntaxException; - import org.a2aproject.sdk.spec.AgentInterface; import org.junit.jupiter.api.Test; @@ -26,14 +25,16 @@ void testBuildBaseUrl_withAgentInterface_noTenant() { @Test void testBuildBaseUrl_withAgentInterface_withDefaultTenant() { - AgentInterface iface = new AgentInterface("JSONRPC", "http://example.com", "/default-tenant"); + AgentInterface iface = + new AgentInterface("JSONRPC", "http://example.com", "/default-tenant"); String url = Utils.buildBaseUrl(iface, null); assertEquals("http://example.com/default-tenant", url); } @Test void testBuildBaseUrl_withAgentInterface_withOverrideTenant() { - AgentInterface iface = new AgentInterface("JSONRPC", "http://example.com", "/default-tenant"); + AgentInterface iface = + new AgentInterface("JSONRPC", "http://example.com", "/default-tenant"); String url = Utils.buildBaseUrl(iface, "/custom-tenant"); assertEquals("http://example.com/custom-tenant", url); } @@ -54,40 +55,58 @@ void testBuildBaseUrl_withAgentInterface_urlWithoutTrailingSlash() { @Test void testBuildBaseUrl_withAgentInterface_nullInterface_throws() { - assertThrows(IllegalArgumentException.class, () -> { - Utils.buildBaseUrl((AgentInterface) null, "/tenant"); - }); + assertThrows( + IllegalArgumentException.class, + () -> { + Utils.buildBaseUrl((AgentInterface) null, "/tenant"); + }); } // ========== Security Validation Tests ========== @Test void testValidateTenant_pathTraversal_throws() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> { - AgentInterface iface = new AgentInterface("JSONRPC", "http://example.com", ""); - Utils.buildBaseUrl(iface, "../../admin"); - }); + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> { + AgentInterface iface = + new AgentInterface("JSONRPC", "http://example.com", ""); + Utils.buildBaseUrl(iface, "../../admin"); + }); assertNotNull(ex.getMessage()); - assertEquals("Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", ex.getMessage()); + assertEquals( + "Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", + ex.getMessage()); } @Test void testValidateTenant_pathTraversalWithSlash_throws() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> { - AgentInterface iface = new AgentInterface("JSONRPC", "http://example.com", ""); - Utils.buildBaseUrl(iface, "/../admin"); - }); + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> { + AgentInterface iface = + new AgentInterface("JSONRPC", "http://example.com", ""); + Utils.buildBaseUrl(iface, "/../admin"); + }); assertNotNull(ex.getMessage()); - assertEquals("Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", ex.getMessage()); + assertEquals( + "Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", + ex.getMessage()); } @Test void testValidateTenant_tooLong_throws() { String longTenant = "a".repeat(257); - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> { - AgentInterface iface = new AgentInterface("JSONRPC", "http://example.com", ""); - Utils.buildBaseUrl(iface, longTenant); - }); + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> { + AgentInterface iface = + new AgentInterface("JSONRPC", "http://example.com", ""); + Utils.buildBaseUrl(iface, longTenant); + }); assertNotNull(ex.getMessage()); assertEquals("Tenant exceeds maximum length of 256 characters", ex.getMessage()); } @@ -103,63 +122,93 @@ void testValidateTenant_maxLengthAllowed_succeeds() { @Test void testValidateTenant_invalidCharactersSpace_throws() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> { - AgentInterface iface = new AgentInterface("JSONRPC", "http://example.com", ""); - Utils.buildBaseUrl(iface, "tenant with spaces"); - }); + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> { + AgentInterface iface = + new AgentInterface("JSONRPC", "http://example.com", ""); + Utils.buildBaseUrl(iface, "tenant with spaces"); + }); assertNotNull(ex.getMessage()); - assertEquals("Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", ex.getMessage()); + assertEquals( + "Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", + ex.getMessage()); } @Test void testValidateTenant_invalidCharactersSpecial_throws() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> { - AgentInterface iface = new AgentInterface("JSONRPC", "http://example.com", ""); - Utils.buildBaseUrl(iface, "tenant@123"); - }); + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> { + AgentInterface iface = + new AgentInterface("JSONRPC", "http://example.com", ""); + Utils.buildBaseUrl(iface, "tenant@123"); + }); assertNotNull(ex.getMessage()); - assertEquals("Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", ex.getMessage()); + assertEquals( + "Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", + ex.getMessage()); } @Test void testValidateTenant_invalidCharactersQuery_throws() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> { - AgentInterface iface = new AgentInterface("JSONRPC", "http://example.com", ""); - Utils.buildBaseUrl(iface, "tenant?param=value"); - }); + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> { + AgentInterface iface = + new AgentInterface("JSONRPC", "http://example.com", ""); + Utils.buildBaseUrl(iface, "tenant?param=value"); + }); assertNotNull(ex.getMessage()); - assertEquals("Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", ex.getMessage()); + assertEquals( + "Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", + ex.getMessage()); } @Test void testValidateTenant_slashInTenant_throws() { - IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> { - AgentInterface iface = new AgentInterface("JSONRPC", "http://example.com", ""); - Utils.buildBaseUrl(iface, "multi/level/tenant"); - }); + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, + () -> { + AgentInterface iface = + new AgentInterface("JSONRPC", "http://example.com", ""); + Utils.buildBaseUrl(iface, "multi/level/tenant"); + }); assertNotNull(ex.getMessage()); - assertEquals("Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", ex.getMessage()); + assertEquals( + "Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed", + ex.getMessage()); } @Test void testValidateTenant_leadingSlashMultiSegment_throws() { - assertThrows(IllegalArgumentException.class, () -> { - Utils.validateTenant("/multi/level"); - }); + assertThrows( + IllegalArgumentException.class, + () -> { + Utils.validateTenant("/multi/level"); + }); } @Test void testValidateTenant_intermediateSegments_throws() { - assertThrows(IllegalArgumentException.class, () -> { - Utils.validateTenant("tenant/api/v1"); - }); + assertThrows( + IllegalArgumentException.class, + () -> { + Utils.validateTenant("tenant/api/v1"); + }); } @Test void testValidateTenant_trailingSlashMultiSegment_throws() { - assertThrows(IllegalArgumentException.class, () -> { - Utils.validateTenant("multi/level/"); - }); + assertThrows( + IllegalArgumentException.class, + () -> { + Utils.validateTenant("multi/level/"); + }); } @Test @@ -193,7 +242,8 @@ void testValidateTenant_emptyString_succeeds() { @Test void testBuildBaseUrl_complexScenario() { // Base URL with trailing slash, default tenant, custom override - AgentInterface iface = new AgentInterface("JSONRPC", "http://example.com:8080/api/", "/default"); + AgentInterface iface = + new AgentInterface("JSONRPC", "http://example.com:8080/api/", "/default"); String url = Utils.buildBaseUrl(iface, "custom-tenant"); assertEquals("http://example.com:8080/api/custom-tenant", url); } @@ -207,7 +257,8 @@ void testBuildBaseUrl_urlWithPort() { @Test void testBuildBaseUrl_httpsUrl() { - AgentInterface iface = new AgentInterface("JSONRPC", "https://secure.example.com", "/tenant"); + AgentInterface iface = + new AgentInterface("JSONRPC", "https://secure.example.com", "/tenant"); String url = Utils.buildBaseUrl(iface, null); assertEquals("https://secure.example.com/tenant", url); } @@ -253,12 +304,14 @@ void testBuildBaseUrl_tenantSubstringOfPathSegment_noFalsePositive() { @Test void testBuildBaseUrl_string_urlAlreadyContainsTenant_noDoubling() { - assertEquals("http://example.com/acme", Utils.buildBaseUrl("http://example.com/acme", "acme")); + assertEquals( + "http://example.com/acme", Utils.buildBaseUrl("http://example.com/acme", "acme")); } @Test void testBuildBaseUrl_string_urlAlreadyContainsTenantWithTrailingSlash_noDoubling() { - assertEquals("http://example.com/acme", Utils.buildBaseUrl("http://example.com/acme/", "acme")); + assertEquals( + "http://example.com/acme", Utils.buildBaseUrl("http://example.com/acme/", "acme")); } // ========== buildBaseUrl(String, String) Tests ========== @@ -275,17 +328,23 @@ void testBuildBaseUrl_string_trailingSlashStripped() { @Test void testBuildBaseUrl_string_withTenant() { - assertEquals("http://example.com/my-tenant", Utils.buildBaseUrl("http://example.com", "my-tenant")); + assertEquals( + "http://example.com/my-tenant", + Utils.buildBaseUrl("http://example.com", "my-tenant")); } @Test void testBuildBaseUrl_string_withTenantLeadingSlash() { - assertEquals("http://example.com/my-tenant", Utils.buildBaseUrl("http://example.com", "/my-tenant")); + assertEquals( + "http://example.com/my-tenant", + Utils.buildBaseUrl("http://example.com", "/my-tenant")); } @Test void testBuildBaseUrl_string_withSubPath() { - assertEquals("http://example.com/spec03/my-tenant", Utils.buildBaseUrl("http://example.com/spec03", "my-tenant")); + assertEquals( + "http://example.com/spec03/my-tenant", + Utils.buildBaseUrl("http://example.com/spec03", "my-tenant")); } @Test @@ -316,31 +375,36 @@ void testValidateAbsoluteUrl_malformed_throws() { @Test void testBuildCardUrl_simple() { - assertEquals("http://example.com/.well-known/agent-card.json", + assertEquals( + "http://example.com/.well-known/agent-card.json", Utils.buildCardUrl("http://example.com", "/.well-known/agent-card.json")); } @Test void testBuildCardUrl_baseTrailingSlash() { - assertEquals("http://example.com/.well-known/agent-card.json", + assertEquals( + "http://example.com/.well-known/agent-card.json", Utils.buildCardUrl("http://example.com/", "/.well-known/agent-card.json")); } @Test void testBuildCardUrl_pathWithoutLeadingSlash() { - assertEquals("http://example.com/.well-known/agent-card.json", + assertEquals( + "http://example.com/.well-known/agent-card.json", Utils.buildCardUrl("http://example.com", ".well-known/agent-card.json")); } @Test void testBuildCardUrl_preservesSubPath() { - assertEquals("http://example.com/spec03/.well-known/agent-card.json", + assertEquals( + "http://example.com/spec03/.well-known/agent-card.json", Utils.buildCardUrl("http://example.com/spec03", "/.well-known/agent-card.json")); } @Test void testBuildCardUrl_noDoubleSlash() { - assertEquals("http://example.com/custom/agent.json", + assertEquals( + "http://example.com/custom/agent.json", Utils.buildCardUrl("http://example.com/", "/custom/agent.json")); } @@ -353,14 +417,17 @@ void testStripWellKnownSuffix_noSuffix() { @Test void testStripWellKnownSuffix_withSuffix() { - assertEquals("http://example.com", + assertEquals( + "http://example.com", Utils.stripWellKnownSuffix("http://example.com/.well-known/agent-card.json")); } @Test void testStripWellKnownSuffix_withSubPathAndSuffix() { - assertEquals("http://example.com/spec03", - Utils.stripWellKnownSuffix("http://example.com/spec03/.well-known/agent-card.json")); + assertEquals( + "http://example.com/spec03", + Utils.stripWellKnownSuffix( + "http://example.com/spec03/.well-known/agent-card.json")); } @Test @@ -370,42 +437,53 @@ void testStripWellKnownSuffix_trailingSlash() { @Test void testStripWellKnownSuffix_unrelatedPath() { - assertEquals("http://example.com/custom/agent.json", + assertEquals( + "http://example.com/custom/agent.json", Utils.stripWellKnownSuffix("http://example.com/custom/agent.json")); } @Test void testStripWellKnownSuffix_tenantSpecific() { - assertEquals("http://example.com", + assertEquals( + "http://example.com", Utils.stripWellKnownSuffix("http://example.com/.well-known/acme/agent-card.json")); } @Test void testStripWellKnownSuffix_tenantSpecificWithSubPath() { - assertEquals("http://example.com/spec03", - Utils.stripWellKnownSuffix("http://example.com/spec03/.well-known/acme/agent-card.json")); + assertEquals( + "http://example.com/spec03", + Utils.stripWellKnownSuffix( + "http://example.com/spec03/.well-known/acme/agent-card.json")); } @Test void testStripWellKnownSuffix_tenantSpecificWithTrailingSlash() { - assertEquals("http://example.com", + assertEquals( + "http://example.com", Utils.stripWellKnownSuffix("http://example.com/.well-known/acme/agent-card.json/")); } @Test void testStripWellKnownSuffix_multiLevelTenantNotStripped() { // Multi-level tenant paths (containing /) are not stripped - assertEquals("http://example.com/.well-known/org/team/agent-card.json", - Utils.stripWellKnownSuffix("http://example.com/.well-known/org/team/agent-card.json")); + assertEquals( + "http://example.com/.well-known/org/team/agent-card.json", + Utils.stripWellKnownSuffix( + "http://example.com/.well-known/org/team/agent-card.json")); } @Test void testStripWellKnownSuffix_invalidTenantCharsNotStripped() { // Tenant with characters outside [a-zA-Z0-9_.-] must not be stripped - assertEquals("http://example.com/.well-known/invalid tenant/agent-card.json", - Utils.stripWellKnownSuffix("http://example.com/.well-known/invalid tenant/agent-card.json")); - assertEquals("http://example.com/.well-known/bad@tenant/agent-card.json", - Utils.stripWellKnownSuffix("http://example.com/.well-known/bad@tenant/agent-card.json")); + assertEquals( + "http://example.com/.well-known/invalid tenant/agent-card.json", + Utils.stripWellKnownSuffix( + "http://example.com/.well-known/invalid tenant/agent-card.json")); + assertEquals( + "http://example.com/.well-known/bad@tenant/agent-card.json", + Utils.stripWellKnownSuffix( + "http://example.com/.well-known/bad@tenant/agent-card.json")); } // ------------------------------------------------------------------------- diff --git a/tck/pom.xml b/tck/pom.xml index 6e3158854..b026b97ae 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 @@ -45,6 +43,25 @@ + + + + io.quarkus.platform + quarkus-maven-plugin + true + + + + build + generate-code + generate-code-tests + + + + + + + multi-version @@ -81,10 +98,10 @@ add-multi-version-source - generate-sources add-source + generate-sources src/multi-version/java @@ -97,23 +114,4 @@ - - - - - io.quarkus.platform - quarkus-maven-plugin - true - - - - build - generate-code - generate-code-tests - - - - - - diff --git a/tck/src/main/java/org/a2aproject/sdk/sut/TckAgentCardProducer.java b/tck/src/main/java/org/a2aproject/sdk/sut/TckAgentCardProducer.java index 3119f6f9b..88e225514 100644 --- a/tck/src/main/java/org/a2aproject/sdk/sut/TckAgentCardProducer.java +++ b/tck/src/main/java/org/a2aproject/sdk/sut/TckAgentCardProducer.java @@ -1,10 +1,8 @@ package org.a2aproject.sdk.sut; -import java.util.List; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - +import java.util.List; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; @@ -28,28 +26,30 @@ public AgentCard agentCard() { String sutJsonRpcUrl = String.format("http://%s", HOST); String sutRestUrl = sutJsonRpcUrl; String sutGrpcUrl = GRPC_HOST; - + return AgentCard.builder() .name("A2A Java SDK System Under Test (SUT)") .description("Auto-generated System Under Test for A2A TCK conformance") .version("1.0.0") - .supportedInterfaces(List.of( - new AgentInterface(TransportProtocol.JSONRPC.asString(), sutJsonRpcUrl), - new AgentInterface(TransportProtocol.GRPC.asString(), sutGrpcUrl), - new AgentInterface(TransportProtocol.HTTP_JSON.asString(), sutRestUrl))) - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .build()) + .supportedInterfaces( + List.of( + new AgentInterface( + TransportProtocol.JSONRPC.asString(), sutJsonRpcUrl), + new AgentInterface(TransportProtocol.GRPC.asString(), sutGrpcUrl), + new AgentInterface( + TransportProtocol.HTTP_JSON.asString(), sutRestUrl))) + .capabilities( + AgentCapabilities.builder().streaming(true).pushNotifications(true).build()) .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) - .skills(List.of( - AgentSkill.builder() - .id("tck") - .name("TCK Conformance") - .description("Handles TCK conformance test messages") - .tags(List.of("tck")) - .build())) + .skills( + List.of( + AgentSkill.builder() + .id("tck") + .name("TCK Conformance") + .description("Handles TCK conformance test messages") + .tags(List.of("tck")) + .build())) .build(); } diff --git a/tck/src/main/java/org/a2aproject/sdk/sut/TckAgentExecutorProducer.java b/tck/src/main/java/org/a2aproject/sdk/sut/TckAgentExecutorProducer.java index 0889b9a08..f505d31ab 100644 --- a/tck/src/main/java/org/a2aproject/sdk/sut/TckAgentExecutorProducer.java +++ b/tck/src/main/java/org/a2aproject/sdk/sut/TckAgentExecutorProducer.java @@ -1,21 +1,17 @@ package org.a2aproject.sdk.sut; -import java.util.List; -import java.util.Map; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; - +import java.util.List; import org.a2aproject.sdk.A2A; import org.a2aproject.sdk.server.agentexecution.AgentExecutor; import org.a2aproject.sdk.server.agentexecution.RequestContext; import org.a2aproject.sdk.server.tasks.AgentEmitter; import org.a2aproject.sdk.spec.A2AError; import org.a2aproject.sdk.spec.DataPart; +import org.a2aproject.sdk.spec.FilePart; import org.a2aproject.sdk.spec.FileWithBytes; import org.a2aproject.sdk.spec.FileWithUri; -import org.a2aproject.sdk.spec.FilePart; -import org.a2aproject.sdk.spec.TaskNotCancelableError; import org.a2aproject.sdk.spec.TextPart; /** @@ -35,29 +31,42 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro if (messageId.startsWith("tck-stream-artifact-chunked")) { emitter.startWork(); - emitter.addArtifact(List.of(new TextPart("chunk-1 ")), null, null, null, true, false); - emitter.addArtifact(List.of(new TextPart("chunk-2")), null, null, null, true, true); + emitter.addArtifact( + List.of(new TextPart("chunk-1 ")), null, null, null, true, false); + emitter.addArtifact( + List.of(new TextPart("chunk-2")), null, null, null, true, true); emitter.complete(); return; } if (messageId.startsWith("test-resubscribe-message-id")) { emitter.startWork(); - try { Thread.sleep(4000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + try { + Thread.sleep(4000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } emitter.complete(); return; } if (messageId.startsWith("tck-stream-artifact-text")) { emitter.startWork(); - emitter.addArtifact(List.of(new TextPart("Streamed text content")), null, null, null); + emitter.addArtifact( + List.of(new TextPart("Streamed text content")), null, null, null); emitter.complete(); return; } if (messageId.startsWith("tck-stream-artifact-file")) { emitter.startWork(); - emitter.addArtifact(List.of(new FilePart(new FileWithBytes("text/plain", "output.txt", "dGNr"))), null, null, null); + emitter.addArtifact( + List.of( + new FilePart( + new FileWithBytes("text/plain", "output.txt", "dGNr"))), + null, + null, + null); emitter.complete(); return; } @@ -70,7 +79,16 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro } if (messageId.startsWith("tck-artifact-file-url")) { - emitter.addArtifact(List.of(new FilePart(new FileWithUri("text/plain", "output.txt", "https://example.com/output.txt"))), null, null, null); + emitter.addArtifact( + List.of( + new FilePart( + new FileWithUri( + "text/plain", + "output.txt", + "https://example.com/output.txt"))), + null, + null, + null); emitter.complete(); return; } @@ -91,19 +109,30 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro } if (messageId.startsWith("tck-artifact-text")) { - emitter.addArtifact(List.of(new TextPart("Generated text content")), null, null, null); + emitter.addArtifact( + List.of(new TextPart("Generated text content")), null, null, null); emitter.complete(); return; } if (messageId.startsWith("tck-artifact-file")) { - emitter.addArtifact(List.of(new FilePart(new FileWithBytes("text/plain", "output.txt", "dGNr"))), null, null, null); + emitter.addArtifact( + List.of( + new FilePart( + new FileWithBytes("text/plain", "output.txt", "dGNr"))), + null, + null, + null); emitter.complete(); return; } if (messageId.startsWith("tck-artifact-data")) { - emitter.addArtifact(List.of(DataPart.fromJson("{\"key\": \"value\", \"count\": 42}")), null, null, null); + emitter.addArtifact( + List.of(DataPart.fromJson("{\"key\": \"value\", \"count\": 42}")), + null, + null, + null); emitter.complete(); return; } @@ -114,7 +143,8 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro if (messageId.startsWith("tck-stream-001")) { emitter.startWork(); - emitter.addArtifact(List.of(new TextPart("Stream hello from TCK")), null, null, null); + emitter.addArtifact( + List.of(new TextPart("Stream hello from TCK")), null, null, null); emitter.complete(); return; } @@ -126,7 +156,8 @@ public void execute(RequestContext context, AgentEmitter emitter) throws A2AErro if (messageId.startsWith("tck-stream-003")) { emitter.startWork(); - emitter.addArtifact(List.of(new TextPart("Stream task lifecycle")), null, null, null); + emitter.addArtifact( + List.of(new TextPart("Stream task lifecycle")), null, null, null); emitter.complete(); return; } diff --git a/test-utils-docker/pom.xml b/test-utils-docker/pom.xml index 84e8905b9..c6a9ec2ce 100644 --- a/test-utils-docker/pom.xml +++ b/test-utils-docker/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 diff --git a/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/DockerAvailability.java b/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/DockerAvailability.java index 744ed5b87..a0f618885 100644 --- a/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/DockerAvailability.java +++ b/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/DockerAvailability.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.testutils.docker; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Utility class for checking Docker/Podman availability and test skip settings. @@ -65,10 +64,11 @@ private static boolean checkDockerAvailable() { private static boolean tryContainerCommand(String command) { try { - Process process = new ProcessBuilder(command, "info") - .redirectOutput(ProcessBuilder.Redirect.DISCARD) - .redirectError(ProcessBuilder.Redirect.DISCARD) - .start(); + Process process = + new ProcessBuilder(command, "info") + .redirectOutput(ProcessBuilder.Redirect.DISCARD) + .redirectError(ProcessBuilder.Redirect.DISCARD) + .start(); boolean finished = process.waitFor(5, TimeUnit.SECONDS); if (!finished) { diff --git a/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/DockerRequiredExtension.java b/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/DockerRequiredExtension.java index 491a63496..cb22c21fc 100644 --- a/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/DockerRequiredExtension.java +++ b/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/DockerRequiredExtension.java @@ -33,7 +33,8 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con if (!dockerAvailable) { // Docker/Podman not available and skip NOT requested - ABORT test throw new TestAbortedException( - "Docker/Podman is not available. Use -DskipDockerTests=true to skip these tests."); + "Docker/Podman is not available. Use -DskipDockerTests=true to skip these" + + " tests."); } // Docker/Podman available and skip not requested - RUN test diff --git a/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/RequiresDocker.java b/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/RequiresDocker.java index 08b7ce0ba..1419dbb0c 100644 --- a/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/RequiresDocker.java +++ b/test-utils-docker/src/main/java/org/a2aproject/sdk/testutils/docker/RequiresDocker.java @@ -4,7 +4,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; - import org.junit.jupiter.api.extension.ExtendWith; /** @@ -32,5 +31,4 @@ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(DockerRequiredExtension.class) -public @interface RequiresDocker { -} +public @interface RequiresDocker {} diff --git a/tests/multiversion/grpc/pom.xml b/tests/multiversion/grpc/pom.xml index 717c5f0a7..0f24cd6be 100644 --- a/tests/multiversion/grpc/pom.xml +++ b/tests/multiversion/grpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/A2ATestResource.java b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/A2ATestResource.java index d30fa6e9f..69d9e4aa6 100644 --- a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/A2ATestResource.java +++ b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/A2ATestResource.java @@ -2,8 +2,6 @@ import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN; -import java.util.concurrent.atomic.AtomicInteger; - import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -18,7 +16,7 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; - +import java.util.concurrent.atomic.AtomicInteger; import org.a2aproject.sdk.compat03.transport.grpc.handler.GrpcHandler_v0_3; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.server.apps.common.TestUtilsBean; @@ -31,8 +29,7 @@ @Path("/test") @ApplicationScoped public class A2ATestResource { - @Inject - TestUtilsBean testUtilsBean; + @Inject TestUtilsBean testUtilsBean; private final AtomicInteger streamingSubscribedCount = new AtomicInteger(0); @@ -73,9 +70,7 @@ public Response deleteTask(@PathParam("taskId") String taskId) { return Response.status(404).build(); } testUtilsBean.deleteTask(taskId); - return Response.ok() - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .build(); + return Response.ok().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).build(); } @POST @@ -87,7 +82,8 @@ public Response ensureQueue(@PathParam("taskId") String taskId) { @POST @Path("/queue/enqueueTaskStatusUpdateEvent/{taskId}") - public Response enqueueTaskStatusUpdateEvent(@PathParam("taskId") String taskId, String body) throws Exception { + public Response enqueueTaskStatusUpdateEvent(@PathParam("taskId") String taskId, String body) + throws Exception { TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); return Response.ok().build(); @@ -95,7 +91,8 @@ public Response enqueueTaskStatusUpdateEvent(@PathParam("taskId") String taskId, @POST @Path("/queue/enqueueTaskArtifactUpdateEvent/{taskId}") - public Response enqueueTaskArtifactUpdateEvent(@PathParam("taskId") String taskId, String body) throws Exception { + public Response enqueueTaskArtifactUpdateEvent(@PathParam("taskId") String taskId, String body) + throws Exception { TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); return Response.ok().build(); @@ -118,15 +115,14 @@ public Response getChildQueueCount(@PathParam("taskId") String taskId) { @DELETE @Path("/task/{taskId}/config/{configId}") - public Response deleteTaskPushNotificationConfig(@PathParam("taskId") String taskId, @PathParam("configId") String configId) { + public Response deleteTaskPushNotificationConfig( + @PathParam("taskId") String taskId, @PathParam("configId") String configId) { Task task = testUtilsBean.getTask(taskId); if (task == null) { return Response.status(404).build(); } testUtilsBean.deleteTaskPushNotificationConfig(taskId, configId); - return Response.ok() - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) - .build(); + return Response.ok().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).build(); } @POST @@ -135,8 +131,10 @@ public Response deleteTaskPushNotificationConfig(@PathParam("taskId") String tas public Response savePushNotificationConfigInStore( @PathParam("taskId") String taskId, @HeaderParam("A2A-Version") String protocolVersion, - String body) throws Exception { - TaskPushNotificationConfig notificationConfig = JsonUtil.fromJson(body, TaskPushNotificationConfig.class); + String body) + throws Exception { + TaskPushNotificationConfig notificationConfig = + JsonUtil.fromJson(body, TaskPushNotificationConfig.class); if (notificationConfig == null) { return Response.status(404).build(); } @@ -155,7 +153,8 @@ public Response savePushNotificationConfigInStore( */ @POST @Path("/queue/awaitPollerStart/{taskId}") - public Response awaitQueuePollerStart(@PathParam("taskId") String taskId) throws InterruptedException { + public Response awaitQueuePollerStart(@PathParam("taskId") String taskId) + throws InterruptedException { testUtilsBean.awaitQueuePollerStart(taskId); return Response.ok().build(); } @@ -173,9 +172,11 @@ public Response awaitQueuePollerStart(@PathParam("taskId") String taskId) throws */ @POST @Path("/queue/awaitChildCountStable/{taskId}/{expectedCount}/{timeoutMs}") - public Response awaitChildQueueCountStable(@PathParam("taskId") String taskId, - @PathParam("expectedCount") int expectedCount, - @PathParam("timeoutMs") long timeoutMs) throws InterruptedException { + public Response awaitChildQueueCountStable( + @PathParam("taskId") String taskId, + @PathParam("expectedCount") int expectedCount, + @PathParam("timeoutMs") long timeoutMs) + throws InterruptedException { boolean stable = testUtilsBean.awaitChildQueueCountStable(taskId, expectedCount, timeoutMs); return Response.ok(String.valueOf(stable), TEXT_PLAIN).build(); } diff --git a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/AuthTestProfile.java b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/AuthTestProfile.java index df13d0dd5..e85094924 100644 --- a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/AuthTestProfile.java +++ b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/AuthTestProfile.java @@ -1,8 +1,7 @@ package org.a2aproject.sdk.tests.multiversion.grpc; -import java.util.Map; - import io.quarkus.test.junit.QuarkusTestProfile; +import java.util.Map; /** * Quarkus test profile that enables security for authentication tests. @@ -41,8 +40,7 @@ public Map getConfigOverrides() { Map.entry("quarkus.http.auth.basic", "true"), // Enable proactive authentication - authenticate at HTTP layer before route handler - Map.entry("quarkus.http.auth.proactive", "true") - ); + Map.entry("quarkus.http.auth.proactive", "true")); } @Override diff --git a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcTest.java b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcTest.java index 32d1f7722..443f85b6e 100644 --- a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcTest.java +++ b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcTest.java @@ -1,10 +1,9 @@ package org.a2aproject.sdk.tests.multiversion.grpc; -import java.util.concurrent.TimeUnit; - import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.quarkus.test.junit.QuarkusTest; +import java.util.concurrent.TimeUnit; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.transport.grpc.GrpcTransport; import org.a2aproject.sdk.client.transport.grpc.GrpcTransportConfigBuilder; @@ -33,10 +32,17 @@ protected String getTransportUrl() { @Override protected void configureTransport(ClientBuilder builder) { - builder.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(target -> { - channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); - return channel; - })); + builder.withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder() + .channelFactory( + target -> { + channel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + return channel; + })); } @AfterAll diff --git a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcWithAuthTest.java b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcWithAuthTest.java index 887a1572c..ddfbed804 100644 --- a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcWithAuthTest.java +++ b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcWithAuthTest.java @@ -1,12 +1,11 @@ package org.a2aproject.sdk.tests.multiversion.grpc; -import java.util.concurrent.TimeUnit; - import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; import jakarta.inject.Inject; +import java.util.concurrent.TimeUnit; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.transport.grpc.GrpcTransport; import org.a2aproject.sdk.client.transport.grpc.GrpcTransportConfigBuilder; @@ -23,9 +22,7 @@ @TestProfile(AuthTestProfile.class) public class MultiVersionGrpcWithAuthTest extends AbstractA2AServerWithAuthTest { - @Inject - @PublicAgentCard - AgentCard agentCard; + @Inject @PublicAgentCard AgentCard agentCard; private static ManagedChannel authenticatedChannel; private static ManagedChannel unauthenticatedChannel; @@ -46,30 +43,40 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder builder) { - AuthInterceptor authInterceptor = new AuthInterceptor( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); + AuthInterceptor authInterceptor = + new AuthInterceptor( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); - builder.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder() - .channelFactory(target -> { - authenticatedChannel = ManagedChannelBuilder - .forTarget(target) - .usePlaintext() - .build(); - return authenticatedChannel; - }) - .addInterceptor(authInterceptor)); + builder.withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder() + .channelFactory( + target -> { + authenticatedChannel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + return authenticatedChannel; + }) + .addInterceptor(authInterceptor)); } @Override protected void configureTransport(ClientBuilder builder) { - builder.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(target -> { - unauthenticatedChannel = ManagedChannelBuilder - .forTarget(target) - .usePlaintext() - .build(); - return unauthenticatedChannel; - })); + builder.withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder() + .channelFactory( + target -> { + unauthenticatedChannel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + return unauthenticatedChannel; + })); } @AfterAll diff --git a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcWithTaskAuthorizationTest.java b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcWithTaskAuthorizationTest.java index 3f7c9e1d3..ef0e06325 100644 --- a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcWithTaskAuthorizationTest.java +++ b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersionGrpcWithTaskAuthorizationTest.java @@ -1,14 +1,13 @@ package org.a2aproject.sdk.tests.multiversion.grpc; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; import jakarta.inject.Inject; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.transport.grpc.GrpcTransport; import org.a2aproject.sdk.client.transport.grpc.GrpcTransportConfigBuilder; @@ -21,11 +20,10 @@ @QuarkusTest @TestProfile(TaskAuthorizationTestProfile.class) -public class MultiVersionGrpcWithTaskAuthorizationTest extends AbstractA2AServerWithTaskAuthorizationTest { +public class MultiVersionGrpcWithTaskAuthorizationTest + extends AbstractA2AServerWithTaskAuthorizationTest { - @Inject - @PublicAgentCard - AgentCard agentCard; + @Inject @PublicAgentCard AgentCard agentCard; private static final Map channels = new ConcurrentHashMap<>(); @@ -45,28 +43,40 @@ protected AgentCard fetchAgentCardFromServer() { } @Override - protected void configureTransportWithCredentials(ClientBuilder builder, String username, String password) { - AuthInterceptor authInterceptor = new AuthInterceptor( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) - ? getEncodedCredentials(username, password) : null); - builder.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder() - .channelFactory(target -> { - ManagedChannel channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); - channels.put(username, channel); - return channel; - }) - .addInterceptor(authInterceptor)); + protected void configureTransportWithCredentials( + ClientBuilder builder, String username, String password) { + AuthInterceptor authInterceptor = + new AuthInterceptor( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials(username, password) + : null); + builder.withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder() + .channelFactory( + target -> { + ManagedChannel channel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + channels.put(username, channel); + return channel; + }) + .addInterceptor(authInterceptor)); } @AfterAll static void closeChannels() { - channels.values().forEach(ch -> { - ch.shutdownNow(); - try { - ch.awaitTermination(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }); + channels.values() + .forEach( + ch -> { + ch.shutdownNow(); + try { + ch.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); } } diff --git a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersion_v0_3_GrpcTest.java b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersion_v0_3_GrpcTest.java index de1acebf2..47f06254d 100644 --- a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersion_v0_3_GrpcTest.java +++ b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersion_v0_3_GrpcTest.java @@ -1,13 +1,12 @@ package org.a2aproject.sdk.tests.multiversion.grpc; -import java.util.concurrent.TimeUnit; - import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.quarkus.test.junit.QuarkusTest; +import java.util.concurrent.TimeUnit; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerServerTest_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; import org.junit.jupiter.api.AfterAll; @@ -33,10 +32,17 @@ protected String getTransportUrl() { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3().channelFactory(target -> { - channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); - return channel; - })); + builder.withTransport( + GrpcTransport_v0_3.class, + new GrpcTransportConfigBuilder_v0_3() + .channelFactory( + target -> { + channel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + return channel; + })); } @AfterAll diff --git a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersion_v0_3_GrpcWithAuthTest.java b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersion_v0_3_GrpcWithAuthTest.java index 70dc46c7b..6b77048f9 100644 --- a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersion_v0_3_GrpcWithAuthTest.java +++ b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/MultiVersion_v0_3_GrpcWithAuthTest.java @@ -1,14 +1,13 @@ package org.a2aproject.sdk.tests.multiversion.grpc; -import java.util.concurrent.TimeUnit; - import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; +import java.util.concurrent.TimeUnit; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.grpc.GrpcTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithAuthTest_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; @@ -39,30 +38,40 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); - builder.withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3() - .channelFactory(target -> { - authenticatedChannel = ManagedChannelBuilder - .forTarget(target) - .usePlaintext() - .build(); - return authenticatedChannel; - }) - .addInterceptor(authInterceptor)); + builder.withTransport( + GrpcTransport_v0_3.class, + new GrpcTransportConfigBuilder_v0_3() + .channelFactory( + target -> { + authenticatedChannel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + return authenticatedChannel; + }) + .addInterceptor(authInterceptor)); } @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(GrpcTransport_v0_3.class, new GrpcTransportConfigBuilder_v0_3().channelFactory(target -> { - unauthenticatedChannel = ManagedChannelBuilder - .forTarget(target) - .usePlaintext() - .build(); - return unauthenticatedChannel; - })); + builder.withTransport( + GrpcTransport_v0_3.class, + new GrpcTransportConfigBuilder_v0_3() + .channelFactory( + target -> { + unauthenticatedChannel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + return unauthenticatedChannel; + })); } @AfterAll diff --git a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/TaskAuthorizationTestProfile.java b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/TaskAuthorizationTestProfile.java index b4cdb2522..6624efa78 100644 --- a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/TaskAuthorizationTestProfile.java +++ b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/TaskAuthorizationTestProfile.java @@ -3,8 +3,6 @@ import java.util.HashMap; import java.util.Map; -import io.quarkus.test.junit.QuarkusTestProfile; - public class TaskAuthorizationTestProfile extends AuthTestProfile { @Override diff --git a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/TestAuthorizationController.java b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/TestAuthorizationController.java index ceed3df67..d1cf32ed3 100644 --- a/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/TestAuthorizationController.java +++ b/tests/multiversion/grpc/src/test/java/org/a2aproject/sdk/tests/multiversion/grpc/TestAuthorizationController.java @@ -1,14 +1,12 @@ package org.a2aproject.sdk.tests.multiversion.grpc; +import io.quarkus.security.spi.runtime.AuthorizationController; import jakarta.annotation.Priority; import jakarta.enterprise.inject.Alternative; import jakarta.inject.Singleton; import jakarta.interceptor.Interceptor; - import org.eclipse.microprofile.config.inject.ConfigProperty; -import io.quarkus.security.spi.runtime.AuthorizationController; - /** * Disables authorization for regular gRPC tests. *

    diff --git a/tests/multiversion/jsonrpc/pom.xml b/tests/multiversion/jsonrpc/pom.xml index 931ac458d..18c6f760c 100644 --- a/tests/multiversion/jsonrpc/pom.xml +++ b/tests/multiversion/jsonrpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/A2ATestRoutes.java b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/A2ATestRoutes.java index 1c06793c6..fd1a4aeca 100644 --- a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/A2ATestRoutes.java +++ b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/A2ATestRoutes.java @@ -4,20 +4,18 @@ import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN; -import java.util.concurrent.atomic.AtomicInteger; - +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; import jakarta.annotation.PostConstruct; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.inject.Singleton; - -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.BodyHandler; +import java.util.concurrent.atomic.AtomicInteger; +import org.a2aproject.sdk.compat03.server.apps.quarkus.A2AServerRoutes_v0_3; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.server.apps.common.TestUtilsBean; import org.a2aproject.sdk.server.apps.quarkus.A2AServerRoutes; -import org.a2aproject.sdk.compat03.server.apps.quarkus.A2AServerRoutes_v0_3; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskArtifactUpdateEvent; import org.a2aproject.sdk.spec.TaskPushNotificationConfig; @@ -25,14 +23,11 @@ @Singleton public class A2ATestRoutes { - @Inject - TestUtilsBean testUtilsBean; + @Inject TestUtilsBean testUtilsBean; - @Inject - A2AServerRoutes a2AServerRoutes; + @Inject A2AServerRoutes a2AServerRoutes; - @Inject - A2AServerRoutes_v0_3 a2AServerRoutes_v0_3; + @Inject A2AServerRoutes_v0_3 a2AServerRoutes_v0_3; AtomicInteger streamingSubscribedCount = new AtomicInteger(0); @@ -45,81 +40,91 @@ public void init() { void setupRoutes(@Observes Router router) { router.post("/test/task") - .consumes(APPLICATION_JSON) - .handler(BodyHandler.create()) - .blockingHandler(ctx -> { - String body = ctx.body().asString(); - saveTask(body, ctx); - }); + .consumes(APPLICATION_JSON) + .handler(BodyHandler.create()) + .blockingHandler( + ctx -> { + String body = ctx.body().asString(); + saveTask(body, ctx); + }); router.get("/test/task/:taskId") - .produces(APPLICATION_JSON) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - getTask(taskId, ctx); - }); + .produces(APPLICATION_JSON) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + getTask(taskId, ctx); + }); router.delete("/test/task/:taskId") - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - deleteTask(taskId, ctx); - }); + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + deleteTask(taskId, ctx); + }); router.post("/test/queue/ensure/:taskId") - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - ensureTaskQueue(taskId, ctx); - }); + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + ensureTaskQueue(taskId, ctx); + }); router.post("/test/queue/enqueueTaskStatusUpdateEvent/:taskId") - .handler(BodyHandler.create()) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - enqueueTaskStatusUpdateEvent(taskId, body, ctx); - }); + .handler(BodyHandler.create()) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + enqueueTaskStatusUpdateEvent(taskId, body, ctx); + }); router.post("/test/queue/enqueueTaskArtifactUpdateEvent/:taskId") - .handler(BodyHandler.create()) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - enqueueTaskArtifactUpdateEvent(taskId, body, ctx); - }); + .handler(BodyHandler.create()) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + enqueueTaskArtifactUpdateEvent(taskId, body, ctx); + }); router.get("/test/streamingSubscribedCount") - .produces(TEXT_PLAIN) - .handler(ctx -> getStreamingSubscribedCount(ctx)); + .produces(TEXT_PLAIN) + .handler(ctx -> getStreamingSubscribedCount(ctx)); router.get("/test/queue/childCount/:taskId") - .produces(TEXT_PLAIN) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - getChildQueueCount(taskId, ctx); - }); + .produces(TEXT_PLAIN) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + getChildQueueCount(taskId, ctx); + }); router.delete("/test/task/:taskId/config/:configId") - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String configId = ctx.pathParam("configId"); - deleteTaskPushNotificationConfig(taskId, configId, ctx); - }); + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String configId = ctx.pathParam("configId"); + deleteTaskPushNotificationConfig(taskId, configId, ctx); + }); router.post("/test/task/:taskId") - .handler(BodyHandler.create()) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - saveTaskPushNotificationConfig(taskId, body, ctx); - }); + .handler(BodyHandler.create()) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + saveTaskPushNotificationConfig(taskId, body, ctx); + }); router.post("/test/queue/awaitChildCountStable/:taskId/:expectedCount/:timeoutMs") - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String expectedCount = ctx.pathParam("expectedCount"); - String timeoutMs = ctx.pathParam("timeoutMs"); - awaitChildQueueCountStable(taskId, expectedCount, timeoutMs, ctx); - }); + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String expectedCount = ctx.pathParam("expectedCount"); + String timeoutMs = ctx.pathParam("timeoutMs"); + awaitChildQueueCountStable(taskId, expectedCount, timeoutMs, ctx); + }); } public void saveTask(String body, RoutingContext rc) { @@ -200,7 +205,8 @@ public void getChildQueueCount(String taskId, RoutingContext rc) { rc.response().setStatusCode(200).end(String.valueOf(count)); } - public void deleteTaskPushNotificationConfig(String taskId, String configId, RoutingContext rc) { + public void deleteTaskPushNotificationConfig( + String taskId, String configId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { @@ -216,7 +222,8 @@ public void deleteTaskPushNotificationConfig(String taskId, String configId, Rou public void saveTaskPushNotificationConfig(String taskId, String body, RoutingContext rc) { try { - TaskPushNotificationConfig notificationConfig = JsonUtil.fromJson(body, TaskPushNotificationConfig.class); + TaskPushNotificationConfig notificationConfig = + JsonUtil.fromJson(body, TaskPushNotificationConfig.class); if (notificationConfig == null) { rc.response().setStatusCode(404).end(); return; @@ -229,11 +236,13 @@ public void saveTaskPushNotificationConfig(String taskId, String body, RoutingCo } } - public void awaitChildQueueCountStable(String taskId, String expectedCountStr, String timeoutMsStr, RoutingContext rc) { + public void awaitChildQueueCountStable( + String taskId, String expectedCountStr, String timeoutMsStr, RoutingContext rc) { try { int expectedCount = Integer.parseInt(expectedCountStr); long timeoutMs = Long.parseLong(timeoutMsStr); - boolean stable = testUtilsBean.awaitChildQueueCountStable(taskId, expectedCount, timeoutMs); + boolean stable = + testUtilsBean.awaitChildQueueCountStable(taskId, expectedCount, timeoutMs); rc.response().setStatusCode(200).end(String.valueOf(stable)); } catch (Throwable t) { errorResponse(t, rc); diff --git a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/AuthTestProfile.java b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/AuthTestProfile.java index 15c480c51..6e337e2ef 100644 --- a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/AuthTestProfile.java +++ b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/AuthTestProfile.java @@ -1,8 +1,7 @@ package org.a2aproject.sdk.tests.multiversion.jsonrpc; -import java.util.Map; - import io.quarkus.test.junit.QuarkusTestProfile; +import java.util.Map; public class AuthTestProfile implements QuarkusTestProfile { @@ -16,8 +15,7 @@ public Map getConfigOverrides() { Map.entry("quarkus.security.users.embedded.users.testuser", "testpass"), Map.entry("quarkus.security.users.embedded.roles.testuser", "user"), Map.entry("quarkus.http.auth.basic", "true"), - Map.entry("quarkus.http.auth.proactive", "true") - ); + Map.entry("quarkus.http.auth.proactive", "true")); } @Override diff --git a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCTest.java b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCTest.java index bd7996d2a..7a1fa9deb 100644 --- a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCTest.java +++ b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCTest.java @@ -2,16 +2,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import com.google.gson.JsonParser; +import io.quarkus.test.junit.QuarkusTest; +import io.vertx.core.Vertx; +import jakarta.inject.Inject; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; - -import jakarta.inject.Inject; - -import com.google.gson.JsonParser; -import io.quarkus.test.junit.QuarkusTest; -import io.vertx.core.Vertx; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; import org.a2aproject.sdk.client.transport.jsonrpc.JSONRPCTransport; @@ -23,8 +21,7 @@ @QuarkusTest public class MultiVersionJSONRPCTest extends AbstractA2AServerTest { - @Inject - Vertx vertx; + @Inject Vertx vertx; public MultiVersionJSONRPCTest() { super(8081); @@ -42,56 +39,80 @@ protected String getTransportUrl() { @Override protected void configureTransport(ClientBuilder builder) { - builder.withTransport(JSONRPCTransport.class, - new JSONRPCTransportConfigBuilder() - .httpClient(new VertxA2AHttpClient(vertx))); + builder.withTransport( + JSONRPCTransport.class, + new JSONRPCTransportConfigBuilder().httpClient(new VertxA2AHttpClient(vertx))); } + @Test public void testUnsupportedVersionBeforeBodyValidation() throws Exception { for (String body : new String[] {"{", "{\"id\":\"recoverable\",\"method\":false}"}) { HttpResponse response = decodeResponse("2.0", "", body); - assertEquals(JsonParser.parseString(""" - {"jsonrpc":"2.0","id":null,"error":{"code":-32009, - "message":"Protocol version '2.0' is not supported. Supported versions: [1.0, 0.3]", - "data":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"VERSION_NOT_SUPPORTED", - "domain":"a2a-protocol.org","metadata":{}}]}} - """), JsonParser.parseString(response.body())); + assertEquals( + JsonParser.parseString( + """ + {"jsonrpc":"2.0","id":null,"error":{"code":-32009, + "message":"Protocol version '2.0' is not supported. Supported versions: [1.0, 0.3]", + "data":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"VERSION_NOT_SUPPORTED", + "domain":"a2a-protocol.org","metadata":{}}]}} + """), + JsonParser.parseString(response.body())); } } @Test public void testSelectedVersionDecodeErrors() throws Exception { - HttpResponse response = decodeResponse("1.0", "", """ - {"id":"original","method":"SendMessage","params":[]} - """); - assertEquals(JsonParser.parseString(""" - {"jsonrpc":"2.0","id":"original","error":{"code":-32600, - "message":"Missing required 'jsonrpc' field. All requests must include 'jsonrpc': '2.0'", - "data":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_REQUEST", - "domain":"a2a-protocol.org","metadata":{}}]}} - """), JsonParser.parseString(response.body())); - response = decodeResponse("1.0", "", """ - {"jsonrpc":"2.0","id":"original","method":"SendMessage","params":{ - "message":{"role":"ROLE_USER","parts":[{"text":"hello"}]}}} - """); - assertEquals(JsonParser.parseString(""" - {"jsonrpc":"2.0","id":"original","error":{"code":-32602, - "message":"Required field cannot be null or empty", - "data":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_PARAMS", - "domain":"a2a-protocol.org","metadata":{}}]}} - """), JsonParser.parseString(response.body())); + HttpResponse response = + decodeResponse( + "1.0", + "", + """ + {"id":"original","method":"SendMessage","params":[]} + """); + assertEquals( + JsonParser.parseString( + """ + {"jsonrpc":"2.0","id":"original","error":{"code":-32600, + "message":"Missing required 'jsonrpc' field. All requests must include 'jsonrpc': '2.0'", + "data":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_REQUEST", + "domain":"a2a-protocol.org","metadata":{}}]}} + """), + JsonParser.parseString(response.body())); + response = + decodeResponse( + "1.0", + "", + """ + {"jsonrpc":"2.0","id":"original","method":"SendMessage","params":{ + "message":{"role":"ROLE_USER","parts":[{"text":"hello"}]}}} + """); + assertEquals( + JsonParser.parseString( + """ + {"jsonrpc":"2.0","id":"original","error":{"code":-32602, + "message":"Required field cannot be null or empty", + "data":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_PARAMS", + "domain":"a2a-protocol.org","metadata":{}}]}} + """), + JsonParser.parseString(response.body())); } - private HttpResponse decodeResponse(String header, String query, String body) throws Exception { - HttpRequest.Builder request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/" + query)) - .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body)); + + private HttpResponse decodeResponse(String header, String query, String body) + throws Exception { + HttpRequest.Builder request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/" + query)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)); if (header != null) { request.header("A2A-Version", header); } - HttpResponse response = HttpClient.newHttpClient().send(request.build(), HttpResponse.BodyHandlers.ofString()); + HttpResponse response = + HttpClient.newHttpClient() + .send(request.build(), HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode()); - assertEquals("application/json", response.headers().firstValue("Content-Type").orElseThrow()); + assertEquals( + "application/json", response.headers().firstValue("Content-Type").orElseThrow()); return response; } - } diff --git a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCWithAuthTest.java b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCWithAuthTest.java index a0464ab0b..652bf2f54 100644 --- a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCWithAuthTest.java +++ b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCWithAuthTest.java @@ -16,8 +16,7 @@ @TestProfile(AuthTestProfile.class) public class MultiVersionJSONRPCWithAuthTest extends AbstractA2AServerWithAuthTest { - @Inject - Vertx vertx; + @Inject Vertx vertx; public MultiVersionJSONRPCWithAuthTest() { super(8081); @@ -35,11 +34,15 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder builder) { - AuthInterceptor authInterceptor = new AuthInterceptor( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); - - builder.withTransport(JSONRPCTransport.class, + AuthInterceptor authInterceptor = + new AuthInterceptor( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); + + builder.withTransport( + JSONRPCTransport.class, new JSONRPCTransportConfigBuilder() .httpClient(new VertxA2AHttpClient(vertx)) .addInterceptor(authInterceptor)); @@ -47,8 +50,8 @@ protected void configureTransportWithAuth(ClientBuilder builder) { @Override protected void configureTransport(ClientBuilder builder) { - builder.withTransport(JSONRPCTransport.class, - new JSONRPCTransportConfigBuilder() - .httpClient(new VertxA2AHttpClient(vertx))); + builder.withTransport( + JSONRPCTransport.class, + new JSONRPCTransportConfigBuilder().httpClient(new VertxA2AHttpClient(vertx))); } } diff --git a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCWithTaskAuthorizationTest.java b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCWithTaskAuthorizationTest.java index 904f37b90..08f2cab7b 100644 --- a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCWithTaskAuthorizationTest.java +++ b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPCWithTaskAuthorizationTest.java @@ -14,10 +14,10 @@ @QuarkusTest @TestProfile(TaskAuthorizationTestProfile.class) -public class MultiVersionJSONRPCWithTaskAuthorizationTest extends AbstractA2AServerWithTaskAuthorizationTest { +public class MultiVersionJSONRPCWithTaskAuthorizationTest + extends AbstractA2AServerWithTaskAuthorizationTest { - @Inject - Vertx vertx; + @Inject Vertx vertx; @Override protected String getTransportProtocol() { @@ -30,11 +30,16 @@ protected String getTransportUrl() { } @Override - protected void configureTransportWithCredentials(ClientBuilder builder, String username, String password) { - AuthInterceptor authInterceptor = new AuthInterceptor( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) - ? getEncodedCredentials(username, password) : null); - builder.withTransport(JSONRPCTransport.class, + protected void configureTransportWithCredentials( + ClientBuilder builder, String username, String password) { + AuthInterceptor authInterceptor = + new AuthInterceptor( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials(username, password) + : null); + builder.withTransport( + JSONRPCTransport.class, new JSONRPCTransportConfigBuilder() .httpClient(new VertxA2AHttpClient(vertx)) .addInterceptor(authInterceptor)); diff --git a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPC_v0_3_Test.java b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPC_v0_3_Test.java index b6c5ce98e..a20a2bb16 100644 --- a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPC_v0_3_Test.java +++ b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPC_v0_3_Test.java @@ -2,13 +2,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import com.google.gson.JsonParser; +import io.quarkus.test.junit.QuarkusTest; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; - -import com.google.gson.JsonParser; -import io.quarkus.test.junit.QuarkusTest; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; @@ -35,17 +34,26 @@ protected String getTransportUrl() { @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()); + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()); } + @Test public void testSelectedVersionDecodeErrors() throws Exception { assertVersionFieldError(decodeResponse("0.3", "", "{\"id\":\"original\",\"method\":null}")); - HttpResponse response = decodeResponse("0.3", "", """ - {"jsonrpc":"2.0","id":"original","method":"message/stream","params":[]} - """); - assertEquals(JsonParser.parseString(""" - {"jsonrpc":"2.0","id":"original","error":{"code":-32602,"message":"Failed to parse JSON"}} - """), JsonParser.parseString(response.body())); + HttpResponse response = + decodeResponse( + "0.3", + "", + """ + {"jsonrpc":"2.0","id":"original","method":"message/stream","params":[]} + """); + assertEquals( + JsonParser.parseString( + """ + {"jsonrpc":"2.0","id":"original","error":{"code":-32602,"message":"Failed to parse JSON"}} + """), + JsonParser.parseString(response.body())); } @Test @@ -55,31 +63,43 @@ public void testVersionSelectionBeforeDecode() throws Exception { assertVersionFieldError(decodeResponse("0.3", "?A2A-Version=1.0", body)); assertVersionFieldError(decodeResponse(null, "?A2A-Version=0.3", body)); HttpResponse response = decodeResponse("1.0", "?A2A-Version=0.3", body); - assertEquals(JsonParser.parseString(""" - {"jsonrpc":"2.0","id":"original","error":{"code":-32600, - "message":"Missing required 'jsonrpc' field. All requests must include 'jsonrpc': '2.0'", - "data":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_REQUEST", - "domain":"a2a-protocol.org","metadata":{}}]}} - """), JsonParser.parseString(response.body())); + assertEquals( + JsonParser.parseString( + """ + {"jsonrpc":"2.0","id":"original","error":{"code":-32600, + "message":"Missing required 'jsonrpc' field. All requests must include 'jsonrpc': '2.0'", + "data":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_REQUEST", + "domain":"a2a-protocol.org","metadata":{}}]}} + """), + JsonParser.parseString(response.body())); } private void assertVersionFieldError(HttpResponse response) { - assertEquals(JsonParser.parseString(""" - {"jsonrpc":"2.0","id":"original","error":{"code":-32600, - "message":"Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"}} - """), JsonParser.parseString(response.body())); + assertEquals( + JsonParser.parseString( + """ + {"jsonrpc":"2.0","id":"original","error":{"code":-32600, + "message":"Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"}} + """), + JsonParser.parseString(response.body())); } - private HttpResponse decodeResponse(String header, String query, String body) throws Exception { - HttpRequest.Builder request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/" + query)) - .header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body)); + + private HttpResponse decodeResponse(String header, String query, String body) + throws Exception { + HttpRequest.Builder request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/" + query)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)); if (header != null) { request.header("A2A-Version", header); } - HttpResponse response = HttpClient.newHttpClient().send(request.build(), HttpResponse.BodyHandlers.ofString()); + HttpResponse response = + HttpClient.newHttpClient() + .send(request.build(), HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode()); - assertEquals("application/json", response.headers().firstValue("Content-Type").orElseThrow()); + assertEquals( + "application/json", response.headers().firstValue("Content-Type").orElseThrow()); return response; } - } diff --git a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPC_v0_3_WithAuthTest.java b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPC_v0_3_WithAuthTest.java index 07e8f8144..2539b9de9 100644 --- a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPC_v0_3_WithAuthTest.java +++ b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/MultiVersionJSONRPC_v0_3_WithAuthTest.java @@ -3,8 +3,8 @@ import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.jsonrpc.JSONRPCTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithAuthTest_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; @@ -29,18 +29,21 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); - - builder.withTransport(JSONRPCTransport_v0_3.class, - new JSONRPCTransportConfigBuilder_v0_3() - .addInterceptor(authInterceptor)); + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); + + builder.withTransport( + JSONRPCTransport_v0_3.class, + new JSONRPCTransportConfigBuilder_v0_3().addInterceptor(authInterceptor)); } @Override protected void configureTransport(ClientBuilder_v0_3 builder) { - builder.withTransport(JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()); + builder.withTransport( + JSONRPCTransport_v0_3.class, new JSONRPCTransportConfigBuilder_v0_3()); } - } diff --git a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/TaskAuthorizationTestProfile.java b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/TaskAuthorizationTestProfile.java index 6440a1a6a..7b5f9830f 100644 --- a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/TaskAuthorizationTestProfile.java +++ b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/TaskAuthorizationTestProfile.java @@ -3,8 +3,6 @@ import java.util.HashMap; import java.util.Map; -import io.quarkus.test.junit.QuarkusTestProfile; - public class TaskAuthorizationTestProfile extends AuthTestProfile { @Override diff --git a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/TestIdentityProvider.java b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/TestIdentityProvider.java index 45a8c301a..0cf0d1eff 100644 --- a/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/TestIdentityProvider.java +++ b/tests/multiversion/jsonrpc/src/test/java/org/a2aproject/sdk/tests/multiversion/jsonrpc/TestIdentityProvider.java @@ -1,7 +1,5 @@ package org.a2aproject.sdk.tests.multiversion.jsonrpc; -import jakarta.enterprise.context.ApplicationScoped; - import io.quarkus.arc.Unremovable; import io.quarkus.arc.properties.IfBuildProperty; import io.quarkus.security.identity.AuthenticationRequestContext; @@ -9,6 +7,7 @@ import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped @Unremovable @@ -16,13 +15,16 @@ public class TestIdentityProvider implements SecurityIdentityAugmentor { @Override - public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context) { + public Uni augment( + SecurityIdentity identity, AuthenticationRequestContext context) { if (identity.isAnonymous()) { - return Uni.createFrom().item(QuarkusSecurityIdentity.builder() - .setPrincipal(() -> "testuser") - .addRole("user") - .setAnonymous(false) - .build()); + return Uni.createFrom() + .item( + QuarkusSecurityIdentity.builder() + .setPrincipal(() -> "testuser") + .addRole("user") + .setAnonymous(false) + .build()); } return Uni.createFrom().item(identity); } diff --git a/tests/multiversion/rest/pom.xml b/tests/multiversion/rest/pom.xml index 594f168d0..4f353f255 100644 --- a/tests/multiversion/rest/pom.xml +++ b/tests/multiversion/rest/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 diff --git a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/A2ATestRoutes.java b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/A2ATestRoutes.java index 038ac6e96..b8f765ff3 100644 --- a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/A2ATestRoutes.java +++ b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/A2ATestRoutes.java @@ -4,17 +4,15 @@ import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN; -import java.util.concurrent.atomic.AtomicInteger; - +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; import jakarta.annotation.PostConstruct; import jakarta.annotation.Priority; import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.inject.Singleton; - -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.handler.BodyHandler; +import java.util.concurrent.atomic.AtomicInteger; import org.a2aproject.sdk.compat03.server.rest.quarkus.A2AServerRoutes_v0_3; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import org.a2aproject.sdk.server.apps.common.TestUtilsBean; @@ -29,14 +27,11 @@ */ @Singleton public class A2ATestRoutes { - @Inject - TestUtilsBean testUtilsBean; + @Inject TestUtilsBean testUtilsBean; - @Inject - A2AServerRoutes a2AServerRoutes; + @Inject A2AServerRoutes a2AServerRoutes; - @Inject - A2AServerRoutes_v0_3 a2AServerRoutes_v0_3; + @Inject A2AServerRoutes_v0_3 a2AServerRoutes_v0_3; AtomicInteger streamingSubscribedCount = new AtomicInteger(0); @@ -54,125 +49,134 @@ void setupRouter(@Observes @Priority(1) Router router) { // POST /test/task - Save task router.post("/test/task") - .order(0) - .consumes(APPLICATION_JSON) - .handler(BodyHandler.create()) - .blockingHandler(ctx -> { - String body = ctx.body().asString(); - if (body == null) { - body = ""; - } - saveTask(body, ctx); - }); + .order(0) + .consumes(APPLICATION_JSON) + .handler(BodyHandler.create()) + .blockingHandler( + ctx -> { + String body = ctx.body().asString(); + if (body == null) { + body = ""; + } + saveTask(body, ctx); + }); // GET /test/task/:taskId - Get task router.get("/test/task/:taskId") - .order(0) - .produces(APPLICATION_JSON) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - getTask(taskId, ctx); - }); + .order(0) + .produces(APPLICATION_JSON) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + getTask(taskId, ctx); + }); // DELETE /test/task/:taskId - Delete task router.delete("/test/task/:taskId") - .order(0) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - deleteTask(taskId, ctx); - }); + .order(0) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + deleteTask(taskId, ctx); + }); // POST /test/queue/ensure/:taskId - Ensure task queue router.post("/test/queue/ensure/:taskId") - .order(0) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - ensureTaskQueue(taskId, ctx); - }); + .order(0) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + ensureTaskQueue(taskId, ctx); + }); // POST /test/queue/enqueueTaskStatusUpdateEvent/:taskId router.post("/test/queue/enqueueTaskStatusUpdateEvent/:taskId") - .order(0) - .handler(BodyHandler.create()) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - if (body == null) { - body = ""; - } - enqueueTaskStatusUpdateEvent(taskId, body, ctx); - }); + .order(0) + .handler(BodyHandler.create()) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + if (body == null) { + body = ""; + } + enqueueTaskStatusUpdateEvent(taskId, body, ctx); + }); // POST /test/queue/enqueueTaskArtifactUpdateEvent/:taskId router.post("/test/queue/enqueueTaskArtifactUpdateEvent/:taskId") - .order(0) - .handler(BodyHandler.create()) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - if (body == null) { - body = ""; - } - enqueueTaskArtifactUpdateEvent(taskId, body, ctx); - }); + .order(0) + .handler(BodyHandler.create()) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + if (body == null) { + body = ""; + } + enqueueTaskArtifactUpdateEvent(taskId, body, ctx); + }); // GET /test/streamingSubscribedCount router.get("/test/streamingSubscribedCount") - .order(0) - .produces(TEXT_PLAIN) - .handler(ctx -> { - getStreamingSubscribedCount(ctx); - }); + .order(0) + .produces(TEXT_PLAIN) + .handler( + ctx -> { + getStreamingSubscribedCount(ctx); + }); // GET /test/queue/childCount/:taskId router.get("/test/queue/childCount/:taskId") - .order(0) - .produces(TEXT_PLAIN) - .handler(ctx -> { - String taskId = ctx.pathParam("taskId"); - getChildQueueCount(taskId, ctx); - }); + .order(0) + .produces(TEXT_PLAIN) + .handler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + getChildQueueCount(taskId, ctx); + }); // DELETE /test/task/:taskId/config/:configId router.delete("/test/task/:taskId/config/:configId") - .order(0) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String configId = ctx.pathParam("configId"); - deleteTaskPushNotificationConfig(taskId, configId, ctx); - }); + .order(0) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String configId = ctx.pathParam("configId"); + deleteTaskPushNotificationConfig(taskId, configId, ctx); + }); // POST /test/task/:taskId - Save task push notification config router.post("/test/task/:taskId") - .order(0) - .handler(BodyHandler.create()) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String body = ctx.body().asString(); - if (body == null) { - body = ""; - } - saveTaskPushNotificationConfig(taskId, body, ctx); - }); + .order(0) + .handler(BodyHandler.create()) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String body = ctx.body().asString(); + if (body == null) { + body = ""; + } + saveTaskPushNotificationConfig(taskId, body, ctx); + }); // POST /test/queue/awaitChildCountStable/:taskId/:expectedCount/:timeoutMs router.post("/test/queue/awaitChildCountStable/:taskId/:expectedCount/:timeoutMs") - .order(0) - .blockingHandler(ctx -> { - String taskId = ctx.pathParam("taskId"); - String expectedCountStr = ctx.pathParam("expectedCount"); - String timeoutMsStr = ctx.pathParam("timeoutMs"); - awaitChildQueueCountStable(taskId, expectedCountStr, timeoutMsStr, ctx); - }); + .order(0) + .blockingHandler( + ctx -> { + String taskId = ctx.pathParam("taskId"); + String expectedCountStr = ctx.pathParam("expectedCount"); + String timeoutMsStr = ctx.pathParam("timeoutMs"); + awaitChildQueueCountStable(taskId, expectedCountStr, timeoutMsStr, ctx); + }); } public void saveTask(String body, RoutingContext rc) { try { Task task = JsonUtil.fromJson(body, Task.class); testUtilsBean.saveTask(task); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -182,9 +186,7 @@ public void getTask(String taskId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } rc.response() @@ -200,15 +202,11 @@ public void deleteTask(String taskId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } testUtilsBean.deleteTask(taskId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -217,9 +215,7 @@ public void deleteTask(String taskId, RoutingContext rc) { public void ensureTaskQueue(String taskId, RoutingContext rc) { try { testUtilsBean.ensureQueue(taskId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -229,9 +225,7 @@ public void enqueueTaskStatusUpdateEvent(String taskId, String body, RoutingCont try { TaskStatusUpdateEvent event = JsonUtil.fromJson(body, TaskStatusUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -241,40 +235,31 @@ public void enqueueTaskArtifactUpdateEvent(String taskId, String body, RoutingCo try { TaskArtifactUpdateEvent event = JsonUtil.fromJson(body, TaskArtifactUpdateEvent.class); testUtilsBean.enqueueEvent(taskId, event); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } } public void getStreamingSubscribedCount(RoutingContext rc) { - rc.response() - .setStatusCode(200) - .end(String.valueOf(streamingSubscribedCount.get())); + rc.response().setStatusCode(200).end(String.valueOf(streamingSubscribedCount.get())); } public void getChildQueueCount(String taskId, RoutingContext rc) { int count = testUtilsBean.getChildQueueCount(taskId); - rc.response() - .setStatusCode(200) - .end(String.valueOf(count)); + rc.response().setStatusCode(200).end(String.valueOf(count)); } - public void deleteTaskPushNotificationConfig(String taskId, String configId, RoutingContext rc) { + public void deleteTaskPushNotificationConfig( + String taskId, String configId, RoutingContext rc) { try { Task task = testUtilsBean.getTask(taskId); if (task == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } testUtilsBean.deleteTaskPushNotificationConfig(taskId, configId); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } @@ -282,31 +267,28 @@ public void deleteTaskPushNotificationConfig(String taskId, String configId, Rou public void saveTaskPushNotificationConfig(String taskId, String body, RoutingContext rc) { try { - TaskPushNotificationConfig notificationConfig = JsonUtil.fromJson(body, TaskPushNotificationConfig.class); + TaskPushNotificationConfig notificationConfig = + JsonUtil.fromJson(body, TaskPushNotificationConfig.class); if (notificationConfig == null) { - rc.response() - .setStatusCode(404) - .end(); + rc.response().setStatusCode(404).end(); return; } testUtilsBean.saveTaskPushNotificationConfig( taskId, notificationConfig, rc.request().getHeader("A2A-Version")); - rc.response() - .setStatusCode(200) - .end(); + rc.response().setStatusCode(200).end(); } catch (Throwable t) { errorResponse(t, rc); } } - public void awaitChildQueueCountStable(String taskId, String expectedCountStr, String timeoutMsStr, RoutingContext rc) { + public void awaitChildQueueCountStable( + String taskId, String expectedCountStr, String timeoutMsStr, RoutingContext rc) { try { int expectedCount = Integer.parseInt(expectedCountStr); long timeoutMs = Long.parseLong(timeoutMsStr); - boolean stable = testUtilsBean.awaitChildQueueCountStable(taskId, expectedCount, timeoutMs); - rc.response() - .setStatusCode(200) - .end(String.valueOf(stable)); + boolean stable = + testUtilsBean.awaitChildQueueCountStable(taskId, expectedCount, timeoutMs); + rc.response().setStatusCode(200).end(String.valueOf(stable)); } catch (Throwable t) { errorResponse(t, rc); } @@ -314,9 +296,6 @@ public void awaitChildQueueCountStable(String taskId, String expectedCountStr, S private void errorResponse(Throwable t, RoutingContext rc) { t.printStackTrace(); - rc.response() - .setStatusCode(500) - .putHeader(CONTENT_TYPE, TEXT_PLAIN) - .end(); + rc.response().setStatusCode(500).putHeader(CONTENT_TYPE, TEXT_PLAIN).end(); } } diff --git a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/AuthTestProfile.java b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/AuthTestProfile.java index 710fc9a08..ea954a1db 100644 --- a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/AuthTestProfile.java +++ b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/AuthTestProfile.java @@ -1,8 +1,7 @@ package org.a2aproject.sdk.tests.multiversion.rest; -import java.util.Map; - import io.quarkus.test.junit.QuarkusTestProfile; +import java.util.Map; public class AuthTestProfile implements QuarkusTestProfile { @@ -16,8 +15,7 @@ public Map getConfigOverrides() { Map.entry("quarkus.security.users.embedded.users.testuser", "testpass"), Map.entry("quarkus.security.users.embedded.roles.testuser", "user"), Map.entry("quarkus.http.auth.basic", "true"), - Map.entry("quarkus.http.auth.proactive", "true") - ); + Map.entry("quarkus.http.auth.proactive", "true")); } @Override diff --git a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestTest.java b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestTest.java index cb347b4e6..17ad2c11b 100644 --- a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestTest.java +++ b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestTest.java @@ -2,16 +2,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import com.google.gson.JsonParser; +import io.quarkus.test.junit.QuarkusTest; +import io.vertx.core.Vertx; +import jakarta.inject.Inject; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; - -import jakarta.inject.Inject; - -import com.google.gson.JsonParser; -import io.quarkus.test.junit.QuarkusTest; -import io.vertx.core.Vertx; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; import org.a2aproject.sdk.client.transport.rest.RestTransport; @@ -23,8 +21,7 @@ @QuarkusTest public class MultiVersionRestTest extends AbstractA2AServerTest { - @Inject - Vertx vertx; + @Inject Vertx vertx; public MultiVersionRestTest() { super(8081); @@ -42,38 +39,55 @@ protected String getTransportUrl() { @Override protected void configureTransport(ClientBuilder builder) { - builder.withTransport(RestTransport.class, - new RestTransportConfigBuilder() - .httpClient(new VertxA2AHttpClient(vertx))); + builder.withTransport( + RestTransport.class, + new RestTransportConfigBuilder().httpClient(new VertxA2AHttpClient(vertx))); } + @Test public void testUnsupportedVersionBeforeMalformedHistory() throws Exception { HttpResponse response = historyResponse("2.0", "historyLength=bad"); assertEquals(400, response.statusCode()); - assertEquals(JsonParser.parseString(""" - {"error":{"code":-32009,"message":"Protocol version '2.0' is not supported. Supported versions: [1.0, 0.3]"}} - """), JsonParser.parseString(response.body())); + assertEquals( + JsonParser.parseString( + """ + {"error":{"code":-32009,"message":"Protocol version '2.0' is not supported. Supported versions: [1.0, 0.3]"}} + """), + JsonParser.parseString(response.body())); } @Test public void testSelectedVersionHistoryValidation() throws Exception { HttpResponse response = historyResponse("1.0", "historyLength=bad"); assertEquals(422, response.statusCode()); - assertEquals(JsonParser.parseString(""" - {"error":{"code":422,"status":"INVALID_ARGUMENT","message":"bad historyLength","details":[{ - "@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_PARAMS", - "domain":"a2a-protocol.org","metadata":{}}]}} - """), JsonParser.parseString(response.body())); + assertEquals( + JsonParser.parseString( + """ + {"error":{"code":422,"status":"INVALID_ARGUMENT","message":"bad historyLength","details":[{ + "@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_PARAMS", + "domain":"a2a-protocol.org","metadata":{}}]}} + """), + JsonParser.parseString(response.body())); } + private HttpResponse historyResponse(String header, String query) throws Exception { - HttpRequest.Builder request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/v1/tasks/missing-history-task?" + query)).GET(); + HttpRequest.Builder request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/v1/tasks/missing-history-task?" + + query)) + .GET(); if (header != null) { request.header("A2A-Version", header); } - HttpResponse response = HttpClient.newHttpClient().send(request.build(), HttpResponse.BodyHandlers.ofString()); - assertEquals("application/json", response.headers().firstValue("Content-Type").orElseThrow()); + HttpResponse response = + HttpClient.newHttpClient() + .send(request.build(), HttpResponse.BodyHandlers.ofString()); + assertEquals( + "application/json", response.headers().firstValue("Content-Type").orElseThrow()); return response; } - } diff --git a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestWithAuthTest.java b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestWithAuthTest.java index 8ad592b5a..4a8fe3076 100644 --- a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestWithAuthTest.java +++ b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestWithAuthTest.java @@ -5,11 +5,11 @@ import io.vertx.core.Vertx; import jakarta.inject.Inject; import org.a2aproject.sdk.client.ClientBuilder; -import org.a2aproject.sdk.common.A2AHeaders; import org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient; import org.a2aproject.sdk.client.transport.rest.RestTransport; import org.a2aproject.sdk.client.transport.rest.RestTransportConfigBuilder; import org.a2aproject.sdk.client.transport.spi.interceptors.auth.AuthInterceptor; +import org.a2aproject.sdk.common.A2AHeaders; import org.a2aproject.sdk.server.apps.common.AbstractA2AServerWithAuthTest; import org.a2aproject.sdk.spec.AgentInterface; import org.a2aproject.sdk.spec.TransportProtocol; @@ -19,8 +19,7 @@ @TestProfile(AuthTestProfile.class) public class MultiVersionRestWithAuthTest extends AbstractA2AServerWithAuthTest { - @Inject - Vertx vertx; + @Inject Vertx vertx; public MultiVersionRestWithAuthTest() { super(8081); @@ -38,11 +37,15 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder builder) { - AuthInterceptor authInterceptor = new AuthInterceptor( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); + AuthInterceptor authInterceptor = + new AuthInterceptor( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); - builder.withTransport(RestTransport.class, + builder.withTransport( + RestTransport.class, new RestTransportConfigBuilder() .httpClient(new VertxA2AHttpClient(vertx)) .addInterceptor(authInterceptor)); @@ -50,9 +53,9 @@ protected void configureTransportWithAuth(ClientBuilder builder) { @Override protected void configureTransport(ClientBuilder builder) { - builder.withTransport(RestTransport.class, - new RestTransportConfigBuilder() - .httpClient(new VertxA2AHttpClient(vertx))); + builder.withTransport( + RestTransport.class, + new RestTransportConfigBuilder().httpClient(new VertxA2AHttpClient(vertx))); } @Test diff --git a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestWithTaskAuthorizationTest.java b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestWithTaskAuthorizationTest.java index d342d391f..9aede7fa1 100644 --- a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestWithTaskAuthorizationTest.java +++ b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersionRestWithTaskAuthorizationTest.java @@ -14,10 +14,10 @@ @QuarkusTest @TestProfile(TaskAuthorizationTestProfile.class) -public class MultiVersionRestWithTaskAuthorizationTest extends AbstractA2AServerWithTaskAuthorizationTest { +public class MultiVersionRestWithTaskAuthorizationTest + extends AbstractA2AServerWithTaskAuthorizationTest { - @Inject - Vertx vertx; + @Inject Vertx vertx; @Override protected String getTransportProtocol() { @@ -30,11 +30,16 @@ protected String getTransportUrl() { } @Override - protected void configureTransportWithCredentials(ClientBuilder builder, String username, String password) { - AuthInterceptor authInterceptor = new AuthInterceptor( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) - ? getEncodedCredentials(username, password) : null); - builder.withTransport(RestTransport.class, + protected void configureTransportWithCredentials( + ClientBuilder builder, String username, String password) { + AuthInterceptor authInterceptor = + new AuthInterceptor( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials(username, password) + : null); + builder.withTransport( + RestTransport.class, new RestTransportConfigBuilder() .httpClient(new VertxA2AHttpClient(vertx)) .addInterceptor(authInterceptor)); diff --git a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersion_v0_3_RestTest.java b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersion_v0_3_RestTest.java index 9462576e2..7915352aa 100644 --- a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersion_v0_3_RestTest.java +++ b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersion_v0_3_RestTest.java @@ -2,13 +2,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import com.google.gson.JsonParser; +import io.quarkus.test.junit.QuarkusTest; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; - -import com.google.gson.JsonParser; -import io.quarkus.test.junit.QuarkusTest; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; @@ -37,6 +36,7 @@ protected String getTransportUrl() { protected void configureTransport(ClientBuilder_v0_3 builder) { builder.withTransport(RestTransport_v0_3.class, new RestTransportConfigBuilder_v0_3()); } + @Test public void testSelectedVersionHistoryValidation() throws Exception { assertAliasConflict(historyResponse("0.3", "history_length=1&historyLength=bad")); @@ -45,31 +45,50 @@ public void testSelectedVersionHistoryValidation() throws Exception { @Test public void testVersionSelectionBeforeHistoryValidation() throws Exception { assertAliasConflict(historyResponse(null, "history_length=1&historyLength=bad")); - assertAliasConflict(historyResponse("0.3", "A2A-Version=1.0&history_length=1&historyLength=bad")); - assertAliasConflict(historyResponse(null, "A2A-Version=0.3&history_length=1&historyLength=bad")); - HttpResponse selectedV1 = historyResponse("1.0", "A2A-Version=0.3&historyLength=bad"); + assertAliasConflict( + historyResponse("0.3", "A2A-Version=1.0&history_length=1&historyLength=bad")); + assertAliasConflict( + historyResponse(null, "A2A-Version=0.3&history_length=1&historyLength=bad")); + HttpResponse selectedV1 = + historyResponse("1.0", "A2A-Version=0.3&historyLength=bad"); assertEquals(422, selectedV1.statusCode()); - assertEquals(JsonParser.parseString(""" - {"error":{"code":422,"status":"INVALID_ARGUMENT","message":"bad historyLength","details":[{ - "@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_PARAMS", - "domain":"a2a-protocol.org","metadata":{}}]}} - """), JsonParser.parseString(selectedV1.body())); + assertEquals( + JsonParser.parseString( + """ + {"error":{"code":422,"status":"INVALID_ARGUMENT","message":"bad historyLength","details":[{ + "@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_PARAMS", + "domain":"a2a-protocol.org","metadata":{}}]}} + """), + JsonParser.parseString(selectedV1.body())); } private void assertAliasConflict(HttpResponse response) { assertEquals(422, response.statusCode()); - assertEquals("{\"error\": \"org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3\", " - + "\"message\": \"Only one of 'history_length' or 'historyLength' may be specified\"}", response.body()); + assertEquals( + "{\"error\": \"org.a2aproject.sdk.compat03.spec.InvalidParamsError_v0_3\"," + + " \"message\": \"Only one of 'history_length' or 'historyLength' may be" + + " specified\"}", + response.body()); } + private HttpResponse historyResponse(String header, String query) throws Exception { - HttpRequest.Builder request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/v1/tasks/missing-history-task?" + query)).GET(); + HttpRequest.Builder request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/v1/tasks/missing-history-task?" + + query)) + .GET(); if (header != null) { request.header("A2A-Version", header); } - HttpResponse response = HttpClient.newHttpClient().send(request.build(), HttpResponse.BodyHandlers.ofString()); - assertEquals("application/json", response.headers().firstValue("Content-Type").orElseThrow()); + HttpResponse response = + HttpClient.newHttpClient() + .send(request.build(), HttpResponse.BodyHandlers.ofString()); + assertEquals( + "application/json", response.headers().firstValue("Content-Type").orElseThrow()); return response; } - } diff --git a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersion_v0_3_RestWithAuthTest.java b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersion_v0_3_RestWithAuthTest.java index dafd299a9..20de635e6 100644 --- a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersion_v0_3_RestWithAuthTest.java +++ b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/MultiVersion_v0_3_RestWithAuthTest.java @@ -3,8 +3,8 @@ import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; import org.a2aproject.sdk.compat03.client.ClientBuilder_v0_3; -import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.rest.RestTransportConfigBuilder_v0_3; +import org.a2aproject.sdk.compat03.client.transport.rest.RestTransport_v0_3; import org.a2aproject.sdk.compat03.client.transport.spi.interceptors.auth.AuthInterceptor_v0_3; import org.a2aproject.sdk.compat03.conversion.AbstractA2AServerWithAuthTest_v0_3; import org.a2aproject.sdk.compat03.spec.TransportProtocol_v0_3; @@ -30,13 +30,16 @@ protected String getTransportUrl() { @Override protected void configureTransportWithAuth(ClientBuilder_v0_3 builder) { - AuthInterceptor_v0_3 authInterceptor = new AuthInterceptor_v0_3( - (schemeName, context) -> BASIC_AUTH_SCHEME_NAME.equals(schemeName) ? getEncodedCredentials() : null - ); - - builder.withTransport(RestTransport_v0_3.class, - new RestTransportConfigBuilder_v0_3() - .addInterceptor(authInterceptor)); + AuthInterceptor_v0_3 authInterceptor = + new AuthInterceptor_v0_3( + (schemeName, context) -> + BASIC_AUTH_SCHEME_NAME.equals(schemeName) + ? getEncodedCredentials() + : null); + + builder.withTransport( + RestTransport_v0_3.class, + new RestTransportConfigBuilder_v0_3().addInterceptor(authInterceptor)); } @Override @@ -49,10 +52,7 @@ protected void configureTransport(ClientBuilder_v0_3 builder) { public void testBasicAuthWorksViaHttp() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); - givenAuthenticated() - .get("/v1/tasks/" + MINIMAL_TASK.id()) - .then() - .statusCode(200); + givenAuthenticated().get("/v1/tasks/" + MINIMAL_TASK.id()).then().statusCode(200); deleteTaskInTaskStore(MINIMAL_TASK.id()); } diff --git a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/TaskAuthorizationTestProfile.java b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/TaskAuthorizationTestProfile.java index 1d2d62f7f..bdb199f56 100644 --- a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/TaskAuthorizationTestProfile.java +++ b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/TaskAuthorizationTestProfile.java @@ -3,8 +3,6 @@ import java.util.HashMap; import java.util.Map; -import io.quarkus.test.junit.QuarkusTestProfile; - public class TaskAuthorizationTestProfile extends AuthTestProfile { @Override diff --git a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/TestIdentityProvider.java b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/TestIdentityProvider.java index 7a34c1278..c45e6eaba 100644 --- a/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/TestIdentityProvider.java +++ b/tests/multiversion/rest/src/test/java/org/a2aproject/sdk/tests/multiversion/rest/TestIdentityProvider.java @@ -1,7 +1,5 @@ package org.a2aproject.sdk.tests.multiversion.rest; -import jakarta.enterprise.context.ApplicationScoped; - import io.quarkus.arc.Unremovable; import io.quarkus.arc.properties.IfBuildProperty; import io.quarkus.security.identity.AuthenticationRequestContext; @@ -9,6 +7,7 @@ import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; @ApplicationScoped @Unremovable @@ -16,13 +15,16 @@ public class TestIdentityProvider implements SecurityIdentityAugmentor { @Override - public Uni augment(SecurityIdentity identity, AuthenticationRequestContext context) { + public Uni augment( + SecurityIdentity identity, AuthenticationRequestContext context) { if (identity.isAnonymous()) { - return Uni.createFrom().item(QuarkusSecurityIdentity.builder() - .setPrincipal(() -> "testuser") - .addRole("user") - .setAnonymous(false) - .build()); + return Uni.createFrom() + .item( + QuarkusSecurityIdentity.builder() + .setPrincipal(() -> "testuser") + .addRole("user") + .setAnonymous(false) + .build()); } return Uni.createFrom().item(identity); } diff --git a/tests/server-common/pom.xml b/tests/server-common/pom.xml index 6025281da..b459c68cf 100644 --- a/tests/server-common/pom.xml +++ b/tests/server-common/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -84,13 +82,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - - true - - org.apache.maven.plugins maven-jar-plugin @@ -101,7 +92,14 @@ - + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + - \ No newline at end of file + diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/A2AGsonObjectMapper.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/A2AGsonObjectMapper.java index f4145e9cc..9572b86c5 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/A2AGsonObjectMapper.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/A2AGsonObjectMapper.java @@ -4,35 +4,33 @@ */ package org.a2aproject.sdk.server.apps.common; -import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; -import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; import io.restassured.mapper.ObjectMapper; import io.restassured.mapper.ObjectMapperDeserializationContext; import io.restassured.mapper.ObjectMapperSerializationContext; - +import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; +import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; public class A2AGsonObjectMapper implements ObjectMapper { public static final A2AGsonObjectMapper INSTANCE = new A2AGsonObjectMapper(); - private A2AGsonObjectMapper() { - } + private A2AGsonObjectMapper() {} - @Override - public Object deserialize(ObjectMapperDeserializationContext context) { - try { - return JsonUtil.fromJson(context.getDataToDeserialize().asString(), context.getType()); - } catch (JsonProcessingException ex) { - throw new RuntimeException(ex); - } + @Override + public Object deserialize(ObjectMapperDeserializationContext context) { + try { + return JsonUtil.fromJson(context.getDataToDeserialize().asString(), context.getType()); + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } } - @Override + @Override public Object serialize(ObjectMapperSerializationContext context) { - try { - return JsonUtil.toJson(context.getObjectToSerialize()); - } catch (JsonProcessingException ex) { - - throw new RuntimeException(ex); - } + try { + return JsonUtil.toJson(context.getObjectToSerialize()); + } catch (JsonProcessingException ex) { + + throw new RuntimeException(ex); + } } } diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerTest.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerTest.java index a700cbbf5..75dd9348e 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerTest.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerTest.java @@ -10,6 +10,10 @@ import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import io.restassured.RestAssured; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.specification.RequestSpecification; +import jakarta.ws.rs.core.MediaType; import java.io.EOFException; import java.io.IOException; import java.net.URI; @@ -31,20 +35,14 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; - -import jakarta.ws.rs.core.MediaType; - -import io.restassured.RestAssured; -import io.restassured.config.ObjectMapperConfig; -import io.restassured.specification.RequestSpecification; import org.a2aproject.sdk.client.Client; -import org.a2aproject.sdk.common.A2AHeaders; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.ClientEvent; import org.a2aproject.sdk.client.MessageEvent; import org.a2aproject.sdk.client.TaskEvent; import org.a2aproject.sdk.client.TaskUpdateEvent; import org.a2aproject.sdk.client.config.ClientConfig; +import org.a2aproject.sdk.common.A2AHeaders; import org.a2aproject.sdk.grpc.utils.JSONRPCUtils; import org.a2aproject.sdk.grpc.utils.ProtoUtils; import org.a2aproject.sdk.jsonrpc.common.json.JsonProcessingException; @@ -98,44 +96,50 @@ */ public abstract class AbstractA2AServerTest { - protected static final Task MINIMAL_TASK = Task.builder() - .id("task-123") - .contextId("session-xyz") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - - private static final Task CANCEL_TASK = Task.builder() - .id("cancel-task-123") - .contextId("session-xyz") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - - private static final Task CANCEL_TASK_NOT_SUPPORTED = Task.builder() - .id("cancel-task-not-supported-123") - .contextId("session-xyz") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - - private static final Task SEND_MESSAGE_NOT_SUPPORTED = Task.builder() - .id("task-not-supported-123") - .contextId("session-xyz") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - - protected static final Message MESSAGE = Message.builder() - .messageId("111") - .role(Message.Role.ROLE_AGENT) - .parts(new TextPart("test message")) - .build(); + protected static final Task MINIMAL_TASK = + Task.builder() + .id("task-123") + .contextId("session-xyz") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + + private static final Task CANCEL_TASK = + Task.builder() + .id("cancel-task-123") + .contextId("session-xyz") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + + private static final Task CANCEL_TASK_NOT_SUPPORTED = + Task.builder() + .id("cancel-task-not-supported-123") + .contextId("session-xyz") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + + private static final Task SEND_MESSAGE_NOT_SUPPORTED = + Task.builder() + .id("task-not-supported-123") + .contextId("session-xyz") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + + protected static final Message MESSAGE = + Message.builder() + .messageId("111") + .role(Message.Role.ROLE_AGENT) + .parts(new TextPart("test message")) + .build(); public static final String APPLICATION_JSON = "application/json"; public static RequestSpecification given() { - return RestAssured.given() - .header(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION) - .config(RestAssured.config() - .objectMapperConfig(new ObjectMapperConfig(A2AGsonObjectMapper.INSTANCE))); -} - + return RestAssured.given() + .header(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION) + .config( + RestAssured.config() + .objectMapperConfig( + new ObjectMapperConfig(A2AGsonObjectMapper.INSTANCE))); + } protected final int serverPort; private Client client; @@ -253,21 +257,24 @@ public void testCancelTaskNotFound() { @Test public void testListTasksSuccess() throws Exception { // Create multiple tasks with different contexts and states - Task task1 = Task.builder() - .id("list-task-1") - .contextId("context-1") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - Task task2 = Task.builder() - .id("list-task-2") - .contextId("context-1") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - Task task3 = Task.builder() - .id("list-task-3") - .contextId("context-2") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + Task task1 = + Task.builder() + .id("list-task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + Task task2 = + Task.builder() + .id("list-task-2") + .contextId("context-1") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + Task task3 = + Task.builder() + .id("list-task-3") + .contextId("context-2") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); saveTaskInTaskStore(task1); saveTaskInTaskStore(task2); @@ -275,7 +282,8 @@ public void testListTasksSuccess() throws Exception { try { // Test listing all tasks (no filters) - org.a2aproject.sdk.spec.ListTasksParams params = ListTasksParams.builder().tenant("").build(); + org.a2aproject.sdk.spec.ListTasksParams params = + ListTasksParams.builder().tenant("").build(); ListTasksResult result = getClient().listTasks(params); assertNotNull(result); @@ -292,21 +300,24 @@ public void testListTasksSuccess() throws Exception { @Test public void testListTasksFilterByContextId() throws Exception { - Task task1 = Task.builder() - .id("list-task-ctx-1") - .contextId("context-filter-1") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - Task task2 = Task.builder() - .id("list-task-ctx-2") - .contextId("context-filter-1") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - Task task3 = Task.builder() - .id("list-task-ctx-3") - .contextId("context-filter-2") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + Task task1 = + Task.builder() + .id("list-task-ctx-1") + .contextId("context-filter-1") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + Task task2 = + Task.builder() + .id("list-task-ctx-2") + .contextId("context-filter-1") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + Task task3 = + Task.builder() + .id("list-task-ctx-3") + .contextId("context-filter-2") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); saveTaskInTaskStore(task1); saveTaskInTaskStore(task2); @@ -314,16 +325,17 @@ public void testListTasksFilterByContextId() throws Exception { try { // Filter by contextId - org.a2aproject.sdk.spec.ListTasksParams params = ListTasksParams.builder() - .contextId("context-filter-1") - .tenant("") - .build(); + org.a2aproject.sdk.spec.ListTasksParams params = + ListTasksParams.builder().contextId("context-filter-1").tenant("").build(); ListTasksResult result = getClient().listTasks(params); assertNotNull(result); assertNotNull(result.tasks()); - assertEquals(2, result.tasks().size(), "Should have exactly 2 tasks with context-filter-1"); - assertTrue(result.tasks().stream().allMatch(t -> "context-filter-1".equals(t.contextId()))); + assertEquals( + 2, result.tasks().size(), "Should have exactly 2 tasks with context-filter-1"); + assertTrue( + result.tasks().stream() + .allMatch(t -> "context-filter-1".equals(t.contextId()))); } finally { deleteTaskInTaskStore(task1.id()); deleteTaskInTaskStore(task2.id()); @@ -333,21 +345,24 @@ public void testListTasksFilterByContextId() throws Exception { @Test public void testListTasksFilterByStatus() throws Exception { - Task task1 = Task.builder() - .id("list-task-status-1") - .contextId("context-status") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - Task task2 = Task.builder() - .id("list-task-status-2") - .contextId("context-status") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); - Task task3 = Task.builder() - .id("list-task-status-3") - .contextId("context-status") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + Task task1 = + Task.builder() + .id("list-task-status-1") + .contextId("context-status") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + Task task2 = + Task.builder() + .id("list-task-status-2") + .contextId("context-status") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); + Task task3 = + Task.builder() + .id("list-task-status-3") + .contextId("context-status") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); saveTaskInTaskStore(task1); saveTaskInTaskStore(task2); @@ -355,18 +370,21 @@ public void testListTasksFilterByStatus() throws Exception { try { // Filter by status WORKING - org.a2aproject.sdk.spec.ListTasksParams params = ListTasksParams.builder() - .status(TaskState.TASK_STATE_WORKING) - .tenant("") - .build(); + org.a2aproject.sdk.spec.ListTasksParams params = + ListTasksParams.builder() + .status(TaskState.TASK_STATE_WORKING) + .tenant("") + .build(); ListTasksResult result = getClient().listTasks(params); assertNotNull(result); assertNotNull(result.tasks()); assertTrue(result.tasks().size() >= 2, "Should have at least 2 WORKING tasks"); - assertTrue(result.tasks().stream() - .filter(t -> t.id().startsWith("list-task-status-")) - .allMatch(t -> TaskState.TASK_STATE_WORKING.equals(t.status().state()))); + assertTrue( + result.tasks().stream() + .filter(t -> t.id().startsWith("list-task-status-")) + .allMatch( + t -> TaskState.TASK_STATE_WORKING.equals(t.status().state()))); } finally { deleteTaskInTaskStore(task1.id()); deleteTaskInTaskStore(task2.id()); @@ -377,21 +395,24 @@ public void testListTasksFilterByStatus() throws Exception { @Test public void testListTasksWithPagination() throws Exception { // Create several tasks - Task task1 = Task.builder() - .id("page-task-1") - .contextId("page-context") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - Task task2 = Task.builder() - .id("page-task-2") - .contextId("page-context") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); - Task task3 = Task.builder() - .id("page-task-3") - .contextId("page-context") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + Task task1 = + Task.builder() + .id("page-task-1") + .contextId("page-context") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + Task task2 = + Task.builder() + .id("page-task-2") + .contextId("page-context") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); + Task task3 = + Task.builder() + .id("page-task-3") + .contextId("page-context") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); saveTaskInTaskStore(task1); saveTaskInTaskStore(task2); @@ -399,11 +420,12 @@ public void testListTasksWithPagination() throws Exception { try { // Get first page with pageSize=2 - org.a2aproject.sdk.spec.ListTasksParams params1 = ListTasksParams.builder() - .contextId("page-context") - .tenant("") - .pageSize(2) - .build(); + org.a2aproject.sdk.spec.ListTasksParams params1 = + ListTasksParams.builder() + .contextId("page-context") + .tenant("") + .pageSize(2) + .build(); ListTasksResult result1 = getClient().listTasks(params1); assertNotNull(result1); @@ -412,12 +434,13 @@ public void testListTasksWithPagination() throws Exception { assertTrue(result1.hasMoreResults()); // Get second page using pageToken - org.a2aproject.sdk.spec.ListTasksParams params2 = ListTasksParams.builder() - .contextId("page-context") - .tenant("") - .pageSize(2) - .pageToken(result1.nextPageToken()) - .build(); + org.a2aproject.sdk.spec.ListTasksParams params2 = + ListTasksParams.builder() + .contextId("page-context") + .tenant("") + .pageSize(2) + .pageToken(result1.nextPageToken()) + .build(); ListTasksResult result2 = getClient().listTasks(params2); assertNotNull(result2); @@ -432,35 +455,40 @@ public void testListTasksWithPagination() throws Exception { @Test public void testListTasksWithHistoryLimit() throws Exception { // Create task with multiple history messages - List history = List.of( - Message.builder(MESSAGE).messageId("msg-1").build(), - Message.builder(MESSAGE).messageId("msg-2").build(), - Message.builder(MESSAGE).messageId("msg-3").build(), - Message.builder(MESSAGE).messageId("msg-4").build() - ); - Task taskWithHistory = Task.builder() - .id("list-task-history") - .contextId("context-history") - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .history(history) - .build(); + List history = + List.of( + Message.builder(MESSAGE).messageId("msg-1").build(), + Message.builder(MESSAGE).messageId("msg-2").build(), + Message.builder(MESSAGE).messageId("msg-3").build(), + Message.builder(MESSAGE).messageId("msg-4").build()); + Task taskWithHistory = + Task.builder() + .id("list-task-history") + .contextId("context-history") + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .history(history) + .build(); saveTaskInTaskStore(taskWithHistory); try { // List with history limited to 2 messages - org.a2aproject.sdk.spec.ListTasksParams params = ListTasksParams.builder() - .contextId("context-history") - .tenant("") - .historyLength(2) - .build(); + org.a2aproject.sdk.spec.ListTasksParams params = + ListTasksParams.builder() + .contextId("context-history") + .tenant("") + .historyLength(2) + .build(); ListTasksResult result = getClient().listTasks(params); assertNotNull(result); assertEquals(1, result.tasks().size()); Task task = result.tasks().get(0); assertNotNull(task.history()); - assertEquals(2, task.history().size(), "History should be limited to 2 most recent messages"); + assertEquals( + 2, + task.history().size(), + "History should be limited to 2 most recent messages"); // Verify we get the most recent messages (msg-3 and msg-4) assertEquals("msg-3", task.history().get(0).messageId()); assertEquals("msg-4", task.history().get(1).messageId()); @@ -471,24 +499,24 @@ public void testListTasksWithHistoryLimit() throws Exception { @Test public void testSendMessageNewMessageSuccess() throws Exception { - Message message = Message.builder(MESSAGE) - .build(); + Message message = Message.builder(MESSAGE).build(); CountDownLatch latch = new CountDownLatch(1); AtomicReference receivedMessage = new AtomicReference<>(); AtomicBoolean wasUnexpectedEvent = new AtomicBoolean(false); - BiConsumer consumer = (event, agentCard) -> { - if (event instanceof MessageEvent messageEvent) { - if (latch.getCount() > 0) { - receivedMessage.set(messageEvent.getMessage()); - latch.countDown(); - } else { - wasUnexpectedEvent.set(true); - } - } else { - wasUnexpectedEvent.set(true); - } - }; + BiConsumer consumer = + (event, agentCard) -> { + if (event instanceof MessageEvent messageEvent) { + if (latch.getCount() > 0) { + receivedMessage.set(messageEvent.getMessage()); + latch.countDown(); + } else { + wasUnexpectedEvent.set(true); + } + } else { + wasUnexpectedEvent.set(true); + } + }; // testing the non-streaming send message getNonStreamingClient().sendMessage(message, List.of(consumer), null); @@ -506,32 +534,40 @@ public void testSendMessageNewMessageSuccess() throws Exception { @Test public void testRequestScopedBeanAvailableOnAgentExecutorThread() throws Exception { - Message message = Message.builder() - .messageId("request-scoped-test") - .role(Message.Role.ROLE_USER) - .parts(new TextPart("request-scoped:test")) - .build(); + Message message = + Message.builder() + .messageId("request-scoped-test") + .role(Message.Role.ROLE_USER) + .parts(new TextPart("request-scoped:test")) + .build(); CountDownLatch latch = new CountDownLatch(1); AtomicReference receivedTask = new AtomicReference<>(); AtomicReference errorRef = new AtomicReference<>(); - getNonStreamingClient().sendMessage(message, List.of((event, agentCard) -> { - if (event instanceof TaskEvent te) { - receivedTask.set(te.getTask()); - if (te.getTask().status().state() == TaskState.TASK_STATE_COMPLETED) { - latch.countDown(); - } - } else if (event instanceof TaskUpdateEvent tue) { - receivedTask.set(tue.getTask()); - if (tue.getTask().status().state() == TaskState.TASK_STATE_COMPLETED) { - latch.countDown(); - } - } - }), error -> { - errorRef.set(error); - latch.countDown(); - }); + getNonStreamingClient() + .sendMessage( + message, + List.of( + (event, agentCard) -> { + if (event instanceof TaskEvent te) { + receivedTask.set(te.getTask()); + if (te.getTask().status().state() + == TaskState.TASK_STATE_COMPLETED) { + latch.countDown(); + } + } else if (event instanceof TaskUpdateEvent tue) { + receivedTask.set(tue.getTask()); + if (tue.getTask().status().state() + == TaskState.TASK_STATE_COMPLETED) { + latch.countDown(); + } + } + }), + error -> { + errorRef.set(error); + latch.countDown(); + }); assertTrue(latch.await(10, TimeUnit.SECONDS), "Request should complete within timeout"); assertNull(errorRef.get(), "Should not have received an error"); @@ -549,34 +585,42 @@ public void testRequestScopedBeanAvailableOnAgentExecutorThread() throws Excepti @Test public void testRequestScopedBeanAvailableOnAgentExecutorThreadStreaming() throws Exception { - Message message = Message.builder() - .messageId("request-scoped-streaming-test") - .role(Message.Role.ROLE_USER) - .parts(new TextPart("request-scoped:test")) - .build(); + Message message = + Message.builder() + .messageId("request-scoped-streaming-test") + .role(Message.Role.ROLE_USER) + .parts(new TextPart("request-scoped:test")) + .build(); CountDownLatch latch = new CountDownLatch(1); AtomicReference receivedTask = new AtomicReference<>(); AtomicReference errorRef = new AtomicReference<>(); - getClient().sendMessage(message, List.of((event, agentCard) -> { - if (event instanceof TaskEvent te) { - receivedTask.set(te.getTask()); - if (te.getTask().status().state() == TaskState.TASK_STATE_COMPLETED) { - latch.countDown(); - } - } else if (event instanceof TaskUpdateEvent tue) { - receivedTask.set(tue.getTask()); - if (tue.getTask().status().state() == TaskState.TASK_STATE_COMPLETED) { - latch.countDown(); - } - } - }), error -> { - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - latch.countDown(); - }); + getClient() + .sendMessage( + message, + List.of( + (event, agentCard) -> { + if (event instanceof TaskEvent te) { + receivedTask.set(te.getTask()); + if (te.getTask().status().state() + == TaskState.TASK_STATE_COMPLETED) { + latch.countDown(); + } + } else if (event instanceof TaskUpdateEvent tue) { + receivedTask.set(tue.getTask()); + if (tue.getTask().status().state() + == TaskState.TASK_STATE_COMPLETED) { + latch.countDown(); + } + } + }), + error -> { + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + latch.countDown(); + }); assertTrue(latch.await(10, TimeUnit.SECONDS), "Request should complete within timeout"); assertNull(errorRef.get(), "Should not have received an error"); @@ -596,26 +640,28 @@ public void testRequestScopedBeanAvailableOnAgentExecutorThreadStreaming() throw public void testSendMessageExistingTaskSuccess() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); CountDownLatch latch = new CountDownLatch(1); AtomicReference receivedMessage = new AtomicReference<>(); AtomicBoolean wasUnexpectedEvent = new AtomicBoolean(false); - BiConsumer consumer = (event, agentCard) -> { - if (event instanceof MessageEvent messageEvent) { - if (latch.getCount() > 0) { - receivedMessage.set(messageEvent.getMessage()); - latch.countDown(); - } else { - wasUnexpectedEvent.set(true); - } - } else { - wasUnexpectedEvent.set(true); - } - }; + BiConsumer consumer = + (event, agentCard) -> { + if (event instanceof MessageEvent messageEvent) { + if (latch.getCount() > 0) { + receivedMessage.set(messageEvent.getMessage()); + latch.countDown(); + } else { + wasUnexpectedEvent.set(true); + } + } else { + wasUnexpectedEvent.set(true); + } + }; // testing the non-streaming send message getNonStreamingClient().sendMessage(message, List.of(consumer), null); @@ -639,21 +685,23 @@ public void testSendMessageExistingTaskSuccess() throws Exception { public void testSetPushNotificationSuccess() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - TaskPushNotificationConfig taskPushConfig - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() .id("c295ea44-7543-4f78-b524-7a38915ad6e4") .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("") .build(); - TaskPushNotificationConfig config = getClient().createTaskPushNotificationConfiguration(taskPushConfig); + TaskPushNotificationConfig config = + getClient().createTaskPushNotificationConfiguration(taskPushConfig); assertEquals(MINIMAL_TASK.id(), config.taskId()); assertEquals("http://example.com", config.url()); assertEquals("c295ea44-7543-4f78-b524-7a38915ad6e4", config.id()); } catch (A2AClientException e) { fail("Unexpected exception during set push notification test: " + e.getMessage(), e); } finally { - deletePushNotificationConfigInStore(MINIMAL_TASK.id(), "c295ea44-7543-4f78-b524-7a38915ad6e4"); + deletePushNotificationConfigInStore( + MINIMAL_TASK.id(), "c295ea44-7543-4f78-b524-7a38915ad6e4"); deleteTaskInTaskStore(MINIMAL_TASK.id()); } } @@ -662,25 +710,31 @@ public void testSetPushNotificationSuccess() throws Exception { public void testGetPushNotificationSuccess() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - TaskPushNotificationConfig taskPushConfig - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() .id("c295ea44-7543-4f78-b524-7a38915ad6e4") .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("") .build(); - TaskPushNotificationConfig setResult = getClient().createTaskPushNotificationConfiguration(taskPushConfig); + TaskPushNotificationConfig setResult = + getClient().createTaskPushNotificationConfiguration(taskPushConfig); assertNotNull(setResult); - TaskPushNotificationConfig config = getClient().getTaskPushNotificationConfiguration( - new GetTaskPushNotificationConfigParams(MINIMAL_TASK.id(), "c295ea44-7543-4f78-b524-7a38915ad6e4")); + TaskPushNotificationConfig config = + getClient() + .getTaskPushNotificationConfiguration( + new GetTaskPushNotificationConfigParams( + MINIMAL_TASK.id(), + "c295ea44-7543-4f78-b524-7a38915ad6e4")); assertEquals(MINIMAL_TASK.id(), config.taskId()); assertEquals("http://example.com", config.url()); } catch (A2AClientException e) { fail("Unexpected exception during get push notification test: " + e.getMessage(), e); } finally { - deletePushNotificationConfigInStore(MINIMAL_TASK.id(), "c295ea44-7543-4f78-b524-7a38915ad6e4"); + deletePushNotificationConfigInStore( + MINIMAL_TASK.id(), "c295ea44-7543-4f78-b524-7a38915ad6e4"); deleteTaskInTaskStore(MINIMAL_TASK.id()); } } @@ -689,10 +743,11 @@ public void testGetPushNotificationSuccess() throws Exception { public void testError() throws Exception { saveTaskInTaskStore(SEND_MESSAGE_NOT_SUPPORTED); try { - Message message = Message.builder(MESSAGE) - .taskId(SEND_MESSAGE_NOT_SUPPORTED.id()) - .contextId(SEND_MESSAGE_NOT_SUPPORTED.contextId()) - .build(); + Message message = + Message.builder(MESSAGE) + .taskId(SEND_MESSAGE_NOT_SUPPORTED.id()) + .contextId(SEND_MESSAGE_NOT_SUPPORTED.contextId()) + .build(); try { getNonStreamingClient().sendMessage(message); @@ -716,11 +771,12 @@ public void testGetExtendedAgentCard() throws A2AClientException { assertEquals("A test agent card", agentCard.description()); assertNotNull(agentCard.supportedInterfaces()); assertFalse(agentCard.supportedInterfaces().isEmpty()); - Optional transportInterface = agentCard.supportedInterfaces().stream() - .filter(i -> getTransportProtocol().equals(i.protocolBinding())) - .findFirst(); + Optional transportInterface = + agentCard.supportedInterfaces().stream() + .filter(i -> getTransportProtocol().equals(i.protocolBinding())) + .findFirst(); assertTrue(transportInterface.isPresent()); - assertEquals(getTransportUrl(),transportInterface.get().url()); + assertEquals(getTransportUrl(), transportInterface.get().url()); assertEquals("1.0", agentCard.version()); assertEquals("http://example.com/docs", agentCard.documentationUrl()); assertTrue(agentCard.capabilities().pushNotifications()); @@ -744,10 +800,15 @@ public void testGetExtendedAgentCard() throws A2AClientException { @Test public void testAgentCardHeaders() throws Exception { HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/.well-known/agent-card.json")) - .GET() - .build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/.well-known/agent-card.json")) + .GET() + .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode()); @@ -755,20 +816,24 @@ public void testAgentCardHeaders() throws Exception { // Verify Cache-Control header with max-age directive (CARD-CACHE-001) Optional cacheControl = response.headers().firstValue("Cache-Control"); assertTrue(cacheControl.isPresent(), "Cache-Control header should be present"); - assertTrue(cacheControl.get().contains("max-age"), + assertTrue( + cacheControl.get().contains("max-age"), "Cache-Control should contain max-age directive, got: " + cacheControl.get()); // Verify ETag header (CARD-CACHE-002) Optional etag = response.headers().firstValue("ETag"); assertTrue(etag.isPresent(), "ETag header should be present"); - assertTrue(etag.get().startsWith("\"") && etag.get().endsWith("\""), + assertTrue( + etag.get().startsWith("\"") && etag.get().endsWith("\""), "ETag should be quoted per HTTP specification, got: " + etag.get()); // Verify Last-Modified header in RFC 1123 format (CARD-CACHE-003) Optional lastModified = response.headers().firstValue("Last-Modified"); assertTrue(lastModified.isPresent(), "Last-Modified header should be present"); - assertTrue(lastModified.get().contains("GMT"), - "Last-Modified should be in RFC 1123 format (containing GMT), got: " + lastModified.get()); + assertTrue( + lastModified.get().contains("GMT"), + "Last-Modified should be in RFC 1123 format (containing GMT), got: " + + lastModified.get()); } @Test @@ -786,8 +851,10 @@ public void testSendMessageStreamExistingTaskSuccess() throws Exception { public void testSubscribeExistingTaskSuccess() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - // attempting to send a streaming message instead of explicitly calling queueManager#createOrTap - // does not work because after the message is sent, the queue becomes null but task resubscription + // attempting to send a streaming message instead of explicitly calling + // queueManager#createOrTap + // does not work because after the message is sent, the queue becomes null but task + // resubscription // requires the queue to still be active ensureQueueForTask(MINIMAL_TASK.id()); @@ -799,41 +866,47 @@ public void testSubscribeExistingTaskSuccess() throws Exception { // Create consumer to handle subscribed events AtomicBoolean receivedInitialTask = new AtomicBoolean(false); - BiConsumer consumer = (event, agentCard) -> { - // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent - if (!receivedInitialTask.get()) { - if (event instanceof TaskEvent) { - receivedInitialTask.set(true); - // Don't count down latch for initial Task - return; - } else { - fail("First event on subscribe MUST be TaskEvent, but was: " + event.getClass().getSimpleName()); - } - } + BiConsumer consumer = + (event, agentCard) -> { + // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent + if (!receivedInitialTask.get()) { + if (event instanceof TaskEvent) { + receivedInitialTask.set(true); + // Don't count down latch for initial Task + return; + } else { + fail( + "First event on subscribe MUST be TaskEvent, but was: " + + event.getClass().getSimpleName()); + } + } - // Process subsequent events - if (event instanceof TaskUpdateEvent taskUpdateEvent) { - if (taskUpdateEvent.getUpdateEvent() instanceof TaskArtifactUpdateEvent artifactEvent) { - artifactUpdateEvent.set(artifactEvent); - eventLatch.countDown(); - } else if (taskUpdateEvent.getUpdateEvent() instanceof TaskStatusUpdateEvent statusEvent) { - statusUpdateEvent.set(statusEvent); - eventLatch.countDown(); - } else { - wasUnexpectedEvent.set(true); - } - } else { - wasUnexpectedEvent.set(true); - } - }; + // Process subsequent events + if (event instanceof TaskUpdateEvent taskUpdateEvent) { + if (taskUpdateEvent.getUpdateEvent() + instanceof TaskArtifactUpdateEvent artifactEvent) { + artifactUpdateEvent.set(artifactEvent); + eventLatch.countDown(); + } else if (taskUpdateEvent.getUpdateEvent() + instanceof TaskStatusUpdateEvent statusEvent) { + statusUpdateEvent.set(statusEvent); + eventLatch.countDown(); + } else { + wasUnexpectedEvent.set(true); + } + } else { + wasUnexpectedEvent.set(true); + } + }; // Create error handler - Consumer errorHandler = error -> { - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - eventLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + eventLatch.countDown(); + }; // Capture child queue count before subscribing (ensureQueueForTask creates one) int childCountBefore = getChildQueueCount(MINIMAL_TASK.id()); @@ -844,31 +917,36 @@ public void testSubscribeExistingTaskSuccess() throws Exception { .whenComplete((unused, throwable) -> subscriptionLatch.countDown()); // subscribe to the task with specific consumer and error handler - getClient().subscribeToTask(new TaskIdParams(MINIMAL_TASK.id()), List.of(consumer), errorHandler); + getClient() + .subscribeToTask( + new TaskIdParams(MINIMAL_TASK.id()), List.of(consumer), errorHandler); // Wait for subscription to be established assertTrue(subscriptionLatch.await(15, TimeUnit.SECONDS)); // Wait for EventConsumer polling loop to start (transport-level subscription // does not guarantee the consumer is ready to receive events) - assertTrue(awaitChildQueueCountStable(MINIMAL_TASK.id(), childCountBefore + 1, 15000), + assertTrue( + awaitChildQueueCountStable(MINIMAL_TASK.id(), childCountBefore + 1, 15000), "subscribeToTask child queue should be created and stable"); // Enqueue events on the server - List events = List.of( - TaskArtifactUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(Artifact.builder() - .artifactId("11") - .parts(new TextPart("text")) - .build()) - .build(), - TaskStatusUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build()); + List events = + List.of( + TaskArtifactUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + Artifact.builder() + .artifactId("11") + .parts(new TextPart("text")) + .build()) + .build(), + TaskStatusUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build()); for (Event event : events) { enqueueEventOnServer(event); @@ -905,8 +983,10 @@ public void testSubscribeExistingTaskSuccess() throws Exception { public void testSubscribeExistingTaskSuccessWithClientConsumers() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - // attempting to send a streaming message instead of explicitly calling queueManager#createOrTap - // does not work because after the message is sent, the queue becomes null but task resubscription + // attempting to send a streaming message instead of explicitly calling + // queueManager#createOrTap + // does not work because after the message is sent, the queue becomes null but task + // resubscription // requires the queue to still be active ensureQueueForTask(MINIMAL_TASK.id()); @@ -921,42 +1001,50 @@ public void testSubscribeExistingTaskSuccessWithClientConsumers() throws Excepti AgentCard agentCard = createTestAgentCard(); ClientConfig clientConfig = createClientConfig(true); - ClientBuilder clientBuilder = Client - .builder(agentCard) - .addConsumer((evt, agentCard1) -> { - // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent - if (!receivedInitialTask.get()) { - if (evt instanceof TaskEvent) { - receivedInitialTask.set(true); - // Don't count down latch for initial Task - return; - } else { - fail("First event on subscribe MUST be TaskEvent, but was: " + evt.getClass().getSimpleName()); - } - } - - // Process subsequent events - if (evt instanceof TaskUpdateEvent taskUpdateEvent) { - if (taskUpdateEvent.getUpdateEvent() instanceof TaskArtifactUpdateEvent artifactEvent) { - artifactUpdateEvent.set(artifactEvent); - eventLatch.countDown(); - } else if (taskUpdateEvent.getUpdateEvent() instanceof TaskStatusUpdateEvent statusEvent) { - statusUpdateEvent.set(statusEvent); - eventLatch.countDown(); - } else { - wasUnexpectedEvent.set(true); - } - } else { - wasUnexpectedEvent.set(true); - } - }) - .streamingErrorHandler(error -> { - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - eventLatch.countDown(); - }) - .clientConfig(clientConfig); + ClientBuilder clientBuilder = + Client.builder(agentCard) + .addConsumer( + (evt, agentCard1) -> { + // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent + if (!receivedInitialTask.get()) { + if (evt instanceof TaskEvent) { + receivedInitialTask.set(true); + // Don't count down latch for initial Task + return; + } else { + fail( + "First event on subscribe MUST be" + + " TaskEvent, but was: " + + evt.getClass().getSimpleName()); + } + } + + // Process subsequent events + if (evt instanceof TaskUpdateEvent taskUpdateEvent) { + if (taskUpdateEvent.getUpdateEvent() + instanceof + TaskArtifactUpdateEvent artifactEvent) { + artifactUpdateEvent.set(artifactEvent); + eventLatch.countDown(); + } else if (taskUpdateEvent.getUpdateEvent() + instanceof TaskStatusUpdateEvent statusEvent) { + statusUpdateEvent.set(statusEvent); + eventLatch.countDown(); + } else { + wasUnexpectedEvent.set(true); + } + } else { + wasUnexpectedEvent.set(true); + } + }) + .streamingErrorHandler( + error -> { + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + eventLatch.countDown(); + }) + .clientConfig(clientConfig); configureTransport(clientBuilder); Client clientWithConsumer = clientBuilder.build(); @@ -977,24 +1065,27 @@ public void testSubscribeExistingTaskSuccessWithClientConsumers() throws Excepti // Wait for EventConsumer polling loop to start (transport-level subscription // does not guarantee the consumer is ready to receive events) - assertTrue(awaitChildQueueCountStable(MINIMAL_TASK.id(), childCountBefore + 1, 15000), + assertTrue( + awaitChildQueueCountStable(MINIMAL_TASK.id(), childCountBefore + 1, 15000), "subscribeToTask child queue should be created and stable"); // Enqueue events on the server - List events = List.of( - TaskArtifactUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(Artifact.builder() - .artifactId("11") - .parts(new TextPart("text")) - .build()) - .build(), - TaskStatusUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build()); + List events = + List.of( + TaskArtifactUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + Artifact.builder() + .artifactId("11") + .parts(new TextPart("text")) + .build()) + .build(), + TaskStatusUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build()); for (Event event : events) { enqueueEventOnServer(event); @@ -1046,39 +1137,50 @@ public void testSubscribeToTaskWithInterruptedStateKeepsStreamOpen() throws Exce AtomicReference taskIdRef = new AtomicReference<>(); try { - // No taskId - server generates one; routing is by message content prefix "input-required:" - Message message = Message.builder(MESSAGE) - .parts(new TextPart("input-required:Trigger INPUT_REQUIRED")) - .build(); + // No taskId - server generates one; routing is by message content prefix + // "input-required:" + Message message = + Message.builder(MESSAGE) + .parts(new TextPart("input-required:Trigger INPUT_REQUIRED")) + .build(); // Send message with non-streaming client - agent will emit INPUT_REQUIRED and complete AtomicReference finalStateRef = new AtomicReference<>(); AtomicReference sendErrorRef = new AtomicReference<>(); CountDownLatch sendLatch = new CountDownLatch(1); - getNonStreamingClient().sendMessage(message, List.of((event, agentCard) -> { - if (event instanceof TaskEvent te) { - taskIdRef.compareAndSet(null, te.getTask().id()); - finalStateRef.set(te.getTask().status().state()); - sendLatch.countDown(); - } else if (event instanceof TaskUpdateEvent tue) { - if (tue.getUpdateEvent() instanceof TaskStatusUpdateEvent statusUpdate) { - taskIdRef.compareAndSet(null, statusUpdate.taskId()); - finalStateRef.set(statusUpdate.status().state()); - } - } - }), error -> { - if (!isStreamClosedError(error)) { - sendErrorRef.set(error); - } - sendLatch.countDown(); - }); + getNonStreamingClient() + .sendMessage( + message, + List.of( + (event, agentCard) -> { + if (event instanceof TaskEvent te) { + taskIdRef.compareAndSet(null, te.getTask().id()); + finalStateRef.set(te.getTask().status().state()); + sendLatch.countDown(); + } else if (event instanceof TaskUpdateEvent tue) { + if (tue.getUpdateEvent() + instanceof TaskStatusUpdateEvent statusUpdate) { + taskIdRef.compareAndSet( + null, statusUpdate.taskId()); + finalStateRef.set(statusUpdate.status().state()); + } + } + }), + error -> { + if (!isStreamClosedError(error)) { + sendErrorRef.set(error); + } + sendLatch.countDown(); + }); assertTrue(sendLatch.await(15, TimeUnit.SECONDS), "SendMessage should complete"); assertNull(sendErrorRef.get(), "SendMessage should not error"); TaskState finalState = finalStateRef.get(); assertNotNull(finalState, "Final state should be captured"); - assertEquals(TaskState.TASK_STATE_INPUT_REQUIRED, finalState, + assertEquals( + TaskState.TASK_STATE_INPUT_REQUIRED, + finalState, "Task should be in INPUT_REQUIRED state after agent completes"); String taskId = taskIdRef.get(); @@ -1094,7 +1196,8 @@ public void testSubscribeToTaskWithInterruptedStateKeepsStreamOpen() throws Exce Thread.sleep(2000); // Track events received through subscription stream - CopyOnWriteArrayList receivedEvents = new CopyOnWriteArrayList<>(); + CopyOnWriteArrayList receivedEvents = + new CopyOnWriteArrayList<>(); AtomicBoolean receivedInitialTask = new AtomicBoolean(false); AtomicBoolean streamClosedPrematurely = new AtomicBoolean(false); AtomicReference subscribeErrorRef = new AtomicReference<>(); @@ -1102,39 +1205,45 @@ public void testSubscribeToTaskWithInterruptedStateKeepsStreamOpen() throws Exce CountDownLatch initialTaskLatch = new CountDownLatch(1); // Consumer to track all events from subscription - BiConsumer consumer = (event, agentCard) -> { - if (event instanceof TaskEvent taskEvent) { - if (!receivedInitialTask.get()) { - receivedInitialTask.set(true); - // First event should be the initial task snapshot in INPUT_REQUIRED state - assertEquals(TaskState.TASK_STATE_INPUT_REQUIRED, - taskEvent.getTask().status().state(), - "Initial task should be in INPUT_REQUIRED state"); - initialTaskLatch.countDown(); - return; - } - } else if (event instanceof TaskUpdateEvent taskUpdateEvent) { - org.a2aproject.sdk.spec.UpdateEvent updateEvent = taskUpdateEvent.getUpdateEvent(); - receivedEvents.add(updateEvent); - - // Check if this is the final terminal state - if (updateEvent instanceof TaskStatusUpdateEvent tue && tue.isFinal()) { - completionLatch.countDown(); - } - } - }; + BiConsumer consumer = + (event, agentCard) -> { + if (event instanceof TaskEvent taskEvent) { + if (!receivedInitialTask.get()) { + receivedInitialTask.set(true); + // First event should be the initial task snapshot in INPUT_REQUIRED + // state + assertEquals( + TaskState.TASK_STATE_INPUT_REQUIRED, + taskEvent.getTask().status().state(), + "Initial task should be in INPUT_REQUIRED state"); + initialTaskLatch.countDown(); + return; + } + } else if (event instanceof TaskUpdateEvent taskUpdateEvent) { + org.a2aproject.sdk.spec.UpdateEvent updateEvent = + taskUpdateEvent.getUpdateEvent(); + receivedEvents.add(updateEvent); + + // Check if this is the final terminal state + if (updateEvent instanceof TaskStatusUpdateEvent tue && tue.isFinal()) { + completionLatch.countDown(); + } + } + }; // Error handler to detect premature stream closure - Consumer errorHandler = error -> { - if (!isStreamClosedError(error)) { - subscribeErrorRef.set(error); - } - // If completion latch hasn't been counted down yet, stream closed prematurely - if (completionLatch.getCount() > 0) { - streamClosedPrematurely.set(true); - } - completionLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (!isStreamClosedError(error)) { + subscribeErrorRef.set(error); + } + // If completion latch hasn't been counted down yet, stream closed + // prematurely + if (completionLatch.getCount() > 0) { + streamClosedPrematurely.set(true); + } + completionLatch.countDown(); + }; // Subscribe to the task - this is AFTER agent completed with INPUT_REQUIRED CountDownLatch subscriptionLatch = new CountDownLatch(1); @@ -1144,40 +1253,49 @@ public void testSubscribeToTaskWithInterruptedStateKeepsStreamOpen() throws Exce getClient().subscribeToTask(new TaskIdParams(taskId), List.of(consumer), errorHandler); // Wait for subscription to be established - assertTrue(subscriptionLatch.await(15, TimeUnit.SECONDS), "Subscription should be established"); + assertTrue( + subscriptionLatch.await(15, TimeUnit.SECONDS), + "Subscription should be established"); // Wait for initial task to be received - assertTrue(initialTaskLatch.await(5, TimeUnit.SECONDS), "Should receive initial task snapshot"); + assertTrue( + initialTaskLatch.await(5, TimeUnit.SECONDS), + "Should receive initial task snapshot"); // Verify stream received initial task and is still open assertTrue(receivedInitialTask.get(), "Should have received initial task snapshot"); - assertFalse(streamClosedPrematurely.get(), + assertFalse( + streamClosedPrematurely.get(), "Stream should NOT close for INPUT_REQUIRED state (interrupted, not terminal)"); // Send a follow-up message to provide the required input // This will trigger the agent again, which will emit COMPLETED - Message followUpMessage = Message.builder() - .messageId("input-response-" + UUID.randomUUID()) - .role(Message.Role.ROLE_USER) - .parts(new TextPart("input-required:User input")) - .taskId(taskId) - .build(); + Message followUpMessage = + Message.builder() + .messageId("input-response-" + UUID.randomUUID()) + .role(Message.Role.ROLE_USER) + .parts(new TextPart("input-required:User input")) + .taskId(taskId) + .build(); getClient().sendMessage(followUpMessage, List.of(), error -> {}); // Stream should now close after receiving COMPLETED event - assertTrue(completionLatch.await(30, TimeUnit.SECONDS), + assertTrue( + completionLatch.await(30, TimeUnit.SECONDS), "Stream should close after terminal state"); // Verify we received the COMPLETED update - assertTrue(receivedEvents.size() >= 1, - "Should receive at least COMPLETED status update"); + assertTrue( + receivedEvents.size() >= 1, "Should receive at least COMPLETED status update"); // Find the COMPLETED event - boolean foundCompleted = receivedEvents.stream() - .filter(e -> e instanceof TaskStatusUpdateEvent) - .map(e -> (TaskStatusUpdateEvent) e) - .anyMatch(tue -> tue.status().state() == TaskState.TASK_STATE_COMPLETED); + boolean foundCompleted = + receivedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent) + .map(e -> (TaskStatusUpdateEvent) e) + .anyMatch( + tue -> tue.status().state() == TaskState.TASK_STATE_COMPLETED); assertTrue(foundCompleted, "Should receive COMPLETED status update"); assertNull(subscribeErrorRef.get(), "Should not have any errors"); @@ -1195,19 +1313,22 @@ public void testSubscribeNoExistingTaskError() throws Exception { AtomicReference errorRef = new AtomicReference<>(); // Create error handler to capture the TaskNotFoundError - Consumer errorHandler = error -> { - if (error == null) { - // Stream completed successfully - ignore, we're waiting for an error - return; - } - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - errorLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (error == null) { + // Stream completed successfully - ignore, we're waiting for an error + return; + } + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + errorLatch.countDown(); + }; try { - getClient().subscribeToTask(new TaskIdParams("non-existent-task"), List.of(), errorHandler); + getClient() + .subscribeToTask( + new TaskIdParams("non-existent-task"), List.of(), errorHandler); // Wait for error to be captured (may come via error handler for streaming) boolean errorReceived = errorLatch.await(10, TimeUnit.SECONDS); @@ -1217,7 +1338,8 @@ public void testSubscribeNoExistingTaskError() throws Exception { Throwable error = errorRef.get(); assertNotNull(error); if (error instanceof A2AClientException) { - assertInstanceOf(TaskNotFoundError.class, ((A2AClientException) error).getCause()); + assertInstanceOf( + TaskNotFoundError.class, ((A2AClientException) error).getCause()); } else { // Check if it's directly a TaskNotFoundError or walk the cause chain Throwable cause = error; @@ -1243,11 +1365,12 @@ public void testSubscribeNoExistingTaskError() throws Exception { @Test public void testSubscribeToTerminalTaskError() throws Exception { // Create a task in terminal state (COMPLETED) - Task completedTask = Task.builder() - .id("terminal-task-test") - .contextId("session-xyz") - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + Task completedTask = + Task.builder() + .id("terminal-task-test") + .contextId("session-xyz") + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); saveTaskInTaskStore(completedTask); try { @@ -1255,18 +1378,20 @@ public void testSubscribeToTerminalTaskError() throws Exception { AtomicReference errorRef = new AtomicReference<>(); // Create error handler to capture the UnsupportedOperationError - Consumer errorHandler = error -> { - if (error == null) { - // Stream completed successfully - ignore, we're waiting for an error - return; - } - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - errorLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (error == null) { + // Stream completed successfully - ignore, we're waiting for an error + return; + } + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + errorLatch.countDown(); + }; - getClient().subscribeToTask(new TaskIdParams(completedTask.id()), List.of(), errorHandler); + getClient() + .subscribeToTask(new TaskIdParams(completedTask.id()), List.of(), errorHandler); // Wait for error to be captured boolean errorReceived = errorLatch.await(10, TimeUnit.SECONDS); @@ -1276,9 +1401,12 @@ public void testSubscribeToTerminalTaskError() throws Exception { Throwable error = errorRef.get(); assertNotNull(error, "Should receive an error when subscribing to terminal task"); - // Per spec STREAM-SUB-003: should return UnsupportedOperationError for terminal tasks + // Per spec STREAM-SUB-003: should return UnsupportedOperationError for terminal + // tasks if (error instanceof A2AClientException) { - assertInstanceOf(UnsupportedOperationError.class, ((A2AClientException) error).getCause(), + assertInstanceOf( + UnsupportedOperationError.class, + ((A2AClientException) error).getCause(), "Error should be UnsupportedOperationError for terminal task"); } else { // Check if it's directly an UnsupportedOperationError or walk the cause chain @@ -1290,7 +1418,9 @@ public void testSubscribeToTerminalTaskError() throws Exception { } cause = cause.getCause(); } - assertTrue(foundUnsupportedOperation, "Expected UnsupportedOperationError in error chain"); + assertTrue( + foundUnsupportedOperation, + "Expected UnsupportedOperationError in error chain"); } } else { fail("Expected UnsupportedOperationError when subscribing to terminal task"); @@ -1325,57 +1455,71 @@ public void testMainQueueReferenceCountingWithMultipleConsumers() throws Excepti AtomicReference firstErrorRef = new AtomicReference<>(); AtomicBoolean firstReceivedInitialTask = new AtomicBoolean(false); - BiConsumer firstConsumer = (event, agentCard) -> { - // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent - if (!firstReceivedInitialTask.get()) { - if (event instanceof TaskEvent) { - firstReceivedInitialTask.set(true); - return; - } else { - fail("First event on subscribe MUST be TaskEvent, but was: " + event.getClass().getSimpleName()); - } - } + BiConsumer firstConsumer = + (event, agentCard) -> { + // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent + if (!firstReceivedInitialTask.get()) { + if (event instanceof TaskEvent) { + firstReceivedInitialTask.set(true); + return; + } else { + fail( + "First event on subscribe MUST be TaskEvent, but was: " + + event.getClass().getSimpleName()); + } + } - // Process subsequent events - if (event instanceof TaskUpdateEvent tue && tue.getUpdateEvent() instanceof TaskArtifactUpdateEvent artifact) { - firstConsumerEvent.set(artifact); - firstConsumerLatch.countDown(); - } else if (!(event instanceof TaskUpdateEvent)) { - firstUnexpectedEvent.set(true); - } - }; + // Process subsequent events + if (event instanceof TaskUpdateEvent tue + && tue.getUpdateEvent() + instanceof TaskArtifactUpdateEvent artifact) { + firstConsumerEvent.set(artifact); + firstConsumerLatch.countDown(); + } else if (!(event instanceof TaskUpdateEvent)) { + firstUnexpectedEvent.set(true); + } + }; - Consumer firstErrorHandler = error -> { - if (!isStreamClosedError(error)) { - firstErrorRef.set(error); - } - firstConsumerLatch.countDown(); - }; + Consumer firstErrorHandler = + error -> { + if (!isStreamClosedError(error)) { + firstErrorRef.set(error); + } + firstConsumerLatch.countDown(); + }; // Wait for first subscription to be established CountDownLatch firstSubscriptionLatch = new CountDownLatch(1); awaitStreamingSubscription() .whenComplete((unused, throwable) -> firstSubscriptionLatch.countDown()); - getClient().subscribeToTask(new TaskIdParams(MINIMAL_TASK.id()), - List.of(firstConsumer), - firstErrorHandler); + getClient() + .subscribeToTask( + new TaskIdParams(MINIMAL_TASK.id()), + List.of(firstConsumer), + firstErrorHandler); - assertTrue(firstSubscriptionLatch.await(15, TimeUnit.SECONDS), "First subscription should be established"); + assertTrue( + firstSubscriptionLatch.await(15, TimeUnit.SECONDS), + "First subscription should be established"); // Enqueue first event - TaskArtifactUpdateEvent event1 = TaskArtifactUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(Artifact.builder() - .artifactId("artifact-1") - .parts(new TextPart("First artifact")) - .build()) - .build(); + TaskArtifactUpdateEvent event1 = + TaskArtifactUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + Artifact.builder() + .artifactId("artifact-1") + .parts(new TextPart("First artifact")) + .build()) + .build(); enqueueEventOnServer(event1); // Wait for first consumer to receive event - assertTrue(firstConsumerLatch.await(15, TimeUnit.SECONDS), "First consumer should receive event"); + assertTrue( + firstConsumerLatch.await(15, TimeUnit.SECONDS), + "First consumer should receive event"); assertFalse(firstUnexpectedEvent.get()); assertNull(firstErrorRef.get()); assertNotNull(firstConsumerEvent.get()); @@ -1394,32 +1538,38 @@ public void testMainQueueReferenceCountingWithMultipleConsumers() throws Excepti AtomicReference secondErrorRef = new AtomicReference<>(); AtomicBoolean secondReceivedInitialTask = new AtomicBoolean(false); - BiConsumer secondConsumer = (event, agentCard) -> { - // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent - if (!secondReceivedInitialTask.get()) { - if (event instanceof TaskEvent) { - secondReceivedInitialTask.set(true); - return; - } else { - fail("First event on subscribe MUST be TaskEvent, but was: " + event.getClass().getSimpleName()); - } - } + BiConsumer secondConsumer = + (event, agentCard) -> { + // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent + if (!secondReceivedInitialTask.get()) { + if (event instanceof TaskEvent) { + secondReceivedInitialTask.set(true); + return; + } else { + fail( + "First event on subscribe MUST be TaskEvent, but was: " + + event.getClass().getSimpleName()); + } + } - // Process subsequent events - if (event instanceof TaskUpdateEvent tue && tue.getUpdateEvent() instanceof TaskArtifactUpdateEvent artifact) { - secondConsumerEvent.set(artifact); - secondConsumerLatch.countDown(); - } else if (!(event instanceof TaskUpdateEvent)) { - secondUnexpectedEvent.set(true); - } - }; + // Process subsequent events + if (event instanceof TaskUpdateEvent tue + && tue.getUpdateEvent() + instanceof TaskArtifactUpdateEvent artifact) { + secondConsumerEvent.set(artifact); + secondConsumerLatch.countDown(); + } else if (!(event instanceof TaskUpdateEvent)) { + secondUnexpectedEvent.set(true); + } + }; - Consumer secondErrorHandler = error -> { - if (!isStreamClosedError(error)) { - secondErrorRef.set(error); - } - secondConsumerLatch.countDown(); - }; + Consumer secondErrorHandler = + error -> { + if (!isStreamClosedError(error)) { + secondErrorRef.set(error); + } + secondConsumerLatch.countDown(); + }; // Wait for second subscription to be established CountDownLatch secondSubscriptionLatch = new CountDownLatch(1); @@ -1428,38 +1578,50 @@ public void testMainQueueReferenceCountingWithMultipleConsumers() throws Excepti // This should succeed with reference counting because MainQueue stays alive // while first consumer's ChildQueue exists - getClient().subscribeToTask(new TaskIdParams(MINIMAL_TASK.id()), - List.of(secondConsumer), - secondErrorHandler); + getClient() + .subscribeToTask( + new TaskIdParams(MINIMAL_TASK.id()), + List.of(secondConsumer), + secondErrorHandler); - assertTrue(secondSubscriptionLatch.await(15, TimeUnit.SECONDS), "Second subscription should be established"); + assertTrue( + secondSubscriptionLatch.await(15, TimeUnit.SECONDS), + "Second subscription should be established"); // Verify child queue count increased (now ensureQueue + first + second) int childCountAfterSecond = getChildQueueCount(MINIMAL_TASK.id()); - assertTrue(childCountAfterSecond > childCountBeforeSecond, + assertTrue( + childCountAfterSecond > childCountBeforeSecond, "Child queue count should increase after second resubscription"); // 4. Enqueue second event - both consumers should receive it - TaskArtifactUpdateEvent event2 = TaskArtifactUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(Artifact.builder() - .artifactId("artifact-2") - .parts(new TextPart("Second artifact")) - .build()) - .build(); + TaskArtifactUpdateEvent event2 = + TaskArtifactUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + Artifact.builder() + .artifactId("artifact-2") + .parts(new TextPart("Second artifact")) + .build()) + .build(); enqueueEventOnServer(event2); // Both consumers should receive the event - assertTrue(secondConsumerLatch.await(15, TimeUnit.SECONDS), "Second consumer should receive event"); + assertTrue( + secondConsumerLatch.await(15, TimeUnit.SECONDS), + "Second consumer should receive event"); assertFalse(secondUnexpectedEvent.get()); - assertNull(secondErrorRef.get(), - "Resubscription should succeed with reference counting (MainQueue stays alive)"); + assertNull( + secondErrorRef.get(), + "Resubscription should succeed with reference counting (MainQueue stays" + + " alive)"); TaskArtifactUpdateEvent receivedEvent = secondConsumerEvent.get(); assertNotNull(receivedEvent); assertEquals("artifact-2", receivedEvent.artifact().artifactId()); - assertEquals("Second artifact", ((TextPart) receivedEvent.artifact().parts().get(0)).text()); + assertEquals( + "Second artifact", ((TextPart) receivedEvent.artifact().parts().get(0)).text()); } finally { deleteTaskInTaskStore(MINIMAL_TASK.id()); @@ -1494,13 +1656,13 @@ private boolean waitForChildQueueCountToBe(String taskId, int expectedCount, lon @Test public void testListPushNotificationConfigsWithConfigId() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); - TaskPushNotificationConfig notificationConfig1 - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig notificationConfig1 = + TaskPushNotificationConfig.builder() .url("http://example.com") .id("config1") .build(); - TaskPushNotificationConfig notificationConfig2 - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig notificationConfig2 = + TaskPushNotificationConfig.builder() .url("http://example.com") .id("config2") .build(); @@ -1508,11 +1670,21 @@ public void testListPushNotificationConfigsWithConfigId() throws Exception { savePushNotificationConfigInStore(MINIMAL_TASK.id(), notificationConfig2); try { - ListTaskPushNotificationConfigsResult result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); + ListTaskPushNotificationConfigsResult result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); assertEquals(2, result.size()); - assertEquals(TaskPushNotificationConfig.builder(notificationConfig1).taskId(MINIMAL_TASK.id()).build(), result.configs().get(0)); - assertEquals(TaskPushNotificationConfig.builder(notificationConfig2).taskId(MINIMAL_TASK.id()).build(), result.configs().get(1)); + assertEquals( + TaskPushNotificationConfig.builder(notificationConfig1) + .taskId(MINIMAL_TASK.id()) + .build(), + result.configs().get(0)); + assertEquals( + TaskPushNotificationConfig.builder(notificationConfig2) + .taskId(MINIMAL_TASK.id()) + .build(), + result.configs().get(1)); } catch (Exception e) { fail(); } finally { @@ -1525,13 +1697,13 @@ public void testListPushNotificationConfigsWithConfigId() throws Exception { @Test public void testListPushNotificationConfigsWithoutConfigId() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); - TaskPushNotificationConfig notificationConfig1 - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig notificationConfig1 = + TaskPushNotificationConfig.builder() .url("http://1.example.com") .id(MINIMAL_TASK.id()) .build(); - TaskPushNotificationConfig notificationConfig2 - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig notificationConfig2 = + TaskPushNotificationConfig.builder() .url("http://2.example.com") .id(MINIMAL_TASK.id()) .build(); @@ -1540,15 +1712,18 @@ public void testListPushNotificationConfigsWithoutConfigId() throws Exception { // will overwrite the previous one savePushNotificationConfigInStore(MINIMAL_TASK.id(), notificationConfig2); try { - ListTaskPushNotificationConfigsResult result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); + ListTaskPushNotificationConfigsResult result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); assertEquals(1, result.size()); - TaskPushNotificationConfig expectedNotificationConfig = TaskPushNotificationConfig.builder() - .url("http://2.example.com") - .id(MINIMAL_TASK.id()) - .taskId(MINIMAL_TASK.id()) - .build(); + TaskPushNotificationConfig expectedNotificationConfig = + TaskPushNotificationConfig.builder() + .url("http://2.example.com") + .id(MINIMAL_TASK.id()) + .taskId(MINIMAL_TASK.id()) + .build(); assertEquals(expectedNotificationConfig, result.configs().get(0)); } catch (Exception e) { fail(); @@ -1561,8 +1736,10 @@ public void testListPushNotificationConfigsWithoutConfigId() throws Exception { @Test public void testListPushNotificationConfigsTaskNotFound() { try { - ListTaskPushNotificationConfigsResult result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigsParams("non-existent-task")); + ListTaskPushNotificationConfigsResult result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigsParams("non-existent-task")); fail(); } catch (A2AClientException e) { assertInstanceOf(TaskNotFoundError.class, e.getCause()); @@ -1573,8 +1750,10 @@ public void testListPushNotificationConfigsTaskNotFound() { public void testListPushNotificationConfigsEmptyList() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - ListTaskPushNotificationConfigsResult result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); + ListTaskPushNotificationConfigsResult result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); assertEquals(0, result.size()); } catch (Exception e) { fail(e.getMessage()); @@ -1586,19 +1765,20 @@ public void testListPushNotificationConfigsEmptyList() throws Exception { @Test public void testDeletePushNotificationConfigWithValidConfigId() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); - saveTaskInTaskStore(Task.builder() - .id("task-456") - .contextId("session-xyz") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build()); - - TaskPushNotificationConfig notificationConfig1 - = TaskPushNotificationConfig.builder() + saveTaskInTaskStore( + Task.builder() + .id("task-456") + .contextId("session-xyz") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build()); + + TaskPushNotificationConfig notificationConfig1 = + TaskPushNotificationConfig.builder() .url("http://example.com") .id("config1") .build(); - TaskPushNotificationConfig notificationConfig2 - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig notificationConfig2 = + TaskPushNotificationConfig.builder() .url("http://example.com") .id("config2") .build(); @@ -1608,17 +1788,23 @@ public void testDeletePushNotificationConfigWithValidConfigId() throws Exception try { // specify the config ID to delete - getClient().deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams(MINIMAL_TASK.id(), "config1")); + getClient() + .deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams( + MINIMAL_TASK.id(), "config1")); // should now be 1 left - ListTaskPushNotificationConfigsResult result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); + ListTaskPushNotificationConfigsResult result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); assertEquals(1, result.size()); // should remain unchanged, this is a different task - result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigsParams("task-456")); + result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigsParams("task-456")); assertEquals(1, result.size()); } catch (Exception e) { fail(e.getMessage()); @@ -1634,13 +1820,13 @@ public void testDeletePushNotificationConfigWithValidConfigId() throws Exception @Test public void testDeletePushNotificationConfigWithNonExistingConfigId() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); - TaskPushNotificationConfig notificationConfig1 - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig notificationConfig1 = + TaskPushNotificationConfig.builder() .url("http://example.com") .id("config1") .build(); - TaskPushNotificationConfig notificationConfig2 - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig notificationConfig2 = + TaskPushNotificationConfig.builder() .url("http://example.com") .id("config2") .build(); @@ -1648,12 +1834,16 @@ public void testDeletePushNotificationConfigWithNonExistingConfigId() throws Exc savePushNotificationConfigInStore(MINIMAL_TASK.id(), notificationConfig2); try { - getClient().deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams(MINIMAL_TASK.id(), "non-existent-config-id")); + getClient() + .deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams( + MINIMAL_TASK.id(), "non-existent-config-id")); // should remain unchanged - ListTaskPushNotificationConfigsResult result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); + ListTaskPushNotificationConfigsResult result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); assertEquals(2, result.size()); } catch (Exception e) { fail(); @@ -1667,9 +1857,10 @@ public void testDeletePushNotificationConfigWithNonExistingConfigId() throws Exc @Test public void testDeletePushNotificationConfigTaskNotFound() { try { - getClient().deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams("non-existent-task", - "non-existent-config-id")); + getClient() + .deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams( + "non-existent-task", "non-existent-config-id")); fail(); } catch (A2AClientException e) { assertInstanceOf(TaskNotFoundError.class, e.getCause()); @@ -1679,13 +1870,13 @@ public void testDeletePushNotificationConfigTaskNotFound() { @Test public void testDeletePushNotificationConfigSetWithoutConfigId() throws Exception { saveTaskInTaskStore(MINIMAL_TASK); - TaskPushNotificationConfig notificationConfig1 - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig notificationConfig1 = + TaskPushNotificationConfig.builder() .url("http://1.example.com") .id(MINIMAL_TASK.id()) .build(); - TaskPushNotificationConfig notificationConfig2 - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig notificationConfig2 = + TaskPushNotificationConfig.builder() .url("http://2.example.com") .id(MINIMAL_TASK.id()) .build(); @@ -1695,12 +1886,17 @@ public void testDeletePushNotificationConfigSetWithoutConfigId() throws Exceptio savePushNotificationConfigInStore(MINIMAL_TASK.id(), notificationConfig2); try { - getClient().deleteTaskPushNotificationConfigurations( - new DeleteTaskPushNotificationConfigParams(MINIMAL_TASK.id(), MINIMAL_TASK.id())); + getClient() + .deleteTaskPushNotificationConfigurations( + new DeleteTaskPushNotificationConfigParams( + MINIMAL_TASK.id(), MINIMAL_TASK.id())); // should now be 0 - ListTaskPushNotificationConfigsResult result = getClient().listTaskPushNotificationConfigurations( - new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id()), null); + ListTaskPushNotificationConfigsResult result = + getClient() + .listTaskPushNotificationConfigurations( + new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id()), + null); assertEquals(0, result.size()); } catch (Exception e) { fail(); @@ -1715,182 +1911,209 @@ public void testDeletePushNotificationConfigSetWithoutConfigId() throws Exceptio public void testNonBlockingWithMultipleMessages() throws Exception { AtomicReference generatedTaskIdRef = new AtomicReference<>(); try { - // 1. Send first non-blocking message without taskId - server generates one - // Routing is by message content prefix "multi-event:first" - Message message1 = Message.builder(MESSAGE) - .parts(new TextPart("multi-event:first")) - .build(); + // 1. Send first non-blocking message without taskId - server generates one + // Routing is by message content prefix "multi-event:first" + Message message1 = + Message.builder(MESSAGE).parts(new TextPart("multi-event:first")).build(); + + AtomicReference taskIdRef = new AtomicReference<>(); + CountDownLatch firstTaskLatch = new CountDownLatch(1); + + BiConsumer firstMessageConsumer = + (event, agentCard) -> { + if (event instanceof TaskEvent te) { + taskIdRef.set(te.getTask().id()); + firstTaskLatch.countDown(); + } else if (event instanceof TaskUpdateEvent tue + && tue.getUpdateEvent() instanceof TaskStatusUpdateEvent status) { + taskIdRef.set(status.taskId()); + firstTaskLatch.countDown(); + } + }; - AtomicReference taskIdRef = new AtomicReference<>(); - CountDownLatch firstTaskLatch = new CountDownLatch(1); - - BiConsumer firstMessageConsumer = (event, agentCard) -> { - if (event instanceof TaskEvent te) { - taskIdRef.set(te.getTask().id()); - firstTaskLatch.countDown(); - } else if (event instanceof TaskUpdateEvent tue && tue.getUpdateEvent() instanceof TaskStatusUpdateEvent status) { - taskIdRef.set(status.taskId()); - firstTaskLatch.countDown(); - } - }; - - // Non-blocking message creates task in WORKING state and returns immediately - // Queue stays open because task is not in final state - getPollingClient().sendMessage(message1, List.of(firstMessageConsumer), null); - - assertTrue(firstTaskLatch.await(10, TimeUnit.SECONDS)); - String taskId = taskIdRef.get(); - assertNotNull(taskId); - generatedTaskIdRef.set(taskId); - - // 2. Subscribe to task (queue should still be open) - CountDownLatch resubEventLatch = new CountDownLatch(2); // artifact-2 + completion - List resubReceivedEvents = new CopyOnWriteArrayList<>(); - AtomicBoolean resubUnexpectedEvent = new AtomicBoolean(false); - AtomicReference resubErrorRef = new AtomicReference<>(); - AtomicBoolean resubReceivedInitialTask = new AtomicBoolean(false); - - BiConsumer resubConsumer = (event, agentCard) -> { - // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent - if (!resubReceivedInitialTask.get()) { - if (event instanceof TaskEvent) { - resubReceivedInitialTask.set(true); - return; - } else { - fail("First event on subscribe MUST be TaskEvent, but was: " + event.getClass().getSimpleName()); - } - } + // Non-blocking message creates task in WORKING state and returns immediately + // Queue stays open because task is not in final state + getPollingClient().sendMessage(message1, List.of(firstMessageConsumer), null); - // Process subsequent events - if (event instanceof TaskUpdateEvent tue) { - resubReceivedEvents.add(tue.getUpdateEvent()); - resubEventLatch.countDown(); - } else { - resubUnexpectedEvent.set(true); - } - }; + assertTrue(firstTaskLatch.await(10, TimeUnit.SECONDS)); + String taskId = taskIdRef.get(); + assertNotNull(taskId); + generatedTaskIdRef.set(taskId); + + // 2. Subscribe to task (queue should still be open) + CountDownLatch resubEventLatch = new CountDownLatch(2); // artifact-2 + completion + List resubReceivedEvents = + new CopyOnWriteArrayList<>(); + AtomicBoolean resubUnexpectedEvent = new AtomicBoolean(false); + AtomicReference resubErrorRef = new AtomicReference<>(); + AtomicBoolean resubReceivedInitialTask = new AtomicBoolean(false); - Consumer resubErrorHandler = error -> { - if (!isStreamClosedError(error)) { - resubErrorRef.set(error); - } - }; - - // Wait for subscription to be active - CountDownLatch subscriptionLatch = new CountDownLatch(1); - awaitStreamingSubscription() - .whenComplete((unused, throwable) -> subscriptionLatch.countDown()); - - getClient().subscribeToTask(new TaskIdParams(taskId), - List.of(resubConsumer), - resubErrorHandler); - - assertTrue(subscriptionLatch.await(15, TimeUnit.SECONDS)); - - // CRITICAL SYNCHRONIZATION: Wait for subscribeToTask's EventConsumer polling loop to start - // - // Race condition: awaitStreamingSubscription() only guarantees transport-level subscription - // (Flow.Subscriber.onSubscribe() called), but EventConsumer polling starts asynchronously - // on a separate thread. Without this check, the agent could emit events before any consumer - // is ready to receive them, causing events to be lost. - // - // This stability check waits for the child queue count to match expectedCount for 3 - // consecutive checks (150ms), ensuring the EventConsumer is actively polling and won't - // miss events when the agent executes. - assertTrue(awaitChildQueueCountStable(taskId, 1, 15000), - "subscribeToTask child queue should be created and stable"); - - // 3. Send second streaming message to same taskId - Message message2 = Message.builder(MESSAGE) - .taskId(taskId) - .parts(new TextPart("multi-event:second")) - .build(); + BiConsumer resubConsumer = + (event, agentCard) -> { + // Per A2A spec 3.1.6: ENFORCE that first event is TaskEvent + if (!resubReceivedInitialTask.get()) { + if (event instanceof TaskEvent) { + resubReceivedInitialTask.set(true); + return; + } else { + fail( + "First event on subscribe MUST be TaskEvent, but was: " + + event.getClass().getSimpleName()); + } + } - CountDownLatch streamEventLatch = new CountDownLatch(2); // artifact-2 + completion - CountDownLatch streamConsumerReadyLatch = new CountDownLatch(1); - List streamReceivedEvents = new CopyOnWriteArrayList<>(); - AtomicBoolean streamUnexpectedEvent = new AtomicBoolean(false); - - BiConsumer streamConsumer = (event, agentCard) -> { - // This consumer is for sendMessage() (not subscribe), so it doesn't get initial TaskEvent - if (event instanceof TaskUpdateEvent tue) { - streamReceivedEvents.add(tue.getUpdateEvent()); - streamEventLatch.countDown(); - streamConsumerReadyLatch.countDown(); // Signal that consumer is receiving events - } else { - streamUnexpectedEvent.set(true); - } - }; - - // Wait for streaming subscription to be established before sending message - CountDownLatch streamSubscriptionLatch = new CountDownLatch(1); - awaitStreamingSubscription() - .whenComplete((unused, throwable) -> streamSubscriptionLatch.countDown()); - - // Streaming message adds artifact-2 and completes task - getClient().sendMessage(message2, List.of(streamConsumer), null); - - // Ensure subscription is established before agent sends events - assertTrue(streamSubscriptionLatch.await(15, TimeUnit.SECONDS), - "Stream subscription should be established"); - - // Wait for stream consumer to start receiving events - assertTrue(streamConsumerReadyLatch.await(5, TimeUnit.SECONDS), - "Stream consumer should start receiving events"); - - // 4. Verify both consumers received artifact-2 and completion - assertTrue(resubEventLatch.await(15, TimeUnit.SECONDS)); - assertTrue(streamEventLatch.await(15, TimeUnit.SECONDS)); - - assertFalse(resubUnexpectedEvent.get()); - assertFalse(streamUnexpectedEvent.get()); - assertNull(resubErrorRef.get()); - - // Both should have received 2 events: artifact-2 and completion - assertEquals(2, resubReceivedEvents.size()); - assertEquals(2, streamReceivedEvents.size()); - - // Verify resubscription events - long resubArtifactCount = resubReceivedEvents.stream() - .filter(e -> e instanceof TaskArtifactUpdateEvent) - .count(); - assertEquals(1, resubArtifactCount); - - long resubCompletionCount = resubReceivedEvents.stream() - .filter(e -> e instanceof TaskStatusUpdateEvent) - .filter(e -> ((TaskStatusUpdateEvent) e).isFinal()) - .count(); - assertEquals(1, resubCompletionCount); - - // Verify streaming events - long streamArtifactCount = streamReceivedEvents.stream() - .filter(e -> e instanceof TaskArtifactUpdateEvent) - .count(); - assertEquals(1, streamArtifactCount); - - long streamCompletionCount = streamReceivedEvents.stream() - .filter(e -> e instanceof TaskStatusUpdateEvent) - .filter(e -> ((TaskStatusUpdateEvent) e).isFinal()) - .count(); - assertEquals(1, streamCompletionCount); - - // Verify artifact-2 details from resubscription - TaskArtifactUpdateEvent resubArtifact = (TaskArtifactUpdateEvent) resubReceivedEvents.stream() - .filter(e -> e instanceof TaskArtifactUpdateEvent) - .findFirst() - .orElseThrow(); - assertEquals("artifact-2", resubArtifact.artifact().artifactId()); - assertEquals("Second message artifact", - ((TextPart) resubArtifact.artifact().parts().get(0)).text()); - - // Verify artifact-2 details from streaming - TaskArtifactUpdateEvent streamArtifact = (TaskArtifactUpdateEvent) streamReceivedEvents.stream() - .filter(e -> e instanceof TaskArtifactUpdateEvent) - .findFirst() - .orElseThrow(); - assertEquals("artifact-2", streamArtifact.artifact().artifactId()); - assertEquals("Second message artifact", - ((TextPart) streamArtifact.artifact().parts().get(0)).text()); + // Process subsequent events + if (event instanceof TaskUpdateEvent tue) { + resubReceivedEvents.add(tue.getUpdateEvent()); + resubEventLatch.countDown(); + } else { + resubUnexpectedEvent.set(true); + } + }; + + Consumer resubErrorHandler = + error -> { + if (!isStreamClosedError(error)) { + resubErrorRef.set(error); + } + }; + + // Wait for subscription to be active + CountDownLatch subscriptionLatch = new CountDownLatch(1); + awaitStreamingSubscription() + .whenComplete((unused, throwable) -> subscriptionLatch.countDown()); + + getClient() + .subscribeToTask( + new TaskIdParams(taskId), List.of(resubConsumer), resubErrorHandler); + + assertTrue(subscriptionLatch.await(15, TimeUnit.SECONDS)); + + // CRITICAL SYNCHRONIZATION: Wait for subscribeToTask's EventConsumer polling loop to + // start + // + // Race condition: awaitStreamingSubscription() only guarantees transport-level + // subscription + // (Flow.Subscriber.onSubscribe() called), but EventConsumer polling starts + // asynchronously + // on a separate thread. Without this check, the agent could emit events before any + // consumer + // is ready to receive them, causing events to be lost. + // + // This stability check waits for the child queue count to match expectedCount for 3 + // consecutive checks (150ms), ensuring the EventConsumer is actively polling and won't + // miss events when the agent executes. + assertTrue( + awaitChildQueueCountStable(taskId, 1, 15000), + "subscribeToTask child queue should be created and stable"); + + // 3. Send second streaming message to same taskId + Message message2 = + Message.builder(MESSAGE) + .taskId(taskId) + .parts(new TextPart("multi-event:second")) + .build(); + + CountDownLatch streamEventLatch = new CountDownLatch(2); // artifact-2 + completion + CountDownLatch streamConsumerReadyLatch = new CountDownLatch(1); + List streamReceivedEvents = new CopyOnWriteArrayList<>(); + AtomicBoolean streamUnexpectedEvent = new AtomicBoolean(false); + + BiConsumer streamConsumer = + (event, agentCard) -> { + // This consumer is for sendMessage() (not subscribe), so it doesn't get + // initial TaskEvent + if (event instanceof TaskUpdateEvent tue) { + streamReceivedEvents.add(tue.getUpdateEvent()); + streamEventLatch.countDown(); + streamConsumerReadyLatch + .countDown(); // Signal that consumer is receiving events + } else { + streamUnexpectedEvent.set(true); + } + }; + + // Wait for streaming subscription to be established before sending message + CountDownLatch streamSubscriptionLatch = new CountDownLatch(1); + awaitStreamingSubscription() + .whenComplete((unused, throwable) -> streamSubscriptionLatch.countDown()); + + // Streaming message adds artifact-2 and completes task + getClient().sendMessage(message2, List.of(streamConsumer), null); + + // Ensure subscription is established before agent sends events + assertTrue( + streamSubscriptionLatch.await(15, TimeUnit.SECONDS), + "Stream subscription should be established"); + + // Wait for stream consumer to start receiving events + assertTrue( + streamConsumerReadyLatch.await(5, TimeUnit.SECONDS), + "Stream consumer should start receiving events"); + + // 4. Verify both consumers received artifact-2 and completion + assertTrue(resubEventLatch.await(15, TimeUnit.SECONDS)); + assertTrue(streamEventLatch.await(15, TimeUnit.SECONDS)); + + assertFalse(resubUnexpectedEvent.get()); + assertFalse(streamUnexpectedEvent.get()); + assertNull(resubErrorRef.get()); + + // Both should have received 2 events: artifact-2 and completion + assertEquals(2, resubReceivedEvents.size()); + assertEquals(2, streamReceivedEvents.size()); + + // Verify resubscription events + long resubArtifactCount = + resubReceivedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent) + .count(); + assertEquals(1, resubArtifactCount); + + long resubCompletionCount = + resubReceivedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent) + .filter(e -> ((TaskStatusUpdateEvent) e).isFinal()) + .count(); + assertEquals(1, resubCompletionCount); + + // Verify streaming events + long streamArtifactCount = + streamReceivedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent) + .count(); + assertEquals(1, streamArtifactCount); + + long streamCompletionCount = + streamReceivedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent) + .filter(e -> ((TaskStatusUpdateEvent) e).isFinal()) + .count(); + assertEquals(1, streamCompletionCount); + + // Verify artifact-2 details from resubscription + TaskArtifactUpdateEvent resubArtifact = + (TaskArtifactUpdateEvent) + resubReceivedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent) + .findFirst() + .orElseThrow(); + assertEquals("artifact-2", resubArtifact.artifact().artifactId()); + assertEquals( + "Second message artifact", + ((TextPart) resubArtifact.artifact().parts().get(0)).text()); + + // Verify artifact-2 details from streaming + TaskArtifactUpdateEvent streamArtifact = + (TaskArtifactUpdateEvent) + streamReceivedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent) + .findFirst() + .orElseThrow(); + assertEquals("artifact-2", streamArtifact.artifact().artifactId()); + assertEquals( + "Second message artifact", + ((TextPart) streamArtifact.artifact().parts().get(0)).text()); } finally { String taskId = generatedTaskIdRef.get(); if (taskId != null) { @@ -1910,19 +2133,31 @@ public void testNonBlockingWithMultipleMessages() throws Exception { * @throws IOException if the HTTP request fails * @throws InterruptedException if interrupted while waiting */ - private boolean awaitChildQueueCountStable(String taskId, int expectedCount, long timeoutMs) throws IOException, InterruptedException { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/queue/awaitChildCountStable/" + - taskId + "/" + expectedCount + "/" + timeoutMs)) - .POST(HttpRequest.BodyPublishers.noBody()) - .build(); + private boolean awaitChildQueueCountStable(String taskId, int expectedCount, long timeoutMs) + throws IOException, InterruptedException { + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/test/queue/awaitChildCountStable/" + + taskId + + "/" + + expectedCount + + "/" + + timeoutMs)) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(response.statusCode() + ": Awaiting child queue count failed! " + response.body()); + throw new RuntimeException( + response.statusCode() + + ": Awaiting child queue count failed! " + + response.body()); } return Boolean.parseBoolean(response.body()); } @@ -1936,34 +2171,38 @@ public void testInputRequiredWorkflow() throws Exception { try { // 1. Send the initial message without taskId or contextId - the server generates both. // Routing is by message content prefix "input-required:". - Message initialMessage = Message.builder(MESSAGE) - .parts(new TextPart("input-required:Initial request")) - .build(); + Message initialMessage = + Message.builder(MESSAGE) + .parts(new TextPart("input-required:Initial request")) + .build(); CountDownLatch initialLatch = new CountDownLatch(1); AtomicReference initialState = new AtomicReference<>(); AtomicBoolean initialUnexpectedEvent = new AtomicBoolean(false); - BiConsumer initialConsumer = (event, agentCard) -> { - // Idempotency guard: prevent late events from modifying state after latch countdown - if (initialLatch.getCount() == 0) { - return; - } - if (event instanceof TaskEvent te) { - Task task = te.getTask(); - inputRequiredTaskRef.compareAndSet(null, task); - inputRequiredTaskIdRef.compareAndSet(null, task.id()); - TaskState state = task.status().state(); - initialState.set(state); - // Only count down when we receive INPUT_REQUIRED, not intermediate states like WORKING - if (state == TaskState.TASK_STATE_INPUT_REQUIRED) { - taskCreated.set(true); - initialLatch.countDown(); - } - } else { - initialUnexpectedEvent.set(true); - } - }; + BiConsumer initialConsumer = + (event, agentCard) -> { + // Idempotency guard: prevent late events from modifying state after latch + // countdown + if (initialLatch.getCount() == 0) { + return; + } + if (event instanceof TaskEvent te) { + Task task = te.getTask(); + inputRequiredTaskRef.compareAndSet(null, task); + inputRequiredTaskIdRef.compareAndSet(null, task.id()); + TaskState state = task.status().state(); + initialState.set(state); + // Only count down when we receive INPUT_REQUIRED, not intermediate + // states like WORKING + if (state == TaskState.TASK_STATE_INPUT_REQUIRED) { + taskCreated.set(true); + initialLatch.countDown(); + } + } else { + initialUnexpectedEvent.set(true); + } + }; // Send initial message - task will go to INPUT_REQUIRED state getNonStreamingClient().sendMessage(initialMessage, List.of(initialConsumer), null); @@ -1975,39 +2214,45 @@ public void testInputRequiredWorkflow() throws Exception { assertNotNull(inputRequiredTaskId, "Should have captured server-generated taskId"); Task inputRequiredTask = inputRequiredTaskRef.get(); assertNotNull(inputRequiredTask, "Should have captured the input-required task"); - assertNotNull(inputRequiredTask.contextId(), "Server should generate a contextId for the task"); + assertNotNull( + inputRequiredTask.contextId(), + "Server should generate a contextId for the task"); // 2. Send a follow-up message with both identifiers from the first response. - Message inputMessage = Message.builder(MESSAGE) - .messageId("input-response-" + UUID.randomUUID()) - .contextId(inputRequiredTask.contextId()) - .taskId(inputRequiredTaskId) - .parts(new TextPart("input-required:User input")) - .build(); + Message inputMessage = + Message.builder(MESSAGE) + .messageId("input-response-" + UUID.randomUUID()) + .contextId(inputRequiredTask.contextId()) + .taskId(inputRequiredTaskId) + .parts(new TextPart("input-required:User input")) + .build(); CountDownLatch completionLatch = new CountDownLatch(1); AtomicReference completedState = new AtomicReference<>(); AtomicReference completedTaskRef = new AtomicReference<>(); AtomicBoolean completionUnexpectedEvent = new AtomicBoolean(false); - BiConsumer completionConsumer = (event, agentCard) -> { - // Idempotency guard: prevent late events from modifying state after latch countdown - if (completionLatch.getCount() == 0) { - return; - } - if (event instanceof TaskEvent te) { - Task completedTask = te.getTask(); - completedTaskRef.set(completedTask); - TaskState state = completedTask.status().state(); - completedState.set(state); - // Only count down when we receive COMPLETED, not intermediate states like WORKING - if (state == TaskState.TASK_STATE_COMPLETED) { - completionLatch.countDown(); - } - } else { - completionUnexpectedEvent.set(true); - } - }; + BiConsumer completionConsumer = + (event, agentCard) -> { + // Idempotency guard: prevent late events from modifying state after latch + // countdown + if (completionLatch.getCount() == 0) { + return; + } + if (event instanceof TaskEvent te) { + Task completedTask = te.getTask(); + completedTaskRef.set(completedTask); + TaskState state = completedTask.status().state(); + completedState.set(state); + // Only count down when we receive COMPLETED, not intermediate states + // like WORKING + if (state == TaskState.TASK_STATE_COMPLETED) { + completionLatch.countDown(); + } + } else { + completionUnexpectedEvent.set(true); + } + }; // Send input - task will be completed getNonStreamingClient().sendMessage(inputMessage, List.of(completionConsumer), null); @@ -2053,107 +2298,134 @@ public void testAuthRequiredWorkflow() throws Exception { try { // 1. Send initial message without taskId - server generates one // Routing is by message content prefix "auth-required:" - Message initialMessage = Message.builder(MESSAGE) - .parts(new TextPart("auth-required:Initial request requiring auth")) - .build(); + Message initialMessage = + Message.builder(MESSAGE) + .parts(new TextPart("auth-required:Initial request requiring auth")) + .build(); CountDownLatch initialLatch = new CountDownLatch(1); AtomicReference initialState = new AtomicReference<>(); AtomicBoolean initialUnexpectedEvent = new AtomicBoolean(false); - BiConsumer initialConsumer = (event, agentCard) -> { - // Idempotency guard: prevent late events from modifying state after latch countdown - if (initialLatch.getCount() == 0) { - return; - } - if (event instanceof TaskEvent te) { - authRequiredTaskIdRef.compareAndSet(null, te.getTask().id()); - TaskState state = te.getTask().status().state(); - initialState.set(state); - // Only count down when we receive AUTH_REQUIRED, not intermediate states like WORKING - if (state == TaskState.TASK_STATE_AUTH_REQUIRED) { - taskCreated.set(true); - initialLatch.countDown(); - } - } else { - initialUnexpectedEvent.set(true); - } - }; + BiConsumer initialConsumer = + (event, agentCard) -> { + // Idempotency guard: prevent late events from modifying state after latch + // countdown + if (initialLatch.getCount() == 0) { + return; + } + if (event instanceof TaskEvent te) { + authRequiredTaskIdRef.compareAndSet(null, te.getTask().id()); + TaskState state = te.getTask().status().state(); + initialState.set(state); + // Only count down when we receive AUTH_REQUIRED, not intermediate + // states like WORKING + if (state == TaskState.TASK_STATE_AUTH_REQUIRED) { + taskCreated.set(true); + initialLatch.countDown(); + } + } else { + initialUnexpectedEvent.set(true); + } + }; // Send initial message - task will go to AUTH_REQUIRED state and return immediately getNonStreamingClient().sendMessage(initialMessage, List.of(initialConsumer), null); - assertTrue(initialLatch.await(10, TimeUnit.SECONDS), "Should receive AUTH_REQUIRED state"); + assertTrue( + initialLatch.await(10, TimeUnit.SECONDS), "Should receive AUTH_REQUIRED state"); assertFalse(initialUnexpectedEvent.get(), "Should only receive TaskEvent"); - assertEquals(TaskState.TASK_STATE_AUTH_REQUIRED, initialState.get(), "Task should be in AUTH_REQUIRED state"); + assertEquals( + TaskState.TASK_STATE_AUTH_REQUIRED, + initialState.get(), + "Task should be in AUTH_REQUIRED state"); String authRequiredTaskId = authRequiredTaskIdRef.get(); assertNotNull(authRequiredTaskId, "Should have captured server-generated taskId"); // 2. Subscribe to task to catch background completion - // Agent continues executing after returning AUTH_REQUIRED (simulating out-of-band auth flow) + // Agent continues executing after returning AUTH_REQUIRED (simulating out-of-band auth + // flow) CountDownLatch completionLatch = new CountDownLatch(1); AtomicReference completedState = new AtomicReference<>(); AtomicBoolean completionUnexpectedEvent = new AtomicBoolean(false); AtomicReference errorRef = new AtomicReference<>(); - BiConsumer subscriptionConsumer = (event, agentCard) -> { - // Idempotency guard: prevent late events from modifying state after latch countdown - if (completionLatch.getCount() == 0) { - return; - } - // subscribeToTask returns initial state as TaskEvent, then subsequent events as TaskUpdateEvent - TaskState state = null; - if (event instanceof TaskEvent te) { - state = te.getTask().status().state(); - } else if (event instanceof TaskUpdateEvent tue) { - org.a2aproject.sdk.spec.UpdateEvent updateEvent = tue.getUpdateEvent(); - if (updateEvent instanceof TaskStatusUpdateEvent statusUpdate) { - state = statusUpdate.status().state(); - } else { - // Ignore other update events like TaskArtifactUpdateEvent as they don't change the task state - return; - } - } else { - completionUnexpectedEvent.set(true); - return; - } + BiConsumer subscriptionConsumer = + (event, agentCard) -> { + // Idempotency guard: prevent late events from modifying state after latch + // countdown + if (completionLatch.getCount() == 0) { + return; + } + // subscribeToTask returns initial state as TaskEvent, then subsequent + // events as TaskUpdateEvent + TaskState state = null; + if (event instanceof TaskEvent te) { + state = te.getTask().status().state(); + } else if (event instanceof TaskUpdateEvent tue) { + org.a2aproject.sdk.spec.UpdateEvent updateEvent = tue.getUpdateEvent(); + if (updateEvent instanceof TaskStatusUpdateEvent statusUpdate) { + state = statusUpdate.status().state(); + } else { + // Ignore other update events like TaskArtifactUpdateEvent as they + // don't change the task state + return; + } + } else { + completionUnexpectedEvent.set(true); + return; + } - completedState.set(state); - // A2A spec: first event from subscribeToTask is TaskEvent with current state (AUTH_REQUIRED) - // Then we receive TaskUpdateEvent with COMPLETED when agent finishes - if (state == TaskState.TASK_STATE_COMPLETED) { - completionLatch.countDown(); - } - }; + completedState.set(state); + // A2A spec: first event from subscribeToTask is TaskEvent with current + // state (AUTH_REQUIRED) + // Then we receive TaskUpdateEvent with COMPLETED when agent finishes + if (state == TaskState.TASK_STATE_COMPLETED) { + completionLatch.countDown(); + } + }; - Consumer errorHandler = error -> { - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - }; + Consumer errorHandler = + error -> { + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + }; // Wait for subscription to be established CountDownLatch subscriptionLatch = new CountDownLatch(1); awaitStreamingSubscription() .whenComplete((unused, throwable) -> subscriptionLatch.countDown()); - getClient().subscribeToTask(new TaskIdParams(authRequiredTaskId), - List.of(subscriptionConsumer), - errorHandler); + getClient() + .subscribeToTask( + new TaskIdParams(authRequiredTaskId), + List.of(subscriptionConsumer), + errorHandler); - assertTrue(subscriptionLatch.await(15, TimeUnit.SECONDS), "Subscription should be established"); + assertTrue( + subscriptionLatch.await(15, TimeUnit.SECONDS), + "Subscription should be established"); - // Note: We don't use awaitChildQueueCountStable() here because the agent is already running - // in the background (sleeping for 2s). By the time we check, it might have already completed. - // The subscriptionLatch already ensures the subscription is established, and completionLatch + // Note: We don't use awaitChildQueueCountStable() here because the agent is already + // running + // in the background (sleeping for 2s). By the time we check, it might have already + // completed. + // The subscriptionLatch already ensures the subscription is established, and + // completionLatch // below will catch the COMPLETED event from the background agent. // 3. Verify subscription receives COMPLETED state from background agent execution // Agent should complete after simulating out-of-band auth delay (2000ms) - assertTrue(completionLatch.await(10, TimeUnit.SECONDS), "Should receive COMPLETED state from background agent"); + assertTrue( + completionLatch.await(10, TimeUnit.SECONDS), + "Should receive COMPLETED state from background agent"); assertFalse(completionUnexpectedEvent.get(), "Should only receive TaskEvent"); assertNull(errorRef.get(), "Should not receive errors"); - assertEquals(TaskState.TASK_STATE_COMPLETED, completedState.get(), "Task should be COMPLETED after background auth"); + assertEquals( + TaskState.TASK_STATE_COMPLETED, + completedState.get(), + "Task should be COMPLETED after background auth"); } finally { if (taskCreated.get()) { @@ -2168,20 +2440,23 @@ public void testAuthRequiredWorkflow() throws Exception { @Test public void testMalformedJSONRPCRequest() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); // missing closing bracket - String malformedRequest = "{\"jsonrpc\": \"2.0\", \"method\": \"message/send\", \"params\": {\"foo\": \"bar\"}"; - A2AErrorResponse response = given() - .contentType(MediaType.APPLICATION_JSON) - .body(malformedRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(A2AErrorResponse.class); + String malformedRequest = + "{\"jsonrpc\": \"2.0\", \"method\": \"message/send\", \"params\": {\"foo\":" + + " \"bar\"}"; + A2AErrorResponse response = + given().contentType(MediaType.APPLICATION_JSON) + .body(malformedRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(A2AErrorResponse.class); assertNotNull(response.getError()); assertEquals(new JSONParseError().getCode(), response.getError().getCode()); } @@ -2189,30 +2464,33 @@ public void testMalformedJSONRPCRequest() { @Test public void testInvalidParamsJSONRPCRequest() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); - String invalidParamsRequest = """ - {"jsonrpc": "2.0", "method": "SendMessage", "params": "not_a_dict", "id": "1"} - """; + String invalidParamsRequest = + """ + {"jsonrpc": "2.0", "method": "SendMessage", "params": "not_a_dict", "id": "1"} + """; testInvalidParams(invalidParamsRequest); - invalidParamsRequest = """ - {"jsonrpc": "2.0", "method": "SendMessage", "params": {"message": {"parts": "invalid"}}, "id": "1"} - """; + invalidParamsRequest = + """ + {"jsonrpc": "2.0", "method": "SendMessage", "params": {"message": {"parts": "invalid"}}, "id": "1"} + """; testInvalidParams(invalidParamsRequest); } private void testInvalidParams(String invalidParamsRequest) { - A2AErrorResponse response = given() - .contentType(MediaType.APPLICATION_JSON) - .body(invalidParamsRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(A2AErrorResponse.class); + A2AErrorResponse response = + given().contentType(MediaType.APPLICATION_JSON) + .body(invalidParamsRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(A2AErrorResponse.class); assertNotNull(response.getError()); assertEquals(new InvalidParamsError().getCode(), response.getError().getCode()); assertEquals("1", response.getId().toString()); @@ -2221,24 +2499,26 @@ private void testInvalidParams(String invalidParamsRequest) { @Test public void testInvalidJSONRPCRequestMissingJsonrpc() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); - String invalidRequest = """ - { - "method": "SendMessage", - "params": {} - } - """; - A2AErrorResponse response = given() - .contentType(MediaType.APPLICATION_JSON) - .body(invalidRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(A2AErrorResponse.class); + String invalidRequest = + """ + { + "method": "SendMessage", + "params": {} + } + """; + A2AErrorResponse response = + given().contentType(MediaType.APPLICATION_JSON) + .body(invalidRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(A2AErrorResponse.class); assertNotNull(response.getError()); assertEquals(new InvalidRequestError().getCode(), response.getError().getCode()); } @@ -2246,21 +2526,23 @@ public void testInvalidJSONRPCRequestMissingJsonrpc() { @Test public void testInvalidJSONRPCRequestMissingMethod() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); - String invalidRequest = """ - {"jsonrpc": "2.0", "params": {}} - """; - A2AErrorResponse response = given() - .contentType(MediaType.APPLICATION_JSON) - .body(invalidRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(A2AErrorResponse.class); + String invalidRequest = + """ + {"jsonrpc": "2.0", "params": {}} + """; + A2AErrorResponse response = + given().contentType(MediaType.APPLICATION_JSON) + .body(invalidRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(A2AErrorResponse.class); assertNotNull(response.getError()); assertEquals(new InvalidRequestError().getCode(), response.getError().getCode()); } @@ -2268,21 +2550,23 @@ public void testInvalidJSONRPCRequestMissingMethod() { @Test public void testInvalidJSONRPCRequestInvalidId() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); - String invalidRequest = """ - {"jsonrpc": "2.0", "method": "SendMessage", "params": {}, "id": {"bad": "type"}} - """; - A2AErrorResponse response = given() - .contentType(MediaType.APPLICATION_JSON) - .body(invalidRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(A2AErrorResponse.class); + String invalidRequest = + """ + {"jsonrpc": "2.0", "method": "SendMessage", "params": {}, "id": {"bad": "type"}} + """; + A2AErrorResponse response = + given().contentType(MediaType.APPLICATION_JSON) + .body(invalidRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(A2AErrorResponse.class); assertNotNull(response.getError()); assertEquals(new InvalidRequestError().getCode(), response.getError().getCode()); } @@ -2290,21 +2574,23 @@ public void testInvalidJSONRPCRequestInvalidId() { @Test public void testInvalidJSONRPCRequestNonExistentMethod() { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); - String invalidRequest = """ - {"jsonrpc": "2.0", "id":"5", "method" : "nonexistent/method", "params": {}} - """; - A2AErrorResponse response = given() - .contentType(MediaType.APPLICATION_JSON) - .body(invalidRequest) - .when() - .post("/") - .then() - .statusCode(200) - .extract() - .as(A2AErrorResponse.class); + String invalidRequest = + """ + {"jsonrpc": "2.0", "id":"5", "method" : "nonexistent/method", "params": {}} + """; + A2AErrorResponse response = + given().contentType(MediaType.APPLICATION_JSON) + .body(invalidRequest) + .when() + .post("/") + .then() + .statusCode(200) + .extract() + .as(A2AErrorResponse.class); assertNotNull(response.getError()); assertEquals(new MethodNotFoundError().getCode(), response.getError().getCode()); } @@ -2312,7 +2598,8 @@ public void testInvalidJSONRPCRequestNonExistentMethod() { @Test public void testNonStreamingMethodWithAcceptHeader() throws Exception { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); testGetTask(MediaType.APPLICATION_JSON); } @@ -2320,7 +2607,8 @@ public void testNonStreamingMethodWithAcceptHeader() throws Exception { @Test public void testStreamingMethodWithAcceptHeader() throws Exception { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); testSendStreamingMessageWithHttpClient(MediaType.SERVER_SENT_EVENTS); @@ -2329,7 +2617,8 @@ public void testStreamingMethodWithAcceptHeader() throws Exception { @Test public void testStreamingMethodWithoutAcceptHeader() throws Exception { // skip this test for non-JSONRPC transports - assumeTrue(TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), + assumeTrue( + TransportProtocol.JSONRPC.asString().equals(getTransportProtocol()), "JSONRPC-specific test"); testSendStreamingMessageWithHttpClient(null); @@ -2338,51 +2627,74 @@ public void testStreamingMethodWithoutAcceptHeader() throws Exception { private void testSendStreamingMessageWithHttpClient(String mediaType) throws Exception { saveTaskInTaskStore(MINIMAL_TASK); try { - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendStreamingMessageRequest request = new SendStreamingMessageRequest( - "1", new MessageSendParams(message, null, null, "")); + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest( + "1", new MessageSendParams(message, null, null, "")); - CompletableFuture>> responseFuture = initialiseStreamingRequest(request, mediaType); + CompletableFuture>> responseFuture = + initialiseStreamingRequest(request, mediaType); - CountDownLatch latch = new CountDownLatch(1); - AtomicReference errorRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference errorRef = new AtomicReference<>(); - responseFuture.thenAccept(response -> { - if (response.statusCode() != 200) { - //errorRef.set(new IllegalStateException("Status code was " + response.statusCode())); - throw new IllegalStateException("Status code was " + response.statusCode()); - } - response.body().forEach(line -> { - try { - SendStreamingMessageResponse jsonResponse = extractJsonResponseFromSseLine(line); - if (jsonResponse != null) { - assertNull(jsonResponse.getError()); - Message messageResponse = (Message) jsonResponse.getResult(); - assertEquals(MESSAGE.messageId(), messageResponse.messageId()); - assertEquals(MESSAGE.role(), messageResponse.role()); - Part part = messageResponse.parts().get(0); - assertTrue(part instanceof TextPart); - assertEquals("test message", ((TextPart) part).text()); - latch.countDown(); - } - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }); - }).exceptionally(t -> { - if (!isStreamClosedError(t)) { - errorRef.set(t); - } - latch.countDown(); - return null; - }); + responseFuture + .thenAccept( + response -> { + if (response.statusCode() != 200) { + // errorRef.set(new IllegalStateException("Status code was " + + // response.statusCode())); + throw new IllegalStateException( + "Status code was " + response.statusCode()); + } + response.body() + .forEach( + line -> { + try { + SendStreamingMessageResponse jsonResponse = + extractJsonResponseFromSseLine( + line); + if (jsonResponse != null) { + assertNull(jsonResponse.getError()); + Message messageResponse = + (Message) + jsonResponse + .getResult(); + assertEquals( + MESSAGE.messageId(), + messageResponse.messageId()); + assertEquals( + MESSAGE.role(), + messageResponse.role()); + Part part = + messageResponse.parts().get(0); + assertTrue(part instanceof TextPart); + assertEquals( + "test message", + ((TextPart) part).text()); + latch.countDown(); + } + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }); + }) + .exceptionally( + t -> { + if (!isStreamClosedError(t)) { + errorRef.set(t); + } + latch.countDown(); + return null; + }); - boolean dataRead = latch.await(20, TimeUnit.SECONDS); - Assertions.assertTrue(dataRead); - Assertions.assertNull(errorRef.get()); + boolean dataRead = latch.await(20, TimeUnit.SECONDS); + Assertions.assertTrue(dataRead); + Assertions.assertNull(errorRef.get()); } finally { deleteTaskInTaskStore(MINIMAL_TASK.id()); @@ -2405,23 +2717,25 @@ public void testSendStreamingMessage(boolean createTask) throws Exception { AtomicBoolean wasUnexpectedEvent = new AtomicBoolean(false); AtomicReference errorRef = new AtomicReference<>(); - BiConsumer consumer = (event, agentCard) -> { - if (event instanceof MessageEvent messageEvent) { - if (latch.getCount() > 0) { - receivedMessage.set(messageEvent.getMessage()); - latch.countDown(); - } else { - wasUnexpectedEvent.set(true); - } - } else { - wasUnexpectedEvent.set(true); - } - }; + BiConsumer consumer = + (event, agentCard) -> { + if (event instanceof MessageEvent messageEvent) { + if (latch.getCount() > 0) { + receivedMessage.set(messageEvent.getMessage()); + latch.countDown(); + } else { + wasUnexpectedEvent.set(true); + } + } else { + wasUnexpectedEvent.set(true); + } + }; - Consumer errorHandler = error -> { - errorRef.set(error); - latch.countDown(); - }; + Consumer errorHandler = + error -> { + errorRef.set(error); + latch.countDown(); + }; // testing the streaming send message getClient().sendMessage(message, List.of(consumer), errorHandler); @@ -2449,20 +2763,23 @@ private CompletableFuture>> initialiseStreamingReque StreamingJSONRPCRequest request, String mediaType) throws Exception { // Create the client - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); String body = ""; if (request instanceof SendStreamingMessageRequest streamingRequest) { - body = JSONRPCUtils.toJsonRPCRequest((String) streamingRequest.getId(), SEND_STREAMING_MESSAGE_METHOD, ProtoUtils.ToProto.sendMessageRequest(streamingRequest.getParams())); + body = + JSONRPCUtils.toJsonRPCRequest( + (String) streamingRequest.getId(), + SEND_STREAMING_MESSAGE_METHOD, + ProtoUtils.ToProto.sendMessageRequest(streamingRequest.getParams())); } // Create the request - HttpRequest.Builder builder = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/")) - .POST(HttpRequest.BodyPublishers.ofString(body)) - .header("Content-Type", APPLICATION_JSON) - .header(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION); + HttpRequest.Builder builder = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/")) + .POST(HttpRequest.BodyPublishers.ofString(body)) + .header("Content-Type", APPLICATION_JSON) + .header(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION); if (mediaType != null) { builder.header("Accept", mediaType); } @@ -2472,10 +2789,13 @@ private CompletableFuture>> initialiseStreamingReque return client.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines()); } - private SendStreamingMessageResponse extractJsonResponseFromSseLine(String line) throws JsonProcessingException { + private SendStreamingMessageResponse extractJsonResponseFromSseLine(String line) + throws JsonProcessingException { line = extractSseData(line); if (line != null) { - return new SendStreamingMessageResponse("", ProtoUtils.FromProto.streamingEventKind(JSONRPCUtils.parseResponseEvent(line))); + return new SendStreamingMessageResponse( + "", + ProtoUtils.FromProto.streamingEventKind(JSONRPCUtils.parseResponseEvent(line))); } return null; } @@ -2498,7 +2818,8 @@ protected boolean isStreamClosedError(Throwable throwable) { if (cause instanceof java.util.concurrent.CancellationException) { return true; } - if (cause instanceof IOException && cause.getMessage() != null + if (cause instanceof IOException + && cause.getMessage() != null && cause.getMessage().contains("cancelled")) { return true; } @@ -2508,65 +2829,85 @@ protected boolean isStreamClosedError(Throwable throwable) { } protected void saveTaskInTaskStore(Task task) throws Exception { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/task")) - .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(task))) - .header("Content-Type", APPLICATION_JSON) - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task")) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(task))) + .header("Content-Type", APPLICATION_JSON) + .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(String.format("Saving task failed! Status: %d, Body: %s", response.statusCode(), response.body())); + throw new RuntimeException( + String.format( + "Saving task failed! Status: %d, Body: %s", + response.statusCode(), response.body())); } } protected Task getTaskFromTaskStore(String taskId) throws Exception { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) - .GET() - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) + .GET() + .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() == 404) { return null; } if (response.statusCode() != 200) { - throw new RuntimeException(String.format("Getting task failed! Status: %d, Body: %s", response.statusCode(), response.body())); + throw new RuntimeException( + String.format( + "Getting task failed! Status: %d, Body: %s", + response.statusCode(), response.body())); } return JsonUtil.fromJson(response.body(), Task.class); } protected void deleteTaskInTaskStore(String taskId) throws Exception { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(("http://localhost:" + serverPort + "/test/task/" + taskId))) - .DELETE() - .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + ("http://localhost:" + + serverPort + + "/test/task/" + + taskId))) + .DELETE() + .build(); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(response.statusCode() + ": Deleting task failed!" + response.body()); + throw new RuntimeException( + response.statusCode() + ": Deleting task failed!" + response.body()); } } protected void ensureQueueForTask(String taskId) throws Exception { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/queue/ensure/" + taskId)) - .POST(HttpRequest.BodyPublishers.noBody()) - .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/test/queue/ensure/" + + taskId)) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(String.format("Ensuring queue failed! Status: %d, Body: %s", response.statusCode(), response.body())); + throw new RuntimeException( + String.format( + "Ensuring queue failed! Status: %d, Body: %s", + response.statusCode(), response.body())); } } @@ -2577,21 +2918,25 @@ protected void enqueueEventOnServer(Event event) throws Exception { } else if (event instanceof TaskStatusUpdateEvent e) { path = "test/queue/enqueueTaskStatusUpdateEvent/" + e.taskId(); } else { - throw new RuntimeException("Unknown event type " + event.getClass() + ". If you need the ability to" - + " handle more types, please add the REST endpoints."); + throw new RuntimeException( + "Unknown event type " + + event.getClass() + + ". If you need the ability to" + + " handle more types, please add the REST endpoints."); } - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/" + path)) - .header("Content-Type", APPLICATION_JSON) - .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(event))) - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/" + path)) + .header("Content-Type", APPLICATION_JSON) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(event))) + .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(response.statusCode() + ": Queueing event failed!" + response.body()); + throw new RuntimeException( + response.statusCode() + ": Queueing event failed!" + response.body()); } } @@ -2599,38 +2944,44 @@ private CompletableFuture awaitStreamingSubscription() { int cnt = getStreamingSubscribedCount(); AtomicInteger initialCount = new AtomicInteger(cnt); - return CompletableFuture.runAsync(() -> { - try { - boolean done = false; - long end = System.currentTimeMillis() + 15000; - while (System.currentTimeMillis() < end) { - int count = getStreamingSubscribedCount(); - if (count > initialCount.get()) { - done = true; - break; + return CompletableFuture.runAsync( + () -> { + try { + boolean done = false; + long end = System.currentTimeMillis() + 15000; + while (System.currentTimeMillis() < end) { + int count = getStreamingSubscribedCount(); + if (count > initialCount.get()) { + done = true; + break; + } + Thread.sleep(500); + } + if (!done) { + throw new RuntimeException("Timed out waiting for subscription"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted"); } - Thread.sleep(500); - } - if (!done) { - throw new RuntimeException("Timed out waiting for subscription"); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("Interrupted"); - } - }); + }); } private int getStreamingSubscribedCount() { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/streamingSubscribedCount")) - .GET() - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/test/streamingSubscribedCount")) + .GET() + .build(); try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send( + request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); String body = response.body().trim(); return Integer.parseInt(body); } catch (IOException | InterruptedException e) { @@ -2639,15 +2990,21 @@ private int getStreamingSubscribedCount() { } protected int getChildQueueCount(String taskId) { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/queue/childCount/" + taskId)) - .GET() - .build(); + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/test/queue/childCount/" + + taskId)) + .GET() + .build(); try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send( + request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); String body = response.body().trim(); return Integer.parseInt(body); } catch (IOException | InterruptedException e) { @@ -2655,33 +3012,48 @@ protected int getChildQueueCount(String taskId) { } } - protected void deletePushNotificationConfigInStore(String taskId, String configId) throws Exception { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(("http://localhost:" + serverPort + "/test/task/" + taskId + "/config/" + configId))) - .DELETE() - .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + protected void deletePushNotificationConfigInStore(String taskId, String configId) + throws Exception { + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + ("http://localhost:" + + serverPort + + "/test/task/" + + taskId + + "/config/" + + configId))) + .DELETE() + .build(); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(response.statusCode() + ": Deleting task failed!" + response.body()); + throw new RuntimeException( + response.statusCode() + ": Deleting task failed!" + response.body()); } } - protected void savePushNotificationConfigInStore(String taskId, TaskPushNotificationConfig notificationConfig) throws Exception { - HttpClient client = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) - .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(notificationConfig))) - .header("Content-Type", APPLICATION_JSON) - .build(); + protected void savePushNotificationConfigInStore( + String taskId, TaskPushNotificationConfig notificationConfig) throws Exception { + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) + .POST( + HttpRequest.BodyPublishers.ofString( + JsonUtil.toJson(notificationConfig))) + .header("Content-Type", APPLICATION_JSON) + .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200) { - throw new RuntimeException(response.statusCode() + ": Creating task push notification config failed! " + response.body()); + throw new RuntimeException( + response.statusCode() + + ": Creating task push notification config failed! " + + response.body()); } } @@ -2722,9 +3094,7 @@ private Client createClient(boolean streaming) throws A2AClientException { AgentCard agentCard = createTestAgentCard(); ClientConfig clientConfig = createClientConfig(streaming); - ClientBuilder clientBuilder = Client - .builder(agentCard) - .clientConfig(clientConfig); + ClientBuilder clientBuilder = Client.builder(agentCard).clientConfig(clientConfig); configureTransport(clientBuilder); @@ -2740,14 +3110,13 @@ private AgentCard createTestAgentCard() { .description("A test agent card") .version("1.0") .documentationUrl("http://example.com/docs") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .build()) + .capabilities( + AgentCapabilities.builder().streaming(true).pushNotifications(true).build()) .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) .skills(List.of()) - .supportedInterfaces(List.of(new AgentInterface(getTransportProtocol(), getTransportUrl()))) + .supportedInterfaces( + List.of(new AgentInterface(getTransportProtocol(), getTransportUrl()))) .build(); } @@ -2755,9 +3124,7 @@ private AgentCard createTestAgentCard() { * Create client configuration with transport-specific settings. */ private ClientConfig createClientConfig(boolean streaming) { - return new ClientConfig.Builder() - .setStreaming(streaming) - .build(); + return new ClientConfig.Builder().setStreaming(streaming).build(); } /** @@ -2765,14 +3132,14 @@ private ClientConfig createClientConfig(boolean streaming) { */ private Client createPollingClient() throws A2AClientException { AgentCard agentCard = createTestAgentCard(); - ClientConfig clientConfig = new ClientConfig.Builder() - .setStreaming(false) // Non-streaming - .setPolling(true) // Polling mode (translates to returnImmediately=true on server) - .build(); + ClientConfig clientConfig = + new ClientConfig.Builder() + .setStreaming(false) // Non-streaming + .setPolling(true) // Polling mode (translates to returnImmediately=true on + // server) + .build(); - ClientBuilder clientBuilder = Client - .builder(agentCard) - .clientConfig(clientConfig); + ClientBuilder clientBuilder = Client.builder(agentCard).clientConfig(clientConfig); configureTransport(clientBuilder); @@ -2796,11 +3163,12 @@ public void testMainQueueStaysOpenForNonFinalTasks() throws Exception { String contextId = "fire-ctx"; // Create task in WORKING state (non-final) - Task workingTask = Task.builder() - .id(taskId) - .contextId(contextId) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + Task workingTask = + Task.builder() + .id(taskId) + .contextId(contextId) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); saveTaskInTaskStore(workingTask); try { @@ -2808,28 +3176,31 @@ public void testMainQueueStaysOpenForNonFinalTasks() throws Exception { ensureQueueForTask(taskId); // Send a message that will leave task in WORKING state (fire-and-forget pattern) - Message message = Message.builder(MESSAGE) - .taskId(taskId) - .contextId(contextId) - .parts(new TextPart("fire and forget")) - .build(); + Message message = + Message.builder(MESSAGE) + .taskId(taskId) + .contextId(contextId) + .parts(new TextPart("fire and forget")) + .build(); CountDownLatch firstEventLatch = new CountDownLatch(1); AtomicReference errorRef = new AtomicReference<>(); - BiConsumer consumer = (event, agentCard) -> { - // Receive any event (Message) to know agent processed the request - if (event instanceof MessageEvent) { - firstEventLatch.countDown(); - } - }; + BiConsumer consumer = + (event, agentCard) -> { + // Receive any event (Message) to know agent processed the request + if (event instanceof MessageEvent) { + firstEventLatch.countDown(); + } + }; - Consumer errorHandler = error -> { - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - firstEventLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + firstEventLatch.countDown(); + }; // Start streaming subscription CountDownLatch subscriptionLatch = new CountDownLatch(1); @@ -2839,12 +3210,13 @@ public void testMainQueueStaysOpenForNonFinalTasks() throws Exception { getClient().sendMessage(message, List.of(consumer), errorHandler); // Wait for subscription to be established - assertTrue(subscriptionLatch.await(15, TimeUnit.SECONDS), + assertTrue( + subscriptionLatch.await(15, TimeUnit.SECONDS), "Subscription should be established"); // Wait for agent to respond (test agent sends Message, not WORKING status) - assertTrue(firstEventLatch.await(15, TimeUnit.SECONDS), - "Should receive agent response"); + assertTrue( + firstEventLatch.await(15, TimeUnit.SECONDS), "Should receive agent response"); assertNull(errorRef.get()); // Give agent time to finish (task remains in WORKING state - non-final) @@ -2856,33 +3228,37 @@ public void testMainQueueStaysOpenForNonFinalTasks() throws Exception { CountDownLatch resubLatch = new CountDownLatch(1); AtomicReference resubErrorRef = new AtomicReference<>(); - BiConsumer resubConsumer = (event, agentCard) -> { - // We might not receive events immediately, but subscription should succeed - resubLatch.countDown(); - }; + BiConsumer resubConsumer = + (event, agentCard) -> { + // We might not receive events immediately, but subscription should succeed + resubLatch.countDown(); + }; - Consumer resubErrorHandler = error -> { - if (!isStreamClosedError(error)) { - resubErrorRef.set(error); - } - resubLatch.countDown(); - }; + Consumer resubErrorHandler = + error -> { + if (!isStreamClosedError(error)) { + resubErrorRef.set(error); + } + resubLatch.countDown(); + }; // This should succeed - MainQueue is still open for non-final task CountDownLatch resubSubscriptionLatch = new CountDownLatch(1); awaitStreamingSubscription() .whenComplete((unused, throwable) -> resubSubscriptionLatch.countDown()); - getClient().subscribeToTask(new TaskIdParams(taskId), - List.of(resubConsumer), - resubErrorHandler); + getClient() + .subscribeToTask( + new TaskIdParams(taskId), List.of(resubConsumer), resubErrorHandler); // Wait for resubscription to be established - assertTrue(resubSubscriptionLatch.await(15, TimeUnit.SECONDS), + assertTrue( + resubSubscriptionLatch.await(15, TimeUnit.SECONDS), "Resubscription should succeed - MainQueue stayed open for non-final task"); // Verify no errors during resubscription - assertNull(resubErrorRef.get(), + assertNull( + resubErrorRef.get(), "Resubscription should not error - validates THE BIG IDEA works end-to-end"); } finally { @@ -2905,47 +3281,46 @@ public void testMainQueueStaysOpenForNonFinalTasks() throws Exception { @Timeout(value = 2, unit = TimeUnit.MINUTES) public void testMainQueueClosesForFinalizedTasks() throws Exception { // Send a message without taskId - server generates one - Message message = Message.builder(MESSAGE) - .parts(new TextPart("complete task")) - .build(); + Message message = Message.builder(MESSAGE).parts(new TextPart("complete task")).build(); CountDownLatch completionLatch = new CountDownLatch(1); AtomicReference errorRef = new AtomicReference<>(); AtomicReference generatedTaskId = new AtomicReference<>(); - BiConsumer consumer = (event, agentCard) -> { - if (event instanceof TaskEvent te) { - generatedTaskId.compareAndSet(null, te.getTask().id()); - // Might get Task with final state - if (te.getTask().status().state().isFinal()) { - completionLatch.countDown(); - } - } else if (event instanceof MessageEvent me) { - // Message is considered a final event - capture taskId from the message - generatedTaskId.compareAndSet(null, me.getMessage().taskId()); - completionLatch.countDown(); - } else if (event instanceof TaskUpdateEvent tue - && tue.getUpdateEvent() instanceof TaskStatusUpdateEvent status) { - generatedTaskId.compareAndSet(null, status.taskId()); - if (status.isFinal()) { - completionLatch.countDown(); - } - } - }; + BiConsumer consumer = + (event, agentCard) -> { + if (event instanceof TaskEvent te) { + generatedTaskId.compareAndSet(null, te.getTask().id()); + // Might get Task with final state + if (te.getTask().status().state().isFinal()) { + completionLatch.countDown(); + } + } else if (event instanceof MessageEvent me) { + // Message is considered a final event - capture taskId from the message + generatedTaskId.compareAndSet(null, me.getMessage().taskId()); + completionLatch.countDown(); + } else if (event instanceof TaskUpdateEvent tue + && tue.getUpdateEvent() instanceof TaskStatusUpdateEvent status) { + generatedTaskId.compareAndSet(null, status.taskId()); + if (status.isFinal()) { + completionLatch.countDown(); + } + } + }; - Consumer errorHandler = error -> { - if (!isStreamClosedError(error)) { - errorRef.set(error); - } - completionLatch.countDown(); - }; + Consumer errorHandler = + error -> { + if (!isStreamClosedError(error)) { + errorRef.set(error); + } + completionLatch.countDown(); + }; try { // Send message and wait for completion getClient().sendMessage(message, List.of(consumer), errorHandler); - assertTrue(completionLatch.await(15, TimeUnit.SECONDS), - "Should receive final event"); + assertTrue(completionLatch.await(15, TimeUnit.SECONDS), "Should receive final event"); assertNull(errorRef.get(), "Should not have errors during message send"); String taskId = generatedTaskId.get(); @@ -2958,25 +3333,25 @@ public void testMainQueueClosesForFinalizedTasks() throws Exception { CountDownLatch errorLatch = new CountDownLatch(1); AtomicReference resubErrorRef = new AtomicReference<>(); - Consumer resubErrorHandler = error -> { - if (error == null) { - // Stream completed successfully - ignore, we're waiting for an error - return; - } - if (!isStreamClosedError(error)) { - resubErrorRef.set(error); - } - errorLatch.countDown(); - }; + Consumer resubErrorHandler = + error -> { + if (error == null) { + // Stream completed successfully - ignore, we're waiting for an error + return; + } + if (!isStreamClosedError(error)) { + resubErrorRef.set(error); + } + errorLatch.countDown(); + }; // Attempt resubscription try { - getClient().subscribeToTask(new TaskIdParams(taskId), - List.of(), - resubErrorHandler); + getClient().subscribeToTask(new TaskIdParams(taskId), List.of(), resubErrorHandler); // Wait for error - assertTrue(errorLatch.await(15, TimeUnit.SECONDS), + assertTrue( + errorLatch.await(15, TimeUnit.SECONDS), "Should receive error for finalized task"); Throwable error = resubErrorRef.get(); @@ -2988,18 +3363,23 @@ public void testMainQueueClosesForFinalizedTasks() throws Exception { while (cause != null && !foundTaskNotFound) { if (cause instanceof TaskNotFoundError || (cause instanceof A2AClientException - && ((A2AClientException) cause).getCause() instanceof TaskNotFoundError)) { + && ((A2AClientException) cause).getCause() + instanceof TaskNotFoundError)) { foundTaskNotFound = true; } cause = cause.getCause(); } - assertTrue(foundTaskNotFound, + assertTrue( + foundTaskNotFound, "Should receive TaskNotFoundError - MainQueue closed for finalized task"); } catch (A2AClientException e) { // Exception might be thrown immediately instead of via error handler - assertInstanceOf(TaskNotFoundError.class, e.getCause(), - "Should fail with TaskNotFoundError - MainQueue cleaned up for finalized task"); + assertInstanceOf( + TaskNotFoundError.class, + e.getCause(), + "Should fail with TaskNotFoundError - MainQueue cleaned up for finalized" + + " task"); } } finally { @@ -3038,24 +3418,32 @@ public void testMainQueueClosesForFinalizedTasks() throws Exception { @Test public void testAgentToAgentDelegation() throws Exception { // No taskId - server generates one; routing is by message content prefix "delegate:" - Message delegationMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(new TextPart("delegate:What is 2+2?")) - .build(); + Message delegationMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(new TextPart("delegate:What is 2+2?")) + .build(); CountDownLatch delegationLatch = new CountDownLatch(1); AtomicReference delegationResultRef = new AtomicReference<>(); AtomicReference delegationErrorRef = new AtomicReference<>(); BiConsumer delegationConsumer = - AgentToAgentClientFactory.createTaskCaptureConsumer(delegationResultRef, delegationLatch); - - getNonStreamingClient().sendMessage(delegationMessage, List.of(delegationConsumer), error -> { - delegationErrorRef.set(error); - delegationLatch.countDown(); - }); - - assertTrue(delegationLatch.await(30, TimeUnit.SECONDS), "Delegation should complete within timeout"); + AgentToAgentClientFactory.createTaskCaptureConsumer( + delegationResultRef, delegationLatch); + + getNonStreamingClient() + .sendMessage( + delegationMessage, + List.of(delegationConsumer), + error -> { + delegationErrorRef.set(error); + delegationLatch.countDown(); + }); + + assertTrue( + delegationLatch.await(30, TimeUnit.SECONDS), + "Delegation should complete within timeout"); Task delegationResult = delegationResultRef.get(); @@ -3066,15 +3454,21 @@ public void testAgentToAgentDelegation() throws Exception { } assertNotNull(delegationResult, "Delegation task should not be null"); - assertEquals(TaskState.TASK_STATE_COMPLETED, delegationResult.status().state(), + assertEquals( + TaskState.TASK_STATE_COMPLETED, + delegationResult.status().state(), "Delegation task should be completed"); assertNotNull(delegationResult.artifacts(), "Delegation should have artifacts"); - assertFalse(delegationResult.artifacts().isEmpty(), "Delegation should have at least one artifact"); + assertFalse( + delegationResult.artifacts().isEmpty(), + "Delegation should have at least one artifact"); // Extract text from result String delegatedText = extractTextFromTask(delegationResult); - assertTrue(delegatedText.contains("Handled locally:"), - "Delegated content should have been handled locally by target agent. Got: " + delegatedText); + assertTrue( + delegatedText.contains("Handled locally:"), + "Delegated content should have been handled locally by target agent. Got: " + + delegatedText); } /** @@ -3093,10 +3487,11 @@ public void testAgentToAgentDelegation() throws Exception { @Test public void testAgentToAgentLocalHandling() throws Exception { // No taskId - server generates one; routing is by message content prefix "a2a-local:" - Message localMessage = Message.builder() - .role(Message.Role.ROLE_USER) - .parts(new TextPart("a2a-local:Hello directly")) - .build(); + Message localMessage = + Message.builder() + .role(Message.Role.ROLE_USER) + .parts(new TextPart("a2a-local:Hello directly")) + .build(); CountDownLatch localLatch = new CountDownLatch(1); AtomicReference localResultRef = new AtomicReference<>(); @@ -3105,12 +3500,18 @@ public void testAgentToAgentLocalHandling() throws Exception { BiConsumer localConsumer = AgentToAgentClientFactory.createTaskCaptureConsumer(localResultRef, localLatch); - getClient().sendMessage(localMessage, List.of(localConsumer), error -> { - localErrorRef.set(error); - localLatch.countDown(); - }); + getClient() + .sendMessage( + localMessage, + List.of(localConsumer), + error -> { + localErrorRef.set(error); + localLatch.countDown(); + }); - assertTrue(localLatch.await(30, TimeUnit.SECONDS), "Local handling should complete within timeout"); + assertTrue( + localLatch.await(30, TimeUnit.SECONDS), + "Local handling should complete within timeout"); Task localResult = localResultRef.get(); @@ -3121,11 +3522,14 @@ public void testAgentToAgentLocalHandling() throws Exception { } assertNotNull(localResult, "Local task should not be null"); - assertEquals(TaskState.TASK_STATE_COMPLETED, localResult.status().state(), + assertEquals( + TaskState.TASK_STATE_COMPLETED, + localResult.status().state(), "Local task should be completed"); String localText = extractTextFromTask(localResult); - assertTrue(localText.contains("Handled locally: Hello directly"), + assertTrue( + localText.contains("Handled locally: Hello directly"), "Should be handled locally without delegation. Got: " + localText); } @@ -3152,54 +3556,75 @@ public void testSendMessageWithHistoryLengthZero() throws Exception { AtomicReference taskIdRef = new AtomicReference<>(); try { - Message initialMessage = Message.builder(MESSAGE) - .parts(new TextPart("input-required:Trigger INPUT_REQUIRED")) - .build(); + Message initialMessage = + Message.builder(MESSAGE) + .parts(new TextPart("input-required:Trigger INPUT_REQUIRED")) + .build(); CountDownLatch initialLatch = new CountDownLatch(1); - getNonStreamingClient().sendMessage(initialMessage, List.of((event, agentCard) -> { - if (event instanceof TaskEvent te) { - taskIdRef.set(te.getTask().id()); - initialLatch.countDown(); - } else if (event instanceof TaskUpdateEvent tue) { - if (tue.getUpdateEvent() instanceof TaskStatusUpdateEvent statusUpdate) { - taskIdRef.set(statusUpdate.taskId()); - } - initialLatch.countDown(); - } - }), null); - - assertTrue(initialLatch.await(15, TimeUnit.SECONDS), "Initial sendMessage should complete"); + getNonStreamingClient() + .sendMessage( + initialMessage, + List.of( + (event, agentCard) -> { + if (event instanceof TaskEvent te) { + taskIdRef.set(te.getTask().id()); + initialLatch.countDown(); + } else if (event instanceof TaskUpdateEvent tue) { + if (tue.getUpdateEvent() + instanceof TaskStatusUpdateEvent statusUpdate) { + taskIdRef.set(statusUpdate.taskId()); + } + initialLatch.countDown(); + } + }), + null); + + assertTrue( + initialLatch.await(15, TimeUnit.SECONDS), + "Initial sendMessage should complete"); String taskId = taskIdRef.get(); assertNotNull(taskId, "Should have captured task ID"); - Message followUp = Message.builder(MESSAGE) - .taskId(taskId) - .parts(new TextPart("input-required:User input")) - .build(); + Message followUp = + Message.builder(MESSAGE) + .taskId(taskId) + .parts(new TextPart("input-required:User input")) + .build(); - MessageSendParams params = MessageSendParams.builder() - .message(followUp) - .configuration(MessageSendConfiguration.builder() - .historyLength(0) - .build()) - .build(); + MessageSendParams params = + MessageSendParams.builder() + .message(followUp) + .configuration( + MessageSendConfiguration.builder().historyLength(0).build()) + .build(); CountDownLatch followUpLatch = new CountDownLatch(1); AtomicReference resultTaskRef = new AtomicReference<>(); - getNonStreamingClient().sendMessage(params, List.of((BiConsumer) (event, agentCard) -> { - if (event instanceof TaskEvent te) { - resultTaskRef.set(te.getTask()); - followUpLatch.countDown(); - } - }), null, null); - - assertTrue(followUpLatch.await(15, TimeUnit.SECONDS), "Follow-up sendMessage should complete"); + getNonStreamingClient() + .sendMessage( + params, + List.of( + (BiConsumer) + (event, agentCard) -> { + if (event instanceof TaskEvent te) { + resultTaskRef.set(te.getTask()); + followUpLatch.countDown(); + } + }), + null, + null); + + assertTrue( + followUpLatch.await(15, TimeUnit.SECONDS), + "Follow-up sendMessage should complete"); Task resultTask = resultTaskRef.get(); assertNotNull(resultTask, "Should have received a Task response"); - assertTrue(resultTask.history().isEmpty(), - "historyLength=0 should return no history, but got " + - resultTask.history().size() + " messages"); + assertTrue( + resultTask.history().isEmpty(), + "historyLength=0 should return no history, but got " + + resultTask.history().size() + + " messages"); } finally { String taskId = taskIdRef.get(); if (taskId != null) { @@ -3214,67 +3639,89 @@ public void testSendStreamingMessageWithHistoryLengthZero() throws Exception { AtomicReference taskIdRef = new AtomicReference<>(); try { - Message initialMessage = Message.builder(MESSAGE) - .parts(new TextPart("input-required:Trigger INPUT_REQUIRED")) - .build(); + Message initialMessage = + Message.builder(MESSAGE) + .parts(new TextPart("input-required:Trigger INPUT_REQUIRED")) + .build(); CountDownLatch initialLatch = new CountDownLatch(1); - getClient().sendMessage(initialMessage, List.of((event, agentCard) -> { - if (event instanceof TaskEvent te) { - taskIdRef.set(te.getTask().id()); - initialLatch.countDown(); - } else if (event instanceof TaskUpdateEvent tue) { - taskIdRef.set(tue.getTask().id()); - if (tue.getTask().status().state() == TaskState.TASK_STATE_INPUT_REQUIRED) { - initialLatch.countDown(); - } - } - }), error -> { - if (!isStreamClosedError(error)) { - initialLatch.countDown(); - } - }); + getClient() + .sendMessage( + initialMessage, + List.of( + (event, agentCard) -> { + if (event instanceof TaskEvent te) { + taskIdRef.set(te.getTask().id()); + initialLatch.countDown(); + } else if (event instanceof TaskUpdateEvent tue) { + taskIdRef.set(tue.getTask().id()); + if (tue.getTask().status().state() + == TaskState.TASK_STATE_INPUT_REQUIRED) { + initialLatch.countDown(); + } + } + }), + error -> { + if (!isStreamClosedError(error)) { + initialLatch.countDown(); + } + }); - assertTrue(initialLatch.await(15, TimeUnit.SECONDS), "Initial streaming sendMessage should complete"); + assertTrue( + initialLatch.await(15, TimeUnit.SECONDS), + "Initial streaming sendMessage should complete"); String taskId = taskIdRef.get(); assertNotNull(taskId, "Should have captured task ID"); - Message followUp = Message.builder(MESSAGE) - .taskId(taskId) - .parts(new TextPart("input-required:User input")) - .build(); + Message followUp = + Message.builder(MESSAGE) + .taskId(taskId) + .parts(new TextPart("input-required:User input")) + .build(); - MessageSendParams params = MessageSendParams.builder() - .message(followUp) - .configuration(MessageSendConfiguration.builder() - .historyLength(0) - .build()) - .build(); + MessageSendParams params = + MessageSendParams.builder() + .message(followUp) + .configuration( + MessageSendConfiguration.builder().historyLength(0).build()) + .build(); CountDownLatch followUpLatch = new CountDownLatch(1); AtomicReference resultTaskRef = new AtomicReference<>(); - getClient().sendMessage(params, List.of((BiConsumer) (event, agentCard) -> { - if (event instanceof TaskEvent te) { - resultTaskRef.set(te.getTask()); - followUpLatch.countDown(); - } else if (event instanceof TaskUpdateEvent tue) { - resultTaskRef.set(tue.getTask()); - if (tue.getTask().status().state() == TaskState.TASK_STATE_COMPLETED) { - followUpLatch.countDown(); - } - } - }), error -> { - if (!isStreamClosedError(error)) { - followUpLatch.countDown(); - } - }, null); + getClient() + .sendMessage( + params, + List.of( + (BiConsumer) + (event, agentCard) -> { + if (event instanceof TaskEvent te) { + resultTaskRef.set(te.getTask()); + followUpLatch.countDown(); + } else if (event instanceof TaskUpdateEvent tue) { + resultTaskRef.set(tue.getTask()); + if (tue.getTask().status().state() + == TaskState.TASK_STATE_COMPLETED) { + followUpLatch.countDown(); + } + } + }), + error -> { + if (!isStreamClosedError(error)) { + followUpLatch.countDown(); + } + }, + null); - assertTrue(followUpLatch.await(15, TimeUnit.SECONDS), "Follow-up streaming sendMessage should complete"); + assertTrue( + followUpLatch.await(15, TimeUnit.SECONDS), + "Follow-up streaming sendMessage should complete"); Task resultTask = resultTaskRef.get(); assertNotNull(resultTask, "Should have received a Task response"); - assertTrue(resultTask.history() == null || resultTask.history().isEmpty(), - "historyLength=0 should return no history, but got " + - (resultTask.history() != null ? resultTask.history().size() : 0) + " messages"); + assertTrue( + resultTask.history() == null || resultTask.history().isEmpty(), + "historyLength=0 should return no history, but got " + + (resultTask.history() != null ? resultTask.history().size() : 0) + + " messages"); } finally { String taskId = taskIdRef.get(); if (taskId != null) { @@ -3282,5 +3729,4 @@ public void testSendStreamingMessageWithHistoryLengthZero() throws Exception { } } } - } diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerWithAuthTest.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerWithAuthTest.java index 98b65fbf4..9ac9e8909 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerWithAuthTest.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerWithAuthTest.java @@ -1,36 +1,25 @@ package org.a2aproject.sdk.server.apps.common; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import io.restassured.RestAssured; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.specification.RequestSpecification; +import jakarta.ws.rs.core.MediaType; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.Base64; -import java.util.List; -import java.util.Map; - - -import io.restassured.RestAssured; -import io.restassured.config.ObjectMapperConfig; -import io.restassured.specification.RequestSpecification; -import jakarta.ws.rs.core.MediaType; import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.config.ClientConfig; import org.a2aproject.sdk.jsonrpc.common.json.JsonUtil; -import org.a2aproject.sdk.server.apps.common.A2AGsonObjectMapper; import org.a2aproject.sdk.spec.A2AClientException; -import org.a2aproject.sdk.spec.A2AClientHTTPError; -import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; -import org.a2aproject.sdk.spec.AgentInterface; -import org.a2aproject.sdk.spec.HTTPAuthSecurityScheme; -import org.a2aproject.sdk.spec.SecurityRequirement; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TaskQueryParams; import org.a2aproject.sdk.spec.TaskState; @@ -71,11 +60,12 @@ protected static String getEncodedCredentials() { } // Minimal test task - protected static final Task MINIMAL_TASK = Task.builder() - .id("task-123") - .contextId("session-xyz") - .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) - .build(); + protected static final Task MINIMAL_TASK = + Task.builder() + .id("task-123") + .contextId("session-xyz") + .status(new TaskStatus(TaskState.TASK_STATE_SUBMITTED)) + .build(); protected final int serverPort; private Client authenticatedClient; @@ -135,12 +125,12 @@ protected Client getUnauthenticatedClient() throws A2AClientException { */ private Client createAuthenticatedClient() throws A2AClientException { AgentCard agentCard = fetchAgentCardFromServer(); - ClientConfig clientConfig = new ClientConfig.Builder() - .setStreaming(false) // Non-streaming for simple tests - .build(); + ClientConfig clientConfig = + new ClientConfig.Builder() + .setStreaming(false) // Non-streaming for simple tests + .build(); - ClientBuilder clientBuilder = Client.builder(agentCard) - .clientConfig(clientConfig); + ClientBuilder clientBuilder = Client.builder(agentCard).clientConfig(clientConfig); configureTransportWithAuth(clientBuilder); @@ -152,14 +142,11 @@ private Client createAuthenticatedClient() throws A2AClientException { */ private Client createUnauthenticatedClient() throws A2AClientException { AgentCard agentCard = fetchAgentCardFromServer(); - ClientConfig clientConfig = new ClientConfig.Builder() - .setStreaming(false) - .build(); + ClientConfig clientConfig = new ClientConfig.Builder().setStreaming(false).build(); - ClientBuilder clientBuilder = Client.builder(agentCard) - .clientConfig(clientConfig); + ClientBuilder clientBuilder = Client.builder(agentCard).clientConfig(clientConfig); - configureTransport(clientBuilder); // No auth + configureTransport(clientBuilder); // No auth return clientBuilder.build(); } @@ -171,12 +158,18 @@ private Client createUnauthenticatedClient() throws A2AClientException { protected AgentCard fetchAgentCardFromServer() { try { HttpClient httpClient = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/.well-known/agent-card.json")) - .GET() - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + "http://localhost:" + + serverPort + + "/.well-known/agent-card.json")) + .GET() + .build(); + + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new RuntimeException("Failed to fetch agent card: " + response.statusCode()); } @@ -192,16 +185,17 @@ protected AgentCard fetchAgentCardFromServer() { */ protected static RequestSpecification given() { return RestAssured.given() - .config(RestAssured.config() - .objectMapperConfig(new ObjectMapperConfig(A2AGsonObjectMapper.INSTANCE))); + .config( + RestAssured.config() + .objectMapperConfig( + new ObjectMapperConfig(A2AGsonObjectMapper.INSTANCE))); } /** * RestAssured helper with authentication. */ protected RequestSpecification givenAuthenticated() { - return given() - .auth().basic(TEST_USERNAME, TEST_PASSWORD); + return given().auth().basic(TEST_USERNAME, TEST_PASSWORD); } /** @@ -215,19 +209,21 @@ protected RequestSpecification givenUnauthenticated() { * Save a task in the test task store via HTTP. */ protected void saveTaskInTaskStore(Task task) throws Exception { - HttpClient httpClient = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/task")) - .header("Content-Type", MediaType.APPLICATION_JSON) - .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(task))) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task")) + .header("Content-Type", MediaType.APPLICATION_JSON) + .POST(HttpRequest.BodyPublishers.ofString(JsonUtil.toJson(task))) + .build(); + + HttpResponse response = + httpClient.send( + request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (response.statusCode() != 200 && response.statusCode() != 204) { - throw new RuntimeException("Failed to save task: " + response.statusCode() + " " + response.body()); + throw new RuntimeException( + "Failed to save task: " + response.statusCode() + " " + response.body()); } } @@ -237,14 +233,17 @@ protected void saveTaskInTaskStore(Task task) throws Exception { protected void deleteTaskInTaskStore(String taskId) throws Exception { HttpClient httpClient = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) - .DELETE() - .build(); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) + .DELETE() + .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200 && response.statusCode() != 204) { - throw new RuntimeException("Failed to delete task: " + response.statusCode() + " " + response.body()); + throw new RuntimeException( + "Failed to delete task: " + response.statusCode() + " " + response.body()); } } @@ -261,13 +260,17 @@ public void testGetTaskRequiresAuthenticationUnauthenticated() throws Exception // Test unauthenticated request fails Client unauthClient = getUnauthenticatedClient(); - A2AClientException error = assertThrows(A2AClientException.class, () -> { - unauthClient.getTask(new TaskQueryParams(MINIMAL_TASK.id())); - }); + A2AClientException error = + assertThrows( + A2AClientException.class, + () -> { + unauthClient.getTask(new TaskQueryParams(MINIMAL_TASK.id())); + }); // Verify it's an authentication failure - assertTrue(error.getMessage().contains("Authentication failed") || - error.getMessage().contains("401") || - error.getMessage().contains("Unauthorized"), + assertTrue( + error.getMessage().contains("Authentication failed") + || error.getMessage().contains("401") + || error.getMessage().contains("Unauthorized"), "Expected authentication error, got: " + error.getMessage()); // Cleanup @@ -301,10 +304,7 @@ public void testGetTaskWithAuthentication() throws Exception { */ @Test public void testGetAgentCardIsPublic() { - givenUnauthenticated() - .get("/.well-known/agent-card.json") - .then() - .statusCode(200); + givenUnauthenticated().get("/.well-known/agent-card.json").then().statusCode(200); } /** @@ -321,7 +321,10 @@ public void testBasicAuthWorksViaHttp() throws Exception { // Test with valid credentials via RestAssured givenAuthenticated() .contentType("application/json") - .body("{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getTask\",\"params\":{\"id\":\"" + MINIMAL_TASK.id() + "\"}}") + .body( + "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getTask\",\"params\":{\"id\":\"" + + MINIMAL_TASK.id() + + "\"}}") .post("/") .then() .statusCode(200); diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerWithTaskAuthorizationTest.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerWithTaskAuthorizationTest.java index 901c00380..c4ab8117b 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerWithTaskAuthorizationTest.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AbstractA2AServerWithTaskAuthorizationTest.java @@ -22,7 +22,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; - import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.TaskEvent; @@ -61,7 +60,8 @@ public abstract class AbstractA2AServerWithTaskAuthorizationTest { protected abstract String getTransportUrl(); - protected abstract void configureTransportWithCredentials(ClientBuilder builder, String username, String password); + protected abstract void configureTransportWithCredentials( + ClientBuilder builder, String username, String password); protected Client createClient(String username, String password) throws A2AClientException { AgentCard agentCard = fetchAgentCardFromServer(); @@ -74,11 +74,13 @@ protected Client createClient(String username, String password) throws A2AClient protected AgentCard fetchAgentCardFromServer() { try { HttpClient httpClient = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(getTransportUrl() + "/.well-known/agent-card.json")) - .GET() - .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(getTransportUrl() + "/.well-known/agent-card.json")) + .GET() + .build(); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new RuntimeException("Failed to fetch agent card: " + response.statusCode()); } @@ -89,36 +91,44 @@ protected AgentCard fetchAgentCardFromServer() { } protected static String getEncodedCredentials(String username, String password) { - return Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder() + .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8)); } protected Task sendMessageAndGetTask(Client client, String messageText) throws Exception { - Message message = Message.builder() - .messageId(UUID.randomUUID().toString()) - .role(Message.Role.ROLE_USER) - .parts(new TextPart("a2a-local:" + messageText)) - .build(); + Message message = + Message.builder() + .messageId(UUID.randomUUID().toString()) + .role(Message.Role.ROLE_USER) + .parts(new TextPart("a2a-local:" + messageText)) + .build(); CountDownLatch latch = new CountDownLatch(1); AtomicReference receivedTask = new AtomicReference<>(); AtomicReference errorRef = new AtomicReference<>(); - client.sendMessage(message, List.of((event, agentCard) -> { - if (event instanceof TaskEvent te) { - receivedTask.set(te.getTask()); - if (te.getTask().status().state() == TaskState.TASK_STATE_COMPLETED) { - latch.countDown(); - } - } else if (event instanceof TaskUpdateEvent tue) { - receivedTask.set(tue.getTask()); - if (tue.getTask().status().state() == TaskState.TASK_STATE_COMPLETED) { + client.sendMessage( + message, + List.of( + (event, agentCard) -> { + if (event instanceof TaskEvent te) { + receivedTask.set(te.getTask()); + if (te.getTask().status().state() + == TaskState.TASK_STATE_COMPLETED) { + latch.countDown(); + } + } else if (event instanceof TaskUpdateEvent tue) { + receivedTask.set(tue.getTask()); + if (tue.getTask().status().state() + == TaskState.TASK_STATE_COMPLETED) { + latch.countDown(); + } + } + }), + error -> { + errorRef.set(error); latch.countDown(); - } - } - }), error -> { - errorRef.set(error); - latch.countDown(); - }); + }); assertTrue(latch.await(10, TimeUnit.SECONDS), "Task should complete within timeout"); assertNull(errorRef.get(), "Should not have received an error: " + errorRef.get()); @@ -145,8 +155,10 @@ public void testNonOwnerCannotGetTask() throws Exception { Task task = sendMessageAndGetTask(clientA, "non-owner-get-test"); Client clientB = createClient(USER_B, USER_B_PASSWORD); - A2AClientException error = assertThrows(A2AClientException.class, () -> - clientB.getTask(new TaskQueryParams(task.id()))); + A2AClientException error = + assertThrows( + A2AClientException.class, + () -> clientB.getTask(new TaskQueryParams(task.id()))); assertInstanceOf(TaskNotFoundError.class, error.getCause()); } @@ -171,8 +183,10 @@ public void testNonOwnerCannotCancelTask() throws Exception { Task task = sendMessageAndGetTask(clientA, "non-owner-cancel-test"); Client clientB = createClient(USER_B, USER_B_PASSWORD); - A2AClientException error = assertThrows(A2AClientException.class, () -> - clientB.cancelTask(new CancelTaskParams(task.id()))); + A2AClientException error = + assertThrows( + A2AClientException.class, + () -> clientB.cancelTask(new CancelTaskParams(task.id()))); assertInstanceOf(TaskNotFoundError.class, error.getCause()); } @@ -207,10 +221,14 @@ public void testUnauthorizedLooksLikeNotFound() throws Exception { Client clientB = createClient(USER_B, USER_B_PASSWORD); - A2AClientException unauthorizedError = assertThrows(A2AClientException.class, () -> - clientB.getTask(new TaskQueryParams(task.id()))); - A2AClientException notFoundError = assertThrows(A2AClientException.class, () -> - clientB.getTask(new TaskQueryParams(UUID.randomUUID().toString()))); + A2AClientException unauthorizedError = + assertThrows( + A2AClientException.class, + () -> clientB.getTask(new TaskQueryParams(task.id()))); + A2AClientException notFoundError = + assertThrows( + A2AClientException.class, + () -> clientB.getTask(new TaskQueryParams(UUID.randomUUID().toString()))); assertInstanceOf(TaskNotFoundError.class, unauthorizedError.getCause()); assertInstanceOf(TaskNotFoundError.class, notFoundError.getCause()); diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentCardProducer.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentCardProducer.java index 710f8bae3..36330aa23 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentCardProducer.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentCardProducer.java @@ -2,6 +2,10 @@ import static org.a2aproject.sdk.spec.TransportProtocol.GRPC; +import io.quarkus.arc.profile.IfBuildProfile; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -9,11 +13,6 @@ import java.util.Collections; import java.util.List; import java.util.Properties; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Produces; -import jakarta.inject.Inject; - import org.a2aproject.sdk.server.ExtendedAgentCard; import org.a2aproject.sdk.server.PublicAgentCard; import org.a2aproject.sdk.spec.AgentCapabilities; @@ -23,8 +22,6 @@ import org.a2aproject.sdk.spec.HTTPAuthSecurityScheme; import org.a2aproject.sdk.spec.SecurityRequirement; import org.eclipse.microprofile.config.inject.ConfigProperty; - -import io.quarkus.arc.profile.IfBuildProfile; import org.junit.jupiter.api.Assertions; @ApplicationScoped @@ -32,7 +29,8 @@ public class AgentCardProducer { private static final String PREFERRED_TRANSPORT = "preferred-transport"; - private static final String A2A_REQUESTHANDLER_TEST_PROPERTIES = "/a2a-requesthandler-test.properties"; + private static final String A2A_REQUESTHANDLER_TEST_PROPERTIES = + "/a2a-requesthandler-test.properties"; private static final String BASIC_AUTH_SCHEME_NAME = "basicAuth"; @Inject @@ -45,36 +43,46 @@ public class AgentCardProducer { public AgentCard agentCard() { String port = System.getProperty("test.agent.card.port", "8081"); String preferredTransport = loadPreferredTransportFromProperties(); - String transportUrl = GRPC.toString().equals(preferredTransport) ? "localhost:" + port : "http://localhost:" + port; - - List interfaces = Collections.singletonList(new AgentInterface(preferredTransport, transportUrl)); - - AgentCard.Builder builder = AgentCard.builder() - .name("test-card") - .description("A test agent card") - .version("1.0") - .documentationUrl("http://example.com/docs") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extendedAgentCard(true) - .build()) - .defaultInputModes(Collections.singletonList("text")) - .defaultOutputModes(Collections.singletonList("text")) - .skills(new ArrayList<>()) - .supportedInterfaces(interfaces); - - Compat03Fields.addCompat03FieldsIfAvailable(builder, interfaces, transportUrl, preferredTransport); + String transportUrl = + GRPC.toString().equals(preferredTransport) + ? "localhost:" + port + : "http://localhost:" + port; + + List interfaces = + Collections.singletonList(new AgentInterface(preferredTransport, transportUrl)); + + AgentCard.Builder builder = + AgentCard.builder() + .name("test-card") + .description("A test agent card") + .version("1.0") + .documentationUrl("http://example.com/docs") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extendedAgentCard(true) + .build()) + .defaultInputModes(Collections.singletonList("text")) + .defaultOutputModes(Collections.singletonList("text")) + .skills(new ArrayList<>()) + .supportedInterfaces(interfaces); + + Compat03Fields.addCompat03FieldsIfAvailable( + builder, interfaces, transportUrl, preferredTransport); // Add security configuration if enabled (for authentication tests) if (securityEnabled) { - builder.securitySchemes(java.util.Map.of( - BASIC_AUTH_SCHEME_NAME, - new HTTPAuthSecurityScheme(null, "basic", "HTTP Basic authentication"))) - .securityRequirements(java.util.List.of( - SecurityRequirement.builder() - .scheme(BASIC_AUTH_SCHEME_NAME, java.util.List.of()) - .build())); + builder.securitySchemes( + java.util.Map.of( + BASIC_AUTH_SCHEME_NAME, + new HTTPAuthSecurityScheme( + null, "basic", "HTTP Basic authentication"))) + .securityRequirements( + java.util.List.of( + SecurityRequirement.builder() + .scheme(BASIC_AUTH_SCHEME_NAME, java.util.List.of()) + .build())); } return builder.build(); @@ -87,7 +95,7 @@ private static String loadPreferredTransportFromProperties() { } Properties properties = new Properties(); try { - try (InputStream in = url.openStream()){ + try (InputStream in = url.openStream()) { properties.load(in); } } catch (IOException e) { @@ -99,4 +107,3 @@ private static String loadPreferredTransportFromProperties() { return preferredTransport; } } - diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentExecutorProducer.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentExecutorProducer.java index b46519e30..c7ff2488d 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentExecutorProducer.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentExecutorProducer.java @@ -2,16 +2,14 @@ import static org.a2aproject.sdk.server.ServerCallContext.TRANSPORT_KEY; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - +import io.quarkus.arc.profile.IfBuildProfile; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; - +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.a2aproject.sdk.A2A; import org.a2aproject.sdk.client.Client; -import org.a2aproject.sdk.client.ClientEvent; import org.a2aproject.sdk.client.TaskEvent; import org.a2aproject.sdk.client.TaskUpdateEvent; import org.a2aproject.sdk.server.PublicAgentCard; @@ -24,26 +22,21 @@ import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.Artifact; import org.a2aproject.sdk.spec.InternalError; -import org.a2aproject.sdk.spec.InvalidParamsError; import org.a2aproject.sdk.spec.Message; import org.a2aproject.sdk.spec.Part; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TextPart; import org.a2aproject.sdk.spec.TransportProtocol; import org.a2aproject.sdk.spec.UnsupportedOperationError; -import io.quarkus.arc.profile.IfBuildProfile; @ApplicationScoped @IfBuildProfile("test") public class AgentExecutorProducer { // Inject the existing AgentCard to avoid special handling for grpc - @Inject - @PublicAgentCard - AgentCard agentCard; + @Inject @PublicAgentCard AgentCard agentCard; - @Inject - RequestScopedBean requestScopedBean; + @Inject RequestScopedBean requestScopedBean; @Produces public AgentExecutor agentExecutor() { @@ -51,7 +44,10 @@ public AgentExecutor agentExecutor() { @Override public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2AError { String taskId = context.getTaskId(); - String input = context.getMessage() != null ? extractTextFromMessage(context.getMessage()) : ""; + String input = + context.getMessage() != null + ? extractTextFromMessage(context.getMessage()) + : ""; // Agent-to-agent communication test (routed by message content prefix) if (input.startsWith("delegate:") || input.startsWith("a2a-local:")) { @@ -76,8 +72,10 @@ public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2 } if (input.startsWith("multi-event:second")) { agentEmitter.addArtifact( - List.of(new TextPart("Second message artifact")), - "artifact-2", "Second Artifact", null); + List.of(new TextPart("Second message artifact")), + "artifact-2", + "Second Artifact", + null); agentEmitter.complete(); return; } @@ -91,17 +89,21 @@ public void execute(RequestContext context, AgentEmitter agentEmitter) throws A2 return; } // First call: emit INPUT_REQUIRED - agentEmitter.requiresInput(agentEmitter.newAgentMessage( - List.of(new TextPart("Please provide additional information")), - context.getMessage().metadata())); + agentEmitter.requiresInput( + agentEmitter.newAgentMessage( + List.of(new TextPart("Please provide additional information")), + context.getMessage().metadata())); return; } // Special handling for auth-required test (routed by message content) if (input.startsWith("auth-required:")) { - agentEmitter.requiresAuth(agentEmitter.newAgentMessage( - List.of(new TextPart("Please authenticate with OAuth provider")), - context.getMessage().metadata())); + agentEmitter.requiresAuth( + agentEmitter.newAgentMessage( + List.of( + new TextPart( + "Please authenticate with OAuth provider")), + context.getMessage().metadata())); try { Thread.sleep(2000); @@ -149,18 +151,23 @@ public void cancel(RequestContext context, AgentEmitter agentEmitter) throws A2A * Handles agent-to-agent communication testing. * Detects "delegate:" prefix and forwards requests to another agent via client. */ - private void handleAgentToAgentTest(RequestContext context, AgentEmitter agentEmitter) throws A2AError { + private void handleAgentToAgentTest(RequestContext context, AgentEmitter agentEmitter) + throws A2AError { try { // Get transport protocol from ServerCallContext ServerCallContext callContext = context.getCallContext(); if (callContext == null) { - agentEmitter.fail(new InternalError("No call context available for agent-to-agent test")); + agentEmitter.fail( + new InternalError( + "No call context available for agent-to-agent test")); return; } - TransportProtocol transportProtocol = (TransportProtocol) callContext.getState().get(TRANSPORT_KEY); + TransportProtocol transportProtocol = + (TransportProtocol) callContext.getState().get(TRANSPORT_KEY); if (transportProtocol == null) { - agentEmitter.fail(new InternalError("Transport type not set in call context")); + agentEmitter.fail( + new InternalError("Transport type not set in call context")); return; } @@ -182,7 +189,8 @@ private void handleAgentToAgentTest(RequestContext context, AgentEmitter agentEm } catch (Exception e) { // Log the full stack trace to help debug intermittent failures e.printStackTrace(); - agentEmitter.fail(new InternalError("Agent-to-agent test failed: " + e.getMessage())); + agentEmitter.fail( + new InternalError("Agent-to-agent test failed: " + e.getMessage())); } } @@ -193,13 +201,16 @@ private void handleAgentToAgentTest(RequestContext context, AgentEmitter agentEm * synchronously without requiring async callbacks and latches. This simplified approach * avoids race conditions between event consumption and callback invocation. */ - private void handleDelegation(String userInput, TransportProtocol transportProtocol, - AgentEmitter agentEmitter) { + private void handleDelegation( + String userInput, + TransportProtocol transportProtocol, + AgentEmitter agentEmitter) { // Strip "delegate:" prefix String delegatedContent = userInput.substring("delegate:".length()).trim(); // Create client for same transport (streaming=false for blocking behavior) - try (Client client = AgentToAgentClientFactory.createClient(agentCard, transportProtocol)) { + try (Client client = + AgentToAgentClientFactory.createClient(agentCard, transportProtocol)) { agentEmitter.startWork(); // Store the result task from blocking call @@ -207,43 +218,57 @@ private void handleDelegation(String userInput, TransportProtocol transportProto // Delegate to another agent (new task on same server) // Add a marker so the receiving agent knows to complete the task - Message delegatedMessage = A2A.toUserMessage("#a2a-delegated#" + delegatedContent); + Message delegatedMessage = + A2A.toUserMessage("#a2a-delegated#" + delegatedContent); // Blocking call should return final task synchronously - client.sendMessage(delegatedMessage, List.of((event, card) -> { - if (event instanceof TaskEvent te) { - taskRef.set(te.getTask()); - } else if (event instanceof TaskUpdateEvent tue) { - taskRef.set(tue.getTask()); - } - }), null); + client.sendMessage( + delegatedMessage, + List.of( + (event, card) -> { + if (event instanceof TaskEvent te) { + taskRef.set(te.getTask()); + } else if (event instanceof TaskUpdateEvent tue) { + taskRef.set(tue.getTask()); + } + }), + null); // Blocking call should have completed before returning Task delegatedResult = taskRef.get(); if (delegatedResult == null) { - agentEmitter.fail(new InternalError("No result received from blocking delegation call")); + agentEmitter.fail( + new InternalError( + "No result received from blocking delegation call")); return; } // DIAGNOSTIC: Check if task is actually final - // If blocking call returns non-final task, it indicates a server-side race condition + // If blocking call returns non-final task, it indicates a server-side race + // condition if (!delegatedResult.status().state().isFinal()) { - String diagnostic = String.format( - "RACE CONDITION DETECTED: Blocking call returned non-final task! " + - "State: %s, TaskId: %s, Artifacts: %d. " + - "This indicates DefaultRequestHandler wait logic failed to synchronize with MainEventBusProcessor.", - delegatedResult.status().state(), - delegatedResult.id(), - delegatedResult.artifacts() != null ? delegatedResult.artifacts().size() : 0); - System.err.println(diagnostic); // Also print to stderr for CI visibility + String diagnostic = + String.format( + "RACE CONDITION DETECTED: Blocking call returned non-final" + + " task! State: %s, TaskId: %s, Artifacts: %d. This" + + " indicates DefaultRequestHandler wait logic failed" + + " to synchronize with MainEventBusProcessor.", + delegatedResult.status().state(), + delegatedResult.id(), + delegatedResult.artifacts() != null + ? delegatedResult.artifacts().size() + : 0); + System.err.println(diagnostic); // Also print to stderr for CI visibility agentEmitter.fail(new InternalError(diagnostic)); return; } // Extract artifacts from delegated task and add to current task - // NOTE: We cannot use emitter.addTask(delegatedResult) because it has a different taskId - if (delegatedResult.artifacts() != null && !delegatedResult.artifacts().isEmpty()) { + // NOTE: We cannot use emitter.addTask(delegatedResult) because it has a + // different taskId + if (delegatedResult.artifacts() != null + && !delegatedResult.artifacts().isEmpty()) { for (Artifact artifact : delegatedResult.artifacts()) { agentEmitter.addArtifact(artifact.parts()); } @@ -252,7 +277,8 @@ private void handleDelegation(String userInput, TransportProtocol transportProto // Complete current task agentEmitter.complete(); } catch (A2AClientException e) { - agentEmitter.fail(new InternalError("Failed to create client: " + e.getMessage())); + agentEmitter.fail( + new InternalError("Failed to create client: " + e.getMessage())); } } @@ -268,7 +294,8 @@ private void handleLocally(String userInput, AgentEmitter agentEmitter) { } catch (Exception e) { // Defensive catch to ensure we always emit a final state e.printStackTrace(); - agentEmitter.fail(new InternalError("Local handling failed: " + e.getMessage())); + agentEmitter.fail( + new InternalError("Local handling failed: " + e.getMessage())); } } }; diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentToAgentClientFactory.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentToAgentClientFactory.java index 6b81c0ef1..6548bbe26 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentToAgentClientFactory.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AgentToAgentClientFactory.java @@ -1,9 +1,10 @@ package org.a2aproject.sdk.server.apps.common; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiConsumer; - import org.a2aproject.sdk.client.Client; import org.a2aproject.sdk.client.ClientBuilder; import org.a2aproject.sdk.client.ClientEvent; @@ -20,8 +21,6 @@ import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.Task; import org.a2aproject.sdk.spec.TransportProtocol; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; /** * Helper class for creating A2A clients for agent-to-agent communication testing. @@ -66,19 +65,19 @@ public static BiConsumer createTaskCaptureConsumer( */ public static Client createClient(AgentCard agentCard, TransportProtocol transportProtocol) throws A2AClientException { - ClientConfig clientConfig = ClientConfig.builder() - .setStreaming(false) - .build(); + ClientConfig clientConfig = ClientConfig.builder().setStreaming(false).build(); - ClientBuilder clientBuilder = Client.builder(agentCard) - .clientConfig(clientConfig); + ClientBuilder clientBuilder = Client.builder(agentCard).clientConfig(clientConfig); - ClientTransportEnhancer enhancer = switch (transportProtocol) { - case JSONRPC -> new JsonRpcClientEnhancer(); - case GRPC -> new GrpcClientEnhancer(); - case HTTP_JSON -> new RestClientEnhancer(); - default -> throw new IllegalArgumentException("Unsupported transport: " + transportProtocol); - }; + ClientTransportEnhancer enhancer = + switch (transportProtocol) { + case JSONRPC -> new JsonRpcClientEnhancer(); + case GRPC -> new GrpcClientEnhancer(); + case HTTP_JSON -> new RestClientEnhancer(); + default -> + throw new IllegalArgumentException( + "Unsupported transport: " + transportProtocol); + }; enhancer.enhance(clientBuilder); return clientBuilder.build(); @@ -92,28 +91,38 @@ interface ClientTransportEnhancer { void enhance(ClientBuilder clientBuilder); } - private static class GrpcClientEnhancer implements AgentToAgentClientFactory.ClientTransportEnhancer { + private static class GrpcClientEnhancer + implements AgentToAgentClientFactory.ClientTransportEnhancer { @Override public void enhance(ClientBuilder clientBuilder) { - clientBuilder.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().channelFactory(target -> { - ManagedChannel channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); - return channel; - })); + clientBuilder.withTransport( + GrpcTransport.class, + new GrpcTransportConfigBuilder() + .channelFactory( + target -> { + ManagedChannel channel = + ManagedChannelBuilder.forTarget(target) + .usePlaintext() + .build(); + return channel; + })); } } - private static class JsonRpcClientEnhancer implements AgentToAgentClientFactory.ClientTransportEnhancer { + private static class JsonRpcClientEnhancer + implements AgentToAgentClientFactory.ClientTransportEnhancer { @Override public void enhance(ClientBuilder clientBuilder) { - clientBuilder.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); + clientBuilder.withTransport( + JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); } } - private static class RestClientEnhancer implements AgentToAgentClientFactory.ClientTransportEnhancer { + private static class RestClientEnhancer + implements AgentToAgentClientFactory.ClientTransportEnhancer { @Override public void enhance(ClientBuilder clientBuilder) { clientBuilder.withTransport(RestTransport.class, new RestTransportConfigBuilder()); } } } - diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AuthTestProfile.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AuthTestProfile.java index 86d1546c3..8bcdf6ba1 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AuthTestProfile.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/AuthTestProfile.java @@ -1,8 +1,7 @@ package org.a2aproject.sdk.server.apps.common; -import java.util.Map; - import io.quarkus.test.junit.QuarkusTestProfile; +import java.util.Map; /** * Quarkus test profile that enables security for authentication tests. @@ -41,8 +40,7 @@ public Map getConfigOverrides() { Map.entry("quarkus.http.auth.basic", "true"), // Enable proactive authentication - authenticate at HTTP layer before route handler - Map.entry("quarkus.http.auth.proactive", "true") - ); + Map.entry("quarkus.http.auth.proactive", "true")); } @Override diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TaskAuthorizationTestProfile.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TaskAuthorizationTestProfile.java index 503142ab3..2eea2dec0 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TaskAuthorizationTestProfile.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TaskAuthorizationTestProfile.java @@ -3,8 +3,6 @@ import java.util.HashMap; import java.util.Map; -import io.quarkus.test.junit.QuarkusTestProfile; - public class TaskAuthorizationTestProfile extends AuthTestProfile { @Override diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestHttpClient.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestHttpClient.java index 82b6de8e6..3ac5d279e 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestHttpClient.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestHttpClient.java @@ -1,5 +1,7 @@ package org.a2aproject.sdk.server.apps.common; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Alternative; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -8,10 +10,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; - -import jakarta.enterprise.context.Dependent; -import jakarta.enterprise.inject.Alternative; - import org.a2aproject.sdk.client.http.A2AHttpClient; import org.a2aproject.sdk.client.http.A2AHttpResponse; import org.a2aproject.sdk.client.http.ServerSentEvent; @@ -42,6 +40,7 @@ public DeleteBuilder createDelete() { class TestPostBuilder implements A2AHttpClient.PostBuilder { private volatile String body; + @Override public PostBuilder body(String body) { this.body = body; @@ -78,7 +77,11 @@ public String body() { } @Override - public CompletableFuture postAsyncSSE(Consumer messageConsumer, Consumer errorConsumer, Runnable completeRunnable) throws IOException, InterruptedException { + public CompletableFuture postAsyncSSE( + Consumer messageConsumer, + Consumer errorConsumer, + Runnable completeRunnable) + throws IOException, InterruptedException { return null; } @@ -97,4 +100,4 @@ public PostBuilder addHeaders(Map headers) { return this; } } -} \ No newline at end of file +} diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestIdentityProvider.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestIdentityProvider.java index 2f964c445..3657d129f 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestIdentityProvider.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestIdentityProvider.java @@ -1,13 +1,12 @@ package org.a2aproject.sdk.server.apps.common; -import jakarta.enterprise.context.ApplicationScoped; - +import io.quarkus.arc.Unremovable; import io.quarkus.arc.properties.IfBuildProperty; import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.SecurityIdentityAugmentor; import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.smallrye.mutiny.Uni; -import io.quarkus.security.identity.SecurityIdentityAugmentor; -import io.quarkus.arc.Unremovable; +import jakarta.enterprise.context.ApplicationScoped; /** * Test identity provider that always returns an authenticated user. @@ -23,14 +22,18 @@ public class TestIdentityProvider implements SecurityIdentityAugmentor { @Override - public Uni augment(SecurityIdentity identity, io.quarkus.security.identity.AuthenticationRequestContext context) { + public Uni augment( + SecurityIdentity identity, + io.quarkus.security.identity.AuthenticationRequestContext context) { // If anonymous, inject a test user if (identity.isAnonymous()) { - return Uni.createFrom().item(QuarkusSecurityIdentity.builder() - .setPrincipal(() -> "testuser") - .addRole("user") - .setAnonymous(false) - .build()); + return Uni.createFrom() + .item( + QuarkusSecurityIdentity.builder() + .setPrincipal(() -> "testuser") + .addRole("user") + .setAnonymous(false) + .build()); } return Uni.createFrom().item(identity); } diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestTaskAuthorizationProvider.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestTaskAuthorizationProvider.java index 3febb92ed..32c3898a1 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestTaskAuthorizationProvider.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestTaskAuthorizationProvider.java @@ -1,18 +1,19 @@ package org.a2aproject.sdk.server.apps.common; -import java.util.concurrent.ConcurrentHashMap; - -import jakarta.enterprise.context.ApplicationScoped; - import io.quarkus.arc.Unremovable; import io.quarkus.arc.properties.IfBuildProperty; +import jakarta.enterprise.context.ApplicationScoped; +import java.util.concurrent.ConcurrentHashMap; import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.auth.TaskAuthorizationProvider; import org.a2aproject.sdk.server.auth.TaskOperation; @ApplicationScoped @Unremovable -@IfBuildProperty(name = "test.task-authorization.enabled", stringValue = "true", enableIfMissing = false) +@IfBuildProperty( + name = "test.task-authorization.enabled", + stringValue = "true", + enableIfMissing = false) public class TestTaskAuthorizationProvider implements TaskAuthorizationProvider { private final ConcurrentHashMap taskOwners = new ConcurrentHashMap<>(); @@ -20,14 +21,16 @@ public class TestTaskAuthorizationProvider implements TaskAuthorizationProvider @Override public boolean checkRead(ServerCallContext context, String taskId, TaskOperation operation) { String owner = taskOwners.get(taskId); - // Intentionally fail-open for testing; production implementations should fail-closed (deny unknown tasks) + // Intentionally fail-open for testing; production implementations should fail-closed (deny + // unknown tasks) return owner == null || owner.equals(context.getUser().getUsername()); } @Override public boolean checkWrite(ServerCallContext context, String taskId, TaskOperation operation) { String owner = taskOwners.get(taskId); - // Intentionally fail-open for testing; production implementations should fail-closed (deny unknown tasks) + // Intentionally fail-open for testing; production implementations should fail-closed (deny + // unknown tasks) return owner == null || owner.equals(context.getUser().getUsername()); } diff --git a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestUtilsBean.java b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestUtilsBean.java index a7360bbd3..010904a57 100644 --- a/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestUtilsBean.java +++ b/tests/server-common/src/test/java/org/a2aproject/sdk/server/apps/common/TestUtilsBean.java @@ -2,13 +2,12 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; - import org.a2aproject.sdk.server.events.QueueManager; import org.a2aproject.sdk.server.tasks.PushNotificationConfigStore; import org.a2aproject.sdk.server.tasks.TaskStore; import org.a2aproject.sdk.spec.Event; -import org.a2aproject.sdk.spec.TaskPushNotificationConfig; import org.a2aproject.sdk.spec.Task; +import org.a2aproject.sdk.spec.TaskPushNotificationConfig; /** * Contains utilities to interact with the server side for the tests. @@ -21,14 +20,11 @@ @ApplicationScoped public class TestUtilsBean { - @Inject - TaskStore taskStore; + @Inject TaskStore taskStore; - @Inject - QueueManager queueManager; + @Inject QueueManager queueManager; - @Inject - PushNotificationConfigStore pushNotificationConfigStore; + @Inject PushNotificationConfigStore pushNotificationConfigStore; public void saveTask(Task task) { taskStore.save(task, false); @@ -58,14 +54,13 @@ public void deleteTaskPushNotificationConfig(String taskId, String configId) { pushNotificationConfigStore.deleteInfo(taskId, configId); } - public void saveTaskPushNotificationConfig(String taskId, TaskPushNotificationConfig notificationConfig) { + public void saveTaskPushNotificationConfig( + String taskId, TaskPushNotificationConfig notificationConfig) { saveTaskPushNotificationConfig(taskId, notificationConfig, null); } public void saveTaskPushNotificationConfig( - String taskId, - TaskPushNotificationConfig notificationConfig, - String protocolVersion) { + String taskId, TaskPushNotificationConfig notificationConfig, String protocolVersion) { pushNotificationConfigStore.setInfo( TaskPushNotificationConfig.builder(notificationConfig).taskId(taskId).build(), protocolVersion); @@ -99,10 +94,12 @@ public void awaitQueuePollerStart(String taskId) throws InterruptedException { * @return true if the count stabilized at the expected value, false if timeout occurred * @throws InterruptedException if interrupted while waiting */ - public boolean awaitChildQueueCountStable(String taskId, int expectedCount, long timeoutMs) throws InterruptedException { + public boolean awaitChildQueueCountStable(String taskId, int expectedCount, long timeoutMs) + throws InterruptedException { long endTime = System.currentTimeMillis() + timeoutMs; int consecutiveMatches = 0; - final int requiredMatches = 3; // Count must match 3 times in a row (150ms) to be considered stable + final int requiredMatches = + 3; // Count must match 3 times in a row (150ms) to be considered stable while (System.currentTimeMillis() < endTime) { int count = queueManager.getActiveChildQueueCount(taskId); diff --git a/transport/grpc/pom.xml b/transport/grpc/pom.xml index 81a0db054..2ed2baaa8 100644 --- a/transport/grpc/pom.xml +++ b/transport/grpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -79,5 +77,5 @@ - + diff --git a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/context/GrpcContextKeys.java b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/context/GrpcContextKeys.java index 221c84f4a..7d2b315c1 100644 --- a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/context/GrpcContextKeys.java +++ b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/context/GrpcContextKeys.java @@ -1,14 +1,11 @@ package org.a2aproject.sdk.transport.grpc.context; - import static java.util.Locale.ROOT; -import java.util.Locale; +import io.grpc.Context; import java.util.Map; - import org.a2aproject.sdk.common.A2AHeaders; import org.a2aproject.sdk.spec.A2AMethods; -import io.grpc.Context; /** * Shared gRPC context keys for A2A protocol data. @@ -48,42 +45,38 @@ public final class GrpcContextKeys { * Set by server interceptors and accessed by service handlers. */ public static final Context.Key VERSION_HEADER_KEY = - Context.key(A2AHeaders.A2A_VERSION.toLowerCase(ROOT)); + Context.key(A2AHeaders.A2A_VERSION.toLowerCase(ROOT)); /** * Context key for storing the a2a-extensions header value. * Set by server interceptors and accessed by service handlers. */ public static final Context.Key EXTENSIONS_HEADER_KEY = - Context.key(A2AHeaders.A2A_EXTENSIONS.toLowerCase(ROOT)); + Context.key(A2AHeaders.A2A_EXTENSIONS.toLowerCase(ROOT)); /** * Context key for storing the complete gRPC Metadata object. * Provides access to all request headers and metadata. */ - public static final Context.Key METADATA_KEY = - Context.key("grpc-metadata"); + public static final Context.Key METADATA_KEY = Context.key("grpc-metadata"); /** * Context key for storing the method name being called. * Equivalent to Python's context.method() functionality. */ - public static final Context.Key GRPC_METHOD_NAME_KEY = - Context.key("grpc-method-name"); - + public static final Context.Key GRPC_METHOD_NAME_KEY = Context.key("grpc-method-name"); + /** * Context key for storing the method name being called. * Equivalent to Python's context.method() functionality. */ - public static final Context.Key METHOD_NAME_KEY = - Context.key("method"); + public static final Context.Key METHOD_NAME_KEY = Context.key("method"); /** * Context key for storing the peer information. * Provides access to client connection details. */ - public static final Context.Key PEER_INFO_KEY = - Context.key("grpc-peer-info"); + public static final Context.Key PEER_INFO_KEY = Context.key("grpc-peer-info"); /** * Mapping from gRPC method names to A2A protocol method names. @@ -108,17 +101,21 @@ public final class GrpcContextKeys { * * @see org.a2aproject.sdk.spec.A2AMethods */ - public static final Map METHOD_MAPPING = Map.of( - "SendMessage", A2AMethods.SEND_MESSAGE_METHOD, - "SendStreamingMessage", A2AMethods.SEND_STREAMING_MESSAGE_METHOD, - "GetTask", A2AMethods.GET_TASK_METHOD, - "ListTask", A2AMethods.LIST_TASK_METHOD, - "CancelTask", A2AMethods.CANCEL_TASK_METHOD, - "SubscribeToTask", A2AMethods.SUBSCRIBE_TO_TASK_METHOD, - "CreateTaskPushNotification", A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, - "GetTaskPushNotification", A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, - "ListTaskPushNotification", A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, - "DeleteTaskPushNotification", A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); + public static final Map METHOD_MAPPING = + Map.of( + "SendMessage", A2AMethods.SEND_MESSAGE_METHOD, + "SendStreamingMessage", A2AMethods.SEND_STREAMING_MESSAGE_METHOD, + "GetTask", A2AMethods.GET_TASK_METHOD, + "ListTask", A2AMethods.LIST_TASK_METHOD, + "CancelTask", A2AMethods.CANCEL_TASK_METHOD, + "SubscribeToTask", A2AMethods.SUBSCRIBE_TO_TASK_METHOD, + "CreateTaskPushNotification", + A2AMethods.SET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + "GetTaskPushNotification", A2AMethods.GET_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + "ListTaskPushNotification", + A2AMethods.LIST_TASK_PUSH_NOTIFICATION_CONFIG_METHOD, + "DeleteTaskPushNotification", + A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD); private GrpcContextKeys() { // Utility class diff --git a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/CallContextFactory.java b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/CallContextFactory.java index ed44ec7b9..2cb50c50f 100644 --- a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/CallContextFactory.java +++ b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/CallContextFactory.java @@ -1,7 +1,7 @@ package org.a2aproject.sdk.transport.grpc.handler; -import org.a2aproject.sdk.server.ServerCallContext; import io.grpc.stub.StreamObserver; +import org.a2aproject.sdk.server.ServerCallContext; /** * Factory interface for creating {@link ServerCallContext} from gRPC request context. @@ -71,4 +71,4 @@ public interface CallContextFactory { * @return a new ServerCallContext with extracted authentication, metadata, and headers */ ServerCallContext create(StreamObserver responseObserver); -} \ No newline at end of file +} diff --git a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java index ea70660b9..e30279fb0 100644 --- a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java +++ b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java @@ -1,13 +1,19 @@ package org.a2aproject.sdk.transport.grpc.handler; +import static org.a2aproject.sdk.grpc.utils.ProtoUtils.FromProto; +import static org.a2aproject.sdk.grpc.utils.ProtoUtils.ToProto; import static org.a2aproject.sdk.server.AgentCardOperation.EXTENDED_CARD; import static org.a2aproject.sdk.server.AgentCardOperation.PUSH_NOTIFICATIONS; import static org.a2aproject.sdk.server.AgentCardOperation.STREAMING; - -import static org.a2aproject.sdk.grpc.utils.ProtoUtils.FromProto; -import static org.a2aproject.sdk.grpc.utils.ProtoUtils.ToProto; import static org.a2aproject.sdk.server.ServerCallContext.TRANSPORT_KEY; +import com.google.protobuf.Empty; +import io.grpc.Context; +import io.grpc.Metadata; +import io.grpc.Status; +import io.grpc.protobuf.StatusProto; +import io.grpc.stub.StreamObserver; +import jakarta.enterprise.inject.Vetoed; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -20,10 +26,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; - -import jakarta.enterprise.inject.Vetoed; - -import com.google.protobuf.Empty; import org.a2aproject.sdk.common.A2AErrorMessages; import org.a2aproject.sdk.grpc.A2AServiceGrpc; import org.a2aproject.sdk.grpc.StreamResponse; @@ -72,11 +74,6 @@ import org.a2aproject.sdk.spec.VersionNotSupportedError; import org.a2aproject.sdk.spec.util.Utils; import org.a2aproject.sdk.transport.grpc.context.GrpcContextKeys; -import io.grpc.Context; -import io.grpc.Metadata; -import io.grpc.Status; -import io.grpc.protobuf.StatusProto; -import io.grpc.stub.StreamObserver; import org.jspecify.annotations.Nullable; /** @@ -169,7 +166,8 @@ public abstract class GrpcHandler extends A2AServiceGrpc.A2AServiceImplBase { private static volatile @Nullable Runnable streamingSubscribedRunnable; private final AgentCardTransportValidator transportValidator = - new CachingAgentCardTransportValidator(this::validateTransportConfigurationWithCorrectClassLoader); + new CachingAgentCardTransportValidator( + this::validateTransportConfigurationWithCorrectClassLoader); private volatile @Nullable AgentCardRequestPolicy requestPolicy; private @Nullable AgentCardResolver agentCardResolver; @@ -179,9 +177,7 @@ public abstract class GrpcHandler extends A2AServiceGrpc.A2AServiceImplBase { /** * Constructs a new GrpcHandler. */ - public GrpcHandler() { - - } + public GrpcHandler() {} /** * Handles a unary (blocking) message send request. @@ -209,15 +205,18 @@ public GrpcHandler() { * @param responseObserver the gRPC response stream observer */ @Override - public void sendMessage(org.a2aproject.sdk.grpc.SendMessageRequest request, - StreamObserver responseObserver) { + public void sendMessage( + org.a2aproject.sdk.grpc.SendMessageRequest request, + StreamObserver responseObserver) { try { String tenant = request.getTenant(); ServerCallContext context = createCallContext(responseObserver, tenant); - MessageSendParams params = FromProto.messageSendParams( - request.toBuilder().setTenant(canonicalTenant(context)).build()); + MessageSendParams params = + FromProto.messageSendParams( + request.toBuilder().setTenant(canonicalTenant(context)).build()); EventKind taskOrMessage = getRequestHandler().onMessageSend(params, context); - org.a2aproject.sdk.grpc.SendMessageResponse response = ToProto.taskOrMessage(taskOrMessage); + org.a2aproject.sdk.grpc.SendMessageResponse response = + ToProto.taskOrMessage(taskOrMessage); responseObserver.onNext(response); responseObserver.onCompleted(); } catch (TenantNotFoundException e) { @@ -232,13 +231,15 @@ public void sendMessage(org.a2aproject.sdk.grpc.SendMessageRequest request, } @Override - public void getTask(org.a2aproject.sdk.grpc.GetTaskRequest request, - StreamObserver responseObserver) { + public void getTask( + org.a2aproject.sdk.grpc.GetTaskRequest request, + StreamObserver responseObserver) { try { String tenant = request.getTenant(); ServerCallContext context = createCallContext(responseObserver, tenant); - TaskQueryParams params = FromProto.taskQueryParams( - request.toBuilder().setTenant(canonicalTenant(context)).build()); + TaskQueryParams params = + FromProto.taskQueryParams( + request.toBuilder().setTenant(canonicalTenant(context)).build()); Task task = getRequestHandler().onGetTask(params, context); if (task != null) { responseObserver.onNext(ToProto.task(task)); @@ -258,13 +259,15 @@ public void getTask(org.a2aproject.sdk.grpc.GetTaskRequest request, } @Override - public void listTasks(org.a2aproject.sdk.grpc.ListTasksRequest request, - StreamObserver responseObserver) { + public void listTasks( + org.a2aproject.sdk.grpc.ListTasksRequest request, + StreamObserver responseObserver) { try { String tenant = request.getTenant(); ServerCallContext context = createCallContext(responseObserver, tenant); - org.a2aproject.sdk.spec.ListTasksParams params = FromProto.listTasksParams( - request.toBuilder().setTenant(canonicalTenant(context)).build()); + org.a2aproject.sdk.spec.ListTasksParams params = + FromProto.listTasksParams( + request.toBuilder().setTenant(canonicalTenant(context)).build()); ListTasksResult result = getRequestHandler().onListTasks(params, context); responseObserver.onNext(ToProto.listTasksResult(result)); responseObserver.onCompleted(); @@ -280,13 +283,15 @@ public void listTasks(org.a2aproject.sdk.grpc.ListTasksRequest request, } @Override - public void cancelTask(org.a2aproject.sdk.grpc.CancelTaskRequest request, - StreamObserver responseObserver) { + public void cancelTask( + org.a2aproject.sdk.grpc.CancelTaskRequest request, + StreamObserver responseObserver) { try { String tenant = request.getTenant(); ServerCallContext context = createCallContext(responseObserver, tenant); - CancelTaskParams params = FromProto.cancelTaskParams( - request.toBuilder().setTenant(canonicalTenant(context)).build()); + CancelTaskParams params = + FromProto.cancelTaskParams( + request.toBuilder().setTenant(canonicalTenant(context)).build()); Task task = getRequestHandler().onCancelTask(params, context); if (task != null) { responseObserver.onNext(ToProto.task(task)); @@ -306,14 +311,18 @@ public void cancelTask(org.a2aproject.sdk.grpc.CancelTaskRequest request, } @Override - public void createTaskPushNotificationConfig(org.a2aproject.sdk.grpc.TaskPushNotificationConfig request, - StreamObserver responseObserver) { + public void createTaskPushNotificationConfig( + org.a2aproject.sdk.grpc.TaskPushNotificationConfig request, + StreamObserver responseObserver) { try { String tenant = request.getTenant(); - ServerCallContext context = createCallContext(responseObserver, tenant, PUSH_NOTIFICATIONS); - TaskPushNotificationConfig config = FromProto.createTaskPushNotificationConfig( - request.toBuilder().setTenant(canonicalTenant(context)).build()); - TaskPushNotificationConfig responseConfig = getRequestHandler().onCreateTaskPushNotificationConfig(config, context); + ServerCallContext context = + createCallContext(responseObserver, tenant, PUSH_NOTIFICATIONS); + TaskPushNotificationConfig config = + FromProto.createTaskPushNotificationConfig( + request.toBuilder().setTenant(canonicalTenant(context)).build()); + TaskPushNotificationConfig responseConfig = + getRequestHandler().onCreateTaskPushNotificationConfig(config, context); responseObserver.onNext(ToProto.taskPushNotificationConfig(responseConfig)); responseObserver.onCompleted(); } catch (TenantNotFoundException e) { @@ -328,14 +337,18 @@ public void createTaskPushNotificationConfig(org.a2aproject.sdk.grpc.TaskPushNot } @Override - public void getTaskPushNotificationConfig(org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest request, - StreamObserver responseObserver) { + public void getTaskPushNotificationConfig( + org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest request, + StreamObserver responseObserver) { try { String tenant = request.getTenant(); - ServerCallContext context = createCallContext(responseObserver, tenant, PUSH_NOTIFICATIONS); - GetTaskPushNotificationConfigParams params = FromProto.getTaskPushNotificationConfigParams( - request.toBuilder().setTenant(canonicalTenant(context)).build()); - TaskPushNotificationConfig config = getRequestHandler().onGetTaskPushNotificationConfig(params, context); + ServerCallContext context = + createCallContext(responseObserver, tenant, PUSH_NOTIFICATIONS); + GetTaskPushNotificationConfigParams params = + FromProto.getTaskPushNotificationConfigParams( + request.toBuilder().setTenant(canonicalTenant(context)).build()); + TaskPushNotificationConfig config = + getRequestHandler().onGetTaskPushNotificationConfig(params, context); responseObserver.onNext(ToProto.taskPushNotificationConfig(config)); responseObserver.onCompleted(); } catch (TenantNotFoundException e) { @@ -350,15 +363,21 @@ public void getTaskPushNotificationConfig(org.a2aproject.sdk.grpc.GetTaskPushNot } @Override - public void listTaskPushNotificationConfigs(org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest request, - StreamObserver responseObserver) { + public void listTaskPushNotificationConfigs( + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest request, + StreamObserver + responseObserver) { try { String tenant = request.getTenant(); - ServerCallContext context = createCallContext(responseObserver, tenant, PUSH_NOTIFICATIONS); - ListTaskPushNotificationConfigsParams params = FromProto.listTaskPushNotificationConfigsParams( - request.toBuilder().setTenant(canonicalTenant(context)).build()); - ListTaskPushNotificationConfigsResult result = getRequestHandler().onListTaskPushNotificationConfigs(params, context); - org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse response = ToProto.listTaskPushNotificationConfigsResponse(result); + ServerCallContext context = + createCallContext(responseObserver, tenant, PUSH_NOTIFICATIONS); + ListTaskPushNotificationConfigsParams params = + FromProto.listTaskPushNotificationConfigsParams( + request.toBuilder().setTenant(canonicalTenant(context)).build()); + ListTaskPushNotificationConfigsResult result = + getRequestHandler().onListTaskPushNotificationConfigs(params, context); + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse response = + ToProto.listTaskPushNotificationConfigsResponse(result); responseObserver.onNext(response); responseObserver.onCompleted(); } catch (TenantNotFoundException e) { @@ -410,15 +429,18 @@ public void listTaskPushNotificationConfigs(org.a2aproject.sdk.grpc.ListTaskPush * @param responseObserver the gRPC response stream observer */ @Override - public void sendStreamingMessage(org.a2aproject.sdk.grpc.SendMessageRequest request, - StreamObserver responseObserver) { + public void sendStreamingMessage( + org.a2aproject.sdk.grpc.SendMessageRequest request, + StreamObserver responseObserver) { try { String tenant = request.getTenant(); ServerCallContext context = createCallContext(responseObserver, tenant, STREAMING); installForkedContextWrapper(context); - MessageSendParams params = FromProto.messageSendParams( - request.toBuilder().setTenant(canonicalTenant(context)).build()); - Flow.Publisher publisher = getRequestHandler().onMessageSendStream(params, context); + MessageSendParams params = + FromProto.messageSendParams( + request.toBuilder().setTenant(canonicalTenant(context)).build()); + Flow.Publisher publisher = + getRequestHandler().onMessageSendStream(params, context); convertToStreamResponse(publisher, responseObserver, context); } catch (TenantNotFoundException e) { handleTenantNotFound(responseObserver, e); @@ -432,15 +454,18 @@ public void sendStreamingMessage(org.a2aproject.sdk.grpc.SendMessageRequest requ } @Override - public void subscribeToTask(org.a2aproject.sdk.grpc.SubscribeToTaskRequest request, - StreamObserver responseObserver) { + public void subscribeToTask( + org.a2aproject.sdk.grpc.SubscribeToTaskRequest request, + StreamObserver responseObserver) { try { String tenant = request.getTenant(); ServerCallContext context = createCallContext(responseObserver, tenant, STREAMING); installForkedContextWrapper(context); - TaskIdParams params = FromProto.taskIdParams( - request.toBuilder().setTenant(canonicalTenant(context)).build()); - Flow.Publisher publisher = getRequestHandler().onSubscribeToTask(params, context); + TaskIdParams params = + FromProto.taskIdParams( + request.toBuilder().setTenant(canonicalTenant(context)).build()); + Flow.Publisher publisher = + getRequestHandler().onSubscribeToTask(params, context); convertToStreamResponse(publisher, responseObserver, context); } catch (TenantNotFoundException e) { handleTenantNotFound(responseObserver, e); @@ -490,114 +515,144 @@ public void subscribeToTask(org.a2aproject.sdk.grpc.SubscribeToTaskRequest reque * @param responseObserver the gRPC response stream observer * @param context the server call context for disconnect detection */ - private void convertToStreamResponse(Flow.Publisher publisher, - StreamObserver responseObserver, - ServerCallContext context) { - CompletableFuture.runAsync(() -> { - publisher.subscribe(new Flow.Subscriber() { - private Flow.@Nullable Subscription subscription; - private final AtomicBoolean completed = new AtomicBoolean(false); - - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - if (this.subscription != null) { - this.subscription.request(1); - } - - // Detect gRPC client disconnect and call EventConsumer.cancel() directly - // This stops the polling loop without relying on subscription cancellation propagation - Context grpcContext = Context.current(); - grpcContext.addListener(new Context.CancellationListener() { - @Override - public void cancelled(Context ctx) { - LOGGER.fine(() -> "gRPC call cancelled by client, calling EventConsumer.cancel() to stop polling loop"); - context.invokeEventConsumerCancelCallback(); - subscription.cancel(); - } - }, getExecutor()); - - // Notify tests that we are subscribed - Runnable runnable = streamingSubscribedRunnable; - if (runnable != null) { - runnable.run(); - } - } - - @Override - public void onNext(StreamingEventKind event) { - StreamResponse response = ToProto.streamResponse(event); - responseObserver.onNext(response); - if (response.hasStatusUpdate()) { - org.a2aproject.sdk.grpc.TaskState state = response.getStatusUpdate().getStatus().getState(); - boolean isFinal = state == org.a2aproject.sdk.grpc.TaskState.TASK_STATE_CANCELED - || state == org.a2aproject.sdk.grpc.TaskState.TASK_STATE_COMPLETED - || state == org.a2aproject.sdk.grpc.TaskState.TASK_STATE_FAILED - || state == org.a2aproject.sdk.grpc.TaskState.TASK_STATE_REJECTED; - if (isFinal) { - // Cancel subscription to prevent onComplete() from being called after we close the stream - if (this.subscription != null) { - subscription.cancel(); - } - completeStream(); - } else { - if (this.subscription != null) { - subscription.request(1); - } - } - } else { - if (this.subscription != null) { - this.subscription.request(1); - } - } - } - - @Override - public void onError(Throwable throwable) { - // Cancel upstream to stop EventConsumer when error occurs - if (this.subscription != null) { - subscription.cancel(); - } - if (throwable instanceof A2AError jsonrpcError) { - handleError(responseObserver, jsonrpcError); - } else { - handleInternalError(responseObserver, throwable); - } - completeStream(); - } - - @Override - public void onComplete() { - completeStream(); - } - - private void completeStream() { - // Atomically check and set - only the first caller will proceed - if (completed.compareAndSet(false, true)) { - responseObserver.onCompleted(); - } - } - }); - }, getExecutor()); + private void convertToStreamResponse( + Flow.Publisher publisher, + StreamObserver responseObserver, + ServerCallContext context) { + CompletableFuture.runAsync( + () -> { + publisher.subscribe( + new Flow.Subscriber() { + private Flow.@Nullable Subscription subscription; + private final AtomicBoolean completed = new AtomicBoolean(false); + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + if (this.subscription != null) { + this.subscription.request(1); + } + + // Detect gRPC client disconnect and call EventConsumer.cancel() + // directly + // This stops the polling loop without relying on subscription + // cancellation propagation + Context grpcContext = Context.current(); + grpcContext.addListener( + new Context.CancellationListener() { + @Override + public void cancelled(Context ctx) { + LOGGER.fine( + () -> + "gRPC call cancelled by client," + + " calling" + + " EventConsumer.cancel()" + + " to stop polling loop"); + context.invokeEventConsumerCancelCallback(); + subscription.cancel(); + } + }, + getExecutor()); + + // Notify tests that we are subscribed + Runnable runnable = streamingSubscribedRunnable; + if (runnable != null) { + runnable.run(); + } + } + + @Override + public void onNext(StreamingEventKind event) { + StreamResponse response = ToProto.streamResponse(event); + responseObserver.onNext(response); + if (response.hasStatusUpdate()) { + org.a2aproject.sdk.grpc.TaskState state = + response.getStatusUpdate().getStatus().getState(); + boolean isFinal = + state + == org.a2aproject.sdk.grpc.TaskState + .TASK_STATE_CANCELED + || state + == org.a2aproject.sdk.grpc.TaskState + .TASK_STATE_COMPLETED + || state + == org.a2aproject.sdk.grpc.TaskState + .TASK_STATE_FAILED + || state + == org.a2aproject.sdk.grpc.TaskState + .TASK_STATE_REJECTED; + if (isFinal) { + // Cancel subscription to prevent onComplete() from + // being called after we close the stream + if (this.subscription != null) { + subscription.cancel(); + } + completeStream(); + } else { + if (this.subscription != null) { + subscription.request(1); + } + } + } else { + if (this.subscription != null) { + this.subscription.request(1); + } + } + } + + @Override + public void onError(Throwable throwable) { + // Cancel upstream to stop EventConsumer when error occurs + if (this.subscription != null) { + subscription.cancel(); + } + if (throwable instanceof A2AError jsonrpcError) { + handleError(responseObserver, jsonrpcError); + } else { + handleInternalError(responseObserver, throwable); + } + completeStream(); + } + + @Override + public void onComplete() { + completeStream(); + } + + private void completeStream() { + // Atomically check and set - only the first caller will proceed + if (completed.compareAndSet(false, true)) { + responseObserver.onCompleted(); + } + } + }); + }, + getExecutor()); } @Override - public void getExtendedAgentCard(org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest request, - StreamObserver responseObserver) { + public void getExtendedAgentCard( + org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest request, + StreamObserver responseObserver) { try { String tenant = request.getTenant(); Utils.validateTenant(tenant); AgentCardResolver resolver = getAgentCardResolver(); AgentCardRequestPolicy policy = getRequestPolicy(); - AgentCard card = policy.resolveAndValidateCapability(tenant, transportValidator, EXTENDED_CARD); + AgentCard card = + policy.resolveAndValidateCapability(tenant, transportValidator, EXTENDED_CARD); ServerCallContext context = createRawCallContext(responseObserver); - String normalizedTenant = policy.validateContextAndAttach(card, tenant, context).tenant(); + String normalizedTenant = + policy.validateContextAndAttach(card, tenant, context).tenant(); AgentCard extendedAgentCard = resolver.resolveExtendedDiscoveryCard(normalizedTenant); if (extendedAgentCard != null) { responseObserver.onNext(ToProto.agentCard(extendedAgentCard)); responseObserver.onCompleted(); } else { - handleError(responseObserver, new ExtendedAgentCardNotConfiguredError(null, "Extended agent card not configured", null)); + handleError( + responseObserver, + new ExtendedAgentCardNotConfiguredError( + null, "Extended agent card not configured", null)); } } catch (TenantNotFoundException e) { handleTenantNotFound(responseObserver, e); @@ -609,13 +664,16 @@ public void getExtendedAgentCard(org.a2aproject.sdk.grpc.GetExtendedAgentCardReq } @Override - public void deleteTaskPushNotificationConfig(org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest request, - StreamObserver responseObserver) { + public void deleteTaskPushNotificationConfig( + org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest request, + StreamObserver responseObserver) { try { String tenant = request.getTenant(); - ServerCallContext context = createCallContext(responseObserver, tenant, PUSH_NOTIFICATIONS); - DeleteTaskPushNotificationConfigParams params = FromProto.deleteTaskPushNotificationConfigParams( - request.toBuilder().setTenant(canonicalTenant(context)).build()); + ServerCallContext context = + createCallContext(responseObserver, tenant, PUSH_NOTIFICATIONS); + DeleteTaskPushNotificationConfigParams params = + FromProto.deleteTaskPushNotificationConfigParams( + request.toBuilder().setTenant(canonicalTenant(context)).build()); getRequestHandler().onDeleteTaskPushNotificationConfig(params, context); responseObserver.onNext(Empty.getDefaultInstance()); responseObserver.onCompleted(); @@ -671,31 +729,38 @@ private ServerCallContext createRawCallContext(StreamObserver responseObs ServerCallContext context; if (factory == null) { // Default implementation when no custom CallContextFactory is provided - // This handles both CDI injection scenarios and test scenarios where callContextFactory is null + // This handles both CDI injection scenarios and test scenarios where callContextFactory + // is null User user = UnauthenticatedUser.INSTANCE; Map state = new HashMap<>(); state.put(TRANSPORT_KEY, TransportProtocol.GRPC); // Enhanced gRPC context access - equivalent to Python's grpc.aio.ServicerContext - // The A2AExtensionsInterceptor captures ServerCall + Metadata and stores them in gRPC Context + // The A2AExtensionsInterceptor captures ServerCall + Metadata and stores them in gRPC + // Context // This provides proper equivalence to Python's ServicerContext for metadata access // Note: StreamObserver is still stored for response handling state.put("grpc_response_observer", responseObserver); - + // Add rich gRPC context information if available (set by interceptor) // This provides equivalent functionality to Python's grpc.aio.ServicerContext try { Context currentContext = Context.current(); if (currentContext != null) { state.put("grpc_context", currentContext); - + // Add specific context information for easy access - io.grpc.Metadata grpcMetadata = GrpcContextKeys.METADATA_KEY.get(currentContext); + io.grpc.Metadata grpcMetadata = + GrpcContextKeys.METADATA_KEY.get(currentContext); if (grpcMetadata != null) { state.put("grpc_metadata", grpcMetadata); Map headers = new HashMap<>(); for (String key : grpcMetadata.keys()) { - headers.put(key, grpcMetadata.get(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER))); + headers.put( + key, + grpcMetadata.get( + Metadata.Key.of( + key, Metadata.ASCII_STRING_MARSHALLER))); } state.put("headers", headers); } @@ -703,7 +768,7 @@ private ServerCallContext createRawCallContext(StreamObserver responseObs if (methodName != null) { state.put("grpc_method_name", methodName); } - + String peerInfo = GrpcContextKeys.PEER_INFO_KEY.get(currentContext); if (peerInfo != null) { state.put("grpc_peer_info", peerInfo); @@ -713,7 +778,7 @@ private ServerCallContext createRawCallContext(StreamObserver responseObs // Context not available - continue without it LOGGER.fine(() -> "Error getting data from current context" + e); } - + // Extract requested protocol version from gRPC context (set by interceptor) // Default to current version since gRPC only handles 1.0 protocol String requestedVersion = getVersionFromContext(); @@ -722,27 +787,33 @@ private ServerCallContext createRawCallContext(StreamObserver responseObs Set requestedExtensions = new HashSet<>(); String extensionsHeader = getExtensionsFromContext(); if (extensionsHeader != null) { - requestedExtensions = A2AExtensions.getRequestedExtensions(List.of(extensionsHeader)); + requestedExtensions = + A2AExtensions.getRequestedExtensions(List.of(extensionsHeader)); } context = new ServerCallContext(user, state, requestedExtensions, requestedVersion); } else { - // TODO: CallContextFactory interface expects ServerCall + Metadata, but we only have StreamObserver + // TODO: CallContextFactory interface expects ServerCall + Metadata, but we only have + // StreamObserver // This is another manifestation of the architectural limitation mentioned above - context = factory.create(responseObserver); // Fall back to basic create() method for now + context = + factory.create(responseObserver); // Fall back to basic create() method for now } return context; } - private ServerCallContext createCallContext(StreamObserver responseObserver, @Nullable String tenant) { + private ServerCallContext createCallContext( + StreamObserver responseObserver, @Nullable String tenant) { ServerCallContext context = createRawCallContext(responseObserver); - getRequestPolicy().preflight( - tenant, context, transportValidator, AgentCardOperation.none()); + getRequestPolicy() + .preflight(tenant, context, transportValidator, AgentCardOperation.none()); return context; } - private ServerCallContext createCallContext(StreamObserver responseObserver, @Nullable String tenant, + private ServerCallContext createCallContext( + StreamObserver responseObserver, + @Nullable String tenant, AgentCardOperation operation) { AgentCardRequestPolicy policy = getRequestPolicy(); AgentCard card = policy.resolveAndValidateCapability(tenant, transportValidator, operation); @@ -763,15 +834,19 @@ private static String canonicalTenant(ServerCallContext context) { */ private void installForkedContextWrapper(ServerCallContext context) { Context forked = Context.current().fork(); - context.getState().put(ServerCallContext.EXECUTION_WRAPPER_KEY, - (java.util.function.UnaryOperator) (runnable -> () -> { - Context prev = forked.attach(); - try { - runnable.run(); - } finally { - forked.detach(prev); - } - })); + context.getState() + .put( + ServerCallContext.EXECUTION_WRAPPER_KEY, + (java.util.function.UnaryOperator) + (runnable -> + () -> { + Context prev = forked.attach(); + try { + runnable.run(); + } finally { + forked.detach(prev); + } + })); } /** @@ -810,19 +885,27 @@ private void handleError(StreamObserver responseObserver, A2AError error) String reason = errorCode != null ? errorCode.name() : "UNKNOWN"; int grpcCode = Status.Code.valueOf(grpcStatusName).value(); - com.google.rpc.ErrorInfo.Builder errorInfoBuilder = com.google.rpc.ErrorInfo.newBuilder() - .setReason(reason) - .setDomain("a2a-protocol.org"); + com.google.rpc.ErrorInfo.Builder errorInfoBuilder = + com.google.rpc.ErrorInfo.newBuilder() + .setReason(reason) + .setDomain("a2a-protocol.org"); if (!error.getDetails().isEmpty()) { - error.getDetails().forEach((k, v) -> - errorInfoBuilder.putMetadata(k, v instanceof String s ? s : JsonUtil.OBJECT_MAPPER.toJson(v))); + error.getDetails() + .forEach( + (k, v) -> + errorInfoBuilder.putMetadata( + k, + v instanceof String s + ? s + : JsonUtil.OBJECT_MAPPER.toJson(v))); } - com.google.rpc.Status rpcStatus = com.google.rpc.Status.newBuilder() - .setCode(grpcCode) - .setMessage(error.getMessage() != null ? error.getMessage() : "") - .addDetails(com.google.protobuf.Any.pack(errorInfoBuilder.build())) - .build(); + com.google.rpc.Status rpcStatus = + com.google.rpc.Status.newBuilder() + .setCode(grpcCode) + .setMessage(error.getMessage() != null ? error.getMessage() : "") + .addDetails(com.google.protobuf.Any.pack(errorInfoBuilder.build())) + .build(); responseObserver.onError(StatusProto.toStatusRuntimeException(rpcStatus)); } @@ -844,27 +927,30 @@ private void handleError(StreamObserver responseObserver, A2AError error) * @param responseObserver the gRPC response stream observer * @param e the security exception */ - private void handleSecurityException(StreamObserver responseObserver, SecurityException e) { + private void handleSecurityException( + StreamObserver responseObserver, SecurityException e) { Status status; String description; String exceptionClassName = e.getClass().getName(); // Attempt to detect common authentication and authorization related exceptions - if (exceptionClassName.contains("Unauthorized") || - exceptionClassName.contains("Unauthenticated") || - exceptionClassName.contains("Authentication")) { + if (exceptionClassName.contains("Unauthorized") + || exceptionClassName.contains("Unauthenticated") + || exceptionClassName.contains("Authentication")) { status = Status.UNAUTHENTICATED; description = A2AErrorMessages.AUTHENTICATION_FAILED; - } else if (exceptionClassName.contains("Forbidden") || - exceptionClassName.contains("AccessDenied") || - exceptionClassName.contains("Authorization")) { + } else if (exceptionClassName.contains("Forbidden") + || exceptionClassName.contains("AccessDenied") + || exceptionClassName.contains("Authorization")) { status = Status.PERMISSION_DENIED; description = A2AErrorMessages.AUTHORIZATION_FAILED; } else { // If the security exception type cannot be detected, default to PERMISSION_DENIED status = Status.PERMISSION_DENIED; - description = "Authorization failed: " + (e.getMessage() != null ? e.getMessage() : "Access denied"); + description = + "Authorization failed: " + + (e.getMessage() != null ? e.getMessage() : "Access denied"); } responseObserver.onError(status.withDescription(description).asRuntimeException()); @@ -878,10 +964,10 @@ private void handleInternalError(StreamObserver responseObserver, Throwab handleError(responseObserver, new InternalError("Internal error")); } - /** Creates a resolver using this handler's overridable card sources and router. */ protected AgentCardResolver createAgentCardResolver() { - return AgentCardResolver.of(this::getAgentCard, this::getExtendedAgentCard, getAgentCardRouter()); + return AgentCardResolver.of( + this::getAgentCard, this::getExtendedAgentCard, getAgentCardRouter()); } private AgentCardRequestPolicy getRequestPolicy() { @@ -904,8 +990,10 @@ private AgentCardResolver getAgentCardResolver() { return Objects.requireNonNull(agentCardResolver); } - private void handleTenantNotFound(StreamObserver responseObserver, TenantNotFoundException e) { - responseObserver.onError(Status.NOT_FOUND.withDescription(e.getResponseMessage()).asRuntimeException()); + private void handleTenantNotFound( + StreamObserver responseObserver, TenantNotFoundException e) { + responseObserver.onError( + Status.NOT_FOUND.withDescription(e.getResponseMessage()).asRuntimeException()); } private void validateTransportConfigurationWithCorrectClassLoader(AgentCard agentCard) { @@ -1032,10 +1120,10 @@ public static void setStreamingSubscribedRunnable(Runnable runnable) { * Utility methods for accessing gRPC context information. * These provide equivalent functionality to Python's grpc.aio.ServicerContext methods. */ - + /** * Generic helper method to safely access gRPC context values. - * + * * @param key the context key to retrieve * @return the context value, or null if not available */ @@ -1047,31 +1135,31 @@ public static void setStreamingSubscribedRunnable(Runnable runnable) { return null; } } - + /** * Gets the complete gRPC metadata from the current context. * Equivalent to Python's context.invocation_metadata. - * + * * @return the gRPC Metadata object, or null if not available */ protected static io.grpc.@Nullable Metadata getCurrentMetadata() { return getFromContext(GrpcContextKeys.METADATA_KEY); } - + /** * Gets the current gRPC method name. * Equivalent to Python's context.method(). - * + * * @return the method name, or null if not available */ protected static @Nullable String getCurrentMethodName() { return getFromContext(GrpcContextKeys.GRPC_METHOD_NAME_KEY); } - + /** * Gets the peer information for the current gRPC call. * Equivalent to Python's context.peer(). - * + * * @return the peer information, or null if not available */ protected static @Nullable String getCurrentPeerInfo() { diff --git a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/package-info.java b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/package-info.java index d2561dab4..da7ef7ac2 100644 --- a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/package-info.java +++ b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/package-info.java @@ -51,4 +51,3 @@ package org.a2aproject.sdk.transport.grpc.handler; import org.jspecify.annotations.NullMarked; - diff --git a/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java b/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java index 510a65c1a..db4953aae 100644 --- a/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java +++ b/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java @@ -2,6 +2,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import com.google.protobuf.Empty; +import com.google.protobuf.Struct; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.internal.testing.StreamRecorder; +import io.grpc.stub.StreamObserver; import java.io.IOException; import java.net.URL; import java.util.ArrayList; @@ -19,13 +25,6 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; - -import com.google.protobuf.Empty; -import com.google.protobuf.Struct; -import io.grpc.Status; -import io.grpc.StatusRuntimeException; -import io.grpc.internal.testing.StreamRecorder; -import io.grpc.stub.StreamObserver; import mutiny.zero.ZeroPublisher; import org.a2aproject.sdk.grpc.AuthenticationInfo; import org.a2aproject.sdk.grpc.CancelTaskRequest; @@ -76,25 +75,35 @@ @Timeout(value = 1, unit = TimeUnit.MINUTES) public class GrpcHandlerTest extends AbstractA2ARequestHandlerTest { - private static final Message GRPC_MESSAGE = Message.newBuilder() - .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .setContextId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) - .setMessageId(AbstractA2ARequestHandlerTest.MESSAGE.messageId()) - .setRole(Role.ROLE_AGENT) - .addParts(Part.newBuilder() - .setText(((TextPart) AbstractA2ARequestHandlerTest.MESSAGE.parts().get(0)).text()) + private static final Message GRPC_MESSAGE = + Message.newBuilder() + .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .setContextId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) + .setMessageId(AbstractA2ARequestHandlerTest.MESSAGE.messageId()) + .setRole(Role.ROLE_AGENT) + .addParts( + Part.newBuilder() + .setText( + ((TextPart) + AbstractA2ARequestHandlerTest.MESSAGE + .parts() + .get(0)) + .text()) + .setMetadata(Struct.newBuilder().build()) + .build()) .setMetadata(Struct.newBuilder().build()) - .build()) - .setMetadata(Struct.newBuilder().build()) - .build(); + .build(); @Test public void testOnGetTaskSuccess() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - GetTaskRequest request = GetTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + GetTaskRequest request = + GetTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.getTask(request, streamRecorder); @@ -113,17 +122,21 @@ public void testOnGetTaskSuccess() throws Exception { @Test public void resolverPolicyIsInitializedOnceAcrossRequests() throws Exception { AtomicInteger resolverCreations = new AtomicInteger(); - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor) { - @Override - protected org.a2aproject.sdk.server.AgentCardResolver createAgentCardResolver() { - resolverCreations.incrementAndGet(); - return super.createAgentCardResolver(); - } - }; + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor) { + @Override + protected org.a2aproject.sdk.server.AgentCardResolver + createAgentCardResolver() { + resolverCreations.incrementAndGet(); + return super.createAgentCardResolver(); + } + }; taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - GetTaskRequest request = GetTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + GetTaskRequest request = + GetTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder first = StreamRecorder.create(); StreamRecorder second = StreamRecorder.create(); @@ -137,10 +150,13 @@ protected org.a2aproject.sdk.server.AgentCardResolver createAgentCardResolver() @Test public void testOnGetTaskNotFound() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); - GetTaskRequest request = GetTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GetTaskRequest request = + GetTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.getTask(request, streamRecorder); @@ -151,20 +167,25 @@ public void testOnGetTaskNotFound() throws Exception { @Test public void testOnCancelTaskSuccess() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorCancel = (context, agentEmitter) -> { - // We need to cancel the task or the EventConsumer never finds a 'final' event. - // Looking at the Python implementation, they typically use AgentExecutors that - // don't support cancellation. So my theory is the Agent updates the task to the CANCEL status - org.a2aproject.sdk.spec.Task task = context.getTask(); - agentEmitter.cancel(); - }; + agentExecutorCancel = + (context, agentEmitter) -> { + // We need to cancel the task or the EventConsumer never finds a 'final' event. + // Looking at the Python implementation, they typically use AgentExecutors that + // don't support cancellation. So my theory is the Agent updates the task to the + // CANCEL status + org.a2aproject.sdk.spec.Task task = context.getTask(); + agentEmitter.cancel(); + }; - CancelTaskRequest request = CancelTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + CancelTaskRequest request = + CancelTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.cancelTask(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -181,16 +202,20 @@ public void testOnCancelTaskSuccess() throws Exception { @Test public void testOnCancelTaskNotSupported() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorCancel = (context, agentEmitter) -> { - throw new UnsupportedOperationError(); - }; + agentExecutorCancel = + (context, agentEmitter) -> { + throw new UnsupportedOperationError(); + }; - CancelTaskRequest request = CancelTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + CancelTaskRequest request = + CancelTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.cancelTask(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -200,10 +225,13 @@ public void testOnCancelTaskNotSupported() throws Exception { @Test public void testOnCancelTaskNotFound() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); - CancelTaskRequest request = CancelTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + CancelTaskRequest request = + CancelTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.cancelTask(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -213,11 +241,14 @@ public void testOnCancelTaskNotFound() throws Exception { @Test public void testOnMessageNewMessageSuccess() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; StreamRecorder streamRecorder = sendMessageRequest(handler); Assertions.assertNull(streamRecorder.getError()); @@ -230,11 +261,14 @@ public void testOnMessageNewMessageSuccess() throws Exception { @Test public void testOnMessageNewMessageWithExistingTaskSuccess() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; StreamRecorder streamRecorder = sendMessageRequest(handler); Assertions.assertNull(streamRecorder.getError()); List result = streamRecorder.getValues(); @@ -246,20 +280,26 @@ public void testOnMessageNewMessageWithExistingTaskSuccess() throws Exception { @Test public void testOnMessageError() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.fail(new UnsupportedOperationError()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.fail(new UnsupportedOperationError()); + }; StreamRecorder streamRecorder = sendMessageRequest(handler); assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED); } @Test public void testSetPushNotificationConfigSuccess() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); - StreamRecorder streamRecorder = createTaskPushNotificationConfigRequest(handler, - AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), "config456"); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + StreamRecorder streamRecorder = + createTaskPushNotificationConfigRequest( + handler, AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), "config456"); Assertions.assertNull(streamRecorder.getError()); List result = streamRecorder.getValues(); @@ -275,18 +315,25 @@ public void testSetPushNotificationConfigSuccess() throws Exception { @Test public void testGetPushNotificationConfigSuccess() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; // first set the task push notification config - StreamRecorder streamRecorder = createTaskPushNotificationConfigRequest(handler, - AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), "config456"); + StreamRecorder streamRecorder = + createTaskPushNotificationConfigRequest( + handler, AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), "config456"); Assertions.assertNull(streamRecorder.getError()); // then get the task push notification config - streamRecorder = getTaskPushNotificationConfigRequest(handler, AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), "config456"); + streamRecorder = + getTaskPushNotificationConfigRequest( + handler, AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), "config456"); Assertions.assertNull(streamRecorder.getError()); List result = streamRecorder.getValues(); Assertions.assertNotNull(result); @@ -303,50 +350,70 @@ public void testGetPushNotificationConfigSuccess() throws Exception { public void testPushNotificationsNotSupportedError() throws Exception { AgentCard card = AbstractA2ARequestHandlerTest.createAgentCard(true, false); GrpcHandler handler = new TestGrpcHandler(card, requestHandler, internalExecutor); - StreamRecorder streamRecorder = createTaskPushNotificationConfigRequest(handler, - AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); + StreamRecorder streamRecorder = + createTaskPushNotificationConfigRequest( + handler, + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED); } @Test public void testOnGetPushNotificationNoPushNotifierConfig() throws Exception { // Create request handler without a push notifier - DefaultRequestHandler requestHandler = DefaultRequestHandler.builder() - .agentExecutor(executor).taskStore(taskStore).queueManager(queueManager) - .mainEventBusProcessor(mainEventBusProcessor) - .executor(internalExecutor).eventConsumerExecutor(internalExecutor) - .authorizationRequired(false) - .build(); + DefaultRequestHandler requestHandler = + DefaultRequestHandler.builder() + .agentExecutor(executor) + .taskStore(taskStore) + .queueManager(queueManager) + .mainEventBusProcessor(mainEventBusProcessor) + .executor(internalExecutor) + .eventConsumerExecutor(internalExecutor) + .authorizationRequired(false) + .build(); AgentCard card = AbstractA2ARequestHandlerTest.createAgentCard(false, true); GrpcHandler handler = new TestGrpcHandler(card, requestHandler, internalExecutor); - StreamRecorder streamRecorder = getTaskPushNotificationConfigRequest(handler, - AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); + StreamRecorder streamRecorder = + getTaskPushNotificationConfigRequest( + handler, + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED); } @Test public void testOnSetPushNotificationNoPushNotifierConfig() throws Exception { // Create request handler without a push notifier - DefaultRequestHandler requestHandler = DefaultRequestHandler.builder() - .agentExecutor(executor).taskStore(taskStore).queueManager(queueManager) - .mainEventBusProcessor(mainEventBusProcessor) - .executor(internalExecutor).eventConsumerExecutor(internalExecutor) - .authorizationRequired(false) - .build(); + DefaultRequestHandler requestHandler = + DefaultRequestHandler.builder() + .agentExecutor(executor) + .taskStore(taskStore) + .queueManager(queueManager) + .mainEventBusProcessor(mainEventBusProcessor) + .executor(internalExecutor) + .eventConsumerExecutor(internalExecutor) + .authorizationRequired(false) + .build(); AgentCard card = AbstractA2ARequestHandlerTest.createAgentCard(false, true); GrpcHandler handler = new TestGrpcHandler(card, requestHandler, internalExecutor); - StreamRecorder streamRecorder = createTaskPushNotificationConfigRequest(handler, - AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); + StreamRecorder streamRecorder = + createTaskPushNotificationConfigRequest( + handler, + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED); } @Test public void testOnMessageStreamNewMessageSuccess() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; StreamRecorder streamRecorder = sendStreamingMessageRequest(handler); Assertions.assertNull(streamRecorder.getError()); @@ -361,36 +428,41 @@ public void testOnMessageStreamNewMessageSuccess() throws Exception { @Test public void testOnMessageStreamNewMessageExistingTaskSuccess() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; - org.a2aproject.sdk.spec.Task task = org.a2aproject.sdk.spec.Task.builder(AbstractA2ARequestHandlerTest.MINIMAL_TASK) - .history(new ArrayList<>()) - .build(); + org.a2aproject.sdk.spec.Task task = + org.a2aproject.sdk.spec.Task.builder(AbstractA2ARequestHandlerTest.MINIMAL_TASK) + .history(new ArrayList<>()) + .build(); taskStore.save(task, false); List results = new ArrayList<>(); List errors = new ArrayList<>(); final CountDownLatch latch = new CountDownLatch(1); httpClient.latch = latch; - StreamObserver streamObserver = new StreamObserver<>() { - @Override - public void onNext(StreamResponse streamResponse) { - results.add(streamResponse); - latch.countDown(); - } + StreamObserver streamObserver = + new StreamObserver<>() { + @Override + public void onNext(StreamResponse streamResponse) { + results.add(streamResponse); + latch.countDown(); + } - @Override - public void onError(Throwable throwable) { - errors.add(throwable); - } + @Override + public void onError(Throwable throwable) { + errors.add(throwable); + } - @Override - public void onCompleted() { - } - }; + @Override + public void onCompleted() {} + }; sendStreamingMessageRequest(handler, streamObserver); Assertions.assertTrue(latch.await(1, TimeUnit.SECONDS)); Assertions.assertTrue(errors.isEmpty()); @@ -399,12 +471,15 @@ public void onCompleted() { Assertions.assertTrue(response.hasTask()); Task taskResponse = response.getTask(); - Task expected = Task.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .setContextId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) - .addAllHistory(List.of(GRPC_MESSAGE)) - .setStatus(TaskStatus.newBuilder().setStateValue(TaskState.TASK_STATE_SUBMITTED_VALUE)) - .build(); + Task expected = + Task.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .setContextId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) + .addAllHistory(List.of(GRPC_MESSAGE)) + .setStatus( + TaskStatus.newBuilder() + .setStateValue(TaskState.TASK_STATE_SUBMITTED_VALUE)) + .build(); assertEquals(expected.getId(), taskResponse.getId()); assertEquals(expected.getContextId(), taskResponse.getContextId()); assertEquals(expected.getStatus().getState(), taskResponse.getStatus().getState()); @@ -413,35 +488,52 @@ public void onCompleted() { @Test public void testOnMessageStreamNewMessageExistingTaskSuccessMocks() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); - - org.a2aproject.sdk.spec.Task task = org.a2aproject.sdk.spec.Task.builder(AbstractA2ARequestHandlerTest.MINIMAL_TASK) - .history(new ArrayList<>()) - .build(); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + + org.a2aproject.sdk.spec.Task task = + org.a2aproject.sdk.spec.Task.builder(AbstractA2ARequestHandlerTest.MINIMAL_TASK) + .history(new ArrayList<>()) + .build(); taskStore.save(task, false); // This is used to send events from a mock - List events = List.of( - TaskArtifactUpdateEvent.builder() - .taskId(task.id()) - .contextId(task.contextId()) - .artifact(Artifact.builder() - .artifactId("11") - .parts(new TextPart("text")) - .build()) - .build(), - TaskStatusUpdateEvent.builder() - .taskId(task.id()) - .contextId(task.contextId()) - .status(new org.a2aproject.sdk.spec.TaskStatus(org.a2aproject.sdk.spec.TaskState.TASK_STATE_WORKING)) - .build()); + List events = + List.of( + TaskArtifactUpdateEvent.builder() + .taskId(task.id()) + .contextId(task.contextId()) + .artifact( + Artifact.builder() + .artifactId("11") + .parts(new TextPart("text")) + .build()) + .build(), + TaskStatusUpdateEvent.builder() + .taskId(task.id()) + .contextId(task.contextId()) + .status( + new org.a2aproject.sdk.spec.TaskStatus( + org.a2aproject.sdk.spec.TaskState + .TASK_STATE_WORKING)) + .build()); StreamRecorder streamRecorder; - try (MockedConstruction mocked = Mockito.mockConstruction( - EventConsumer.class, - (mock, context) -> { - Mockito.doReturn(ZeroPublisher.fromIterable(events.stream().map(AbstractA2ARequestHandlerTest::wrapEvent).toList())).when(mock).consumeAll(); - })) { + try (MockedConstruction mocked = + Mockito.mockConstruction( + EventConsumer.class, + (mock, context) -> { + Mockito.doReturn( + ZeroPublisher.fromIterable( + events.stream() + .map( + AbstractA2ARequestHandlerTest + ::wrapEvent) + .toList())) + .when(mock) + .consumeAll(); + })) { streamRecorder = sendStreamingMessageRequest(handler); } Assertions.assertNull(streamRecorder.getError()); @@ -449,14 +541,16 @@ public void testOnMessageStreamNewMessageExistingTaskSuccessMocks() throws Excep Assertions.assertEquals(2, result.size()); StreamResponse first = result.get(0); Assertions.assertTrue(first.hasArtifactUpdate()); - org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent taskArtifactUpdateEvent = first.getArtifactUpdate(); + org.a2aproject.sdk.grpc.TaskArtifactUpdateEvent taskArtifactUpdateEvent = + first.getArtifactUpdate(); assertEquals(task.id(), taskArtifactUpdateEvent.getTaskId()); assertEquals(task.contextId(), taskArtifactUpdateEvent.getContextId()); assertEquals("11", taskArtifactUpdateEvent.getArtifact().getArtifactId()); assertEquals("text", taskArtifactUpdateEvent.getArtifact().getParts(0).getText()); StreamResponse second = result.get(1); Assertions.assertTrue(second.hasStatusUpdate()); - org.a2aproject.sdk.grpc.TaskStatusUpdateEvent taskStatusUpdateEvent = second.getStatusUpdate(); + org.a2aproject.sdk.grpc.TaskStatusUpdateEvent taskStatusUpdateEvent = + second.getStatusUpdate(); assertEquals(task.id(), taskStatusUpdateEvent.getTaskId()); assertEquals(task.contextId(), taskStatusUpdateEvent.getContextId()); assertEquals(TaskState.TASK_STATE_WORKING, taskStatusUpdateEvent.getStatus().getState()); @@ -469,54 +563,67 @@ public void testOnMessageStreamNewMessageSendPushNotificationSuccess() throws Ex mainEventBusProcessor.setPushNotificationExecutor(Runnable::run); try { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); - List events = List.of( - AbstractA2ARequestHandlerTest.MINIMAL_TASK, - TaskArtifactUpdateEvent.builder() - .taskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .contextId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) - .artifact(Artifact.builder() - .artifactId("11") - .parts(new TextPart("text")) - .build()) - .build(), - TaskStatusUpdateEvent.builder() - .taskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .contextId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) - .status(new org.a2aproject.sdk.spec.TaskStatus(org.a2aproject.sdk.spec.TaskState.TASK_STATE_COMPLETED)) - .build()); - - agentExecutorExecute = (context, agentEmitter) -> { - // Hardcode the events to send here - for (Event event : events) { - agentEmitter.emitEvent(event); - } - }; + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + List events = + List.of( + AbstractA2ARequestHandlerTest.MINIMAL_TASK, + TaskArtifactUpdateEvent.builder() + .taskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .contextId( + AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) + .artifact( + Artifact.builder() + .artifactId("11") + .parts(new TextPart("text")) + .build()) + .build(), + TaskStatusUpdateEvent.builder() + .taskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .contextId( + AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) + .status( + new org.a2aproject.sdk.spec.TaskStatus( + org.a2aproject.sdk.spec.TaskState + .TASK_STATE_COMPLETED)) + .build()); + + agentExecutorExecute = + (context, agentEmitter) -> { + // Hardcode the events to send here + for (Event event : events) { + agentEmitter.emitEvent(event); + } + }; - StreamRecorder pushStreamRecorder = createTaskPushNotificationConfigRequest( - handler, AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); + StreamRecorder pushStreamRecorder = + createTaskPushNotificationConfigRequest( + handler, + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); Assertions.assertNull(pushStreamRecorder.getError()); List results = new ArrayList<>(); List errors = new ArrayList<>(); final CountDownLatch latch = new CountDownLatch(6); httpClient.latch = latch; - StreamObserver streamObserver = new StreamObserver<>() { - @Override - public void onNext(StreamResponse streamResponse) { - results.add(streamResponse); - latch.countDown(); - } + StreamObserver streamObserver = + new StreamObserver<>() { + @Override + public void onNext(StreamResponse streamResponse) { + results.add(streamResponse); + latch.countDown(); + } - @Override - public void onError(Throwable throwable) { - errors.add(throwable); - } + @Override + public void onError(Throwable throwable) { + errors.add(throwable); + } - @Override - public void onCompleted() { - } - }; + @Override + public void onCompleted() {} + }; sendStreamingMessageRequest(handler, streamObserver); Assertions.assertTrue(latch.await(5, TimeUnit.SECONDS)); Assertions.assertTrue(errors.isEmpty()); @@ -524,27 +631,47 @@ public void onCompleted() { Assertions.assertEquals(3, httpClient.events.size()); // Event 0: Task event - Assertions.assertTrue(httpClient.events.get(0) instanceof org.a2aproject.sdk.spec.Task, "First event should be Task"); - org.a2aproject.sdk.spec.Task task1 = (org.a2aproject.sdk.spec.Task) httpClient.events.get(0); + Assertions.assertTrue( + httpClient.events.get(0) instanceof org.a2aproject.sdk.spec.Task, + "First event should be Task"); + org.a2aproject.sdk.spec.Task task1 = + (org.a2aproject.sdk.spec.Task) httpClient.events.get(0); Assertions.assertEquals(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), task1.id()); - Assertions.assertEquals(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId(), task1.contextId()); - Assertions.assertEquals(AbstractA2ARequestHandlerTest.MINIMAL_TASK.status().state(), task1.status().state()); + Assertions.assertEquals( + AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId(), task1.contextId()); + Assertions.assertEquals( + AbstractA2ARequestHandlerTest.MINIMAL_TASK.status().state(), + task1.status().state()); Assertions.assertEquals(0, task1.artifacts() == null ? 0 : task1.artifacts().size()); // Event 1: TaskArtifactUpdateEvent - Assertions.assertTrue(httpClient.events.get(1) instanceof TaskArtifactUpdateEvent, "Second event should be TaskArtifactUpdateEvent"); - TaskArtifactUpdateEvent artifactUpdate = (TaskArtifactUpdateEvent) httpClient.events.get(1); - Assertions.assertEquals(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), artifactUpdate.taskId()); - Assertions.assertEquals(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId(), artifactUpdate.contextId()); + Assertions.assertTrue( + httpClient.events.get(1) instanceof TaskArtifactUpdateEvent, + "Second event should be TaskArtifactUpdateEvent"); + TaskArtifactUpdateEvent artifactUpdate = + (TaskArtifactUpdateEvent) httpClient.events.get(1); + Assertions.assertEquals( + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), artifactUpdate.taskId()); + Assertions.assertEquals( + AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId(), + artifactUpdate.contextId()); Assertions.assertEquals(1, artifactUpdate.artifact().parts().size()); - Assertions.assertEquals("text", ((TextPart) artifactUpdate.artifact().parts().get(0)).text()); + Assertions.assertEquals( + "text", ((TextPart) artifactUpdate.artifact().parts().get(0)).text()); // Event 2: TaskStatusUpdateEvent - Assertions.assertTrue(httpClient.events.get(2) instanceof TaskStatusUpdateEvent, "Third event should be TaskStatusUpdateEvent"); + Assertions.assertTrue( + httpClient.events.get(2) instanceof TaskStatusUpdateEvent, + "Third event should be TaskStatusUpdateEvent"); TaskStatusUpdateEvent statusUpdate = (TaskStatusUpdateEvent) httpClient.events.get(2); - Assertions.assertEquals(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), statusUpdate.taskId()); - Assertions.assertEquals(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId(), statusUpdate.contextId()); - Assertions.assertEquals(org.a2aproject.sdk.spec.TaskState.TASK_STATE_COMPLETED, statusUpdate.status().state()); + Assertions.assertEquals( + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), statusUpdate.taskId()); + Assertions.assertEquals( + AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId(), + statusUpdate.contextId()); + Assertions.assertEquals( + org.a2aproject.sdk.spec.TaskState.TASK_STATE_COMPLETED, + statusUpdate.status().state()); } finally { mainEventBusProcessor.setPushNotificationExecutor(null); } @@ -552,10 +679,13 @@ public void onCompleted() { @Test public void testOnSubscribeNoExistingTaskError() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); - SubscribeToTaskRequest request = SubscribeToTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + SubscribeToTaskRequest request = + SubscribeToTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.subscribeToTask(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -564,24 +694,27 @@ public void testOnSubscribeNoExistingTaskError() throws Exception { @Test public void testOnSubscribeExistingTaskSuccess() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); queueManager.createOrTap(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; StreamRecorder streamRecorder = StreamRecorder.create(); - SubscribeToTaskRequest request = SubscribeToTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + SubscribeToTaskRequest request = + SubscribeToTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); handler.subscribeToTask(request, streamRecorder); // We need to send some events in order for those to end up in the queue - SendMessageRequest sendMessageRequest = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + SendMessageRequest sendMessageRequest = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); StreamRecorder messageRecorder = StreamRecorder.create(); handler.sendStreamingMessage(sendMessageRequest, messageRecorder); messageRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -595,47 +728,63 @@ public void testOnSubscribeExistingTaskSuccess() throws Exception { Assertions.assertEquals(2, result.size()); // ENFORCE that first event is Task - Assertions.assertTrue(result.get(0).hasTask(), - "First event on subscribe MUST be Task (current state)"); + Assertions.assertTrue( + result.get(0).hasTask(), "First event on subscribe MUST be Task (current state)"); // Second event should be Message from agent executor StreamResponse response = result.get(1); - Assertions.assertTrue(response.hasMessage(), - "Expected Message after initial Task"); + Assertions.assertTrue(response.hasMessage(), "Expected Message after initial Task"); assertEquals(GRPC_MESSAGE, response.getMessage()); Assertions.assertNull(streamRecorder.getError()); } @Test public void testOnSubscribeExistingTaskSuccessMocks() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); queueManager.createOrTap(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); - List events = List.of( - TaskArtifactUpdateEvent.builder() - .taskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .contextId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) - .artifact(Artifact.builder() - .artifactId("11") - .parts(new TextPart("text")) - .build()) - .build(), - TaskStatusUpdateEvent.builder() - .taskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .contextId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) - .status(new org.a2aproject.sdk.spec.TaskStatus(org.a2aproject.sdk.spec.TaskState.TASK_STATE_WORKING)) - .build()); + List events = + List.of( + TaskArtifactUpdateEvent.builder() + .taskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .contextId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) + .artifact( + Artifact.builder() + .artifactId("11") + .parts(new TextPart("text")) + .build()) + .build(), + TaskStatusUpdateEvent.builder() + .taskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .contextId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.contextId()) + .status( + new org.a2aproject.sdk.spec.TaskStatus( + org.a2aproject.sdk.spec.TaskState + .TASK_STATE_WORKING)) + .build()); StreamRecorder streamRecorder = StreamRecorder.create(); - SubscribeToTaskRequest request = SubscribeToTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); - try (MockedConstruction mocked = Mockito.mockConstruction( - EventConsumer.class, - (mock, context) -> { - Mockito.doReturn(ZeroPublisher.fromIterable(events.stream().map(AbstractA2ARequestHandlerTest::wrapEvent).toList())).when(mock).consumeAll(); - })) { + SubscribeToTaskRequest request = + SubscribeToTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); + try (MockedConstruction mocked = + Mockito.mockConstruction( + EventConsumer.class, + (mock, context) -> { + Mockito.doReturn( + ZeroPublisher.fromIterable( + events.stream() + .map( + AbstractA2ARequestHandlerTest + ::wrapEvent) + .toList())) + .when(mock) + .consumeAll(); + })) { handler.subscribeToTask(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); } @@ -669,9 +818,10 @@ public void testStreamingNotSupportedErrorOnSubscribeToTask() throws Exception { // This test does not exist in the Python implementation AgentCard card = AbstractA2ARequestHandlerTest.createAgentCard(false, true); GrpcHandler handler = new TestGrpcHandler(card, requestHandler, internalExecutor); - SubscribeToTaskRequest request = SubscribeToTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + SubscribeToTaskRequest request = + SubscribeToTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.subscribeToTask(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -681,8 +831,12 @@ public void testStreamingNotSupportedErrorOnSubscribeToTask() throws Exception { @Test public void testOnMessageStreamInternalError() throws Exception { DefaultRequestHandler mocked = Mockito.mock(DefaultRequestHandler.class); - Mockito.doThrow(new InternalError("Internal Error")).when(mocked).onMessageSendStream(Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, mocked, internalExecutor); + Mockito.doThrow(new InternalError("Internal Error")) + .when(mocked) + .onMessageSendStream( + Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); + GrpcHandler handler = + new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, mocked, internalExecutor); StreamRecorder streamRecorder = sendStreamingMessageRequest(handler); assertGrpcError(streamRecorder, Status.Code.INTERNAL); } @@ -692,13 +846,18 @@ public void testOnMessageInternalErrorIsSanitized() throws Exception { // A non-A2AError exception must not leak its message to the client DefaultRequestHandler mocked = Mockito.mock(DefaultRequestHandler.class); Mockito.doThrow(new RuntimeException("sensitive detail: /var/lib/secret/config.db")) - .when(mocked).onMessageSend(Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, mocked, internalExecutor); - - org.a2aproject.sdk.grpc.SendMessageRequest request = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); - StreamRecorder responseObserver = StreamRecorder.create(); + .when(mocked) + .onMessageSend( + Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); + GrpcHandler handler = + new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, mocked, internalExecutor); + + org.a2aproject.sdk.grpc.SendMessageRequest request = + org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder() + .setMessage(GRPC_MESSAGE) + .build(); + StreamRecorder responseObserver = + StreamRecorder.create(); handler.sendMessage(request, responseObserver); responseObserver.awaitCompletion(5, TimeUnit.SECONDS); @@ -707,26 +866,36 @@ public void testOnMessageInternalErrorIsSanitized() throws Exception { StatusRuntimeException sre = (StatusRuntimeException) responseObserver.getError(); Assertions.assertEquals(Status.Code.INTERNAL, sre.getStatus().getCode()); Assertions.assertEquals("Internal error", sre.getStatus().getDescription()); - Assertions.assertFalse(sre.getStatus().getDescription().contains("sensitive"), + Assertions.assertFalse( + sre.getStatus().getDescription().contains("sensitive"), "Internal exception message must not be leaked to the client"); } @Test public void testListPushNotificationConfig() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; - StreamRecorder pushRecorder = createTaskPushNotificationConfigRequest(handler, - AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); + StreamRecorder pushRecorder = + createTaskPushNotificationConfigRequest( + handler, + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); Assertions.assertNull(pushRecorder.getError()); - ListTaskPushNotificationConfigsRequest request = ListTaskPushNotificationConfigsRequest.newBuilder() - .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); - StreamRecorder streamRecorder = StreamRecorder.create(); + ListTaskPushNotificationConfigsRequest request = + ListTaskPushNotificationConfigsRequest.newBuilder() + .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); + StreamRecorder streamRecorder = + StreamRecorder.create(); handler.listTaskPushNotificationConfigs(request, streamRecorder); Assertions.assertNull(streamRecorder.getError()); List result = streamRecorder.getValues(); @@ -741,70 +910,98 @@ public void testListPushNotificationConfigNotSupported() throws Exception { AgentCard card = AbstractA2ARequestHandlerTest.createAgentCard(true, false); GrpcHandler handler = new TestGrpcHandler(card, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; - ListTaskPushNotificationConfigsRequest request = ListTaskPushNotificationConfigsRequest.newBuilder() - .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); - StreamRecorder streamRecorder = StreamRecorder.create(); + ListTaskPushNotificationConfigsRequest request = + ListTaskPushNotificationConfigsRequest.newBuilder() + .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); + StreamRecorder streamRecorder = + StreamRecorder.create(); handler.listTaskPushNotificationConfigs(request, streamRecorder); assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED); } @Test public void testListPushNotificationConfigNoPushConfigStore() { - DefaultRequestHandler requestHandler = DefaultRequestHandler.builder() - .agentExecutor(executor).taskStore(taskStore).queueManager(queueManager) - .mainEventBusProcessor(mainEventBusProcessor) - .executor(internalExecutor).eventConsumerExecutor(internalExecutor) - .authorizationRequired(false) - .build(); - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + DefaultRequestHandler requestHandler = + DefaultRequestHandler.builder() + .agentExecutor(executor) + .taskStore(taskStore) + .queueManager(queueManager) + .mainEventBusProcessor(mainEventBusProcessor) + .executor(internalExecutor) + .eventConsumerExecutor(internalExecutor) + .authorizationRequired(false) + .build(); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; - ListTaskPushNotificationConfigsRequest request = ListTaskPushNotificationConfigsRequest.newBuilder() - .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); - StreamRecorder streamRecorder = StreamRecorder.create(); + ListTaskPushNotificationConfigsRequest request = + ListTaskPushNotificationConfigsRequest.newBuilder() + .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); + StreamRecorder streamRecorder = + StreamRecorder.create(); handler.listTaskPushNotificationConfigs(request, streamRecorder); assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED); } @Test public void testListPushNotificationConfigTaskNotFound() { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; - ListTaskPushNotificationConfigsRequest request = ListTaskPushNotificationConfigsRequest.newBuilder() - .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); - StreamRecorder streamRecorder = StreamRecorder.create(); + ListTaskPushNotificationConfigsRequest request = + ListTaskPushNotificationConfigsRequest.newBuilder() + .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); + StreamRecorder streamRecorder = + StreamRecorder.create(); handler.listTaskPushNotificationConfigs(request, streamRecorder); assertGrpcError(streamRecorder, Status.Code.NOT_FOUND); } @Test public void testDeletePushNotificationConfig() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; - StreamRecorder pushRecorder = createTaskPushNotificationConfigRequest(handler, AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), - AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; + StreamRecorder pushRecorder = + createTaskPushNotificationConfigRequest( + handler, + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id(), + AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()); Assertions.assertNull(pushRecorder.getError()); - DeleteTaskPushNotificationConfigRequest request = DeleteTaskPushNotificationConfigRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + DeleteTaskPushNotificationConfigRequest request = + DeleteTaskPushNotificationConfigRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.deleteTaskPushNotificationConfig(request, streamRecorder); Assertions.assertNull(streamRecorder.getError()); @@ -817,13 +1014,16 @@ public void testDeletePushNotificationConfigNotSupported() throws Exception { AgentCard card = AbstractA2ARequestHandlerTest.createAgentCard(true, false); GrpcHandler handler = new TestGrpcHandler(card, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; - DeleteTaskPushNotificationConfigRequest request = DeleteTaskPushNotificationConfigRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; + DeleteTaskPushNotificationConfigRequest request = + DeleteTaskPushNotificationConfigRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.deleteTaskPushNotificationConfig(request, streamRecorder); assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED); @@ -831,17 +1031,24 @@ public void testDeletePushNotificationConfigNotSupported() throws Exception { @Test public void testDeletePushNotificationConfigNoPushConfigStore() { - DefaultRequestHandler requestHandler = DefaultRequestHandler.builder() - .agentExecutor(executor).taskStore(taskStore).queueManager(queueManager) - .mainEventBusProcessor(mainEventBusProcessor) - .executor(internalExecutor).eventConsumerExecutor(internalExecutor) - .authorizationRequired(false) - .build(); - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); - DeleteTaskPushNotificationConfigRequest request = DeleteTaskPushNotificationConfigRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + DefaultRequestHandler requestHandler = + DefaultRequestHandler.builder() + .agentExecutor(executor) + .taskStore(taskStore) + .queueManager(queueManager) + .mainEventBusProcessor(mainEventBusProcessor) + .executor(internalExecutor) + .eventConsumerExecutor(internalExecutor) + .authorizationRequired(false) + .build(); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + DeleteTaskPushNotificationConfigRequest request = + DeleteTaskPushNotificationConfigRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .setTaskId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.deleteTaskPushNotificationConfig(request, streamRecorder); assertGrpcError(streamRecorder, Status.Code.UNIMPLEMENTED); @@ -849,20 +1056,26 @@ public void testDeletePushNotificationConfigNoPushConfigStore() { @Test public void testExtendedAgentCardWithRouterKnownTenant() throws Exception { - AgentCard cardWithExtCapability = AgentCard.builder(AbstractA2ARequestHandlerTest.CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); + AgentCard cardWithExtCapability = + AgentCard.builder(AbstractA2ARequestHandlerTest.CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); AgentCard tenantCard = AgentCard.builder(cardWithExtCapability).name("acme-card").build(); - AgentCardRouter router = tenant -> "acme".equals(tenant) ? tenantCard : cardWithExtCapability; + AgentCardRouter router = + tenant -> "acme".equals(tenant) ? tenantCard : cardWithExtCapability; - GrpcHandler handler = new TestGrpcHandler(cardWithExtCapability, requestHandler, internalExecutor) { - @Override - protected AgentCardRouter getAgentCardRouter() { - return router; - } - }; + GrpcHandler handler = + new TestGrpcHandler(cardWithExtCapability, requestHandler, internalExecutor) { + @Override + protected AgentCardRouter getAgentCardRouter() { + return router; + } + }; org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest request = - org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.newBuilder().setTenant("acme").build(); + org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.newBuilder() + .setTenant("acme") + .build(); StreamRecorder recorder = StreamRecorder.create(); handler.getExtendedAgentCard(request, recorder); @@ -873,29 +1086,35 @@ protected AgentCardRouter getAgentCardRouter() { @Test public void testExtendedAgentCardWithRouterReturnsNull() throws Exception { - AgentCard cardWithExtCapability = AgentCard.builder(AbstractA2ARequestHandlerTest.CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); - AgentCardRouter router = new AgentCardRouter() { - @Override - public AgentCard resolveExtendedCard(String tenant) { - return null; - } + AgentCard cardWithExtCapability = + AgentCard.builder(AbstractA2ARequestHandlerTest.CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); + AgentCardRouter router = + new AgentCardRouter() { + @Override + public AgentCard resolveExtendedCard(String tenant) { + return null; + } - @Override - public AgentCard resolvePublicCard(String tenant) { - return cardWithExtCapability; - } - }; + @Override + public AgentCard resolvePublicCard(String tenant) { + return cardWithExtCapability; + } + }; - GrpcHandler handler = new TestGrpcHandler(cardWithExtCapability, requestHandler, internalExecutor) { - @Override - protected AgentCardRouter getAgentCardRouter() { - return router; - } - }; + GrpcHandler handler = + new TestGrpcHandler(cardWithExtCapability, requestHandler, internalExecutor) { + @Override + protected AgentCardRouter getAgentCardRouter() { + return router; + } + }; org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest request = - org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.newBuilder().setTenant("acme").build(); + org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.newBuilder() + .setTenant("acme") + .build(); StreamRecorder recorder = StreamRecorder.create(); handler.getExtendedAgentCard(request, recorder); @@ -904,39 +1123,42 @@ protected AgentCardRouter getAgentCardRouter() { @Test public void testSendMessageReturnsNotFoundForUnknownTenant() throws Exception { - AgentCardRouter router = new AgentCardRouter() { - @Override - public AgentCard resolveExtendedCard(String tenant) { - return null; - } + AgentCardRouter router = + new AgentCardRouter() { + @Override + public AgentCard resolveExtendedCard(String tenant) { + return null; + } - @Override - public AgentCard resolvePublicCard(String tenant) { - return "known".equals(tenant) ? AbstractA2ARequestHandlerTest.CARD : null; - } - }; + @Override + public AgentCard resolvePublicCard(String tenant) { + return "known".equals(tenant) ? AbstractA2ARequestHandlerTest.CARD : null; + } + }; - GrpcHandler handler = new TestGrpcHandler(null, requestHandler, internalExecutor) { - @Override - protected @org.jspecify.annotations.Nullable AgentCard getAgentCard() { - return null; - } + GrpcHandler handler = + new TestGrpcHandler(null, requestHandler, internalExecutor) { + @Override + protected @org.jspecify.annotations.Nullable AgentCard getAgentCard() { + return null; + } - @Override - protected AgentCard getExtendedAgentCard() { - return null; - } + @Override + protected AgentCard getExtendedAgentCard() { + return null; + } - @Override - protected AgentCardRouter getAgentCardRouter() { - return router; - } - }; + @Override + protected AgentCardRouter getAgentCardRouter() { + return router; + } + }; - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .setTenant("unknown-tenant") - .build(); + SendMessageRequest request = + SendMessageRequest.newBuilder() + .setMessage(GRPC_MESSAGE) + .setTenant("unknown-tenant") + .build(); StreamRecorder recorder = StreamRecorder.create(); handler.sendMessage(request, recorder); @@ -948,10 +1170,13 @@ protected AgentCardRouter getAgentCardRouter() { @Test public void testExtendedAgentCardWithoutRouter() throws Exception { - AgentCard cardWithExtCapability = AgentCard.builder(AbstractA2ARequestHandlerTest.CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); + AgentCard cardWithExtCapability = + AgentCard.builder(AbstractA2ARequestHandlerTest.CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); - GrpcHandler handler = new TestGrpcHandler(cardWithExtCapability, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler(cardWithExtCapability, requestHandler, internalExecutor); org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest request = org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.newBuilder().build(); @@ -964,7 +1189,9 @@ public void testExtendedAgentCardWithoutRouter() throws Exception { @Test public void testStreamingDoesNotBlockMainThread() throws Exception { - GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); + GrpcHandler handler = + new TestGrpcHandler( + AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); // Track if the main thread gets blocked during streaming @@ -972,43 +1199,46 @@ public void testStreamingDoesNotBlockMainThread() throws Exception { CountDownLatch streamStarted = new CountDownLatch(1); GrpcHandler.setStreamingSubscribedRunnable(streamStarted::countDown); CountDownLatch eventProcessed = new CountDownLatch(1); - agentExecutorExecute = (context, agentEmitter) -> { - // Wait a bit to ensure the main thread continues - try { - Thread.sleep(100); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + // Wait a bit to ensure the main thread continues + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + agentEmitter.sendMessage(context.getMessage()); + }; // Start streaming with a custom StreamObserver List results = new ArrayList<>(); List errors = new ArrayList<>(); - StreamObserver streamObserver = new StreamObserver<>() { - @Override - public void onNext(StreamResponse streamResponse) { - results.add(streamResponse); - eventReceived.set(true); - eventProcessed.countDown(); - } + StreamObserver streamObserver = + new StreamObserver<>() { + @Override + public void onNext(StreamResponse streamResponse) { + results.add(streamResponse); + eventReceived.set(true); + eventProcessed.countDown(); + } - @Override - public void onError(Throwable throwable) { - errors.add(throwable); - eventProcessed.countDown(); - } + @Override + public void onError(Throwable throwable) { + errors.add(throwable); + eventProcessed.countDown(); + } - @Override - public void onCompleted() { - eventProcessed.countDown(); - } - }; + @Override + public void onCompleted() { + eventProcessed.countDown(); + } + }; sendStreamingMessageRequest(handler, streamObserver); // The main thread should not be blocked - we should be able to continue immediately - Assertions.assertTrue(streamStarted.await(100, TimeUnit.MILLISECONDS), + Assertions.assertTrue( + streamStarted.await(100, TimeUnit.MILLISECONDS), "Streaming subscription should start quickly without blocking main thread"); // This proves the main thread is not blocked - we can do other work @@ -1016,7 +1246,8 @@ public void onCompleted() { Thread.sleep(50); // Wait for the actual event processing to complete - Assertions.assertTrue(eventProcessed.await(2, TimeUnit.SECONDS), + Assertions.assertTrue( + eventProcessed.await(2, TimeUnit.SECONDS), "Event should be processed within reasonable time"); // Verify we received the event and no errors occurred @@ -1028,31 +1259,36 @@ public void onCompleted() { @Test public void testExtensionSupportRequiredErrorOnSendMessage() throws Exception { // Create AgentCard with a required extension - AgentCard cardWithExtension = AgentCard.builder() - .name("test-card") - .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("GRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extensions(List.of( - AgentExtension.builder() - .uri("https://example.com/test-extension") - .required(true) - .build() - )) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); - - GrpcHandler handler = new TestGrpcHandler(cardWithExtension, requestHandler, internalExecutor); - - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + AgentCard cardWithExtension = + AgentCard.builder() + .name("test-card") + .description("Test card with required extension") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("GRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extensions( + List.of( + AgentExtension.builder() + .uri( + "https://example.com/test-extension") + .required(true) + .build())) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + + GrpcHandler handler = + new TestGrpcHandler(cardWithExtension, requestHandler, internalExecutor); + + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -1063,31 +1299,36 @@ public void testExtensionSupportRequiredErrorOnSendMessage() throws Exception { @Test public void testExtensionSupportRequiredErrorOnSendStreamingMessage() throws Exception { // Create AgentCard with a required extension - AgentCard cardWithExtension = AgentCard.builder() - .name("test-card") - .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("GRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extensions(List.of( - AgentExtension.builder() - .uri("https://example.com/streaming-extension") - .required(true) - .build() - )) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); - - GrpcHandler handler = new TestGrpcHandler(cardWithExtension, requestHandler, internalExecutor); - - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + AgentCard cardWithExtension = + AgentCard.builder() + .name("test-card") + .description("Test card with required extension") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("GRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extensions( + List.of( + AgentExtension.builder() + .uri( + "https://example.com/streaming-extension") + .required(true) + .build())) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + + GrpcHandler handler = + new TestGrpcHandler(cardWithExtension, requestHandler, internalExecutor); + + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendStreamingMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -1098,54 +1339,59 @@ public void testExtensionSupportRequiredErrorOnSendStreamingMessage() throws Exc @Test public void testRequiredExtensionProvidedSuccess() throws Exception { // Create AgentCard with a required extension - AgentCard cardWithExtension = AgentCard.builder() - .name("test-card") - .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("GRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extensions(List.of( - AgentExtension.builder() - .uri("https://example.com/required-extension") - .required(true) - .build() - )) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard cardWithExtension = + AgentCard.builder() + .name("test-card") + .description("Test card with required extension") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("GRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extensions( + List.of( + AgentExtension.builder() + .uri( + "https://example.com/required-extension") + .required(true) + .build())) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); // Create a TestGrpcHandler that provides the required extension in the context - GrpcHandler handler = new TestGrpcHandler(cardWithExtension, requestHandler, internalExecutor) { - @Override - protected CallContextFactory getCallContextFactory() { - return new CallContextFactory() { + GrpcHandler handler = + new TestGrpcHandler(cardWithExtension, requestHandler, internalExecutor) { @Override - public ServerCallContext create(StreamObserver streamObserver) { - Set requestedExtensions = new HashSet<>(); - requestedExtensions.add("https://example.com/required-extension"); - return new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("grpc_response_observer", streamObserver), - requestedExtensions, - "1.0" - ); + protected CallContextFactory getCallContextFactory() { + return new CallContextFactory() { + @Override + public ServerCallContext create(StreamObserver streamObserver) { + Set requestedExtensions = new HashSet<>(); + requestedExtensions.add("https://example.com/required-extension"); + return new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("grpc_response_observer", streamObserver), + requestedExtensions, + "1.0"); + } + }; } }; - } - }; taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -1158,41 +1404,45 @@ public ServerCallContext create(StreamObserver streamObserver) { @Test public void testVersionNotSupportedErrorOnSendMessage() throws Exception { // Create AgentCard with protocol version 1.0 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("GRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("GRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); // Create handler that provides incompatible version 2.0 in the context - GrpcHandler handler = new TestGrpcHandler(agentCard, requestHandler, internalExecutor) { - @Override - protected CallContextFactory getCallContextFactory() { - return new CallContextFactory() { + GrpcHandler handler = + new TestGrpcHandler(agentCard, requestHandler, internalExecutor) { @Override - public ServerCallContext create(StreamObserver streamObserver) { - return new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("grpc_response_observer", streamObserver), - new HashSet<>(), - "2.0" // Incompatible version - ); + protected CallContextFactory getCallContextFactory() { + return new CallContextFactory() { + @Override + public ServerCallContext create(StreamObserver streamObserver) { + return new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("grpc_response_observer", streamObserver), + new HashSet<>(), + "2.0" // Incompatible version + ); + } + }; } }; - } - }; - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -1203,41 +1453,45 @@ public ServerCallContext create(StreamObserver streamObserver) { @Test public void testVersionNotSupportedErrorOnSendStreamingMessage() throws Exception { // Create AgentCard with protocol version 1.0 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("GRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("GRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); // Create handler that provides incompatible version 2.0 in the context - GrpcHandler handler = new TestGrpcHandler(agentCard, requestHandler, internalExecutor) { - @Override - protected CallContextFactory getCallContextFactory() { - return new CallContextFactory() { + GrpcHandler handler = + new TestGrpcHandler(agentCard, requestHandler, internalExecutor) { @Override - public ServerCallContext create(StreamObserver streamObserver) { - return new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("grpc_response_observer", streamObserver), - new HashSet<>(), - "2.0" // Incompatible version - ); + protected CallContextFactory getCallContextFactory() { + return new CallContextFactory() { + @Override + public ServerCallContext create(StreamObserver streamObserver) { + return new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("grpc_response_observer", streamObserver), + new HashSet<>(), + "2.0" // Incompatible version + ); + } + }; } }; - } - }; - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendStreamingMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -1249,41 +1503,47 @@ public ServerCallContext create(StreamObserver streamObserver) { public void testVersionNotSupportedErrorOnGetTask() throws Exception { // Regression test: getTask previously skipped A2A protocol version // and extension validation, unlike sendMessage/sendStreamingMessage. - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("GRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("GRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); // Create handler that provides incompatible version 2.0 in the context - GrpcHandler handler = new TestGrpcHandler(agentCard, requestHandler, internalExecutor) { - @Override - protected CallContextFactory getCallContextFactory() { - return new CallContextFactory() { + GrpcHandler handler = + new TestGrpcHandler(agentCard, requestHandler, internalExecutor) { @Override - public ServerCallContext create(StreamObserver streamObserver) { - return new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("grpc_response_observer", streamObserver), - new HashSet<>(), - "2.0" // Incompatible version - ); + protected CallContextFactory getCallContextFactory() { + return new CallContextFactory() { + @Override + public ServerCallContext create(StreamObserver streamObserver) { + return new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("grpc_response_observer", streamObserver), + new HashSet<>(), + "2.0" // Incompatible version + ); + } + }; } }; - } - }; - GetTaskRequest request = GetTaskRequest.newBuilder() - .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) - .build(); + GetTaskRequest request = + GetTaskRequest.newBuilder() + .setId(AbstractA2ARequestHandlerTest.MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.getTask(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -1294,46 +1554,51 @@ public ServerCallContext create(StreamObserver streamObserver) { @Test public void testCompatibleVersionSuccess() throws Exception { // Create AgentCard with protocol version 1.0 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("GRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("GRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); // Create handler that provides compatible version 1.1 in the context - GrpcHandler handler = new TestGrpcHandler(agentCard, requestHandler, internalExecutor) { - @Override - protected CallContextFactory getCallContextFactory() { - return new CallContextFactory() { + GrpcHandler handler = + new TestGrpcHandler(agentCard, requestHandler, internalExecutor) { @Override - public ServerCallContext create(StreamObserver streamObserver) { - return new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("grpc_response_observer", streamObserver), - new HashSet<>(), - "1.1" // Compatible version (same major version) - ); + protected CallContextFactory getCallContextFactory() { + return new CallContextFactory() { + @Override + public ServerCallContext create(StreamObserver streamObserver) { + return new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("grpc_response_observer", streamObserver), + new HashSet<>(), + "1.1" // Compatible version (same major version) + ); + } + }; } }; - } - }; taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -1346,72 +1611,79 @@ public ServerCallContext create(StreamObserver streamObserver) { @Test public void testNoVersionDefaultsTo0_3_RejectedByV10OnlyServer() throws Exception { // Per spec Section 3.6.2: missing A2A-Version defaults to 0.3 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("GRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("GRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); // Create handler that provides null version — defaults to 0.3, incompatible with v1.0-only - GrpcHandler handler = new TestGrpcHandler(agentCard, requestHandler, internalExecutor) { - @Override - protected CallContextFactory getCallContextFactory() { - return new CallContextFactory() { + GrpcHandler handler = + new TestGrpcHandler(agentCard, requestHandler, internalExecutor) { @Override - public ServerCallContext create(StreamObserver streamObserver) { - return new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("grpc_response_observer", streamObserver), - new HashSet<>(), - null - ); + protected CallContextFactory getCallContextFactory() { + return new CallContextFactory() { + @Override + public ServerCallContext create(StreamObserver streamObserver) { + return new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("grpc_response_observer", streamObserver), + new HashSet<>(), + null); + } + }; } }; - } - }; taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); // Should fail with VersionNotSupportedError (0.3 is not supported by v1.0-only server) Assertions.assertNotNull(streamRecorder.getError()); - Assertions.assertInstanceOf(io.grpc.StatusRuntimeException.class, streamRecorder.getError()); + Assertions.assertInstanceOf( + io.grpc.StatusRuntimeException.class, streamRecorder.getError()); } - private StreamRecorder sendMessageRequest(GrpcHandler handler) throws Exception { - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + private StreamRecorder sendMessageRequest(GrpcHandler handler) + throws Exception { + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); return streamRecorder; } - private StreamRecorder createTaskPushNotificationConfigRequest(GrpcHandler handler, String taskId, String id) throws Exception { + private StreamRecorder createTaskPushNotificationConfigRequest( + GrpcHandler handler, String taskId, String id) throws Exception { taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - TaskPushNotificationConfig setRequest = TaskPushNotificationConfig.newBuilder() - .setUrl("http://example.com") - .setId("config456") - .setTaskId(MINIMAL_TASK.id()) - .build(); + TaskPushNotificationConfig setRequest = + TaskPushNotificationConfig.newBuilder() + .setUrl("http://example.com") + .setId("config456") + .setTaskId(MINIMAL_TASK.id()) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.createTaskPushNotificationConfig(setRequest, streamRecorder); @@ -1419,35 +1691,38 @@ private StreamRecorder createTaskPushNotificationCon return streamRecorder; } - private StreamRecorder getTaskPushNotificationConfigRequest(GrpcHandler handler, String taskId, String id) throws Exception { - GetTaskPushNotificationConfigRequest request = GetTaskPushNotificationConfigRequest.newBuilder() - .setTaskId(taskId) - .setId(id) - .build(); + private StreamRecorder getTaskPushNotificationConfigRequest( + GrpcHandler handler, String taskId, String id) throws Exception { + GetTaskPushNotificationConfigRequest request = + GetTaskPushNotificationConfigRequest.newBuilder() + .setTaskId(taskId) + .setId(id) + .build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.getTaskPushNotificationConfig(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); return streamRecorder; } - private StreamRecorder sendStreamingMessageRequest(GrpcHandler handler) throws Exception { - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + private StreamRecorder sendStreamingMessageRequest(GrpcHandler handler) + throws Exception { + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); StreamRecorder streamRecorder = StreamRecorder.create(); handler.sendStreamingMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); return streamRecorder; } - private void sendStreamingMessageRequest(GrpcHandler handler, StreamObserver streamObserver) throws Exception { - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + private void sendStreamingMessageRequest( + GrpcHandler handler, StreamObserver streamObserver) throws Exception { + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); handler.sendStreamingMessage(request, streamObserver); } - private void assertGrpcError(StreamRecorder streamRecorder, Status.Code expectedStatusCode) { + private void assertGrpcError( + StreamRecorder streamRecorder, Status.Code expectedStatusCode) { Assertions.assertNotNull(streamRecorder.getError()); Assertions.assertInstanceOf(StatusRuntimeException.class, streamRecorder.getError()); StatusRuntimeException sre = (StatusRuntimeException) streamRecorder.getError(); @@ -1459,7 +1734,8 @@ private void assertGrpcError(StreamRecorder streamRecorder, Status.Code e Assertions.assertNotNull(rpcStatus, "rpc status should be present"); Assertions.assertFalse(rpcStatus.getDetailsList().isEmpty(), "details should not be empty"); try { - com.google.rpc.ErrorInfo errorInfo = rpcStatus.getDetails(0).unpack(com.google.rpc.ErrorInfo.class); + com.google.rpc.ErrorInfo errorInfo = + rpcStatus.getDetails(0).unpack(com.google.rpc.ErrorInfo.class); Assertions.assertEquals("a2a-protocol.org", errorInfo.getDomain()); Assertions.assertFalse(errorInfo.getReason().isEmpty(), "reason should not be empty"); } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -1469,23 +1745,30 @@ private void assertGrpcError(StreamRecorder streamRecorder, Status.Code e @Test public void testGetAgentCardReturnsNullFallsBackToExtendedCard() throws Exception { - AgentCard extendedCard = AgentCard.builder(AbstractA2ARequestHandlerTest.CARD) - .capabilities(AgentCapabilities.builder().streaming(true).pushNotifications(true).build()).build(); - GrpcHandler handler = new TestGrpcHandler(null, requestHandler, internalExecutor) { - @Override - protected AgentCard getExtendedAgentCard() { - return extendedCard; - } - }; + AgentCard extendedCard = + AgentCard.builder(AbstractA2ARequestHandlerTest.CARD) + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .build()) + .build(); + GrpcHandler handler = + new TestGrpcHandler(null, requestHandler, internalExecutor) { + @Override + protected AgentCard getExtendedAgentCard() { + return extendedCard; + } + }; taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; StreamRecorder streamRecorder = StreamRecorder.create(); - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); handler.sendMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -1495,21 +1778,22 @@ protected AgentCard getExtendedAgentCard() { @Test public void testNoCardAtAllThrowsError() throws Exception { - GrpcHandler handler = new TestGrpcHandler(null, requestHandler, internalExecutor) { - @Override - protected AgentCard getExtendedAgentCard() { - return null; - } - }; + GrpcHandler handler = + new TestGrpcHandler(null, requestHandler, internalExecutor) { + @Override + protected AgentCard getExtendedAgentCard() { + return null; + } + }; taskStore.save(AbstractA2ARequestHandlerTest.MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; StreamRecorder streamRecorder = StreamRecorder.create(); - SendMessageRequest request = SendMessageRequest.newBuilder() - .setMessage(GRPC_MESSAGE) - .build(); + SendMessageRequest request = + SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE).build(); handler.sendMessage(request, streamRecorder); streamRecorder.awaitCompletion(5, TimeUnit.SECONDS); @@ -1522,10 +1806,12 @@ public void testListTasksNegativeTimestampReturnsInvalidArgument() { StreamRecorder responseObserver = StreamRecorder.create(); // Negative timestamp should trigger validation error - ListTasksRequest request = ListTasksRequest.newBuilder() - .setStatusTimestampAfter(com.google.protobuf.Timestamp.newBuilder().setSeconds(-1L).build()) - .setTenant("") - .build(); + ListTasksRequest request = + ListTasksRequest.newBuilder() + .setStatusTimestampAfter( + com.google.protobuf.Timestamp.newBuilder().setSeconds(-1L).build()) + .setTenant("") + .build(); handler.listTasks(request, responseObserver); @@ -1541,10 +1827,11 @@ public void testListTasksEmptyResultIncludesAllFields() throws Exception { StreamRecorder responseObserver = StreamRecorder.create(); // Query for a context that doesn't exist - should return empty result - ListTasksRequest request = ListTasksRequest.newBuilder() - .setContextId("nonexistent-context-id") - .setTenant("") - .build(); + ListTasksRequest request = + ListTasksRequest.newBuilder() + .setContextId("nonexistent-context-id") + .setTenant("") + .build(); handler.listTasks(request, responseObserver); @@ -1569,7 +1856,9 @@ public void testNormalOperationInvalidTenantMapsToInvalidParams() throws Excepti GrpcHandler handler = new TestGrpcHandler(CARD, customHandler, internalExecutor); StreamRecorder recorder = StreamRecorder.create(); - handler.getTask(GetTaskRequest.newBuilder().setId("task").setTenant("bad/tenant").build(), recorder); + handler.getTask( + GetTaskRequest.newBuilder().setId("task").setTenant("bad/tenant").build(), + recorder); assertGrpcReason(recorder, Status.Code.INVALID_ARGUMENT, "INVALID_PARAMS"); Mockito.verifyNoInteractions(customHandler); @@ -1582,9 +1871,15 @@ public void testCapabilityOperationsInvalidTenantMapsToInvalidParams() throws Ex StreamRecorder streaming = StreamRecorder.create(); StreamRecorder push = StreamRecorder.create(); - handler.sendStreamingMessage(SendMessageRequest.newBuilder().setTenant("bad/tenant").build(), streaming); - handler.deleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigRequest.newBuilder() - .setTaskId("task").setId("config").setTenant("bad/tenant").build(), push); + handler.sendStreamingMessage( + SendMessageRequest.newBuilder().setTenant("bad/tenant").build(), streaming); + handler.deleteTaskPushNotificationConfig( + DeleteTaskPushNotificationConfigRequest.newBuilder() + .setTaskId("task") + .setId("config") + .setTenant("bad/tenant") + .build(), + push); assertGrpcReason(streaming, Status.Code.INVALID_ARGUMENT, "INVALID_PARAMS"); assertGrpcReason(push, Status.Code.INVALID_ARGUMENT, "INVALID_PARAMS"); @@ -1593,7 +1888,8 @@ public void testCapabilityOperationsInvalidTenantMapsToInvalidParams() throws Ex @Test public void testCapabilityFailuresPrecedeInvalidVersion() throws Exception { - AgentCard card = AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().build()).build(); + AgentCard card = + AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().build()).build(); RequestHandler customHandler = Mockito.mock(RequestHandler.class); GrpcHandler handler = handlerWithVersion(card, customHandler, "2.0"); StreamRecorder send = StreamRecorder.create(); @@ -1605,24 +1901,37 @@ public void testCapabilityFailuresPrecedeInvalidVersion() throws Exception { handler.sendStreamingMessage(SendMessageRequest.getDefaultInstance(), send); handler.subscribeToTask(SubscribeToTaskRequest.getDefaultInstance(), subscribe); - handler.createTaskPushNotificationConfig(TaskPushNotificationConfig.getDefaultInstance(), create); - handler.getTaskPushNotificationConfig(GetTaskPushNotificationConfigRequest.getDefaultInstance(), get); - handler.listTaskPushNotificationConfigs(ListTaskPushNotificationConfigsRequest.getDefaultInstance(), list); - handler.deleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigRequest.getDefaultInstance(), delete); + handler.createTaskPushNotificationConfig( + TaskPushNotificationConfig.getDefaultInstance(), create); + handler.getTaskPushNotificationConfig( + GetTaskPushNotificationConfigRequest.getDefaultInstance(), get); + handler.listTaskPushNotificationConfigs( + ListTaskPushNotificationConfigsRequest.getDefaultInstance(), list); + handler.deleteTaskPushNotificationConfig( + DeleteTaskPushNotificationConfigRequest.getDefaultInstance(), delete); assertGrpcReason(send, Status.Code.UNIMPLEMENTED, "UNSUPPORTED_OPERATION"); - assertEquals("Streaming is not supported by the agent", Status.fromThrowable(send.getError()).getDescription()); + assertEquals( + "Streaming is not supported by the agent", + Status.fromThrowable(send.getError()).getDescription()); assertGrpcReason(subscribe, Status.Code.UNIMPLEMENTED, "UNSUPPORTED_OPERATION"); for (StreamRecorder recorder : List.of(create, get, list, delete)) { - assertGrpcReason(recorder, Status.Code.UNIMPLEMENTED, "PUSH_NOTIFICATION_NOT_SUPPORTED"); + assertGrpcReason( + recorder, Status.Code.UNIMPLEMENTED, "PUSH_NOTIFICATION_NOT_SUPPORTED"); } Mockito.verifyNoInteractions(customHandler); } @Test public void testInvalidVersionWithEnabledCapabilitiesMapsToA2AError() throws Exception { - AgentCard card = AgentCard.builder(CARD).capabilities(AgentCapabilities.builder() - .streaming(true).pushNotifications(true).build()).build(); + AgentCard card = + AgentCard.builder(CARD) + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .build()) + .build(); RequestHandler customHandler = Mockito.mock(RequestHandler.class); GrpcHandler handler = handlerWithVersion(card, customHandler, "2.0"); StreamRecorder ordinary = StreamRecorder.create(); @@ -1631,7 +1940,8 @@ public void testInvalidVersionWithEnabledCapabilitiesMapsToA2AError() throws Exc handler.getTask(GetTaskRequest.getDefaultInstance(), ordinary); handler.sendStreamingMessage(SendMessageRequest.getDefaultInstance(), streaming); - handler.deleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigRequest.getDefaultInstance(), push); + handler.deleteTaskPushNotificationConfig( + DeleteTaskPushNotificationConfigRequest.getDefaultInstance(), push); for (StreamRecorder recorder : List.of(ordinary, streaming, push)) { assertGrpcReason(recorder, Status.Code.UNIMPLEMENTED, "VERSION_NOT_SUPPORTED"); @@ -1642,25 +1952,30 @@ public void testInvalidVersionWithEnabledCapabilitiesMapsToA2AError() throws Exc @Test public void testCanonicalTenantReachesRouterAndCustomHandler() throws Exception { RequestHandler customHandler = Mockito.mock(RequestHandler.class); - Mockito.when(customHandler.onGetTask(Mockito.any(), Mockito.any())).thenAnswer(invocation -> { - org.a2aproject.sdk.spec.TaskQueryParams params = invocation.getArgument(0); - ServerCallContext context = invocation.getArgument(1); - assertEquals("acme", params.tenant()); - assertEquals("acme", context.getEffectiveAgentCard().tenant()); - Assertions.assertSame(CARD, context.getEffectiveAgentCard().card()); - return MINIMAL_TASK; - }); + Mockito.when(customHandler.onGetTask(Mockito.any(), Mockito.any())) + .thenAnswer( + invocation -> { + org.a2aproject.sdk.spec.TaskQueryParams params = + invocation.getArgument(0); + ServerCallContext context = invocation.getArgument(1); + assertEquals("acme", params.tenant()); + assertEquals("acme", context.getEffectiveAgentCard().tenant()); + Assertions.assertSame(CARD, context.getEffectiveAgentCard().card()); + return MINIMAL_TASK; + }); AgentCardRouter router = Mockito.mock(AgentCardRouter.class); Mockito.when(router.resolvePublicCard("acme")).thenReturn(CARD); - GrpcHandler handler = new TestGrpcHandler(CARD, customHandler, internalExecutor) { - @Override - protected AgentCardRouter getAgentCardRouter() { - return router; - } - }; + GrpcHandler handler = + new TestGrpcHandler(CARD, customHandler, internalExecutor) { + @Override + protected AgentCardRouter getAgentCardRouter() { + return router; + } + }; StreamRecorder recorder = StreamRecorder.create(); - handler.getTask(GetTaskRequest.newBuilder().setId("task").setTenant("/acme/").build(), recorder); + handler.getTask( + GetTaskRequest.newBuilder().setId("task").setTenant("/acme/").build(), recorder); Assertions.assertNull(recorder.getError()); assertEquals(1, recorder.getValues().size()); @@ -1671,46 +1986,69 @@ protected AgentCardRouter getAgentCardRouter() { @Test public void testStreamingAndPushUseOneResolvedCardAndCanonicalTenant() throws Exception { - AgentCard card = AgentCard.builder(CARD).capabilities(AgentCapabilities.builder() - .streaming(true).pushNotifications(true).build()).build(); + AgentCard card = + AgentCard.builder(CARD) + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .build()) + .build(); RequestHandler customHandler = Mockito.mock(RequestHandler.class); - Mockito.when(customHandler.onMessageSendStream(Mockito.any(), Mockito.any())).thenAnswer(invocation -> { - MessageSendParams params = invocation.getArgument(0); - ServerCallContext context = invocation.getArgument(1); - assertEquals("acme", params.tenant()); - assertEquals("acme", context.getEffectiveAgentCard().tenant()); - Assertions.assertSame(card, context.getEffectiveAgentCard().card()); - return ZeroPublisher.fromIterable(List.of()); - }); - Mockito.doAnswer(invocation -> { - org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams params = invocation.getArgument(0); - ServerCallContext context = invocation.getArgument(1); - assertEquals("acme", params.tenant()); - Assertions.assertSame(card, context.getEffectiveAgentCard().card()); - return null; - }).when(customHandler).onDeleteTaskPushNotificationConfig(Mockito.any(), Mockito.any()); + Mockito.when(customHandler.onMessageSendStream(Mockito.any(), Mockito.any())) + .thenAnswer( + invocation -> { + MessageSendParams params = invocation.getArgument(0); + ServerCallContext context = invocation.getArgument(1); + assertEquals("acme", params.tenant()); + assertEquals("acme", context.getEffectiveAgentCard().tenant()); + Assertions.assertSame(card, context.getEffectiveAgentCard().card()); + return ZeroPublisher.fromIterable(List.of()); + }); + Mockito.doAnswer( + invocation -> { + org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams params = + invocation.getArgument(0); + ServerCallContext context = invocation.getArgument(1); + assertEquals("acme", params.tenant()); + Assertions.assertSame(card, context.getEffectiveAgentCard().card()); + return null; + }) + .when(customHandler) + .onDeleteTaskPushNotificationConfig(Mockito.any(), Mockito.any()); AgentCardRouter router = Mockito.mock(AgentCardRouter.class); Mockito.when(router.resolvePublicCard("acme")).thenReturn(card); - GrpcHandler handler = new TestGrpcHandler(CARD, customHandler, internalExecutor) { - @Override - protected AgentCardRouter getAgentCardRouter() { - return router; - } - }; + GrpcHandler handler = + new TestGrpcHandler(CARD, customHandler, internalExecutor) { + @Override + protected AgentCardRouter getAgentCardRouter() { + return router; + } + }; StreamRecorder streaming = StreamRecorder.create(); StreamRecorder push = StreamRecorder.create(); - handler.sendStreamingMessage(SendMessageRequest.newBuilder().setMessage(GRPC_MESSAGE) - .setTenant("/acme/").build(), streaming); - handler.deleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigRequest.newBuilder() - .setTaskId("task").setId("config").setTenant("/acme/").build(), push); + handler.sendStreamingMessage( + SendMessageRequest.newBuilder() + .setMessage(GRPC_MESSAGE) + .setTenant("/acme/") + .build(), + streaming); + handler.deleteTaskPushNotificationConfig( + DeleteTaskPushNotificationConfigRequest.newBuilder() + .setTaskId("task") + .setId("config") + .setTenant("/acme/") + .build(), + push); streaming.awaitCompletion(5, TimeUnit.SECONDS); Assertions.assertNull(streaming.getError()); Assertions.assertNull(push.getError()); assertEquals(1, push.getValues().size()); Mockito.verify(customHandler).onMessageSendStream(Mockito.any(), Mockito.any()); - Mockito.verify(customHandler).onDeleteTaskPushNotificationConfig(Mockito.any(), Mockito.any()); + Mockito.verify(customHandler) + .onDeleteTaskPushNotificationConfig(Mockito.any(), Mockito.any()); Mockito.verify(router, Mockito.times(2)).resolvePublicCard("acme"); Mockito.verifyNoMoreInteractions(router); } @@ -1719,22 +2057,25 @@ protected AgentCardRouter getAgentCardRouter() { public void testTransportValidationUsesDeploymentTcclAndRestoresCallerTccl() { ClassLoader callerTccl = Thread.currentThread().getContextClassLoader(); AtomicInteger validations = new AtomicInteger(); - ClassLoader deploymentCl = new ClassLoader(callerTccl) { - @Override - public Enumeration getResources(String name) throws IOException { - if (name.equals("META-INF/services/" + TransportMetadata.class.getName())) { - Assertions.assertSame(this, Thread.currentThread().getContextClassLoader()); - validations.incrementAndGet(); - } - return super.getResources(name); - } - }; - GrpcHandler handler = new TestGrpcHandler(CARD, requestHandler, internalExecutor) { - @Override - protected ClassLoader getDeploymentClassLoader() { - return deploymentCl; - } - }; + ClassLoader deploymentCl = + new ClassLoader(callerTccl) { + @Override + public Enumeration getResources(String name) throws IOException { + if (name.equals("META-INF/services/" + TransportMetadata.class.getName())) { + Assertions.assertSame( + this, Thread.currentThread().getContextClassLoader()); + validations.incrementAndGet(); + } + return super.getResources(name); + } + }; + GrpcHandler handler = + new TestGrpcHandler(CARD, requestHandler, internalExecutor) { + @Override + protected ClassLoader getDeploymentClassLoader() { + return deploymentCl; + } + }; StreamRecorder recorder = StreamRecorder.create(); handler.getTask(GetTaskRequest.newBuilder().setId("missing").build(), recorder); @@ -1751,38 +2092,41 @@ public void testConcurrentRequestsWaitForTransportValidation() throws Exception CountDownLatch secondResolving = new CountDownLatch(1); AtomicInteger resolutions = new AtomicInteger(); AtomicInteger validations = new AtomicInteger(); - ClassLoader deploymentCl = new ClassLoader(Thread.currentThread().getContextClassLoader()) { - @Override - public Enumeration getResources(String name) throws IOException { - if (name.equals("META-INF/services/" + TransportMetadata.class.getName())) { - validations.incrementAndGet(); - validating.countDown(); - try { - if (!releaseValidation.await(5, TimeUnit.SECONDS)) { - throw new IOException("Timed out waiting to release transport validation"); + ClassLoader deploymentCl = + new ClassLoader(Thread.currentThread().getContextClassLoader()) { + @Override + public Enumeration getResources(String name) throws IOException { + if (name.equals("META-INF/services/" + TransportMetadata.class.getName())) { + validations.incrementAndGet(); + validating.countDown(); + try { + if (!releaseValidation.await(5, TimeUnit.SECONDS)) { + throw new IOException( + "Timed out waiting to release transport validation"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e); + } } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException(e); + return super.getResources(name); + } + }; + GrpcHandler handler = + new TestGrpcHandler(CARD, requestHandler, internalExecutor) { + @Override + protected AgentCard getAgentCard() { + if (resolutions.incrementAndGet() == 2) { + secondResolving.countDown(); + } + return CARD; } - } - return super.getResources(name); - } - }; - GrpcHandler handler = new TestGrpcHandler(CARD, requestHandler, internalExecutor) { - @Override - protected AgentCard getAgentCard() { - if (resolutions.incrementAndGet() == 2) { - secondResolving.countDown(); - } - return CARD; - } - @Override - protected ClassLoader getDeploymentClassLoader() { - return deploymentCl; - } - }; + @Override + protected ClassLoader getDeploymentClassLoader() { + return deploymentCl; + } + }; ExecutorService callers = Executors.newFixedThreadPool(2); StreamRecorder first = StreamRecorder.create(); StreamRecorder second = StreamRecorder.create(); @@ -1792,7 +2136,8 @@ protected ClassLoader getDeploymentClassLoader() { Assertions.assertTrue(validating.await(5, TimeUnit.SECONDS)); Future secondCall = callers.submit(() -> handler.getTask(request, second)); Assertions.assertTrue(secondResolving.await(5, TimeUnit.SECONDS)); - Assertions.assertThrows(TimeoutException.class, () -> secondCall.get(200, TimeUnit.MILLISECONDS)); + Assertions.assertThrows( + TimeoutException.class, () -> secondCall.get(200, TimeUnit.MILLISECONDS)); releaseValidation.countDown(); firstCall.get(5, TimeUnit.SECONDS); secondCall.get(5, TimeUnit.SECONDS); @@ -1807,38 +2152,50 @@ protected ClassLoader getDeploymentClassLoader() { @Test public void testExtendedCardCapabilityFailurePrecedesInvalidVersion() throws Exception { - AgentCard card = AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().build()).build(); + AgentCard card = + AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().build()).build(); GrpcHandler handler = handlerWithVersion(card, requestHandler, "2.0"); StreamRecorder recorder = StreamRecorder.create(); - handler.getExtendedAgentCard(org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.getDefaultInstance(), recorder); + handler.getExtendedAgentCard( + org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.getDefaultInstance(), recorder); assertGrpcReason(recorder, Status.Code.UNIMPLEMENTED, "UNSUPPORTED_OPERATION"); } @Test public void testExtendedCardMissingAfterCapabilitySuccess() throws Exception { - AgentCard card = AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); - GrpcHandler handler = new TestGrpcHandler(card, requestHandler, internalExecutor) { - @Override - protected AgentCard getExtendedAgentCard() { - return null; - } - }; + AgentCard card = + AgentCard.builder(CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); + GrpcHandler handler = + new TestGrpcHandler(card, requestHandler, internalExecutor) { + @Override + protected AgentCard getExtendedAgentCard() { + return null; + } + }; StreamRecorder recorder = StreamRecorder.create(); - handler.getExtendedAgentCard(org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.getDefaultInstance(), recorder); + handler.getExtendedAgentCard( + org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.getDefaultInstance(), recorder); - assertGrpcReason(recorder, Status.Code.FAILED_PRECONDITION, "EXTENDED_AGENT_CARD_NOT_CONFIGURED"); + assertGrpcReason( + recorder, Status.Code.FAILED_PRECONDITION, "EXTENDED_AGENT_CARD_NOT_CONFIGURED"); } @Test public void testExtendedCardInvalidVersionMapsToA2AError() throws Exception { - AgentCard card = AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); + AgentCard card = + AgentCard.builder(CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); GrpcHandler handler = handlerWithVersion(card, requestHandler, "2.0"); StreamRecorder recorder = StreamRecorder.create(); - handler.getExtendedAgentCard(org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.getDefaultInstance(), recorder); + handler.getExtendedAgentCard( + org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.getDefaultInstance(), recorder); assertGrpcReason(recorder, Status.Code.UNIMPLEMENTED, "VERSION_NOT_SUPPORTED"); } @@ -1848,31 +2205,41 @@ public void testExtendedCardInvalidTenantRetainsInternalMapping() throws Excepti GrpcHandler handler = new TestGrpcHandler(CARD, requestHandler, internalExecutor); StreamRecorder recorder = StreamRecorder.create(); - handler.getExtendedAgentCard(org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.newBuilder() - .setTenant("bad/tenant").build(), recorder); + handler.getExtendedAgentCard( + org.a2aproject.sdk.grpc.GetExtendedAgentCardRequest.newBuilder() + .setTenant("bad/tenant") + .build(), + recorder); assertGrpcReason(recorder, Status.Code.INTERNAL, "INTERNAL"); } - private GrpcHandler handlerWithVersion(AgentCard card, RequestHandler customHandler, String version) { + private GrpcHandler handlerWithVersion( + AgentCard card, RequestHandler customHandler, String version) { return new TestGrpcHandler(card, customHandler, internalExecutor) { @Override protected CallContextFactory getCallContextFactory() { return new CallContextFactory() { @Override public ServerCallContext create(StreamObserver observer) { - return new ServerCallContext(UnauthenticatedUser.INSTANCE, - Map.of("grpc_response_observer", observer), Set.of(), version); + return new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("grpc_response_observer", observer), + Set.of(), + version); } }; } }; } - private void assertGrpcReason(StreamRecorder recorder, Status.Code code, String reason) throws Exception { + private void assertGrpcReason(StreamRecorder recorder, Status.Code code, String reason) + throws Exception { assertGrpcError(recorder, code); - com.google.rpc.Status status = io.grpc.protobuf.StatusProto.fromThrowable(recorder.getError()); - assertEquals(reason, status.getDetails(0).unpack(com.google.rpc.ErrorInfo.class).getReason()); + com.google.rpc.Status status = + io.grpc.protobuf.StatusProto.fromThrowable(recorder.getError()); + assertEquals( + reason, status.getDetails(0).unpack(com.google.rpc.ErrorInfo.class).getReason()); } private static class TestGrpcHandler extends GrpcHandler { @@ -1881,7 +2248,8 @@ private static class TestGrpcHandler extends GrpcHandler { private final RequestHandler handler; private final java.util.concurrent.Executor executor; - TestGrpcHandler(AgentCard card, RequestHandler handler, java.util.concurrent.Executor executor) { + TestGrpcHandler( + AgentCard card, RequestHandler handler, java.util.concurrent.Executor executor) { this.card = card; this.handler = handler; this.executor = executor; @@ -1911,8 +2279,7 @@ public ServerCallContext create(StreamObserver streamObserver) { UnauthenticatedUser.INSTANCE, Map.of("grpc_response_observer", streamObserver), new HashSet<>(), - "1.0" - ); + "1.0"); } }; } diff --git a/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcTestTransportMetadata.java b/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcTestTransportMetadata.java index 6b3d2de38..b42d35a20 100644 --- a/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcTestTransportMetadata.java +++ b/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcTestTransportMetadata.java @@ -8,5 +8,4 @@ public class GrpcTestTransportMetadata implements TransportMetadata { public String getTransportProtocol() { return TransportProtocol.GRPC.asString(); } - } diff --git a/transport/jsonrpc/pom.xml b/transport/jsonrpc/pom.xml index 38211993f..1f6da5b9a 100644 --- a/transport/jsonrpc/pom.xml +++ b/transport/jsonrpc/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -65,5 +63,5 @@ test - + diff --git a/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/context/JSONRPCContextKeys.java b/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/context/JSONRPCContextKeys.java index dfb47798d..e9d1dc216 100644 --- a/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/context/JSONRPCContextKeys.java +++ b/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/context/JSONRPCContextKeys.java @@ -19,7 +19,7 @@ * @see org.a2aproject.sdk.server.ServerCallContext */ public final class JSONRPCContextKeys { - + /** * Context key for storing the headers. */ diff --git a/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java b/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java index a15a181d5..3e05e6540 100644 --- a/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java +++ b/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java @@ -3,20 +3,17 @@ import static org.a2aproject.sdk.server.AgentCardOperation.EXTENDED_CARD; import static org.a2aproject.sdk.server.AgentCardOperation.PUSH_NOTIFICATIONS; import static org.a2aproject.sdk.server.AgentCardOperation.STREAMING; - import static org.a2aproject.sdk.server.util.async.AsyncUtils.createTubeConfig; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Flow; import java.util.logging.Level; import java.util.logging.Logger; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Any; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; - import mutiny.zero.ZeroPublisher; import org.a2aproject.sdk.jsonrpc.common.wrappers.CancelTaskRequest; import org.a2aproject.sdk.jsonrpc.common.wrappers.CancelTaskResponse; @@ -161,7 +158,8 @@ public class JSONRPCHandler { private RequestHandler requestHandler; private Executor executor; private final CachingAgentCardTransportValidator transportValidator = - new CachingAgentCardTransportValidator(AgentCardValidator::validateTransportConfiguration); + new CachingAgentCardTransportValidator( + AgentCardValidator::validateTransportConfiguration); private AgentCardResolver agentCardResolver; private AgentCardRequestPolicy requestPolicy; @@ -191,16 +189,21 @@ protected JSONRPCHandler() { * @param agentCardRouterInstance optional agent card router instance for multitenancy */ @Inject - public JSONRPCHandler(@PublicAgentCard Instance agentCardInstance, - @Nullable @ExtendedAgentCard Instance extendedAgentCard, - RequestHandler requestHandler, @Internal Executor executor, - @Any @Nullable Instance agentCardRouterInstance) { + public JSONRPCHandler( + @PublicAgentCard Instance agentCardInstance, + @Nullable @ExtendedAgentCard Instance extendedAgentCard, + RequestHandler requestHandler, + @Internal Executor executor, + @Any @Nullable Instance agentCardRouterInstance) { this.agentCardInstance = agentCardInstance; this.extendedAgentCard = extendedAgentCard; this.requestHandler = requestHandler; this.executor = executor; - this.agentCardResolver = AgentCardResolver.of(() -> CdiUtils.resolveDefault(this.agentCardInstance), - () -> CdiUtils.resolveDefault(this.extendedAgentCard), CdiUtils.getIfResolvable(agentCardRouterInstance)); + this.agentCardResolver = + AgentCardResolver.of( + () -> CdiUtils.resolveDefault(this.agentCardInstance), + () -> CdiUtils.resolveDefault(this.extendedAgentCard), + CdiUtils.getIfResolvable(agentCardRouterInstance)); this.requestPolicy = new AgentCardRequestPolicy(agentCardResolver); } @@ -211,9 +214,14 @@ public JSONRPCHandler(@PublicAgentCard Instance agentCardInstance, * @param requestHandler the handler for processing A2A requests * @param executor the executor for asynchronous operations */ - public JSONRPCHandler(@Nullable AgentCard agentCard, RequestHandler requestHandler, Executor executor) { - this(agentCard != null ? new FixedInstance<>(agentCard) : FixedInstance.empty(), - null, requestHandler, executor, null); + public JSONRPCHandler( + @Nullable AgentCard agentCard, RequestHandler requestHandler, Executor executor) { + this( + agentCard != null ? new FixedInstance<>(agentCard) : FixedInstance.empty(), + null, + requestHandler, + executor, + null); } /** @@ -258,13 +266,20 @@ public JSONRPCHandler(@Nullable AgentCard agentCard, RequestHandler requestHandl * @see #onMessageSendStream(SendStreamingMessageRequest, ServerCallContext) * @see RequestHandler#onMessageSend(org.a2aproject.sdk.spec.MessageSendParams, ServerCallContext) */ - public SendMessageResponse onMessageSend(SendMessageRequest request, ServerCallContext context) { + public SendMessageResponse onMessageSend( + SendMessageRequest request, ServerCallContext context) { try { String tenant = request.getParams() != null ? request.getParams().tenant() : null; EffectiveAgentCard effective = preflight(tenant, context, AgentCardOperation.none()); MessageSendParams params = request.getParams(); - EventKind taskOrMessage = requestHandler.onMessageSend(new MessageSendParams(params.message(), - params.configuration(), params.metadata(), effective.tenant()), context); + EventKind taskOrMessage = + requestHandler.onMessageSend( + new MessageSendParams( + params.message(), + params.configuration(), + params.metadata(), + effective.tenant()), + context); return new SendMessageResponse(request.getId(), taskOrMessage); } catch (A2AError e) { return new SendMessageResponse(request.getId(), e); @@ -316,15 +331,22 @@ public Flow.Publisher onMessageSendStream( EffectiveAgentCard effective = preflight(tenant, context, STREAMING); MessageSendParams params = request.getParams(); Flow.Publisher publisher = - requestHandler.onMessageSendStream(new MessageSendParams(params.message(), params.configuration(), - params.metadata(), effective.tenant()), context); - // We can't use the convertingProcessor convenience method since that propagates any errors as an error handled + requestHandler.onMessageSendStream( + new MessageSendParams( + params.message(), + params.configuration(), + params.metadata(), + effective.tenant()), + context); + // We can't use the convertingProcessor convenience method since that propagates any + // errors as an error handled // via Subscriber.onError() rather than as part of the SendStreamingResponse payload return convertToSendStreamingMessageResponse(request.getId(), publisher); } catch (A2AError e) { return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), e)); } catch (Throwable throwable) { - return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), internalError(throwable))); + return ZeroPublisher.fromItems( + new SendStreamingMessageResponse(request.getId(), internalError(throwable))); } } @@ -358,7 +380,11 @@ public CancelTaskResponse onCancelTask(CancelTaskRequest request, ServerCallCont String tenant = request.getParams() != null ? request.getParams().tenant() : null; EffectiveAgentCard effective = preflight(tenant, context, AgentCardOperation.none()); CancelTaskParams params = request.getParams(); - Task task = requestHandler.onCancelTask(new CancelTaskParams(params.id(), effective.tenant(), params.metadata()), context); + Task task = + requestHandler.onCancelTask( + new CancelTaskParams( + params.id(), effective.tenant(), params.metadata()), + context); if (task != null) { return new CancelTaskResponse(request.getId(), task); } @@ -413,18 +439,23 @@ public Flow.Publisher onSubscribeToTask( } catch (A2AError e) { return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), e)); } - requestHandler.authorizeTaskAccess(request.getParams().id(), context, TaskOperation.SUBSCRIBE_TO_TASK); + requestHandler.authorizeTaskAccess( + request.getParams().id(), context, TaskOperation.SUBSCRIBE_TO_TASK); try { Flow.Publisher publisher = - requestHandler.onSubscribeToTask(new TaskIdParams(request.getParams().id(), effective.tenant()), context); - // We can't use the convertingProcessor convenience method since that propagates any errors as an error handled + requestHandler.onSubscribeToTask( + new TaskIdParams(request.getParams().id(), effective.tenant()), + context); + // We can't use the convertingProcessor convenience method since that propagates any + // errors as an error handled // via Subscriber.onError() rather than as part of the SendStreamingResponse payload return convertToSendStreamingMessageResponse(request.getId(), publisher); } catch (A2AError e) { // Other A2AError types - wrap inline as part of the stream return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), e)); } catch (Throwable throwable) { - return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), internalError(throwable))); + return ZeroPublisher.fromItems( + new SendStreamingMessageResponse(request.getId(), internalError(throwable))); } } @@ -459,8 +490,10 @@ public GetTaskPushNotificationConfigResponse getPushNotificationConfig( EffectiveAgentCard effective = preflight(tenant, context, PUSH_NOTIFICATIONS); GetTaskPushNotificationConfigParams params = request.getParams(); TaskPushNotificationConfig config = - requestHandler.onGetTaskPushNotificationConfig(new GetTaskPushNotificationConfigParams( - params.taskId(), params.id(), effective.tenant()), context); + requestHandler.onGetTaskPushNotificationConfig( + new GetTaskPushNotificationConfigParams( + params.taskId(), params.id(), effective.tenant()), + context); return new GetTaskPushNotificationConfigResponse(request.getId(), config); } catch (A2AError e) { return new GetTaskPushNotificationConfigResponse(request.getId(), e); @@ -501,8 +534,15 @@ public CreateTaskPushNotificationConfigResponse setPushNotificationConfig( EffectiveAgentCard effective = preflight(tenant, context, PUSH_NOTIFICATIONS); TaskPushNotificationConfig params = request.getParams(); TaskPushNotificationConfig config = - requestHandler.onCreateTaskPushNotificationConfig(new TaskPushNotificationConfig(params.id(), - params.taskId(), params.url(), params.token(), params.authentication(), effective.tenant()), context); + requestHandler.onCreateTaskPushNotificationConfig( + new TaskPushNotificationConfig( + params.id(), + params.taskId(), + params.url(), + params.token(), + params.authentication(), + effective.tenant()), + context); return new CreateTaskPushNotificationConfigResponse(request.getId(), config); } catch (A2AError e) { return new CreateTaskPushNotificationConfigResponse(request.getId(), e); @@ -540,7 +580,11 @@ public GetTaskResponse onGetTask(GetTaskRequest request, ServerCallContext conte String tenant = request.getParams() != null ? request.getParams().tenant() : null; EffectiveAgentCard effective = preflight(tenant, context, AgentCardOperation.none()); TaskQueryParams params = request.getParams(); - Task task = requestHandler.onGetTask(new TaskQueryParams(params.id(), params.historyLength(), effective.tenant()), context); + Task task = + requestHandler.onGetTask( + new TaskQueryParams( + params.id(), params.historyLength(), effective.tenant()), + context); return new GetTaskResponse(request.getId(), task); } catch (A2AError e) { return new GetTaskResponse(request.getId(), e); @@ -590,9 +634,18 @@ public ListTasksResponse onListTasks(ListTasksRequest request, ServerCallContext String tenant = request.getParams() != null ? request.getParams().tenant() : null; EffectiveAgentCard effective = preflight(tenant, context, AgentCardOperation.none()); ListTasksParams params = request.getParams(); - ListTasksResult result = requestHandler.onListTasks(new ListTasksParams(params.contextId(), params.status(), - params.pageSize(), params.pageToken(), params.historyLength(), params.statusTimestampAfter(), - params.includeArtifacts(), effective.tenant()), context); + ListTasksResult result = + requestHandler.onListTasks( + new ListTasksParams( + params.contextId(), + params.status(), + params.pageSize(), + params.pageToken(), + params.historyLength(), + params.statusTimestampAfter(), + params.includeArtifacts(), + effective.tenant()), + context); return new ListTasksResponse(request.getId(), result); } catch (A2AError e) { return new ListTasksResponse(request.getId(), e); @@ -632,8 +685,13 @@ public ListTaskPushNotificationConfigsResponse listPushNotificationConfigs( EffectiveAgentCard effective = preflight(tenant, context, PUSH_NOTIFICATIONS); ListTaskPushNotificationConfigsParams params = request.getParams(); ListTaskPushNotificationConfigsResult result = - requestHandler.onListTaskPushNotificationConfigs(new ListTaskPushNotificationConfigsParams( - params.id(), params.pageSize(), params.pageToken(), effective.tenant()), context); + requestHandler.onListTaskPushNotificationConfigs( + new ListTaskPushNotificationConfigsParams( + params.id(), + params.pageSize(), + params.pageToken(), + effective.tenant()), + context); return new ListTaskPushNotificationConfigsResponse(request.getId(), result); } catch (A2AError e) { return new ListTaskPushNotificationConfigsResponse(request.getId(), e); @@ -673,8 +731,10 @@ public DeleteTaskPushNotificationConfigResponse deletePushNotificationConfig( try { EffectiveAgentCard effective = preflight(tenant, context, PUSH_NOTIFICATIONS); DeleteTaskPushNotificationConfigParams params = request.getParams(); - requestHandler.onDeleteTaskPushNotificationConfig(new DeleteTaskPushNotificationConfigParams( - params.taskId(), params.id(), effective.tenant()), context); + requestHandler.onDeleteTaskPushNotificationConfig( + new DeleteTaskPushNotificationConfigParams( + params.taskId(), params.id(), effective.tenant()), + context); return new DeleteTaskPushNotificationConfigResponse(request.getId()); } catch (A2AError e) { return new DeleteTaskPushNotificationConfigResponse(request.getId(), e); @@ -714,8 +774,10 @@ public GetExtendedAgentCardResponse onGetExtendedCardRequest( EffectiveAgentCard effective = preflight(tenant, context, EXTENDED_CARD); AgentCard extCard = agentCardResolver.resolveExtendedDiscoveryCard(effective.tenant()); if (extCard == null) { - return new GetExtendedAgentCardResponse(request.getId(), - new ExtendedAgentCardNotConfiguredError(null, "Extended Card not configured", null)); + return new GetExtendedAgentCardResponse( + request.getId(), + new ExtendedAgentCardNotConfiguredError( + null, "Extended Card not configured", null)); } return new GetExtendedAgentCardResponse(request.getId(), extCard); } catch (A2AError e) { @@ -734,7 +796,8 @@ public GetExtendedAgentCardResponse onGetExtendedCardRequest( * @param context the server call context carrying the requested version and extensions * @throws A2AError if the requested version or a required extension is not supported */ - private EffectiveAgentCard preflight(@Nullable String tenant, ServerCallContext context, AgentCardOperation operation) { + private EffectiveAgentCard preflight( + @Nullable String tenant, ServerCallContext context, AgentCardOperation operation) { try { return requestPolicy.preflight(tenant, context, transportValidator, operation); } catch (TenantNotFoundException ex) { @@ -779,49 +842,62 @@ private EffectiveAgentCard preflight(@Nullable String tenant, ServerCallContext } private Flow.Publisher convertToSendStreamingMessageResponse( - Object requestId, - Flow.Publisher publisher) { - // We can't use the normal convertingProcessor since that propagates any errors as an error handled - // via Subscriber.onError() rather than as part of the SendStreamingResponse payload - return ZeroPublisher.create(createTubeConfig(), tube -> { - CompletableFuture.runAsync(() -> { - publisher.subscribe(new Flow.Subscriber() { - @SuppressWarnings("NullAway") - Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + Object requestId, Flow.Publisher publisher) { + // We can't use the normal convertingProcessor since that propagates any errors as an error + // handled + // via Subscriber.onError() rather than as part of the SendStreamingResponse payload + return ZeroPublisher.create( + createTubeConfig(), + tube -> { + CompletableFuture.runAsync( + () -> { + publisher.subscribe( + new Flow.Subscriber() { + @SuppressWarnings("NullAway") + Flow.Subscription subscription; + + @Override + public void onSubscribe( + Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(StreamingEventKind item) { - tube.send(new SendStreamingMessageResponse(requestId, item)); - subscription.request(1); - } + @Override + public void onNext(StreamingEventKind item) { + tube.send( + new SendStreamingMessageResponse( + requestId, item)); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - if (throwable instanceof A2AError jsonrpcError) { - tube.send(new SendStreamingMessageResponse(requestId, jsonrpcError)); - } else { - tube.send( - new SendStreamingMessageResponse( - requestId, internalError(throwable))); - } - onComplete(); - } + @Override + public void onError(Throwable throwable) { + if (throwable instanceof A2AError jsonrpcError) { + tube.send( + new SendStreamingMessageResponse( + requestId, jsonrpcError)); + } else { + tube.send( + new SendStreamingMessageResponse( + requestId, + internalError(throwable))); + } + onComplete(); + } - @Override - public void onComplete() { - tube.complete(); - } - }); - }, executor); - }); + @Override + public void onComplete() { + tube.complete(); + } + }); + }, + executor); + }); } - public void authorizeTaskAccess(String requestedTaskId, ServerCallContext context, TaskOperation operation) { + public void authorizeTaskAccess( + String requestedTaskId, ServerCallContext context, TaskOperation operation) { requestHandler.authorizeTaskAccess(requestedTaskId, context, operation); } diff --git a/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/package-info.java b/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/package-info.java index 87670f3ec..b6c8d31b3 100644 --- a/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/package-info.java +++ b/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/package-info.java @@ -33,4 +33,3 @@ package org.a2aproject.sdk.transport.jsonrpc.handler; import org.jspecify.annotations.NullMarked; - diff --git a/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java b/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java index cb3cd9527..4a262643d 100644 --- a/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java +++ b/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java @@ -9,6 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import jakarta.enterprise.inject.Instance; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -22,9 +23,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; - -import jakarta.enterprise.inject.Instance; - import mutiny.zero.ZeroPublisher; import org.a2aproject.sdk.jsonrpc.common.wrappers.CancelTaskRequest; import org.a2aproject.sdk.jsonrpc.common.wrappers.CancelTaskResponse; @@ -103,7 +101,9 @@ @Timeout(value = 1, unit = TimeUnit.MINUTES) public class JSONRPCHandlerTest extends AbstractA2ARequestHandlerTest { - private final ServerCallContext callContext = new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "1.0"); + private final ServerCallContext callContext = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "1.0"); private static MessageSendConfiguration defaultConfiguration() { return MessageSendConfiguration.builder() @@ -114,94 +114,211 @@ private static MessageSendConfiguration defaultConfiguration() { @Test void canonicalTenantAndEffectiveCardReachCustomHandlerForEveryOperation() throws Exception { - for (String operation : List.of("send", "stream", "get", "list", "cancel", "subscribe", "createPush", "getPush", "listPush", "deletePush")) { + for (String operation : + List.of( + "send", + "stream", + "get", + "list", + "cancel", + "subscribe", + "createPush", + "getPush", + "listPush", + "deletePush")) { assertCanonicalTenantDispatch(operation); } } private void assertCanonicalTenantDispatch(String operation) throws Exception { - ServerCallContext callContext = new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of(), Set.of(), "1.0"); + ServerCallContext callContext = + new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of(), Set.of(), "1.0"); AtomicReference expectedParams = new AtomicReference<>(); AtomicBoolean dispatched = new AtomicBoolean(); AgentCard card = versionTestCard(); - RequestHandler customHandler = Mockito.mock(RequestHandler.class, invocation -> { - if (invocation.getMethod().getName().equals("authorizeTaskAccess")) { - return null; - } - assertEquals(expectedParams.get(), invocation.getArgument(0)); - ServerCallContext context = invocation.getArgument(1); - assertNotNull(context.getEffectiveAgentCard()); - assertEquals("acme", context.getEffectiveAgentCard().tenant()); - Assertions.assertSame(card, context.getEffectiveAgentCard().card()); - dispatched.set(true); - return switch (operation) { - case "send", "get", "cancel" -> MINIMAL_TASK; - case "stream", "subscribe" -> ZeroPublisher.fromItems(MINIMAL_TASK); - case "list" -> new ListTasksResult(List.of()); - case "createPush", "getPush" -> new TaskPushNotificationConfig("config", "task", "https://example.com", null, null, "acme"); - case "listPush" -> new ListTaskPushNotificationConfigsResult(List.of()); - default -> null; - }; - }); - AgentCardRouter router = new AgentCardRouter() { - @Override - public AgentCard resolvePublicCard(String tenant) { - assertEquals("acme", tenant); - return card; - } + RequestHandler customHandler = + Mockito.mock( + RequestHandler.class, + invocation -> { + if (invocation.getMethod().getName().equals("authorizeTaskAccess")) { + return null; + } + assertEquals(expectedParams.get(), invocation.getArgument(0)); + ServerCallContext context = invocation.getArgument(1); + assertNotNull(context.getEffectiveAgentCard()); + assertEquals("acme", context.getEffectiveAgentCard().tenant()); + Assertions.assertSame(card, context.getEffectiveAgentCard().card()); + dispatched.set(true); + return switch (operation) { + case "send", "get", "cancel" -> MINIMAL_TASK; + case "stream", "subscribe" -> ZeroPublisher.fromItems(MINIMAL_TASK); + case "list" -> new ListTasksResult(List.of()); + case "createPush", "getPush" -> + new TaskPushNotificationConfig( + "config", + "task", + "https://example.com", + null, + null, + "acme"); + case "listPush" -> + new ListTaskPushNotificationConfigsResult(List.of()); + default -> null; + }; + }); + AgentCardRouter router = + new AgentCardRouter() { + @Override + public AgentCard resolvePublicCard(String tenant) { + assertEquals("acme", tenant); + return card; + } - @Override - public AgentCard resolveExtendedCard(String tenant) { - return null; - } - }; - JSONRPCHandler handler = new JSONRPCHandler(new FixedInstance<>(card), null, customHandler, - internalExecutor, new FixedInstance<>(router)); + @Override + public AgentCard resolveExtendedCard(String tenant) { + return null; + } + }; + JSONRPCHandler handler = + new JSONRPCHandler( + new FixedInstance<>(card), + null, + customHandler, + internalExecutor, + new FixedInstance<>(router)); A2AError error; switch (operation) { case "send", "stream" -> { - expectedParams.set(new MessageSendParams(MESSAGE, defaultConfiguration(), Map.of("key", "value"), "acme")); - MessageSendParams params = new MessageSendParams(MESSAGE, defaultConfiguration(), Map.of("key", "value"), "/acme/"); - error = operation.equals("send") - ? handler.onMessageSend(new SendMessageRequest("1", params), callContext).getError() - : drainStream(handler.onMessageSendStream(new SendStreamingMessageRequest("1", params), callContext)).get(0).getError(); + expectedParams.set( + new MessageSendParams( + MESSAGE, defaultConfiguration(), Map.of("key", "value"), "acme")); + MessageSendParams params = + new MessageSendParams( + MESSAGE, defaultConfiguration(), Map.of("key", "value"), "/acme/"); + error = + operation.equals("send") + ? handler.onMessageSend( + new SendMessageRequest("1", params), callContext) + .getError() + : drainStream( + handler.onMessageSendStream( + new SendStreamingMessageRequest( + "1", params), + callContext)) + .get(0) + .getError(); } case "get" -> { expectedParams.set(new TaskQueryParams("task", 3, "acme")); - error = handler.onGetTask(new GetTaskRequest("1", new TaskQueryParams("task", 3, "/acme/")), callContext).getError(); + error = + handler.onGetTask( + new GetTaskRequest( + "1", new TaskQueryParams("task", 3, "/acme/")), + callContext) + .getError(); } case "list" -> { - expectedParams.set(new ListTasksParams("context", TaskState.TASK_STATE_WORKING, 7, "page", 3, null, true, "acme")); - error = handler.onListTasks(new ListTasksRequest("1", new ListTasksParams("context", TaskState.TASK_STATE_WORKING, - 7, "page", 3, null, true, "/acme/")), callContext).getError(); + expectedParams.set( + new ListTasksParams( + "context", + TaskState.TASK_STATE_WORKING, + 7, + "page", + 3, + null, + true, + "acme")); + error = + handler.onListTasks( + new ListTasksRequest( + "1", + new ListTasksParams( + "context", + TaskState.TASK_STATE_WORKING, + 7, + "page", + 3, + null, + true, + "/acme/")), + callContext) + .getError(); } case "cancel" -> { expectedParams.set(new CancelTaskParams("task", "acme", Map.of("key", "value"))); - error = handler.onCancelTask(new CancelTaskRequest("1", new CancelTaskParams("task", "/acme/", Map.of("key", "value"))), callContext).getError(); + error = + handler.onCancelTask( + new CancelTaskRequest( + "1", + new CancelTaskParams( + "task", "/acme/", Map.of("key", "value"))), + callContext) + .getError(); } case "subscribe" -> { expectedParams.set(new TaskIdParams("task", "acme")); - error = drainStream(handler.onSubscribeToTask(new SubscribeToTaskRequest("1", new TaskIdParams("task", "/acme/")), callContext)).get(0).getError(); + error = + drainStream( + handler.onSubscribeToTask( + new SubscribeToTaskRequest( + "1", new TaskIdParams("task", "/acme/")), + callContext)) + .get(0) + .getError(); } case "createPush" -> { - expectedParams.set(new TaskPushNotificationConfig("config", "task", "https://example.com", "token", null, "acme")); - error = handler.setPushNotificationConfig(new CreateTaskPushNotificationConfigRequest("1", - new TaskPushNotificationConfig("config", "task", "https://example.com", "token", null, "/acme/")), callContext).getError(); + expectedParams.set( + new TaskPushNotificationConfig( + "config", "task", "https://example.com", "token", null, "acme")); + error = + handler.setPushNotificationConfig( + new CreateTaskPushNotificationConfigRequest( + "1", + new TaskPushNotificationConfig( + "config", + "task", + "https://example.com", + "token", + null, + "/acme/")), + callContext) + .getError(); } case "getPush" -> { - expectedParams.set(new GetTaskPushNotificationConfigParams("task", "config", "acme")); - error = handler.getPushNotificationConfig(new GetTaskPushNotificationConfigRequest("1", - new GetTaskPushNotificationConfigParams("task", "config", "/acme/")), callContext).getError(); + expectedParams.set( + new GetTaskPushNotificationConfigParams("task", "config", "acme")); + error = + handler.getPushNotificationConfig( + new GetTaskPushNotificationConfigRequest( + "1", + new GetTaskPushNotificationConfigParams( + "task", "config", "/acme/")), + callContext) + .getError(); } case "listPush" -> { - expectedParams.set(new ListTaskPushNotificationConfigsParams("task", 7, "page", "acme")); - error = handler.listPushNotificationConfigs(new ListTaskPushNotificationConfigsRequest("1", - new ListTaskPushNotificationConfigsParams("task", 7, "page", "/acme/")), callContext).getError(); + expectedParams.set( + new ListTaskPushNotificationConfigsParams("task", 7, "page", "acme")); + error = + handler.listPushNotificationConfigs( + new ListTaskPushNotificationConfigsRequest( + "1", + new ListTaskPushNotificationConfigsParams( + "task", 7, "page", "/acme/")), + callContext) + .getError(); } case "deletePush" -> { - expectedParams.set(new DeleteTaskPushNotificationConfigParams("task", "config", "acme")); - error = handler.deletePushNotificationConfig(new DeleteTaskPushNotificationConfigRequest("1", - new DeleteTaskPushNotificationConfigParams("task", "config", "/acme/")), callContext).getError(); + expectedParams.set( + new DeleteTaskPushNotificationConfigParams("task", "config", "acme")); + error = + handler.deletePushNotificationConfig( + new DeleteTaskPushNotificationConfigRequest( + "1", + new DeleteTaskPushNotificationConfigParams( + "task", "config", "/acme/")), + callContext) + .getError(); } default -> throw new AssertionError(operation); } @@ -212,18 +329,26 @@ public AgentCard resolveExtendedCard(String tenant) { @Test void defaultTenantSpellingsReachCustomHandlerAsNull() { RequestHandler customHandler = Mockito.mock(RequestHandler.class); - Mockito.when(customHandler.onGetTask(Mockito.any(), Mockito.any())).thenAnswer(invocation -> { - TaskQueryParams params = invocation.getArgument(0); - assertNull(params.tenant()); - ServerCallContext context = invocation.getArgument(1); - assertNull(context.getEffectiveAgentCard().tenant()); - return MINIMAL_TASK; - }); + Mockito.when(customHandler.onGetTask(Mockito.any(), Mockito.any())) + .thenAnswer( + invocation -> { + TaskQueryParams params = invocation.getArgument(0); + assertNull(params.tenant()); + ServerCallContext context = invocation.getArgument(1); + assertNull(context.getEffectiveAgentCard().tenant()); + return MINIMAL_TASK; + }); JSONRPCHandler handler = new JSONRPCHandler(CARD, customHandler, internalExecutor); for (String tenant : List.of("", " ", "/")) { - ServerCallContext context = new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of(), Set.of(), "1.0"); - assertNull(handler.onGetTask(new GetTaskRequest("1", new TaskQueryParams("task", null, tenant)), context).getError()); + ServerCallContext context = + new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of(), Set.of(), "1.0"); + assertNull( + handler.onGetTask( + new GetTaskRequest( + "1", new TaskQueryParams("task", null, tenant)), + context) + .getError()); } Mockito.verify(customHandler, Mockito.times(3)).onGetTask(Mockito.any(), Mockito.any()); } @@ -234,9 +359,11 @@ void invalidDecodedTenantWinsOverUnsupportedVersion() { TaskQueryParams params = Mockito.mock(TaskQueryParams.class); Mockito.when(params.tenant()).thenReturn("bad/tenant"); RequestHandler customHandler = Mockito.mock(RequestHandler.class); - JSONRPCHandler handler = new JSONRPCHandler(versionTestCard(), customHandler, internalExecutor); + JSONRPCHandler handler = + new JSONRPCHandler(versionTestCard(), customHandler, internalExecutor); - GetTaskResponse response = handler.onGetTask(new GetTaskRequest("1", params), incompatibleVersionContext()); + GetTaskResponse response = + handler.onGetTask(new GetTaskRequest("1", params), incompatibleVersionContext()); assertInstanceOf(InvalidParamsError.class, response.getError()); Mockito.verifyNoInteractions(customHandler); @@ -244,13 +371,25 @@ void invalidDecodedTenantWinsOverUnsupportedVersion() { @Test void extendedDiscoveryDistinguishesDisabledCapabilityFromMissingCard() { - JSONRPCHandler disabled = new JSONRPCHandler(AgentCard.builder(CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(false).build()).build(), requestHandler, internalExecutor); - JSONRPCHandler missing = new JSONRPCHandler(versionTestCard(), requestHandler, internalExecutor); - - assertInstanceOf(UnsupportedOperationError.class, - disabled.onGetExtendedCardRequest(new GetExtendedAgentCardRequest("1"), callContext).getError()); - GetExtendedAgentCardResponse missingResponse = missing.onGetExtendedCardRequest(new GetExtendedAgentCardRequest("1"), callContext); + JSONRPCHandler disabled = + new JSONRPCHandler( + AgentCard.builder(CARD) + .capabilities( + AgentCapabilities.builder() + .extendedAgentCard(false) + .build()) + .build(), + requestHandler, + internalExecutor); + JSONRPCHandler missing = + new JSONRPCHandler(versionTestCard(), requestHandler, internalExecutor); + + assertInstanceOf( + UnsupportedOperationError.class, + disabled.onGetExtendedCardRequest(new GetExtendedAgentCardRequest("1"), callContext) + .getError()); + GetExtendedAgentCardResponse missingResponse = + missing.onGetExtendedCardRequest(new GetExtendedAgentCardRequest("1"), callContext); assertInstanceOf(ExtendedAgentCardNotConfiguredError.class, missingResponse.getError()); assertEquals("Extended Card not configured", missingResponse.getError().getMessage()); } @@ -261,32 +400,44 @@ void malformedTenantExtendedDiscoveryReturnsInvalidParams() { Mockito.when(params.tenant()).thenReturn("bad/tenant"); JSONRPCHandler handler = versionTestHandler(); - assertInstanceOf(InvalidParamsError.class, handler.onGetExtendedCardRequest( - new GetExtendedAgentCardRequest("2.0", "1", params), callContext).getError()); + assertInstanceOf( + InvalidParamsError.class, + handler.onGetExtendedCardRequest( + new GetExtendedAgentCardRequest("2.0", "1", params), callContext) + .getError()); } @Test void routedExtendedDiscoveryUsesCanonicalTenant() { AgentCard card = versionTestCard(); AgentCard extended = AgentCard.builder(card).description("extended").build(); - AgentCardRouter router = new AgentCardRouter() { - @Override - public AgentCard resolvePublicCard(String tenant) { - assertEquals("acme", tenant); - return card; - } + AgentCardRouter router = + new AgentCardRouter() { + @Override + public AgentCard resolvePublicCard(String tenant) { + assertEquals("acme", tenant); + return card; + } - @Override - public AgentCard resolveExtendedCard(String tenant) { - assertEquals("acme", tenant); - return extended; - } - }; - JSONRPCHandler handler = new JSONRPCHandler(new FixedInstance<>(card), null, requestHandler, - internalExecutor, new FixedInstance<>(router)); + @Override + public AgentCard resolveExtendedCard(String tenant) { + assertEquals("acme", tenant); + return extended; + } + }; + JSONRPCHandler handler = + new JSONRPCHandler( + new FixedInstance<>(card), + null, + requestHandler, + internalExecutor, + new FixedInstance<>(router)); - GetExtendedAgentCardResponse response = handler.onGetExtendedCardRequest( - new GetExtendedAgentCardRequest("2.0", "1", new GetExtendedAgentCardParams("/acme/")), callContext); + GetExtendedAgentCardResponse response = + handler.onGetExtendedCardRequest( + new GetExtendedAgentCardRequest( + "2.0", "1", new GetExtendedAgentCardParams("/acme/")), + callContext); assertNull(response.getError()); Assertions.assertSame(extended, response.getResult()); @@ -294,20 +445,37 @@ public AgentCard resolveExtendedCard(String tenant) { @Test void capabilityErrorsRetainStreamingMessageAndPushErrorType() throws Exception { - AgentCard card = AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().build()).build(); + AgentCard card = + AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().build()).build(); RequestHandler customHandler = Mockito.mock(RequestHandler.class); JSONRPCHandler handler = new JSONRPCHandler(card, customHandler, internalExecutor); - SendStreamingMessageResponse streaming = drainStream(handler.onMessageSendStream( - new SendStreamingMessageRequest("1", new MessageSendParams(MESSAGE, null, null)), callContext)).get(0); - SendStreamingMessageResponse subscription = drainStream(handler.onSubscribeToTask( - new SubscribeToTaskRequest("1", new TaskIdParams("task")), callContext)).get(0); + SendStreamingMessageResponse streaming = + drainStream( + handler.onMessageSendStream( + new SendStreamingMessageRequest( + "1", new MessageSendParams(MESSAGE, null, null)), + callContext)) + .get(0); + SendStreamingMessageResponse subscription = + drainStream( + handler.onSubscribeToTask( + new SubscribeToTaskRequest("1", new TaskIdParams("task")), + callContext)) + .get(0); assertInstanceOf(UnsupportedOperationError.class, streaming.getError()); assertEquals("Streaming is not supported by the agent", streaming.getError().getMessage()); assertInstanceOf(UnsupportedOperationError.class, subscription.getError()); - assertEquals("Streaming is not supported by the agent", subscription.getError().getMessage()); - assertInstanceOf(PushNotificationNotSupportedError.class, handler.getPushNotificationConfig( - new GetTaskPushNotificationConfigRequest("1", new GetTaskPushNotificationConfigParams("task", "config")), callContext).getError()); + assertEquals( + "Streaming is not supported by the agent", subscription.getError().getMessage()); + assertInstanceOf( + PushNotificationNotSupportedError.class, + handler.getPushNotificationConfig( + new GetTaskPushNotificationConfigRequest( + "1", + new GetTaskPushNotificationConfigParams("task", "config")), + callContext) + .getError()); Mockito.verifyNoInteractions(customHandler); } @@ -337,15 +505,18 @@ public void testOnCancelTaskSuccess() throws Exception { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorCancel = (context, agentEmitter) -> { - // We need to cancel the task or the EventConsumer never finds a 'final' event. - // Looking at the Python implementation, they typically use AgentExecutors that - // don't support cancellation. So my theory is the Agent updates the task to the CANCEL status - Task task = context.getTask(); - agentEmitter.cancel(); - }; - - CancelTaskRequest request = new CancelTaskRequest("111", new CancelTaskParams(MINIMAL_TASK.id())); + agentExecutorCancel = + (context, agentEmitter) -> { + // We need to cancel the task or the EventConsumer never finds a 'final' event. + // Looking at the Python implementation, they typically use AgentExecutors that + // don't support cancellation. So my theory is the Agent updates the task to the + // CANCEL status + Task task = context.getTask(); + agentEmitter.cancel(); + }; + + CancelTaskRequest request = + new CancelTaskRequest("111", new CancelTaskParams(MINIMAL_TASK.id())); CancelTaskResponse response = handler.onCancelTask(request, callContext); assertNull(response.getError()); @@ -361,11 +532,13 @@ public void testOnCancelTaskNotSupported() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorCancel = (context, agentEmitter) -> { - throw new UnsupportedOperationError(); - }; + agentExecutorCancel = + (context, agentEmitter) -> { + throw new UnsupportedOperationError(); + }; - CancelTaskRequest request = new CancelTaskRequest("1", new CancelTaskParams(MINIMAL_TASK.id())); + CancelTaskRequest request = + new CancelTaskRequest("1", new CancelTaskParams(MINIMAL_TASK.id())); CancelTaskResponse response = handler.onCancelTask(request, callContext); assertEquals(request.getId(), response.getId()); assertNull(response.getResult()); @@ -375,7 +548,8 @@ public void testOnCancelTaskNotSupported() { @Test public void testOnCancelTaskNotFound() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); - CancelTaskRequest request = new CancelTaskRequest("1", new CancelTaskParams(MINIMAL_TASK.id())); + CancelTaskRequest request = + new CancelTaskRequest("1", new CancelTaskParams(MINIMAL_TASK.id())); CancelTaskResponse response = handler.onCancelTask(request, callContext); assertEquals(request.getId(), response.getId()); assertNull(response.getResult()); @@ -386,14 +560,17 @@ public void testOnCancelTaskNotFound() { public void testOnMessageNewMessageSuccess() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(message, null, null)); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response = handler.onMessageSend(request, callContext); assertNull(response.getError()); Assertions.assertSame(message, response.getResult()); @@ -403,14 +580,17 @@ public void testOnMessageNewMessageSuccess() { public void testOnMessageNewMessageWithExistingTaskSuccess() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(message, null, null)); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response = handler.onMessageSend(request, callContext); assertNull(response.getError()); Assertions.assertSame(message, response.getResult()); @@ -418,19 +598,22 @@ public void testOnMessageNewMessageWithExistingTaskSuccess() { @Test public void testOnMessageError() { - // See testMessageOnErrorMocks() for a test more similar to the Python implementation, using mocks for + // See testMessageOnErrorMocks() for a test more similar to the Python implementation, using + // mocks for // EventConsumer.consumeAll() JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.fail(new UnsupportedOperationError()); - }; - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendMessageRequest request = new SendMessageRequest( - "1", new MessageSendParams(message, null, null)); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.fail(new UnsupportedOperationError()); + }; + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response = handler.onMessageSend(request, callContext); assertInstanceOf(UnsupportedOperationError.class, response.getError()); assertNull(response.getResult()); @@ -440,18 +623,24 @@ public void testOnMessageError() { public void testOnMessageErrorMocks() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendMessageRequest request = new SendMessageRequest( - "1", new MessageSendParams(message, null, null)); + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response; - try (MockedConstruction mocked = Mockito.mockConstruction( - EventConsumer.class, - (mock, context) -> { - Mockito.doReturn(ZeroPublisher.fromItems(wrapEvent(new UnsupportedOperationError()))).when(mock).consumeAll(); - })) { + try (MockedConstruction mocked = + Mockito.mockConstruction( + EventConsumer.class, + (mock, context) -> { + Mockito.doReturn( + ZeroPublisher.fromItems( + wrapEvent(new UnsupportedOperationError()))) + .when(mock) + .consumeAll(); + })) { response = handler.onMessageSend(request, callContext); } @@ -463,48 +652,52 @@ public void testOnMessageErrorMocks() { public void testOnMessageStreamNewMessageSuccess() throws InterruptedException { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); - SendStreamingMessageRequest request = new SendStreamingMessageRequest( - "1", new MessageSendParams(message, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); List results = new ArrayList<>(); CountDownLatch latch = new CountDownLatch(1); - response.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item.getResult()); - subscription.request(1); - latch.countDown(); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item.getResult()); + subscription.request(1); + latch.countDown(); + } - @Override - public void onError(Throwable throwable) { - subscription.cancel(); - } + @Override + public void onError(Throwable throwable) { + subscription.cancel(); + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + } + }); latch.await(); @@ -512,7 +705,8 @@ public void onComplete() { // implementation, a Message is considered a 'final' Event in EventConsumer.consumeAll() // so there would be no more Events. // - // See testOnMessageStreamNewMessageSuccessMocks() for a test more similar to the Python implementation + // See testOnMessageStreamNewMessageSuccessMocks() for a test more similar to the Python + // implementation assertEquals(1, results.size()); Assertions.assertSame(message, results.get(0)); } @@ -524,84 +718,93 @@ public void testOnMessageStreamNewMessageMultipleEventsSuccess() throws Interrup JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - // Create multiple events to be sent during streaming - Task taskEvent = Task.builder(MINIMAL_TASK) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build(); + // Create multiple events to be sent during streaming + Task taskEvent = + Task.builder(MINIMAL_TASK) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build(); - TaskArtifactUpdateEvent artifactEvent = TaskArtifactUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(Artifact.builder() - .artifactId("artifact-1") - .parts(new TextPart("Generated artifact content")) - .build()) - .build(); + TaskArtifactUpdateEvent artifactEvent = + TaskArtifactUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + Artifact.builder() + .artifactId("artifact-1") + .parts(new TextPart("Generated artifact content")) + .build()) + .build(); - TaskStatusUpdateEvent statusEvent = TaskStatusUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build(); + TaskStatusUpdateEvent statusEvent = + TaskStatusUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build(); // Configure the agent executor to enqueue multiple events - agentExecutorExecute = (context, agentEmitter) -> { - // Enqueue the task with WORKING state - agentEmitter.emitEvent(taskEvent); - // Enqueue an artifact update event - agentEmitter.emitEvent(artifactEvent); - // Enqueue a status update event to complete the task (this is the "final" event) - agentEmitter.emitEvent(statusEvent); - }; - - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); + agentExecutorExecute = + (context, agentEmitter) -> { + // Enqueue the task with WORKING state + agentEmitter.emitEvent(taskEvent); + // Enqueue an artifact update event + agentEmitter.emitEvent(artifactEvent); + // Enqueue a status update event to complete the task (this is the "final" + // event) + agentEmitter.emitEvent(statusEvent); + }; + + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); - SendStreamingMessageRequest request = new SendStreamingMessageRequest( - "1", new MessageSendParams(message, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); List results = new ArrayList<>(); CountDownLatch latch = new CountDownLatch(3); // Expect 3 events AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item.getResult()); - subscription.request(1); - latch.countDown(); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item.getResult()); + subscription.request(1); + latch.countDown(); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - // Release latch to prevent timeout - while (latch.getCount() > 0) { - latch.countDown(); - } - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + // Release latch to prevent timeout + while (latch.getCount() > 0) { + latch.countDown(); + } + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + } + }); // Wait for all events to be received (increased timeout for async processing) - assertTrue(latch.await(10, TimeUnit.SECONDS), - "Expected to receive 3 events within timeout"); + assertTrue( + latch.await(10, TimeUnit.SECONDS), "Expected to receive 3 events within timeout"); // Assert no error occurred during streaming Assertions.assertNull(error.get(), "No error should occur during streaming"); @@ -610,27 +813,35 @@ public void onComplete() { assertEquals(3, results.size(), "Should have received exactly 3 events"); // Verify the first event is the task - Task receivedTask = assertInstanceOf(Task.class, results.get(0), "First event should be a Task"); + Task receivedTask = + assertInstanceOf(Task.class, results.get(0), "First event should be a Task"); assertEquals(MINIMAL_TASK.id(), receivedTask.id()); assertEquals(MINIMAL_TASK.contextId(), receivedTask.contextId()); assertEquals(TaskState.TASK_STATE_WORKING, receivedTask.status().state()); // Verify the second event is the artifact update - TaskArtifactUpdateEvent receivedArtifact = assertInstanceOf(TaskArtifactUpdateEvent.class, results.get(1), - "Second event should be a TaskArtifactUpdateEvent"); + TaskArtifactUpdateEvent receivedArtifact = + assertInstanceOf( + TaskArtifactUpdateEvent.class, + results.get(1), + "Second event should be a TaskArtifactUpdateEvent"); assertEquals(MINIMAL_TASK.id(), receivedArtifact.taskId()); assertEquals("artifact-1", receivedArtifact.artifact().artifactId()); // Verify the third event is the status update - TaskStatusUpdateEvent receivedStatus = assertInstanceOf(TaskStatusUpdateEvent.class, results.get(2), - "Third event should be a TaskStatusUpdateEvent"); + TaskStatusUpdateEvent receivedStatus = + assertInstanceOf( + TaskStatusUpdateEvent.class, + results.get(2), + "Third event should be a TaskStatusUpdateEvent"); assertEquals(MINIMAL_TASK.id(), receivedStatus.taskId()); assertEquals(TaskState.TASK_STATE_COMPLETED, receivedStatus.status().state()); // Verify events were persisted to TaskStore (poll for final state) for (int i = 0; i < 50; i++) { Task storedTask = taskStore.get(MINIMAL_TASK.id()); - if (storedTask != null && storedTask.status() != null + if (storedTask != null + && storedTask.status() != null && TaskState.TASK_STATE_COMPLETED.equals(storedTask.status().state())) { return; // Success - task finalized in TaskStore } @@ -645,66 +856,79 @@ public void testOnMessageStreamNewMessageSuccessMocks() { taskStore.save(MINIMAL_TASK, false); // This is used to send events from a mock - List events = List.of( - MINIMAL_TASK, - TaskArtifactUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(Artifact.builder() - .artifactId("art1") - .parts(new TextPart("text")) - .build()) - .build(), - TaskStatusUpdateEvent.builder() + List events = + List.of( + MINIMAL_TASK, + TaskArtifactUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + Artifact.builder() + .artifactId("art1") + .parts(new TextPart("text")) + .build()) + .build(), + TaskStatusUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build()); + + Message message = + Message.builder(MESSAGE) .taskId(MINIMAL_TASK.id()) .contextId(MINIMAL_TASK.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build()); - - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); + .build(); - SendStreamingMessageRequest request = new SendStreamingMessageRequest( - "1", new MessageSendParams(message, null, null)); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); Flow.Publisher response; - try (MockedConstruction mocked = Mockito.mockConstruction( - EventConsumer.class, - (mock, context) -> { - Mockito.doReturn(ZeroPublisher.fromIterable(events.stream().map(AbstractA2ARequestHandlerTest::wrapEvent).toList())).when(mock).consumeAll(); - })) { + try (MockedConstruction mocked = + Mockito.mockConstruction( + EventConsumer.class, + (mock, context) -> { + Mockito.doReturn( + ZeroPublisher.fromIterable( + events.stream() + .map( + AbstractA2ARequestHandlerTest + ::wrapEvent) + .toList())) + .when(mock) + .consumeAll(); + })) { response = handler.onMessageSendStream(request, callContext); } CompletableFuture future = new CompletableFuture<>(); List results = new ArrayList<>(); - response.subscribe(new Flow.Subscriber() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add((Event) item.getResult()); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add((Event) item.getResult()); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - future.completeExceptionally(throwable); - } + @Override + public void onError(Throwable throwable) { + future.completeExceptionally(throwable); + } - @Override - public void onComplete() { - future.complete(null); - } - }); + @Override + public void onComplete() { + future.complete(null); + } + }); future.join(); Assertions.assertEquals(events, results); @@ -713,66 +937,68 @@ public void onComplete() { @Test public void testOnMessageStreamNewMessageExistingTaskSuccess() throws Exception { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; - Task task = Task.builder(MINIMAL_TASK) - .history(new ArrayList<>()) - .build(); + Task task = Task.builder(MINIMAL_TASK).history(new ArrayList<>()).build(); taskStore.save(task, false); - Message message = Message.builder(MESSAGE) - .taskId(task.id()) - .contextId(task.contextId()) - .build(); + Message message = + Message.builder(MESSAGE).taskId(task.id()).contextId(task.contextId()).build(); - SendStreamingMessageRequest request = new SendStreamingMessageRequest( - "1", new MessageSendParams(message, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); // This Publisher never completes so we subscribe in a new thread. - // I _think_ that is as expected, and testOnMessageStreamNewMessageSendPushNotificationSuccess seems + // I _think_ that is as expected, and + // testOnMessageStreamNewMessageSendPushNotificationSuccess seems // to confirm this final List results = new ArrayList<>(); final AtomicReference subscriptionRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(1); - Executors.newSingleThreadExecutor().execute(() -> { - response.subscribe(new Flow.Subscriber<>() { - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscriptionRef.set(subscription); - subscription.request(1); - } - - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item.getResult()); - subscriptionRef.get().request(1); - latch.countDown(); - } - - @Override - public void onError(Throwable throwable) { - subscriptionRef.get().cancel(); - } - - @Override - public void onComplete() { - subscriptionRef.get().cancel(); - } - }); - }); + Executors.newSingleThreadExecutor() + .execute( + () -> { + response.subscribe( + new Flow.Subscriber<>() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscriptionRef.set(subscription); + subscription.request(1); + } + + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item.getResult()); + subscriptionRef.get().request(1); + latch.countDown(); + } + + @Override + public void onError(Throwable throwable) { + subscriptionRef.get().cancel(); + } + + @Override + public void onComplete() { + subscriptionRef.get().cancel(); + } + }); + }); assertTrue(latch.await(1, TimeUnit.SECONDS)); subscriptionRef.get().cancel(); // The Python implementation has several events emitted since it uses mocks. // - // See testOnMessageStreamNewMessageExistingTaskSuccessMocks() for a test more similar to the Python implementation - Task expected = Task.builder(task) - .history(message) - .build(); + // See testOnMessageStreamNewMessageExistingTaskSuccessMocks() for a test more similar to + // the Python implementation + Task expected = Task.builder(task).history(message).build(); assertEquals(1, results.size()); StreamingEventKind receivedType = results.get(0); assertInstanceOf(Task.class, receivedType); @@ -787,74 +1013,83 @@ public void onComplete() { public void testOnMessageStreamNewMessageExistingTaskSuccessMocks() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); - Task task = Task.builder(MINIMAL_TASK) - .history(new ArrayList<>()) - .build(); + Task task = Task.builder(MINIMAL_TASK).history(new ArrayList<>()).build(); taskStore.save(task, false); // This is used to send events from a mock - List events = List.of( - TaskArtifactUpdateEvent.builder() - .taskId(task.id()) - .contextId(task.contextId()) - .artifact(Artifact.builder() - .artifactId("11") - .parts(new TextPart("text")) - .build()) - .build(), - TaskStatusUpdateEvent.builder() - .taskId(task.id()) - .contextId(task.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build()); - - Message message = Message.builder(MESSAGE) - .taskId(task.id()) - .contextId(task.contextId()) - .build(); - - SendStreamingMessageRequest request = new SendStreamingMessageRequest( - "1", new MessageSendParams(message, null, null)); + List events = + List.of( + TaskArtifactUpdateEvent.builder() + .taskId(task.id()) + .contextId(task.contextId()) + .artifact( + Artifact.builder() + .artifactId("11") + .parts(new TextPart("text")) + .build()) + .build(), + TaskStatusUpdateEvent.builder() + .taskId(task.id()) + .contextId(task.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build()); + + Message message = + Message.builder(MESSAGE).taskId(task.id()).contextId(task.contextId()).build(); + + SendStreamingMessageRequest request = + new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); Flow.Publisher response; - try (MockedConstruction mocked = Mockito.mockConstruction( - EventConsumer.class, - (mock, context) -> { - Mockito.doReturn(ZeroPublisher.fromIterable(events.stream().map(AbstractA2ARequestHandlerTest::wrapEvent).toList())).when(mock).consumeAll(); - })) { + try (MockedConstruction mocked = + Mockito.mockConstruction( + EventConsumer.class, + (mock, context) -> { + Mockito.doReturn( + ZeroPublisher.fromIterable( + events.stream() + .map( + AbstractA2ARequestHandlerTest + ::wrapEvent) + .toList())) + .when(mock) + .consumeAll(); + })) { response = handler.onMessageSendStream(request, callContext); } CompletableFuture future = new CompletableFuture<>(); List results = new ArrayList<>(); - // Unlike testOnMessageStreamNewMessageExistingTaskSuccess() the ZeroPublisher.fromIterable() + // Unlike testOnMessageStreamNewMessageExistingTaskSuccess() the + // ZeroPublisher.fromIterable() // used to mock the events completes once it has sent all the items. So no special thread // handling is needed. - response.subscribe(new Flow.Subscriber() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add((Event) item.getResult()); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add((Event) item.getResult()); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - future.completeExceptionally(throwable); - } + @Override + public void onError(Throwable throwable) { + future.completeExceptionally(throwable); + } - @Override - public void onComplete() { - future.complete(null); - } - }); + @Override + public void onComplete() { + future.complete(null); + } + }); future.join(); @@ -866,17 +1101,19 @@ public void testSetPushNotificationConfigSuccess() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - TaskPushNotificationConfig taskPushConfig - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() .id("c295ea44-7543-4f78-b524-7a38915ad6e4") .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("tenant") .build(); - CreateTaskPushNotificationConfigRequest request = new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); - CreateTaskPushNotificationConfigResponse response = handler.setPushNotificationConfig(request, callContext); - TaskPushNotificationConfig taskPushConfigResult - = TaskPushNotificationConfig.builder() + CreateTaskPushNotificationConfigRequest request = + new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); + CreateTaskPushNotificationConfigResponse response = + handler.setPushNotificationConfig(request, callContext); + TaskPushNotificationConfig taskPushConfigResult = + TaskPushNotificationConfig.builder() .id("c295ea44-7543-4f78-b524-7a38915ad6e4") .taskId(MINIMAL_TASK.id()) .url("http://example.com") @@ -890,33 +1127,39 @@ public void testSetPushNotificationConfigSuccess() { public void testGetPushNotificationConfigSuccess() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; - - TaskPushNotificationConfig taskPushConfig - = TaskPushNotificationConfig.builder() + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() .id("c295ea44-7543-4f78-b524-7a38915ad6e4") .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("tenant") .build(); - CreateTaskPushNotificationConfigRequest request = new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); + CreateTaskPushNotificationConfigRequest request = + new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); handler.setPushNotificationConfig(request, callContext); - GetTaskPushNotificationConfigRequest getRequest - = new GetTaskPushNotificationConfigRequest("111", new GetTaskPushNotificationConfigParams( - MINIMAL_TASK.id(), - "c295ea44-7543-4f78-b524-7a38915ad6e4")); - GetTaskPushNotificationConfigResponse getResponse = handler.getPushNotificationConfig(getRequest, newCallContext()); - - TaskPushNotificationConfig expectedConfig = TaskPushNotificationConfig.builder() - .id("c295ea44-7543-4f78-b524-7a38915ad6e4") - .taskId(MINIMAL_TASK.id()) - .url("http://example.com") - .tenant("tenant") - .build(); + GetTaskPushNotificationConfigRequest getRequest = + new GetTaskPushNotificationConfigRequest( + "111", + new GetTaskPushNotificationConfigParams( + MINIMAL_TASK.id(), "c295ea44-7543-4f78-b524-7a38915ad6e4")); + GetTaskPushNotificationConfigResponse getResponse = + handler.getPushNotificationConfig(getRequest, newCallContext()); + + TaskPushNotificationConfig expectedConfig = + TaskPushNotificationConfig.builder() + .id("c295ea44-7543-4f78-b524-7a38915ad6e4") + .taskId(MINIMAL_TASK.id()) + .url("http://example.com") + .tenant("tenant") + .build(); assertEquals(expectedConfig, getResponse.getResult()); } @@ -933,80 +1176,90 @@ public void testOnMessageStreamNewMessageSendPushNotificationSuccess() throws Ex JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - List events = List.of( - MINIMAL_TASK, - TaskArtifactUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(Artifact.builder() - .artifactId("11") - .parts(new TextPart("text")) - .build()) - .build(), - TaskStatusUpdateEvent.builder() + List events = + List.of( + MINIMAL_TASK, + TaskArtifactUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + Artifact.builder() + .artifactId("11") + .parts(new TextPart("text")) + .build()) + .build(), + TaskStatusUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) + .build()); + + agentExecutorExecute = + (context, agentEmitter) -> { + // Hardcode the events to send here + for (Event event : events) { + agentEmitter.emitEvent(event); + } + }; + + TaskPushNotificationConfig config = + TaskPushNotificationConfig.builder() + .id("c295ea44-7543-4f78-b524-7a38915ad6e4") .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_COMPLETED)) - .build()); - - - agentExecutorExecute = (context, agentEmitter) -> { - // Hardcode the events to send here - for (Event event : events) { - agentEmitter.emitEvent(event); - } - }; - - TaskPushNotificationConfig config = TaskPushNotificationConfig.builder() - .id("c295ea44-7543-4f78-b524-7a38915ad6e4") - .taskId(MINIMAL_TASK.id()) - .url("http://example.com") - .tenant("tenant") - .build(); - - CreateTaskPushNotificationConfigRequest stpnRequest = new CreateTaskPushNotificationConfigRequest("1", config); - CreateTaskPushNotificationConfigResponse stpnResponse = handler.setPushNotificationConfig(stpnRequest, callContext); + .url("http://example.com") + .tenant("tenant") + .build(); + + CreateTaskPushNotificationConfigRequest stpnRequest = + new CreateTaskPushNotificationConfigRequest("1", config); + CreateTaskPushNotificationConfigResponse stpnResponse = + handler.setPushNotificationConfig(stpnRequest, callContext); assertNull(stpnResponse.getError()); - Message msg = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .build(); - SendStreamingMessageRequest request = new SendStreamingMessageRequest("1", new MessageSendParams(msg, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, newCallContext()); + Message msg = Message.builder(MESSAGE).taskId(MINIMAL_TASK.id()).build(); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest("1", new MessageSendParams(msg, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, newCallContext()); - final List results = Collections.synchronizedList(new ArrayList<>()); + final List results = + Collections.synchronizedList(new ArrayList<>()); final AtomicReference subscriptionRef = new AtomicReference<>(); final CountDownLatch latch = new CountDownLatch(6); httpClient.latch = latch; - Executors.newSingleThreadExecutor().execute(() -> { - response.subscribe(new Flow.Subscriber<>() { - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscriptionRef.set(subscription); - subscription.request(1); - } - - @Override - public void onNext(SendStreamingMessageResponse item) { - System.out.println("-> " + item.getResult()); - results.add(item.getResult()); - System.out.println(results); - subscriptionRef.get().request(1); - latch.countDown(); - } - - @Override - public void onError(Throwable throwable) { - subscriptionRef.get().cancel(); - } - - @Override - public void onComplete() { - subscriptionRef.get().cancel(); - } - }); - }); + Executors.newSingleThreadExecutor() + .execute( + () -> { + response.subscribe( + new Flow.Subscriber<>() { + @Override + public void onSubscribe( + Flow.Subscription subscription) { + subscriptionRef.set(subscription); + subscription.request(1); + } + + @Override + public void onNext(SendStreamingMessageResponse item) { + System.out.println("-> " + item.getResult()); + results.add(item.getResult()); + System.out.println(results); + subscriptionRef.get().request(1); + latch.countDown(); + } + + @Override + public void onError(Throwable throwable) { + subscriptionRef.get().cancel(); + } + + @Override + public void onComplete() { + subscriptionRef.get().cancel(); + } + }); + }); assertTrue(latch.await(5, TimeUnit.SECONDS)); @@ -1024,15 +1277,20 @@ public void onComplete() { assertEquals(0, task1.artifacts() == null ? 0 : task1.artifacts().size()); // Event 1: TaskArtifactUpdateEvent - assertTrue(httpClient.events.get(1) instanceof TaskArtifactUpdateEvent, "Second event should be TaskArtifactUpdateEvent"); - TaskArtifactUpdateEvent artifactUpdate = (TaskArtifactUpdateEvent) httpClient.events.get(1); + assertTrue( + httpClient.events.get(1) instanceof TaskArtifactUpdateEvent, + "Second event should be TaskArtifactUpdateEvent"); + TaskArtifactUpdateEvent artifactUpdate = + (TaskArtifactUpdateEvent) httpClient.events.get(1); assertEquals(MINIMAL_TASK.id(), artifactUpdate.taskId()); assertEquals(MINIMAL_TASK.contextId(), artifactUpdate.contextId()); assertEquals(1, artifactUpdate.artifact().parts().size()); assertEquals("text", ((TextPart) artifactUpdate.artifact().parts().get(0)).text()); // Event 2: TaskStatusUpdateEvent - assertTrue(httpClient.events.get(2) instanceof TaskStatusUpdateEvent, "Third event should be TaskStatusUpdateEvent"); + assertTrue( + httpClient.events.get(2) instanceof TaskStatusUpdateEvent, + "Third event should be TaskStatusUpdateEvent"); TaskStatusUpdateEvent statusUpdate = (TaskStatusUpdateEvent) httpClient.events.get(2); assertEquals(MINIMAL_TASK.id(), statusUpdate.taskId()); assertEquals(MINIMAL_TASK.contextId(), statusUpdate.contextId()); @@ -1048,24 +1306,30 @@ public void testOnSubscribeExistingTaskSuccess() { taskStore.save(MINIMAL_TASK, false); queueManager.createOrTap(MINIMAL_TASK.id()); - agentExecutorExecute = (context, agentEmitter) -> { - // The only thing hitting the agent is the onMessageSend() and we should use the message - agentEmitter.sendMessage(context.getMessage()); - //agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + // The only thing hitting the agent is the onMessageSend() and we should use the + // message + agentEmitter.sendMessage(context.getMessage()); + // agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : + // context.getMessage()); + }; - SubscribeToTaskRequest request = new SubscribeToTaskRequest("1", new TaskIdParams(MINIMAL_TASK.id())); - Flow.Publisher response = handler.onSubscribeToTask(request, callContext); + SubscribeToTaskRequest request = + new SubscribeToTaskRequest("1", new TaskIdParams(MINIMAL_TASK.id())); + Flow.Publisher response = + handler.onSubscribeToTask(request, callContext); // We need to send some events in order for those to end up in the queue - Message message = Message.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .role(Message.Role.ROLE_AGENT) - .parts(new TextPart("text")) - .build(); - SendMessageResponse smr - = handler.onMessageSend( + Message message = + Message.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .role(Message.Role.ROLE_AGENT) + .parts(new TextPart("text")) + .build(); + SendMessageResponse smr = + handler.onMessageSend( new SendMessageRequest("1", new MessageSendParams(message, null, null)), callContext); assertNull(smr.getError()); @@ -1074,46 +1338,52 @@ public void testOnSubscribeExistingTaskSuccess() { List results = new ArrayList<>(); AtomicBoolean receivedInitialTask = new AtomicBoolean(false); - response.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - StreamingEventKind event = item.getResult(); - results.add(event); - - // Per A2A Protocol Spec 3.1.6: ENFORCE that first event is Task - if (!receivedInitialTask.get()) { - assertTrue(event instanceof Task, - "First event on subscribe MUST be Task (current state), but was: " + event.getClass().getSimpleName()); - receivedInitialTask.set(true); - } else { - // Subsequent events should be the expected type (Message in this case) - assertTrue(event instanceof Message, - "Expected Message after initial Task, but was: " + event.getClass().getSimpleName()); - } - - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + StreamingEventKind event = item.getResult(); + results.add(event); + + // Per A2A Protocol Spec 3.1.6: ENFORCE that first event is Task + if (!receivedInitialTask.get()) { + assertTrue( + event instanceof Task, + "First event on subscribe MUST be Task (current state), but" + + " was: " + + event.getClass().getSimpleName()); + receivedInitialTask.set(true); + } else { + // Subsequent events should be the expected type (Message in this case) + assertTrue( + event instanceof Message, + "Expected Message after initial Task, but was: " + + event.getClass().getSimpleName()); + } - @Override - public void onError(Throwable throwable) { - subscription.cancel(); - future.completeExceptionally(throwable); - } + subscription.request(1); + } - @Override - public void onComplete() { - subscription.cancel(); - future.complete(null); - } - }); + @Override + public void onError(Throwable throwable) { + subscription.cancel(); + future.completeExceptionally(throwable); + } + + @Override + public void onComplete() { + subscription.cancel(); + future.complete(null); + } + }); future.join(); @@ -1128,28 +1398,40 @@ public void testOnSubscribeExistingTaskSuccessMocks() throws Exception { taskStore.save(MINIMAL_TASK, false); queueManager.createOrTap(MINIMAL_TASK.id()); - List events = List.of( - TaskArtifactUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .artifact(Artifact.builder() - .artifactId("11") - .parts(new TextPart("text")) - .build()) - .build(), - TaskStatusUpdateEvent.builder() - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) - .build()); - - SubscribeToTaskRequest request = new SubscribeToTaskRequest("1", new TaskIdParams(MINIMAL_TASK.id())); + List events = + List.of( + TaskArtifactUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .artifact( + Artifact.builder() + .artifactId("11") + .parts(new TextPart("text")) + .build()) + .build(), + TaskStatusUpdateEvent.builder() + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .status(new TaskStatus(TaskState.TASK_STATE_WORKING)) + .build()); + + SubscribeToTaskRequest request = + new SubscribeToTaskRequest("1", new TaskIdParams(MINIMAL_TASK.id())); Flow.Publisher response; - try (MockedConstruction mocked = Mockito.mockConstruction( - EventConsumer.class, - (mock, context) -> { - Mockito.doReturn(ZeroPublisher.fromIterable(events.stream().map(AbstractA2ARequestHandlerTest::wrapEvent).toList())).when(mock).consumeAll(); - })) { + try (MockedConstruction mocked = + Mockito.mockConstruction( + EventConsumer.class, + (mock, context) -> { + Mockito.doReturn( + ZeroPublisher.fromIterable( + events.stream() + .map( + AbstractA2ARequestHandlerTest + ::wrapEvent) + .toList())) + .when(mock) + .consumeAll(); + })) { response = handler.onSubscribeToTask(request, callContext); } @@ -1159,33 +1441,34 @@ public void testOnSubscribeExistingTaskSuccessMocks() throws Exception { // Unlike testOnSubscribeExistingTaskSuccess() the ZeroPublisher.fromIterable() // used to mock the events completes once it has sent all the items. So no special thread // handling is needed. - response.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item.getResult()); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item.getResult()); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - subscription.cancel(); - future.completeExceptionally(throwable); - } + @Override + public void onError(Throwable throwable) { + subscription.cancel(); + future.completeExceptionally(throwable); + } - @Override - public void onComplete() { - subscription.cancel(); - future.complete(null); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + future.complete(null); + } + }); future.join(); @@ -1193,7 +1476,8 @@ public void onComplete() { // insertingProcessor prepends MINIMAL_TASK, then mock events follow assertEquals(3, results.size()); assertInstanceOf(Task.class, results.get(0), "First event must be initial Task snapshot"); - assertEquals(events.get(0), results.get(1), "Second event should be TaskArtifactUpdateEvent"); + assertEquals( + events.get(0), results.get(1), "Second event should be TaskArtifactUpdateEvent"); assertEquals(events.get(1), results.get(2), "Third event should be TaskStatusUpdateEvent"); } @@ -1201,13 +1485,15 @@ public void onComplete() { public void testOnSubscribeNoExistingTaskError() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); - SubscribeToTaskRequest request = new SubscribeToTaskRequest("1", new TaskIdParams(MINIMAL_TASK.id())); + SubscribeToTaskRequest request = + new SubscribeToTaskRequest("1", new TaskIdParams(MINIMAL_TASK.id())); // Per spec: TaskNotFoundError should be thrown immediately for non-existent tasks // The routing layer will catch this and convert to JSON-RPC error response - TaskNotFoundError thrown = Assertions.assertThrows( - TaskNotFoundError.class, - () -> handler.onSubscribeToTask(request, callContext)); + TaskNotFoundError thrown = + Assertions.assertThrows( + TaskNotFoundError.class, + () -> handler.onSubscribeToTask(request, callContext)); assertNotNull(thrown); } @@ -1217,47 +1503,52 @@ public void testStreamingNotSupportedError() { AgentCard card = createAgentCard(false, true); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); - SendStreamingMessageRequest request = SendStreamingMessageRequest.builder() - .id("1") - .params(MessageSendParams.builder() - .message(MESSAGE) - .configuration(defaultConfiguration()) - .build()) - .build(); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest request = + SendStreamingMessageRequest.builder() + .id("1") + .params( + MessageSendParams.builder() + .message(MESSAGE) + .configuration(defaultConfiguration()) + .build()) + .build(); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); List results = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } - - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item); - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item); + subscription.request(1); + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + } + + @Override + public void onComplete() { + subscription.cancel(); + } + }); assertEquals(1, results.size()); - if (results.get(0).getError() != null && results.get(0).getError() instanceof UnsupportedOperationError ire) { + if (results.get(0).getError() != null + && results.get(0).getError() instanceof UnsupportedOperationError ire) { assertEquals("Streaming is not supported by the agent", ire.getMessage()); } else { fail("Expected a response containing an error"); @@ -1270,41 +1561,45 @@ public void testStreamingNotSupportedErrorOnSubscribeToTask() { AgentCard card = createAgentCard(false, true); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); - SubscribeToTaskRequest request = new SubscribeToTaskRequest("1", new TaskIdParams(MINIMAL_TASK.id())); - Flow.Publisher response = handler.onSubscribeToTask(request, callContext); + SubscribeToTaskRequest request = + new SubscribeToTaskRequest("1", new TaskIdParams(MINIMAL_TASK.id())); + Flow.Publisher response = + handler.onSubscribeToTask(request, callContext); List results = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + } + }); assertEquals(1, results.size()); - if (results.get(0).getError() != null && results.get(0).getError() instanceof UnsupportedOperationError ire) { + if (results.get(0).getError() != null + && results.get(0).getError() instanceof UnsupportedOperationError ire) { assertEquals("Streaming is not supported by the agent", ire.getMessage()); } else { fail("Expected a response containing an error"); @@ -1317,39 +1612,46 @@ public void testPushNotificationsNotSupportedError() { JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - TaskPushNotificationConfig config - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig config = + TaskPushNotificationConfig.builder() .id("c295ea44-7543-4f78-b524-7a38915ad6e4") .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("tenant") .build(); - CreateTaskPushNotificationConfigRequest request = CreateTaskPushNotificationConfigRequest.builder() - .params(config) - .build(); - CreateTaskPushNotificationConfigResponse response = handler.setPushNotificationConfig(request, callContext); + CreateTaskPushNotificationConfigRequest request = + CreateTaskPushNotificationConfigRequest.builder().params(config).build(); + CreateTaskPushNotificationConfigResponse response = + handler.setPushNotificationConfig(request, callContext); assertInstanceOf(PushNotificationNotSupportedError.class, response.getError()); } @Test public void testOnGetPushNotificationNoPushNotifierConfig() { // Create request handler without a push notifier - DefaultRequestHandler requestHandler = DefaultRequestHandler.builder() - .agentExecutor(executor).taskStore(taskStore).queueManager(queueManager) - .mainEventBusProcessor(mainEventBusProcessor) - .executor(internalExecutor).eventConsumerExecutor(internalExecutor) - .authorizationRequired(false) - .build(); + DefaultRequestHandler requestHandler = + DefaultRequestHandler.builder() + .agentExecutor(executor) + .taskStore(taskStore) + .queueManager(queueManager) + .mainEventBusProcessor(mainEventBusProcessor) + .executor(internalExecutor) + .eventConsumerExecutor(internalExecutor) + .authorizationRequired(false) + .build(); AgentCard card = createAgentCard(false, true); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - GetTaskPushNotificationConfigRequest request - = new GetTaskPushNotificationConfigRequest("id", new GetTaskPushNotificationConfigParams( - MINIMAL_TASK.id(),"c295ea44-7543-4f78-b524-7a38915ad6e4")); - GetTaskPushNotificationConfigResponse response = handler.getPushNotificationConfig(request, callContext); + GetTaskPushNotificationConfigRequest request = + new GetTaskPushNotificationConfigRequest( + "id", + new GetTaskPushNotificationConfigParams( + MINIMAL_TASK.id(), "c295ea44-7543-4f78-b524-7a38915ad6e4")); + GetTaskPushNotificationConfigResponse response = + handler.getPushNotificationConfig(request, callContext); assertNotNull(response.getError()); assertInstanceOf(UnsupportedOperationError.class, response.getError()); @@ -1359,29 +1661,33 @@ public void testOnGetPushNotificationNoPushNotifierConfig() { @Test public void testOnSetPushNotificationNoPushNotifierConfig() { // Create request handler without a push notifier - DefaultRequestHandler requestHandler = DefaultRequestHandler.builder() - .agentExecutor(executor).taskStore(taskStore).queueManager(queueManager) - .mainEventBusProcessor(mainEventBusProcessor) - .executor(internalExecutor).eventConsumerExecutor(internalExecutor) - .authorizationRequired(false) - .build(); + DefaultRequestHandler requestHandler = + DefaultRequestHandler.builder() + .agentExecutor(executor) + .taskStore(taskStore) + .queueManager(queueManager) + .mainEventBusProcessor(mainEventBusProcessor) + .executor(internalExecutor) + .eventConsumerExecutor(internalExecutor) + .authorizationRequired(false) + .build(); AgentCard card = createAgentCard(false, true); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - TaskPushNotificationConfig config - = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig config = + TaskPushNotificationConfig.builder() .id("c295ea44-7543-4f78-b524-7a38915ad6e4") .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("tenant") .build(); - CreateTaskPushNotificationConfigRequest request = CreateTaskPushNotificationConfigRequest.builder() - .params(config) - .build(); - CreateTaskPushNotificationConfigResponse response = handler.setPushNotificationConfig(request, callContext); + CreateTaskPushNotificationConfigRequest request = + CreateTaskPushNotificationConfigRequest.builder().params(config).build(); + CreateTaskPushNotificationConfigResponse response = + handler.setPushNotificationConfig(request, callContext); assertInstanceOf(UnsupportedOperationError.class, response.getError()); assertEquals("This operation is not supported", response.getError().getMessage()); @@ -1390,12 +1696,16 @@ public void testOnSetPushNotificationNoPushNotifierConfig() { @Test public void testOnMessageSendInternalError() { DefaultRequestHandler mocked = Mockito.mock(DefaultRequestHandler.class); - Mockito.doThrow(new InternalError("Internal Error")).when(mocked) - .onMessageSend(Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); + Mockito.doThrow(new InternalError("Internal Error")) + .when(mocked) + .onMessageSend( + Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); JSONRPCHandler handler = new JSONRPCHandler(CARD, mocked, internalExecutor); - SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(MESSAGE, defaultConfiguration(), null)); + SendMessageRequest request = + new SendMessageRequest( + "1", new MessageSendParams(MESSAGE, defaultConfiguration(), null)); SendMessageResponse response = handler.onMessageSend(request, callContext); assertInstanceOf(InternalError.class, response.getError()); @@ -1407,59 +1717,69 @@ public void testOnMessageSendSanitizesUnexpectedException() { DefaultRequestHandler mocked = Mockito.mock(DefaultRequestHandler.class); Mockito.doThrow(new RuntimeException("sensitive detail: /var/lib/secret/config.db")) .when(mocked) - .onMessageSend(Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); + .onMessageSend( + Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); JSONRPCHandler handler = new JSONRPCHandler(CARD, mocked, internalExecutor); - SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(MESSAGE, defaultConfiguration(), null)); + SendMessageRequest request = + new SendMessageRequest( + "1", new MessageSendParams(MESSAGE, defaultConfiguration(), null)); SendMessageResponse response = handler.onMessageSend(request, callContext); assertInstanceOf(InternalError.class, response.getError()); assertEquals("Internal error", response.getError().getMessage()); - assertFalse(response.getError().getMessage().contains("sensitive"), + assertFalse( + response.getError().getMessage().contains("sensitive"), "Internal exception message must not be leaked to the client"); } @Test public void testOnMessageStreamInternalError() { DefaultRequestHandler mocked = Mockito.mock(DefaultRequestHandler.class); - Mockito.doThrow(new InternalError("Internal Error")).when(mocked) - .onMessageSendStream(Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); + Mockito.doThrow(new InternalError("Internal Error")) + .when(mocked) + .onMessageSendStream( + Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); JSONRPCHandler handler = new JSONRPCHandler(CARD, mocked, internalExecutor); - SendStreamingMessageRequest request = new SendStreamingMessageRequest("1", new MessageSendParams(MESSAGE, defaultConfiguration(), null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest( + "1", new MessageSendParams(MESSAGE, defaultConfiguration(), null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); List results = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + } + }); assertEquals(1, results.size()); assertInstanceOf(InternalError.class, results.get(0).getError()); @@ -1474,39 +1794,44 @@ public void testDefaultRequestHandlerWithCustomComponents() { @Test public void testOnMessageSendErrorHandling() { - DefaultRequestHandler requestHandler = DefaultRequestHandler.builder() - .agentExecutor(executor).taskStore(taskStore).queueManager(queueManager) - .mainEventBusProcessor(mainEventBusProcessor) - .executor(internalExecutor).eventConsumerExecutor(internalExecutor) - .authorizationRequired(false) - .build(); + DefaultRequestHandler requestHandler = + DefaultRequestHandler.builder() + .agentExecutor(executor) + .taskStore(taskStore) + .queueManager(queueManager) + .mainEventBusProcessor(mainEventBusProcessor) + .executor(internalExecutor) + .eventConsumerExecutor(internalExecutor) + .authorizationRequired(false) + .build(); AgentCard card = createAgentCard(false, true); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); - SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(message, null, null)); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response; - try (MockedConstruction mocked = Mockito.mockConstruction( - ResultAggregator.class, - (mock, context) -> { - Mockito.doThrow( - new UnsupportedOperationError()) - .when(mock).consumeAndBreakOnInterrupt( - Mockito.any(EventConsumer.class), - Mockito.anyBoolean()); - })) { + try (MockedConstruction mocked = + Mockito.mockConstruction( + ResultAggregator.class, + (mock, context) -> { + Mockito.doThrow(new UnsupportedOperationError()) + .when(mock) + .consumeAndBreakOnInterrupt( + Mockito.any(EventConsumer.class), Mockito.anyBoolean()); + })) { response = handler.onMessageSend(request, callContext); } assertInstanceOf(UnsupportedOperationError.class, response.getError()); - } @Test @@ -1514,61 +1839,67 @@ public void testOnMessageSendTaskIdMismatch() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = ((context, agentEmitter) -> { - agentEmitter.emitEvent(MINIMAL_TASK); - }); - SendMessageRequest request = new SendMessageRequest("1", - new MessageSendParams(MESSAGE, defaultConfiguration(), null)); + agentExecutorExecute = + ((context, agentEmitter) -> { + agentEmitter.emitEvent(MINIMAL_TASK); + }); + SendMessageRequest request = + new SendMessageRequest( + "1", new MessageSendParams(MESSAGE, defaultConfiguration(), null)); SendMessageResponse response = handler.onMessageSend(request, callContext); assertInstanceOf(InternalError.class, response.getError()); - } @Test public void testOnMessageStreamTaskIdMismatch() throws InterruptedException { // Note: Do NOT set callback - DefaultRequestHandler has a permanent callback JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); - taskStore.save(MINIMAL_TASK, false); + taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = ((context, agentEmitter) -> { - agentEmitter.emitEvent(MINIMAL_TASK); - }); + agentExecutorExecute = + ((context, agentEmitter) -> { + agentEmitter.emitEvent(MINIMAL_TASK); + }); - SendStreamingMessageRequest request = new SendStreamingMessageRequest("1", new MessageSendParams(MESSAGE, defaultConfiguration(), null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest( + "1", new MessageSendParams(MESSAGE, defaultConfiguration(), null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); CompletableFuture future = new CompletableFuture<>(); List results = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - future.completeExceptionally(throwable); - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + future.completeExceptionally(throwable); + } - @Override - public void onComplete() { - subscription.cancel(); - future.complete(null); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + future.complete(null); + } + }); future.join(); @@ -1581,28 +1912,34 @@ public void onComplete() { public void testListPushNotificationConfig() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; - - TaskPushNotificationConfig taskPushConfig - = TaskPushNotificationConfig.builder() + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() .id(MINIMAL_TASK.id()) .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("tenant") .build(); - CreateTaskPushNotificationConfigRequest request = new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); + CreateTaskPushNotificationConfigRequest request = + new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); handler.setPushNotificationConfig(request, callContext); - TaskPushNotificationConfig result = TaskPushNotificationConfig.builder() + TaskPushNotificationConfig result = + TaskPushNotificationConfig.builder() .id(MINIMAL_TASK.id()) .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("tenant") .build(); - ListTaskPushNotificationConfigsRequest listRequest - = new ListTaskPushNotificationConfigsRequest("111", new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); - ListTaskPushNotificationConfigsResponse listResponse = handler.listPushNotificationConfigs(listRequest, newCallContext()); + ListTaskPushNotificationConfigsRequest listRequest = + new ListTaskPushNotificationConfigsRequest( + "111", new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); + ListTaskPushNotificationConfigsResponse listResponse = + handler.listPushNotificationConfigs(listRequest, newCallContext()); assertEquals("111", listResponse.getId()); assertEquals(1, listResponse.getResult().size()); @@ -1614,24 +1951,28 @@ public void testListPushNotificationConfigNotSupported() { AgentCard card = createAgentCard(true, false); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; - - TaskPushNotificationConfig taskPushConfig - = TaskPushNotificationConfig.builder() + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() .id(MINIMAL_TASK.id()) .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("tenant") .build(); - CreateTaskPushNotificationConfigRequest request = new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); + CreateTaskPushNotificationConfigRequest request = + new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); handler.setPushNotificationConfig(request, callContext); - ListTaskPushNotificationConfigsRequest listRequest - = new ListTaskPushNotificationConfigsRequest("111", new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); - ListTaskPushNotificationConfigsResponse listResponse - = handler.listPushNotificationConfigs(listRequest, callContext); + ListTaskPushNotificationConfigsRequest listRequest = + new ListTaskPushNotificationConfigsRequest( + "111", new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); + ListTaskPushNotificationConfigsResponse listResponse = + handler.listPushNotificationConfigs(listRequest, callContext); assertEquals("111", listResponse.getId()); assertNull(listResponse.getResult()); @@ -1640,22 +1981,29 @@ public void testListPushNotificationConfigNotSupported() { @Test public void testListPushNotificationConfigNoPushConfigStore() { - DefaultRequestHandler requestHandler = DefaultRequestHandler.builder() - .agentExecutor(executor).taskStore(taskStore).queueManager(queueManager) - .mainEventBusProcessor(mainEventBusProcessor) - .executor(internalExecutor).eventConsumerExecutor(internalExecutor) - .authorizationRequired(false) - .build(); + DefaultRequestHandler requestHandler = + DefaultRequestHandler.builder() + .agentExecutor(executor) + .taskStore(taskStore) + .queueManager(queueManager) + .mainEventBusProcessor(mainEventBusProcessor) + .executor(internalExecutor) + .eventConsumerExecutor(internalExecutor) + .authorizationRequired(false) + .build(); JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; - - ListTaskPushNotificationConfigsRequest listRequest - = new ListTaskPushNotificationConfigsRequest("111", new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); - ListTaskPushNotificationConfigsResponse listResponse - = handler.listPushNotificationConfigs(listRequest, callContext); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + ListTaskPushNotificationConfigsRequest listRequest = + new ListTaskPushNotificationConfigsRequest( + "111", new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); + ListTaskPushNotificationConfigsResponse listResponse = + handler.listPushNotificationConfigs(listRequest, callContext); assertEquals("111", listResponse.getId()); assertNull(listResponse.getResult()); @@ -1665,14 +2013,17 @@ public void testListPushNotificationConfigNoPushConfigStore() { @Test public void testListPushNotificationConfigTaskNotFound() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; - - ListTaskPushNotificationConfigsRequest listRequest - = new ListTaskPushNotificationConfigsRequest("111", new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); - ListTaskPushNotificationConfigsResponse listResponse - = handler.listPushNotificationConfigs(listRequest, callContext); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + ListTaskPushNotificationConfigsRequest listRequest = + new ListTaskPushNotificationConfigsRequest( + "111", new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id())); + ListTaskPushNotificationConfigsResponse listResponse = + handler.listPushNotificationConfigs(listRequest, callContext); assertEquals("111", listResponse.getId()); assertNull(listResponse.getResult()); @@ -1683,24 +2034,30 @@ public void testListPushNotificationConfigTaskNotFound() { public void testDeletePushNotificationConfig() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; - - TaskPushNotificationConfig taskPushConfig - = TaskPushNotificationConfig.builder() + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() .id(MINIMAL_TASK.id()) .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("tenant") .build(); - CreateTaskPushNotificationConfigRequest request = new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); + CreateTaskPushNotificationConfigRequest request = + new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); handler.setPushNotificationConfig(request, callContext); - DeleteTaskPushNotificationConfigRequest deleteRequest - = new DeleteTaskPushNotificationConfigRequest("111", new DeleteTaskPushNotificationConfigParams(MINIMAL_TASK.id(), MINIMAL_TASK.id())); - DeleteTaskPushNotificationConfigResponse deleteResponse - = handler.deletePushNotificationConfig(deleteRequest, newCallContext()); + DeleteTaskPushNotificationConfigRequest deleteRequest = + new DeleteTaskPushNotificationConfigRequest( + "111", + new DeleteTaskPushNotificationConfigParams( + MINIMAL_TASK.id(), MINIMAL_TASK.id())); + DeleteTaskPushNotificationConfigResponse deleteResponse = + handler.deletePushNotificationConfig(deleteRequest, newCallContext()); assertEquals("111", deleteResponse.getId()); assertNull(deleteResponse.getError()); @@ -1712,24 +2069,30 @@ public void testDeletePushNotificationConfigNotSupported() { AgentCard card = createAgentCard(true, false); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; - - TaskPushNotificationConfig taskPushConfig - = TaskPushNotificationConfig.builder() + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() .id(MINIMAL_TASK.id()) .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("tenant") .build(); - CreateTaskPushNotificationConfigRequest request = new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); + CreateTaskPushNotificationConfigRequest request = + new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); handler.setPushNotificationConfig(request, callContext); - DeleteTaskPushNotificationConfigRequest deleteRequest - = new DeleteTaskPushNotificationConfigRequest("111", new DeleteTaskPushNotificationConfigParams(MINIMAL_TASK.id(), MINIMAL_TASK.id())); - DeleteTaskPushNotificationConfigResponse deleteResponse - = handler.deletePushNotificationConfig(deleteRequest, newCallContext()); + DeleteTaskPushNotificationConfigRequest deleteRequest = + new DeleteTaskPushNotificationConfigRequest( + "111", + new DeleteTaskPushNotificationConfigParams( + MINIMAL_TASK.id(), MINIMAL_TASK.id())); + DeleteTaskPushNotificationConfigResponse deleteResponse = + handler.deletePushNotificationConfig(deleteRequest, newCallContext()); assertEquals("111", deleteResponse.getId()); assertNull(deleteResponse.getResult()); @@ -1740,31 +2103,40 @@ public void testDeletePushNotificationConfigNotSupported() { public void testDeletePushNotificationConfigNoPushConfigStore() { DefaultRequestHandler requestHandler = DefaultRequestHandler.builder() - .agentExecutor(executor).taskStore(taskStore).queueManager(queueManager) - .mainEventBusProcessor(mainEventBusProcessor) - .executor(internalExecutor).eventConsumerExecutor(internalExecutor) - .authorizationRequired(false) - .build(); + .agentExecutor(executor) + .taskStore(taskStore) + .queueManager(queueManager) + .mainEventBusProcessor(mainEventBusProcessor) + .executor(internalExecutor) + .eventConsumerExecutor(internalExecutor) + .authorizationRequired(false) + .build(); JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.emitEvent(context.getTask() != null ? context.getTask() : context.getMessage()); - }; - - TaskPushNotificationConfig taskPushConfig - = TaskPushNotificationConfig.builder() + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.emitEvent( + context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + TaskPushNotificationConfig.builder() .id(MINIMAL_TASK.id()) .taskId(MINIMAL_TASK.id()) .url("http://example.com") .tenant("tenant") .build(); - CreateTaskPushNotificationConfigRequest request = new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); + CreateTaskPushNotificationConfigRequest request = + new CreateTaskPushNotificationConfigRequest("1", taskPushConfig); handler.setPushNotificationConfig(request, callContext); - DeleteTaskPushNotificationConfigRequest deleteRequest - = new DeleteTaskPushNotificationConfigRequest("111", new DeleteTaskPushNotificationConfigParams(MINIMAL_TASK.id(), MINIMAL_TASK.id())); - DeleteTaskPushNotificationConfigResponse deleteResponse - = handler.deletePushNotificationConfig(deleteRequest, newCallContext()); + DeleteTaskPushNotificationConfigRequest deleteRequest = + new DeleteTaskPushNotificationConfigRequest( + "111", + new DeleteTaskPushNotificationConfigParams( + MINIMAL_TASK.id(), MINIMAL_TASK.id())); + DeleteTaskPushNotificationConfigResponse deleteResponse = + handler.deletePushNotificationConfig(deleteRequest, newCallContext()); assertEquals("111", deleteResponse.getId()); assertNull(deleteResponse.getResult()); @@ -1775,7 +2147,8 @@ public void testDeletePushNotificationConfigNoPushConfigStore() { public void testOnGetExtendedAgentCard() throws Exception { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); GetExtendedAgentCardRequest request = new GetExtendedAgentCardRequest("1"); - GetExtendedAgentCardResponse response = handler.onGetExtendedCardRequest(request, callContext); + GetExtendedAgentCardResponse response = + handler.onGetExtendedCardRequest(request, callContext); assertEquals(request.getId(), response.getId()); assertInstanceOf(UnsupportedOperationError.class, response.getError()); assertNull(response.getResult()); @@ -1783,19 +2156,29 @@ public void testOnGetExtendedAgentCard() throws Exception { @Test public void testExtendedAgentCardWithRouterKnownTenant() throws Exception { - AgentCard cardWithExtCapability = AgentCard.builder(CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); + AgentCard cardWithExtCapability = + AgentCard.builder(CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); AgentCard tenantCard = AgentCard.builder(cardWithExtCapability).name("acme-card").build(); - AgentCardRouter router = tenant -> "acme".equals(tenant) ? tenantCard : cardWithExtCapability; - - JSONRPCHandler handler = new JSONRPCHandler(new FixedInstance<>(cardWithExtCapability), null, - requestHandler, internalExecutor, new FixedInstance<>(router)); - - GetExtendedAgentCardRequest request = GetExtendedAgentCardRequest.builder() - .id("1") - .params(new GetExtendedAgentCardParams("acme")) - .build(); - GetExtendedAgentCardResponse response = handler.onGetExtendedCardRequest(request, callContext); + AgentCardRouter router = + tenant -> "acme".equals(tenant) ? tenantCard : cardWithExtCapability; + + JSONRPCHandler handler = + new JSONRPCHandler( + new FixedInstance<>(cardWithExtCapability), + null, + requestHandler, + internalExecutor, + new FixedInstance<>(router)); + + GetExtendedAgentCardRequest request = + GetExtendedAgentCardRequest.builder() + .id("1") + .params(new GetExtendedAgentCardParams("acme")) + .build(); + GetExtendedAgentCardResponse response = + handler.onGetExtendedCardRequest(request, callContext); assertNull(response.getError()); assertNotNull(response.getResult()); @@ -1804,28 +2187,38 @@ public void testExtendedAgentCardWithRouterKnownTenant() throws Exception { @Test public void testExtendedAgentCardWithRouterReturnsNull() throws Exception { - AgentCard cardWithExtCapability = AgentCard.builder(CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); - AgentCardRouter router = new AgentCardRouter() { - @Override - public AgentCard resolveExtendedCard(String tenant) { - return null; - } - - @Override - public AgentCard resolvePublicCard(String tenant) { - return cardWithExtCapability; - } - }; - - JSONRPCHandler handler = new JSONRPCHandler(new FixedInstance<>(cardWithExtCapability), null, - requestHandler, internalExecutor, new FixedInstance<>(router)); + AgentCard cardWithExtCapability = + AgentCard.builder(CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); + AgentCardRouter router = + new AgentCardRouter() { + @Override + public AgentCard resolveExtendedCard(String tenant) { + return null; + } - GetExtendedAgentCardRequest request = GetExtendedAgentCardRequest.builder() - .id("1") - .params(new GetExtendedAgentCardParams("acme")) - .build(); - GetExtendedAgentCardResponse response = handler.onGetExtendedCardRequest(request, callContext); + @Override + public AgentCard resolvePublicCard(String tenant) { + return cardWithExtCapability; + } + }; + + JSONRPCHandler handler = + new JSONRPCHandler( + new FixedInstance<>(cardWithExtCapability), + null, + requestHandler, + internalExecutor, + new FixedInstance<>(router)); + + GetExtendedAgentCardRequest request = + GetExtendedAgentCardRequest.builder() + .id("1") + .params(new GetExtendedAgentCardParams("acme")) + .build(); + GetExtendedAgentCardResponse response = + handler.onGetExtendedCardRequest(request, callContext); assertInstanceOf(ExtendedAgentCardNotConfiguredError.class, response.getError()); assertNull(response.getResult()); @@ -1833,16 +2226,25 @@ public AgentCard resolvePublicCard(String tenant) { @Test public void testExtendedAgentCardWithoutRouter() throws Exception { - AgentCard cardWithExtCapability = AgentCard.builder(CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); - AgentCard extended = AgentCard.builder(cardWithExtCapability).description("extended").build(); + AgentCard cardWithExtCapability = + AgentCard.builder(CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); + AgentCard extended = + AgentCard.builder(cardWithExtCapability).description("extended").build(); Instance extendedInstance = new FixedInstance<>(extended); - JSONRPCHandler handler = new JSONRPCHandler( - new FixedInstance<>(cardWithExtCapability), extendedInstance, requestHandler, internalExecutor, null); + JSONRPCHandler handler = + new JSONRPCHandler( + new FixedInstance<>(cardWithExtCapability), + extendedInstance, + requestHandler, + internalExecutor, + null); GetExtendedAgentCardRequest request = new GetExtendedAgentCardRequest("1"); - GetExtendedAgentCardResponse response = handler.onGetExtendedCardRequest(request, callContext); + GetExtendedAgentCardResponse response = + handler.onGetExtendedCardRequest(request, callContext); assertNull(response.getError()); assertNotNull(response.getResult()); @@ -1859,57 +2261,63 @@ public void testStreamingDoesNotBlockMainThread() throws Exception { CountDownLatch streamStarted = new CountDownLatch(1); CountDownLatch eventProcessed = new CountDownLatch(1); - agentExecutorExecute = (context, agentEmitter) -> { - // Wait a bit to ensure the main thread continues - try { - Thread.sleep(100); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + // Wait a bit to ensure the main thread continues + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + agentEmitter.sendMessage(context.getMessage()); + }; - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendStreamingMessageRequest request = new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); // Start streaming - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); - response.subscribe(new Flow.Subscriber() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - streamStarted.countDown(); - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + streamStarted.countDown(); + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - eventReceived.set(true); - eventProcessed.countDown(); - subscription.cancel(); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + eventReceived.set(true); + eventProcessed.countDown(); + subscription.cancel(); + } - @Override - public void onError(Throwable throwable) { - subscription.cancel(); - eventProcessed.countDown(); - } + @Override + public void onError(Throwable throwable) { + subscription.cancel(); + eventProcessed.countDown(); + } - @Override - public void onComplete() { - subscription.cancel(); - eventProcessed.countDown(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + eventProcessed.countDown(); + } + }); // The main thread should not be blocked - we should be able to continue immediately - assertTrue(streamStarted.await(100, TimeUnit.MILLISECONDS), + assertTrue( + streamStarted.await(100, TimeUnit.MILLISECONDS), "Streaming subscription should start quickly without blocking main thread"); // This proves the main thread is not blocked - we can do other work @@ -1919,7 +2327,8 @@ public void onComplete() { mainThreadBlocked.set(false); // If we get here, main thread was not blocked // Wait for the actual event processing to complete - assertTrue(eventProcessed.await(2, TimeUnit.SECONDS), + assertTrue( + eventProcessed.await(2, TimeUnit.SECONDS), "Event should be processed within reasonable time"); // Verify we received the event and main thread was not blocked @@ -1930,34 +2339,42 @@ public void onComplete() { @Test public void testExtensionSupportRequiredErrorOnMessageSend() { // Create AgentCard with a required extension - AgentCard cardWithExtension = AgentCard.builder() - .name("test-card") - .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("JSONRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extensions(List.of( - AgentExtension.builder() - .uri("https://example.com/test-extension") - .required(true) - .build() - )) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard cardWithExtension = + AgentCard.builder() + .name("test-card") + .description("Test card with required extension") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("JSONRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extensions( + List.of( + AgentExtension.builder() + .uri( + "https://example.com/test-extension") + .required(true) + .build())) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); - JSONRPCHandler handler = new JSONRPCHandler(cardWithExtension, requestHandler, internalExecutor); + JSONRPCHandler handler = + new JSONRPCHandler(cardWithExtension, requestHandler, internalExecutor); // Use callContext which has empty requestedExtensions set - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(message, null, null)); + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response = handler.onMessageSend(request, callContext); assertInstanceOf(ExtensionSupportRequiredError.class, response.getError()); @@ -1968,116 +2385,139 @@ public void testExtensionSupportRequiredErrorOnMessageSend() { @Test public void testExtensionSupportRequiredErrorOnMessageSendStream() { // Create AgentCard with a required extension - AgentCard cardWithExtension = AgentCard.builder() - .name("test-card") - .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("JSONRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extensions(List.of( - AgentExtension.builder() - .uri("https://example.com/streaming-extension") - .required(true) - .build() - )) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard cardWithExtension = + AgentCard.builder() + .name("test-card") + .description("Test card with required extension") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("JSONRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extensions( + List.of( + AgentExtension.builder() + .uri( + "https://example.com/streaming-extension") + .required(true) + .build())) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); - JSONRPCHandler handler = new JSONRPCHandler(cardWithExtension, requestHandler, internalExecutor); + JSONRPCHandler handler = + new JSONRPCHandler(cardWithExtension, requestHandler, internalExecutor); - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendStreamingMessageRequest request = new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, callContext); + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, callContext); List results = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); - response.subscribe(new Flow.Subscriber() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item); - subscription.request(1); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item); + subscription.request(1); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - subscription.cancel(); - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + subscription.cancel(); + } - @Override - public void onComplete() { - subscription.cancel(); - } - }); + @Override + public void onComplete() { + subscription.cancel(); + } + }); assertEquals(1, results.size()); assertInstanceOf(ExtensionSupportRequiredError.class, results.get(0).getError()); - assertTrue(results.get(0).getError().getMessage().contains("https://example.com/streaming-extension")); + assertTrue( + results.get(0) + .getError() + .getMessage() + .contains("https://example.com/streaming-extension")); assertNull(results.get(0).getResult()); } @Test public void testRequiredExtensionProvidedSuccess() { // Create AgentCard with a required extension - AgentCard cardWithExtension = AgentCard.builder() - .name("test-card") - .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("JSONRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extensions(List.of( - AgentExtension.builder() - .uri("https://example.com/required-extension") - .required(true) - .build() - )) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard cardWithExtension = + AgentCard.builder() + .name("test-card") + .description("Test card with required extension") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("JSONRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extensions( + List.of( + AgentExtension.builder() + .uri( + "https://example.com/required-extension") + .required(true) + .build())) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); - JSONRPCHandler handler = new JSONRPCHandler(cardWithExtension, requestHandler, internalExecutor); + JSONRPCHandler handler = + new JSONRPCHandler(cardWithExtension, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); // Create context WITH the required extension Set requestedExtensions = new HashSet<>(); requestedExtensions.add("https://example.com/required-extension"); - ServerCallContext contextWithExtension = new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("foo", "bar"), - requestedExtensions, - "1.0" - ); - - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; - - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(message, null, null)); + ServerCallContext contextWithExtension = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + requestedExtensions, + "1.0"); + + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; + + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response = handler.onMessageSend(request, contextWithExtension); // Should succeed without ExtensionSupportRequiredError @@ -2088,35 +2528,42 @@ public void testRequiredExtensionProvidedSuccess() { @Test public void testVersionNotSupportedErrorOnMessageSend() { // Create AgentCard with protocol version 1.0 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("JSONRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("JSONRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); JSONRPCHandler handler = new JSONRPCHandler(agentCard, requestHandler, internalExecutor); // Create context with incompatible version 2.0 - ServerCallContext contextWithVersion = new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("foo", "bar"), - new HashSet<>(), - "2.0" // Incompatible version - ); - - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(message, null, null)); + ServerCallContext contextWithVersion = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + new HashSet<>(), + "2.0" // Incompatible version + ); + + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response = handler.onMessageSend(request, contextWithVersion); assertInstanceOf(VersionNotSupportedError.class, response.getError()); @@ -2127,71 +2574,81 @@ public void testVersionNotSupportedErrorOnMessageSend() { @Test public void testVersionNotSupportedErrorOnMessageSendStream() throws Exception { // Create AgentCard with protocol version 1.0 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("JSONRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("JSONRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); JSONRPCHandler handler = new JSONRPCHandler(agentCard, requestHandler, internalExecutor); // Create context with incompatible version 2.0 - ServerCallContext contextWithVersion = new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("foo", "bar"), - new HashSet<>(), - "2.0" // Incompatible version - ); - - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendStreamingMessageRequest request = new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); - Flow.Publisher response = handler.onMessageSendStream(request, contextWithVersion); + ServerCallContext contextWithVersion = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + new HashSet<>(), + "2.0" // Incompatible version + ); + + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendStreamingMessageRequest request = + new SendStreamingMessageRequest("1", new MessageSendParams(message, null, null)); + Flow.Publisher response = + handler.onMessageSendStream(request, contextWithVersion); List results = new ArrayList<>(); AtomicReference error = new AtomicReference<>(); CountDownLatch latch = new CountDownLatch(1); - response.subscribe(new Flow.Subscriber() { - private Flow.Subscription subscription; + response.subscribe( + new Flow.Subscriber() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item); - subscription.request(1); - latch.countDown(); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item); + subscription.request(1); + latch.countDown(); + } - @Override - public void onError(Throwable throwable) { - error.set(throwable); - latch.countDown(); - } + @Override + public void onError(Throwable throwable) { + error.set(throwable); + latch.countDown(); + } - @Override - public void onComplete() { - latch.countDown(); - } - }); + @Override + public void onComplete() { + latch.countDown(); + } + }); // Wait for async processing - assertTrue(latch.await(2, TimeUnit.SECONDS), "Expected to receive error event within timeout"); + assertTrue( + latch.await(2, TimeUnit.SECONDS), "Expected to receive error event within timeout"); assertEquals(1, results.size()); SendStreamingMessageResponse result = results.get(0); @@ -2203,40 +2660,48 @@ public void onComplete() { @Test public void testCompatibleVersionSuccess() { // Create AgentCard with protocol version 1.0 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("JSONRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("JSONRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); JSONRPCHandler handler = new JSONRPCHandler(agentCard, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); // Create context with compatible version 1.1 - ServerCallContext contextWithVersion = new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("foo", "bar"), - new HashSet<>(), - "1.1" // Compatible version (same major version) - ); - - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; - - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(message, null, null)); + ServerCallContext contextWithVersion = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + new HashSet<>(), + "1.1" // Compatible version (same major version) + ); + + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; + + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response = handler.onMessageSend(request, contextWithVersion); // Should succeed without error @@ -2247,36 +2712,44 @@ public void testCompatibleVersionSuccess() { @Test public void testNoVersionDefaultsTo0_3_RejectedByV10OnlyServer() { // Per spec Section 3.6.2: missing A2A-Version defaults to 0.3 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("JSONRPC", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("JSONRPC", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); JSONRPCHandler handler = new JSONRPCHandler(agentCard, requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; // Context with no version — defaults to 0.3, which is incompatible with v1.0-only server - ServerCallContext noVersionContext = new ServerCallContext( - UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>()); + ServerCallContext noVersionContext = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>()); - Message message = Message.builder(MESSAGE) - .taskId(MINIMAL_TASK.id()) - .contextId(MINIMAL_TASK.contextId()) - .build(); - SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(message, null, null)); + Message message = + Message.builder(MESSAGE) + .taskId(MINIMAL_TASK.id()) + .contextId(MINIMAL_TASK.contextId()) + .build(); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response = handler.onMessageSend(request, noVersionContext); // Should return VersionNotSupportedError @@ -2290,10 +2763,8 @@ public void testListTasksEmptyResultIncludesAllFields() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler, internalExecutor); // Query for a context that doesn't exist - should return empty result - ListTasksParams params = ListTasksParams.builder() - .contextId("nonexistent-context-id") - .tenant("") - .build(); + ListTasksParams params = + ListTasksParams.builder().contextId("nonexistent-context-id").tenant("").build(); ListTasksRequest request = new ListTasksRequest("1", params); ListTasksResponse response = handler.onListTasks(request, callContext); @@ -2312,7 +2783,8 @@ public void testListTasksEmptyResultIncludesAllFields() { void constructorDoesNotResolveAgentCardInstances() { Instance throwOnGet = TestInstances.throwOnGet(); - assertDoesNotThrow(() -> new JSONRPCHandler(throwOnGet, null, requestHandler, internalExecutor, null)); + assertDoesNotThrow( + () -> new JSONRPCHandler(throwOnGet, null, requestHandler, internalExecutor, null)); } @Test @@ -2332,8 +2804,11 @@ public void testVersionNotSupportedErrorOnListTasks() throws Exception { JSONRPCHandler handler = versionTestHandler(); taskStore.save(MINIMAL_TASK, false); - ListTasksParams params = ListTasksParams.builder().contextId(MINIMAL_TASK.contextId()).tenant("").build(); - ListTasksResponse response = handler.onListTasks(new ListTasksRequest("1", params), incompatibleVersionContext()); + ListTasksParams params = + ListTasksParams.builder().contextId(MINIMAL_TASK.contextId()).tenant("").build(); + ListTasksResponse response = + handler.onListTasks( + new ListTasksRequest("1", params), incompatibleVersionContext()); assertVersionRejected(response.getError()); assertNull(response.getResult()); @@ -2348,7 +2823,8 @@ public void testVersionNotSupportedErrorOnCancelTask() throws Exception { // waits forever for a final event rather than failing the assertion. agentExecutorCancel = (context, agentEmitter) -> agentEmitter.cancel(); - CancelTaskRequest request = new CancelTaskRequest("1", new CancelTaskParams(MINIMAL_TASK.id())); + CancelTaskRequest request = + new CancelTaskRequest("1", new CancelTaskParams(MINIMAL_TASK.id())); CancelTaskResponse response = handler.onCancelTask(request, incompatibleVersionContext()); assertVersionRejected(response.getError()); @@ -2360,14 +2836,16 @@ public void testVersionNotSupportedErrorOnSetPushNotificationConfig() throws Exc JSONRPCHandler handler = versionTestHandler(); taskStore.save(MINIMAL_TASK, false); - TaskPushNotificationConfig config = TaskPushNotificationConfig.builder() - .id(MINIMAL_TASK.id()) - .taskId(MINIMAL_TASK.id()) - .url("http://example.com") - .build(); + TaskPushNotificationConfig config = + TaskPushNotificationConfig.builder() + .id(MINIMAL_TASK.id()) + .taskId(MINIMAL_TASK.id()) + .url("http://example.com") + .build(); CreateTaskPushNotificationConfigResponse response = handler.setPushNotificationConfig( - new CreateTaskPushNotificationConfigRequest("1", config), incompatibleVersionContext()); + new CreateTaskPushNotificationConfigRequest("1", config), + incompatibleVersionContext()); assertVersionRejected(response.getError()); assertNull(response.getResult()); @@ -2382,7 +2860,8 @@ public void testVersionNotSupportedErrorOnGetPushNotificationConfig() throws Exc new GetTaskPushNotificationConfigParams(MINIMAL_TASK.id(), MINIMAL_TASK.id()); GetTaskPushNotificationConfigResponse response = handler.getPushNotificationConfig( - new GetTaskPushNotificationConfigRequest("1", params), incompatibleVersionContext()); + new GetTaskPushNotificationConfigRequest("1", params), + incompatibleVersionContext()); assertVersionRejected(response.getError()); assertNull(response.getResult()); @@ -2397,7 +2876,8 @@ public void testVersionNotSupportedErrorOnListPushNotificationConfigs() throws E new ListTaskPushNotificationConfigsParams(MINIMAL_TASK.id()); ListTaskPushNotificationConfigsResponse response = handler.listPushNotificationConfigs( - new ListTaskPushNotificationConfigsRequest("1", params), incompatibleVersionContext()); + new ListTaskPushNotificationConfigsRequest("1", params), + incompatibleVersionContext()); assertVersionRejected(response.getError()); assertNull(response.getResult()); @@ -2412,7 +2892,8 @@ public void testVersionNotSupportedErrorOnDeletePushNotificationConfig() throws new DeleteTaskPushNotificationConfigParams(MINIMAL_TASK.id(), MINIMAL_TASK.id()); DeleteTaskPushNotificationConfigResponse response = handler.deletePushNotificationConfig( - new DeleteTaskPushNotificationConfigRequest("1", params), incompatibleVersionContext()); + new DeleteTaskPushNotificationConfigRequest("1", params), + incompatibleVersionContext()); assertVersionRejected(response.getError()); } @@ -2422,11 +2903,17 @@ public void testVersionNotSupportedErrorOnGetExtendedCard() throws Exception { AgentCard extended = AgentCard.builder(versionTestCard()).description("extended").build(); Instance extendedInstance = new FixedInstance<>(extended); - JSONRPCHandler handler = new JSONRPCHandler( - new FixedInstance<>(versionTestCard()), extendedInstance, requestHandler, internalExecutor, null); + JSONRPCHandler handler = + new JSONRPCHandler( + new FixedInstance<>(versionTestCard()), + extendedInstance, + requestHandler, + internalExecutor, + null); GetExtendedAgentCardResponse response = - handler.onGetExtendedCardRequest(new GetExtendedAgentCardRequest("1"), incompatibleVersionContext()); + handler.onGetExtendedCardRequest( + new GetExtendedAgentCardRequest("1"), incompatibleVersionContext()); assertVersionRejected(response.getError()); assertNull(response.getResult()); @@ -2437,7 +2924,8 @@ public void testVersionNotSupportedErrorOnSubscribeToTask() throws Exception { JSONRPCHandler handler = versionTestHandler(); taskStore.save(MINIMAL_TASK, false); - SubscribeToTaskRequest request = new SubscribeToTaskRequest("1", new TaskIdParams(MINIMAL_TASK.id())); + SubscribeToTaskRequest request = + new SubscribeToTaskRequest("1", new TaskIdParams(MINIMAL_TASK.id())); Flow.Publisher publisher = handler.onSubscribeToTask(request, incompatibleVersionContext()); @@ -2456,13 +2944,16 @@ private static AgentCard versionTestCard() { return AgentCard.builder() .name("test-card") .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("JSONRPC", "http://localhost:9999"))) + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("JSONRPC", "http://localhost:9999"))) .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extendedAgentCard(true) - .build()) + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extendedAgentCard(true) + .build()) .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) .skills(List.of()) @@ -2478,11 +2969,13 @@ private JSONRPCHandler versionTestHandler() { * {@link #versionTestCard()} and is therefore incompatible under section 3.6.2. */ private static ServerCallContext incompatibleVersionContext() { - return new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "2.0"); + return new ServerCallContext( + UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "2.0"); } private static ServerCallContext newCallContext() { - return new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "1.0"); + return new ServerCallContext( + UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "1.0"); } /** @@ -2508,60 +3001,84 @@ private static List drainStream( Flow.Publisher publisher) throws InterruptedException { List results = new ArrayList<>(); CountDownLatch latch = new CountDownLatch(1); - publisher.subscribe(new Flow.Subscriber<>() { - private Flow.Subscription subscription; + publisher.subscribe( + new Flow.Subscriber<>() { + private Flow.Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(SendStreamingMessageResponse item) { - results.add(item); - subscription.request(1); - latch.countDown(); - } + @Override + public void onNext(SendStreamingMessageResponse item) { + results.add(item); + subscription.request(1); + latch.countDown(); + } - @Override - public void onError(Throwable throwable) { - latch.countDown(); - } + @Override + public void onError(Throwable throwable) { + latch.countDown(); + } - @Override - public void onComplete() { - latch.countDown(); - } - }); + @Override + public void onComplete() { + latch.countDown(); + } + }); assertTrue(latch.await(2, TimeUnit.SECONDS), "Expected an event within timeout"); return results; } @Test public void testGetAgentCardReturnsNullWhenNoPublicCard() { - AgentCard extendedCard = AgentCard.builder(CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).streaming(true).build()).build(); - JSONRPCHandler handler = new JSONRPCHandler( - FixedInstance.empty(), new FixedInstance<>(extendedCard), requestHandler, internalExecutor, null); + AgentCard extendedCard = + AgentCard.builder(CARD) + .capabilities( + AgentCapabilities.builder() + .extendedAgentCard(true) + .streaming(true) + .build()) + .build(); + JSONRPCHandler handler = + new JSONRPCHandler( + FixedInstance.empty(), + new FixedInstance<>(extendedCard), + requestHandler, + internalExecutor, + null); assertNull(handler.getAgentCard()); } @Test public void testCapabilityChecksFallBackToExtendedCard() throws Exception { - AgentCard extendedCard = AgentCard.builder(CARD) - .capabilities(AgentCapabilities.builder().streaming(true).pushNotifications(true).build()).build(); - JSONRPCHandler handler = new JSONRPCHandler( - FixedInstance.empty(), new FixedInstance<>(extendedCard), requestHandler, internalExecutor, null); - - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + AgentCard extendedCard = + AgentCard.builder(CARD) + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .build()) + .build(); + JSONRPCHandler handler = + new JSONRPCHandler( + FixedInstance.empty(), + new FixedInstance<>(extendedCard), + requestHandler, + internalExecutor, + null); + + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; taskStore.save(MINIMAL_TASK, false); Message message = Message.builder(MESSAGE).contextId(MINIMAL_TASK.contextId()).build(); - SendMessageRequest request = new SendMessageRequest("1", - new MessageSendParams(message, null, null)); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response = handler.onMessageSend(request, callContext); assertNull(response.getError()); @@ -2569,12 +3086,17 @@ public void testCapabilityChecksFallBackToExtendedCard() throws Exception { @Test public void testNoCardAtAllThrowsIllegalStateException() { - JSONRPCHandler handler = new JSONRPCHandler( - FixedInstance.empty(), FixedInstance.empty(), requestHandler, internalExecutor, null); + JSONRPCHandler handler = + new JSONRPCHandler( + FixedInstance.empty(), + FixedInstance.empty(), + requestHandler, + internalExecutor, + null); Message message = Message.builder(MESSAGE).contextId("ctx").build(); - SendMessageRequest request = new SendMessageRequest("1", - new MessageSendParams(message, null, null)); + SendMessageRequest request = + new SendMessageRequest("1", new MessageSendParams(message, null, null)); SendMessageResponse response = handler.onMessageSend(request, callContext); assertNotNull(response.getError()); diff --git a/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCTestTransportMetadata.java b/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCTestTransportMetadata.java index 09d9a344c..f5a7b5f63 100644 --- a/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCTestTransportMetadata.java +++ b/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCTestTransportMetadata.java @@ -8,5 +8,4 @@ public class JSONRPCTestTransportMetadata implements TransportMetadata { public String getTransportProtocol() { return TransportProtocol.JSONRPC.asString(); } - } diff --git a/transport/rest/pom.xml b/transport/rest/pom.xml index 2636bde37..9ccb8031e 100644 --- a/transport/rest/pom.xml +++ b/transport/rest/pom.xml @@ -1,7 +1,5 @@ - - + + 4.0.0 @@ -73,4 +71,4 @@ - \ No newline at end of file + diff --git a/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/context/RestContextKeys.java b/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/context/RestContextKeys.java index 71035d7d3..92d4e60e0 100644 --- a/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/context/RestContextKeys.java +++ b/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/context/RestContextKeys.java @@ -19,7 +19,7 @@ * @see org.a2aproject.sdk.server.ServerCallContext */ public final class RestContextKeys { - + /** * Context key for storing the headers. */ @@ -29,6 +29,7 @@ public final class RestContextKeys { * Context key for storing the method name being called. */ public static final String METHOD_NAME_KEY = "method"; + /** * Context key for storing the tenant identifier extracted from the request path. */ diff --git a/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java b/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java index ae48c890a..01a82dacb 100644 --- a/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java +++ b/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java @@ -1,12 +1,19 @@ package org.a2aproject.sdk.transport.rest.handler; +import static org.a2aproject.sdk.common.MediaType.APPLICATION_JSON; import static org.a2aproject.sdk.server.AgentCardOperation.EXTENDED_CARD; import static org.a2aproject.sdk.server.AgentCardOperation.PUSH_NOTIFICATIONS; import static org.a2aproject.sdk.server.AgentCardOperation.STREAMING; - -import static org.a2aproject.sdk.common.MediaType.APPLICATION_JSON; import static org.a2aproject.sdk.server.util.async.AsyncUtils.createTubeConfig; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; import java.time.Instant; import java.time.format.DateTimeParseException; import java.util.Arrays; @@ -19,16 +26,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.inject.Any; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; - -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.util.JsonFormat; import mutiny.zero.ZeroPublisher; import org.a2aproject.sdk.grpc.utils.ProtoJsonUtils; import org.a2aproject.sdk.grpc.utils.ProtoUtils; @@ -142,7 +139,8 @@ public class RestHandler { private RequestHandler requestHandler; private Executor executor; private final CachingAgentCardTransportValidator transportValidator = - new CachingAgentCardTransportValidator(AgentCardValidator::validateTransportConfiguration); + new CachingAgentCardTransportValidator( + AgentCardValidator::validateTransportConfiguration); /** * No-args constructor for CDI proxy creation. @@ -166,12 +164,18 @@ protected RestHandler() { * @param agentCardRouterInstance optional agent card router instance for multitenancy */ @Inject - public RestHandler(@PublicAgentCard Instance agentCardInstance, + public RestHandler( + @PublicAgentCard Instance agentCardInstance, @ExtendedAgentCard Instance extendedAgentCard, - AgentCardCacheMetadata cacheMetadata, RequestHandler requestHandler, @Internal Executor executor, + AgentCardCacheMetadata cacheMetadata, + RequestHandler requestHandler, + @Internal Executor executor, @Any @Nullable Instance agentCardRouterInstance) { - this.agentCardResolver = AgentCardResolver.of(() -> CdiUtils.resolveDefault(agentCardInstance), - () -> CdiUtils.resolveDefault(extendedAgentCard), CdiUtils.getIfResolvable(agentCardRouterInstance)); + this.agentCardResolver = + AgentCardResolver.of( + () -> CdiUtils.resolveDefault(agentCardInstance), + () -> CdiUtils.resolveDefault(extendedAgentCard), + CdiUtils.getIfResolvable(agentCardRouterInstance)); this.agentCardPolicy = new AgentCardRequestPolicy(agentCardResolver); this.cacheMetadata = cacheMetadata; this.requestHandler = requestHandler; @@ -186,10 +190,18 @@ public RestHandler(@PublicAgentCard Instance agentCardInstance, * @param requestHandler the handler for processing A2A requests * @param executor the executor for asynchronous operations */ - public RestHandler(@Nullable AgentCard agentCard, AgentCardCacheMetadata cacheMetadata, - RequestHandler requestHandler, Executor executor) { - this(agentCard != null ? new FixedInstance<>(agentCard) : FixedInstance.empty(), FixedInstance.empty(), - cacheMetadata, requestHandler, executor, null); + public RestHandler( + @Nullable AgentCard agentCard, + AgentCardCacheMetadata cacheMetadata, + RequestHandler requestHandler, + Executor executor) { + this( + agentCard != null ? new FixedInstance<>(agentCard) : FixedInstance.empty(), + FixedInstance.empty(), + cacheMetadata, + requestHandler, + executor, + null); } /** @@ -240,12 +252,19 @@ public RestHandler(@Nullable AgentCard agentCard, AgentCardCacheMetadata cacheMe */ public HTTPRestResponse sendMessage(ServerCallContext context, String tenant, String body) { try { - EffectiveAgentCard effectiveCard = preflight(tenant, context, AgentCardOperation.none()); - org.a2aproject.sdk.grpc.SendMessageRequest.Builder request = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); + EffectiveAgentCard effectiveCard = + preflight(tenant, context, AgentCardOperation.none()); + org.a2aproject.sdk.grpc.SendMessageRequest.Builder request = + org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); parseRequestBody(body, request); request.setTenant(Utils.defaultIfNull(effectiveCard.tenant(), "")); - EventKind result = requestHandler.onMessageSend(ProtoUtils.FromProto.messageSendParams(request), context); - return createSuccessResponse(200, org.a2aproject.sdk.grpc.SendMessageResponse.newBuilder(ProtoUtils.ToProto.taskOrMessage(result))); + EventKind result = + requestHandler.onMessageSend( + ProtoUtils.FromProto.messageSendParams(request), context); + return createSuccessResponse( + 200, + org.a2aproject.sdk.grpc.SendMessageResponse.newBuilder( + ProtoUtils.ToProto.taskOrMessage(result))); } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { @@ -302,7 +321,8 @@ public HTTPRestResponse sendMessage(ServerCallContext context, String tenant, St * @see RequestHandler#onMessageSendStream(org.a2aproject.sdk.spec.MessageSendParams, ServerCallContext) * @see HTTPRestStreamingResponse */ - public HTTPRestResponse sendStreamingMessage(ServerCallContext context, String tenant, String body) { + public HTTPRestResponse sendStreamingMessage( + ServerCallContext context, String tenant, String body) { try { EffectiveAgentCard effectiveCard; try { @@ -310,21 +330,27 @@ public HTTPRestResponse sendStreamingMessage(ServerCallContext context, String t } catch (UnsupportedOperationError e) { return createErrorResponse(e); } - org.a2aproject.sdk.grpc.SendMessageRequest.Builder request = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); + org.a2aproject.sdk.grpc.SendMessageRequest.Builder request = + org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); parseRequestBody(body, request); request.setTenant(Utils.defaultIfNull(effectiveCard.tenant(), "")); MessageSendParams params = ProtoUtils.FromProto.messageSendParams(request); try { - requestHandler.authorizeTaskAccess(params.message().taskId(), context, TaskOperation.MESSAGE_SEND_STREAM); + requestHandler.authorizeTaskAccess( + params.message().taskId(), context, TaskOperation.MESSAGE_SEND_STREAM); } catch (A2AError e) { return createErrorResponse(e); } - Flow.Publisher publisher = requestHandler.onMessageSendStream(params, context); + Flow.Publisher publisher = + requestHandler.onMessageSendStream(params, context); return createStreamingResponse(publisher); } catch (A2AError e) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(e).toJson())); + return new HTTPRestStreamingResponse( + ZeroPublisher.fromItems(new HTTPRestErrorResponse(e).toJson())); } catch (Throwable throwable) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(internalError(throwable)).toJson())); + return new HTTPRestStreamingResponse( + ZeroPublisher.fromItems( + new HTTPRestErrorResponse(internalError(throwable)).toJson())); } } @@ -352,18 +378,27 @@ public HTTPRestResponse sendStreamingMessage(ServerCallContext context, String t * @see org.a2aproject.sdk.server.agentexecution.AgentExecutor#cancel */ @SuppressWarnings("unchecked") - public HTTPRestResponse cancelTask(ServerCallContext context, String tenant, String body, @Nullable String taskId) { + public HTTPRestResponse cancelTask( + ServerCallContext context, String tenant, String body, @Nullable String taskId) { String requiredTaskId = requireNonBlankParam(taskId); if (requiredTaskId == null) { return createErrorResponse(new InvalidParamsError("bad task id")); } try { - EffectiveAgentCard effectiveCard = preflight(tenant, context, AgentCardOperation.none()); + EffectiveAgentCard effectiveCard = + preflight(tenant, context, AgentCardOperation.none()); Map metadata = JsonUtil.readMetadata(body); - CancelTaskParams params = CancelTaskParams.builder().id(requiredTaskId).tenant(effectiveCard.tenant()).metadata(metadata).build(); + CancelTaskParams params = + CancelTaskParams.builder() + .id(requiredTaskId) + .tenant(effectiveCard.tenant()) + .metadata(metadata) + .build(); Task task = requestHandler.onCancelTask(params, context); if (task != null) { - return createSuccessResponse(200, org.a2aproject.sdk.grpc.Task.newBuilder(ProtoUtils.ToProto.task(task))); + return createSuccessResponse( + 200, + org.a2aproject.sdk.grpc.Task.newBuilder(ProtoUtils.ToProto.task(task))); } throw new UnsupportedOperationError(); } catch (A2AError e) { @@ -382,24 +417,37 @@ public HTTPRestResponse cancelTask(ServerCallContext context, String tenant, Str * @param taskId the ID of the task * @return the HTTP response containing the created configuration */ - public HTTPRestResponse createTaskPushNotificationConfiguration(ServerCallContext context, String tenant, String body, @Nullable String taskId) { + public HTTPRestResponse createTaskPushNotificationConfiguration( + ServerCallContext context, String tenant, String body, @Nullable String taskId) { String requiredTaskId = requireNonBlankParam(taskId); if (requiredTaskId == null) { return createErrorResponse(new InvalidParamsError("bad task id")); } try { EffectiveAgentCard effectiveCard = preflight(tenant, context, PUSH_NOTIFICATIONS); - org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); parseRequestBody(body, builder); String taskIdFromBody = builder.getTaskId(); if (!taskIdFromBody.isEmpty() && !taskIdFromBody.equals(requiredTaskId)) { - throw new InvalidParamsError("Task ID in request body (" + taskIdFromBody + ") does not match task ID in URL path (" + requiredTaskId + ")."); + throw new InvalidParamsError( + "Task ID in request body (" + + taskIdFromBody + + ") does not match task ID in URL path (" + + requiredTaskId + + ")."); } builder.setTenant(Utils.defaultIfNull(effectiveCard.tenant(), "")); builder.setTaskId(requiredTaskId); - TaskPushNotificationConfig result = requestHandler.onCreateTaskPushNotificationConfig(ProtoUtils.FromProto.createTaskPushNotificationConfig(builder), context); - return createSuccessResponse(201, org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(ProtoUtils.ToProto.taskPushNotificationConfig(result))); + TaskPushNotificationConfig result = + requestHandler.onCreateTaskPushNotificationConfig( + ProtoUtils.FromProto.createTaskPushNotificationConfig(builder), + context); + return createSuccessResponse( + 201, + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder( + ProtoUtils.ToProto.taskPushNotificationConfig(result))); } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { @@ -439,7 +487,8 @@ public HTTPRestResponse createTaskPushNotificationConfiguration(ServerCallContex * @see RequestHandler#onSubscribeToTask(TaskIdParams, ServerCallContext) * @see #sendStreamingMessage(ServerCallContext, String, String) */ - public HTTPRestResponse subscribeToTask(ServerCallContext context, String tenant, @Nullable String taskId) { + public HTTPRestResponse subscribeToTask( + ServerCallContext context, String tenant, @Nullable String taskId) { String requiredTaskId = requireNonBlankParam(taskId); if (requiredTaskId == null) { return createErrorResponse(new InvalidParamsError("bad task id")); @@ -451,18 +500,27 @@ public HTTPRestResponse subscribeToTask(ServerCallContext context, String tenant } catch (UnsupportedOperationError e) { return createErrorResponse(e); } - TaskIdParams params = TaskIdParams.builder().id(requiredTaskId).tenant(effectiveCard.tenant()).build(); + TaskIdParams params = + TaskIdParams.builder() + .id(requiredTaskId) + .tenant(effectiveCard.tenant()) + .build(); try { - requestHandler.authorizeTaskAccess(params.id(), context, TaskOperation.SUBSCRIBE_TO_TASK); + requestHandler.authorizeTaskAccess( + params.id(), context, TaskOperation.SUBSCRIBE_TO_TASK); } catch (A2AError e) { return createErrorResponse(e); } - Flow.Publisher publisher = requestHandler.onSubscribeToTask(params, context); + Flow.Publisher publisher = + requestHandler.onSubscribeToTask(params, context); return createStreamingResponse(publisher); } catch (A2AError e) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(e).toJson())); + return new HTTPRestStreamingResponse( + ZeroPublisher.fromItems(new HTTPRestErrorResponse(e).toJson())); } catch (Throwable throwable) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(internalError(throwable)).toJson())); + return new HTTPRestStreamingResponse( + ZeroPublisher.fromItems( + new HTTPRestErrorResponse(internalError(throwable)).toJson())); } } @@ -475,7 +533,11 @@ public HTTPRestResponse subscribeToTask(ServerCallContext context, String tenant * @param historyLength the maximum number of history entries to include * @return the HTTP response containing the task */ - public HTTPRestResponse getTask(ServerCallContext context, String tenant, @Nullable String taskId, @Nullable String historyLength) { + public HTTPRestResponse getTask( + ServerCallContext context, + String tenant, + @Nullable String taskId, + @Nullable String historyLength) { String requiredTaskId = requireNonBlankParam(taskId); if (requiredTaskId == null) { return createErrorResponse(new InvalidParamsError("bad task id")); @@ -489,11 +551,16 @@ public HTTPRestResponse getTask(ServerCallContext context, String tenant, @Nulla return createErrorResponse(new InvalidParamsError("bad historyLength")); } try { - EffectiveAgentCard effectiveCard = preflight(tenant, context, AgentCardOperation.none()); - TaskQueryParams params = new TaskQueryParams(requiredTaskId, parsedHistoryLength, effectiveCard.tenant()); + EffectiveAgentCard effectiveCard = + preflight(tenant, context, AgentCardOperation.none()); + TaskQueryParams params = + new TaskQueryParams( + requiredTaskId, parsedHistoryLength, effectiveCard.tenant()); Task task = requestHandler.onGetTask(params, context); if (task != null) { - return createSuccessResponse(200, org.a2aproject.sdk.grpc.Task.newBuilder(ProtoUtils.ToProto.task(task))); + return createSuccessResponse( + 200, + org.a2aproject.sdk.grpc.Task.newBuilder(ProtoUtils.ToProto.task(task))); } throw new TaskNotFoundError(); } catch (IllegalArgumentException e) { @@ -545,10 +612,15 @@ public HTTPRestResponse getTask(ServerCallContext context, String tenant, @Nulla * @see RequestHandler#onListTasks(ListTasksParams, ServerCallContext) * @see TaskState */ - public HTTPRestResponse listTasks(ServerCallContext context, String tenant, - @Nullable String contextId, @Nullable String status, - @Nullable String pageSize, @Nullable String pageToken, - @Nullable String historyLength, @Nullable String statusTimestampAfter, + public HTTPRestResponse listTasks( + ServerCallContext context, + String tenant, + @Nullable String contextId, + @Nullable String status, + @Nullable String pageSize, + @Nullable String pageToken, + @Nullable String historyLength, + @Nullable String statusTimestampAfter, @Nullable String includeArtifacts) { Integer parsedPageSize = null; Integer parsedHistoryLength = null; @@ -567,12 +639,15 @@ public HTTPRestResponse listTasks(ServerCallContext context, String tenant, parsedIncludeArtifacts = Boolean.valueOf(includeArtifacts); } } catch (NumberFormatException e) { - return createErrorResponse(new InvalidParamsError("Invalid number format in parameters")); + return createErrorResponse( + new InvalidParamsError("Invalid number format in parameters")); } catch (IllegalArgumentException e) { - return createErrorResponse(new InvalidParamsError("Invalid parameter value: " + e.getMessage())); + return createErrorResponse( + new InvalidParamsError("Invalid parameter value: " + e.getMessage())); } try { - EffectiveAgentCard effectiveCard = preflight(tenant, context, AgentCardOperation.none()); + EffectiveAgentCard effectiveCard = + preflight(tenant, context, AgentCardOperation.none()); // Build params ListTasksParams.Builder paramsBuilder = ListTasksParams.builder(); if (contextId != null) { @@ -584,9 +659,10 @@ public HTTPRestResponse listTasks(ServerCallContext context, String tenant, try { taskState = TaskState.valueOf(status); } catch (IllegalArgumentException e) { - String validStates = Arrays.stream(TaskState.values()) - .map(TaskState::name) - .collect(Collectors.joining(", ")); + String validStates = + Arrays.stream(TaskState.values()) + .map(TaskState::name) + .collect(Collectors.joining(", ")); Map errorData = new HashMap<>(); errorData.put("parameter", "status"); errorData.put("reason", "Must be one of: " + validStates); @@ -621,7 +697,10 @@ public HTTPRestResponse listTasks(ServerCallContext context, String tenant, ListTasksParams params = paramsBuilder.build(); ListTasksResult result = requestHandler.onListTasks(params, context); - return createSuccessResponse(200, org.a2aproject.sdk.grpc.ListTasksResponse.newBuilder(ProtoUtils.ToProto.listTasksResult(result))); + return createSuccessResponse( + 200, + org.a2aproject.sdk.grpc.ListTasksResponse.newBuilder( + ProtoUtils.ToProto.listTasksResult(result))); } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { @@ -638,7 +717,11 @@ public HTTPRestResponse listTasks(ServerCallContext context, String tenant, * @param configId the ID of the configuration to retrieve * @return the HTTP response containing the configuration */ - public HTTPRestResponse getTaskPushNotificationConfiguration(ServerCallContext context, String tenant, @Nullable String taskId, @Nullable String configId) { + public HTTPRestResponse getTaskPushNotificationConfiguration( + ServerCallContext context, + String tenant, + @Nullable String taskId, + @Nullable String configId) { String requiredTaskId = requireNonBlankParam(taskId); if (requiredTaskId == null) { return createErrorResponse(new InvalidParamsError("bad task id")); @@ -649,9 +732,15 @@ public HTTPRestResponse getTaskPushNotificationConfiguration(ServerCallContext c } try { EffectiveAgentCard effectiveCard = preflight(tenant, context, PUSH_NOTIFICATIONS); - GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams(requiredTaskId, requiredConfigId, effectiveCard.tenant()); - TaskPushNotificationConfig config = requestHandler.onGetTaskPushNotificationConfig(params, context); - return createSuccessResponse(200, org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(ProtoUtils.ToProto.taskPushNotificationConfig(config))); + GetTaskPushNotificationConfigParams params = + new GetTaskPushNotificationConfigParams( + requiredTaskId, requiredConfigId, effectiveCard.tenant()); + TaskPushNotificationConfig config = + requestHandler.onGetTaskPushNotificationConfig(params, context); + return createSuccessResponse( + 200, + org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder( + ProtoUtils.ToProto.taskPushNotificationConfig(config))); } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { @@ -669,7 +758,12 @@ public HTTPRestResponse getTaskPushNotificationConfiguration(ServerCallContext c * @param pageToken the token for pagination * @return the HTTP response containing the list of configurations */ - public HTTPRestResponse listTaskPushNotificationConfigurations(ServerCallContext context, String tenant, @Nullable String taskId, @Nullable String pageSize, @Nullable String pageToken) { + public HTTPRestResponse listTaskPushNotificationConfigurations( + ServerCallContext context, + String tenant, + @Nullable String taskId, + @Nullable String pageSize, + @Nullable String pageToken) { String requiredTaskId = requireNonBlankParam(taskId); if (requiredTaskId == null) { return createErrorResponse(new InvalidParamsError("bad task id")); @@ -685,9 +779,18 @@ public HTTPRestResponse listTaskPushNotificationConfigurations(ServerCallContext String normalizedPageToken = Utils.defaultIfNull(pageToken, ""); try { EffectiveAgentCard effectiveCard = preflight(tenant, context, PUSH_NOTIFICATIONS); - ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams(requiredTaskId, parsedPageSize, normalizedPageToken, effectiveCard.tenant()); - ListTaskPushNotificationConfigsResult result = requestHandler.onListTaskPushNotificationConfigs(params, context); - return createSuccessResponse(200, org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.newBuilder(ProtoUtils.ToProto.listTaskPushNotificationConfigsResponse(result))); + ListTaskPushNotificationConfigsParams params = + new ListTaskPushNotificationConfigsParams( + requiredTaskId, + parsedPageSize, + normalizedPageToken, + effectiveCard.tenant()); + ListTaskPushNotificationConfigsResult result = + requestHandler.onListTaskPushNotificationConfigs(params, context); + return createSuccessResponse( + 200, + org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsResponse.newBuilder( + ProtoUtils.ToProto.listTaskPushNotificationConfigsResponse(result))); } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { @@ -704,7 +807,11 @@ public HTTPRestResponse listTaskPushNotificationConfigurations(ServerCallContext * @param configId the ID of the configuration to delete * @return the HTTP response with no content on success */ - public HTTPRestResponse deleteTaskPushNotificationConfiguration(ServerCallContext context, String tenant, @Nullable String taskId, @Nullable String configId) { + public HTTPRestResponse deleteTaskPushNotificationConfiguration( + ServerCallContext context, + String tenant, + @Nullable String taskId, + @Nullable String configId) { String requiredTaskId = requireNonBlankParam(taskId); if (requiredTaskId == null) { return createErrorResponse(new InvalidParamsError("bad task id")); @@ -715,7 +822,9 @@ public HTTPRestResponse deleteTaskPushNotificationConfiguration(ServerCallContex } try { EffectiveAgentCard effectiveCard = preflight(tenant, context, PUSH_NOTIFICATIONS); - DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams(requiredTaskId, requiredConfigId, effectiveCard.tenant()); + DeleteTaskPushNotificationConfigParams params = + new DeleteTaskPushNotificationConfigParams( + requiredTaskId, requiredConfigId, effectiveCard.tenant()); requestHandler.onDeleteTaskPushNotificationConfig(params, context); return new HTTPRestResponse(204, APPLICATION_JSON, ""); } catch (A2AError e) { @@ -737,7 +846,8 @@ public HTTPRestResponse deleteTaskPushNotificationConfiguration(ServerCallContex * @return the selected card and canonical tenant * @throws A2AError if request preflight fails */ - private EffectiveAgentCard preflight(@Nullable String tenant, ServerCallContext context, AgentCardOperation operation) { + private EffectiveAgentCard preflight( + @Nullable String tenant, ServerCallContext context, AgentCardOperation operation) { try { return agentCardPolicy.preflight(tenant, context, transportValidator, operation); } catch (TenantNotFoundException ex) { @@ -752,7 +862,8 @@ private EffectiveAgentCard preflight(@Nullable String tenant, ServerCallContext return value; } - private void parseRequestBody(String body, com.google.protobuf.Message.Builder builder) throws A2AError { + private void parseRequestBody(String body, com.google.protobuf.Message.Builder builder) + throws A2AError { try { if (body == null || body.trim().isEmpty()) { throw new InvalidRequestError("Request body is required"); @@ -760,7 +871,9 @@ private void parseRequestBody(String body, com.google.protobuf.Message.Builder b validate(body); JsonFormat.parser().merge(body, builder); } catch (InvalidProtocolBufferException e) { - LOGGER.log(Level.SEVERE, "Error parsing JSON request body (length={0})", + LOGGER.log( + Level.SEVERE, + "Error parsing JSON request body (length={0})", body != null ? body.length() : 0); LOGGER.log(Level.SEVERE, "Parse error details", e); throw new InvalidParamsError("Failed to parse request body: " + e.getMessage()); @@ -775,10 +888,13 @@ private void validate(String json) { } } - private HTTPRestResponse createSuccessResponse(int statusCode, com.google.protobuf.Message.Builder builder) { + private HTTPRestResponse createSuccessResponse( + int statusCode, com.google.protobuf.Message.Builder builder) { try { // Include default value fields to ensure empty arrays, zeros, etc. are present in JSON - String jsonBody = ProtoJsonUtils.toJson(JsonFormat.printer().alwaysPrintFieldsWithNoPresence(), builder); + String jsonBody = + ProtoJsonUtils.toJson( + JsonFormat.printer().alwaysPrintFieldsWithNoPresence(), builder); return new HTTPRestResponse(statusCode, APPLICATION_JSON, jsonBody); } catch (InvalidProtocolBufferException e) { LOGGER.log(Level.SEVERE, "Failed to serialize response", e); @@ -818,72 +934,134 @@ private static InternalError internalError(Throwable t) { return new InternalError("Internal error"); } - private HTTPRestStreamingResponse createStreamingResponse(Flow.Publisher publisher) { + private HTTPRestStreamingResponse createStreamingResponse( + Flow.Publisher publisher) { return new HTTPRestStreamingResponse(convertToSendStreamingMessageResponse(publisher)); } @SuppressWarnings("FutureReturnValueIgnored") private Flow.Publisher convertToSendStreamingMessageResponse( Flow.Publisher publisher) { - // We can't use the normal convertingProcessor since that propagates any errors as an error handled + // We can't use the normal convertingProcessor since that propagates any errors as an error + // handled // via Subscriber.onError() rather than as part of the SendStreamingResponse payload - LOGGER.log(Level.FINE, "REST: convertToSendStreamingMessageResponse called, creating ZeroPublisher"); - return ZeroPublisher.create(createTubeConfig(), tube -> { - LOGGER.log(Level.FINE, "REST: ZeroPublisher tube created, starting CompletableFuture.runAsync"); - CompletableFuture.runAsync(() -> { - LOGGER.log(Level.FINE, "REST: Inside CompletableFuture, subscribing to EventKind publisher"); - publisher.subscribe(new Flow.Subscriber() { - Flow.@Nullable Subscription subscription; + LOGGER.log( + Level.FINE, + "REST: convertToSendStreamingMessageResponse called, creating ZeroPublisher"); + return ZeroPublisher.create( + createTubeConfig(), + tube -> { + LOGGER.log( + Level.FINE, + "REST: ZeroPublisher tube created, starting" + + " CompletableFuture.runAsync"); + CompletableFuture.runAsync( + () -> { + LOGGER.log( + Level.FINE, + "REST: Inside CompletableFuture, subscribing to EventKind" + + " publisher"); + publisher.subscribe( + new Flow.Subscriber() { + Flow.@Nullable Subscription subscription; - @Override - public void onSubscribe(Flow.Subscription subscription) { - LOGGER.log(Level.FINE, "REST: onSubscribe called, storing subscription and requesting first event"); - this.subscription = subscription; - subscription.request(1); - } + @Override + public void onSubscribe( + Flow.Subscription subscription) { + LOGGER.log( + Level.FINE, + "REST: onSubscribe called, storing" + + " subscription and requesting first" + + " event"); + this.subscription = subscription; + subscription.request(1); + } - @Override - public void onNext(StreamingEventKind item) { - LOGGER.log(Level.FINE, "REST: onNext called with event: {0}", item.getClass().getSimpleName()); - try { - String payload = ProtoJsonUtils.toJson( - JsonFormat.printer().omittingInsignificantWhitespace(), ProtoUtils.ToProto.taskOrMessageStream(item)); - LOGGER.log(Level.FINE, "REST: Converted to JSON, sending via tube: {0}", payload.substring(0, Math.min(100, payload.length()))); - tube.send(payload); - LOGGER.log(Level.FINE, "REST: tube.send() completed, requesting next event from EventConsumer"); - // Request next event from EventConsumer (Chain 1: EventConsumer → RestHandler) - // This is safe because ZeroPublisher buffers items - // Chain 2 (ZeroPublisher → MultiSseSupport) controls actual delivery via request(1) in onWriteDone() - if (subscription != null) { - subscription.request(1); - } else { - LOGGER.log(Level.WARNING, "REST: subscription is null in onNext!"); - } - } catch (InvalidProtocolBufferException ex) { - LOGGER.log(Level.SEVERE, "REST: JSON conversion failed", ex); - onError(ex); - } - } + @Override + public void onNext(StreamingEventKind item) { + LOGGER.log( + Level.FINE, + "REST: onNext called with event: {0}", + item.getClass().getSimpleName()); + try { + String payload = + ProtoJsonUtils.toJson( + JsonFormat.printer() + .omittingInsignificantWhitespace(), + ProtoUtils.ToProto + .taskOrMessageStream( + item)); + LOGGER.log( + Level.FINE, + "REST: Converted to JSON, sending via" + + " tube: {0}", + payload.substring( + 0, + Math.min( + 100, + payload.length()))); + tube.send(payload); + LOGGER.log( + Level.FINE, + "REST: tube.send() completed," + + " requesting next event from" + + " EventConsumer"); + // Request next event from EventConsumer (Chain + // 1: EventConsumer → RestHandler) + // This is safe because ZeroPublisher buffers + // items + // Chain 2 (ZeroPublisher → MultiSseSupport) + // controls actual delivery via request(1) in + // onWriteDone() + if (subscription != null) { + subscription.request(1); + } else { + LOGGER.log( + Level.WARNING, + "REST: subscription is null in" + + " onNext!"); + } + } catch (InvalidProtocolBufferException ex) { + LOGGER.log( + Level.SEVERE, + "REST: JSON conversion failed", + ex); + onError(ex); + } + } - @Override - public void onError(Throwable throwable) { - LOGGER.log(Level.SEVERE, "REST: onError called", throwable); - if (throwable instanceof A2AError jsonrpcError) { - tube.send(new HTTPRestErrorResponse(jsonrpcError).toJson()); - } else { - tube.send(new HTTPRestErrorResponse(internalError(throwable)).toJson()); - } - onComplete(); - } + @Override + public void onError(Throwable throwable) { + LOGGER.log( + Level.SEVERE, + "REST: onError called", + throwable); + if (throwable instanceof A2AError jsonrpcError) { + tube.send( + new HTTPRestErrorResponse(jsonrpcError) + .toJson()); + } else { + tube.send( + new HTTPRestErrorResponse( + internalError( + throwable)) + .toJson()); + } + onComplete(); + } - @Override - public void onComplete() { - LOGGER.log(Level.FINE, "REST: onComplete called, calling tube.complete()"); - tube.complete(); - } + @Override + public void onComplete() { + LOGGER.log( + Level.FINE, + "REST: onComplete called, calling" + + " tube.complete()"); + tube.complete(); + } + }); + }, + executor); }); - }, executor); - }); } /** @@ -922,14 +1100,16 @@ private static int mapErrorToHttpStatus(A2AError error) { * @see #getAgentCard() * @see AgentCard */ - public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, @Nullable String tenant) { + public HTTPRestResponse getExtendedAgentCard( + ServerCallContext context, @Nullable String tenant) { try { Utils.validateTenant(tenant); - EffectiveAgentCard effectiveCard = preflight(tenant, context, - EXTENDED_CARD); - AgentCard extCard = agentCardResolver.resolveExtendedDiscoveryCard(effectiveCard.tenant()); + EffectiveAgentCard effectiveCard = preflight(tenant, context, EXTENDED_CARD); + AgentCard extCard = + agentCardResolver.resolveExtendedDiscoveryCard(effectiveCard.tenant()); if (extCard == null) { - throw new ExtendedAgentCardNotConfiguredError(null, "Extended Card not configured", null); + throw new ExtendedAgentCardNotConfiguredError( + null, "Extended Card not configured", null); } return new HTTPRestResponse(200, APPLICATION_JSON, JsonUtil.toJson(extCard)); } catch (A2AError e) { @@ -997,7 +1177,9 @@ public HTTPRestResponse getAgentCard(@Nullable String tenant) { return new HTTPRestResponse(404, "text/plain", "Public agent card not configured"); } transportValidator.validate(card); - return new HTTPRestResponse(200, APPLICATION_JSON, + return new HTTPRestResponse( + 200, + APPLICATION_JSON, JsonUtil.toJson(card), cacheMetadata.getHttpHeadersMap()); } catch (TenantNotFoundException e) { @@ -1036,7 +1218,8 @@ public HTTPRestResponse(int statusCode, String contentType, String body) { * @param body the response body * @param headers additional HTTP headers */ - public HTTPRestResponse(int statusCode, String contentType, String body, Map headers) { + public HTTPRestResponse( + int statusCode, String contentType, String body, Map headers) { this.statusCode = statusCode; this.contentType = contentType; this.body = body; @@ -1081,7 +1264,16 @@ public Map getHeaders() { @Override public String toString() { - return "HTTPRestResponse{" + "statusCode=" + statusCode + ", contentType=" + contentType + ", body=" + body + ", headers=" + headers + '}'; + return "HTTPRestResponse{" + + "statusCode=" + + statusCode + + ", contentType=" + + contentType + + ", body=" + + body + + ", headers=" + + headers + + '}'; } } @@ -1141,11 +1333,15 @@ private static class HTTPRestErrorResponse { private HTTPRestErrorResponse(A2AError a2aError) { A2AErrorCodes errorCode = A2AErrorCodes.fromCode(a2aError.getCode()); int httpCode = mapErrorToHttpStatus(a2aError); - String status = errorCode != null - ? errorCode.grpcStatus() - : A2AErrorCodes.INTERNAL.grpcStatus(); + String status = + errorCode != null + ? errorCode.grpcStatus() + : A2AErrorCodes.INTERNAL.grpcStatus(); String reason = errorCode != null ? errorCode.name() : "INTERNAL"; - String message = a2aError.getMessage() == null ? a2aError.getClass().getName() : a2aError.getMessage(); + String message = + a2aError.getMessage() == null + ? a2aError.getClass().getName() + : a2aError.getMessage(); ErrorDetail detail = ErrorDetail.of(reason, a2aError.getDetails()); this.error = new ErrorBody(httpCode, status, message, List.of(detail)); @@ -1156,7 +1352,8 @@ private String toJson() { return JsonUtil.toJson(this); } catch (JsonProcessingException ex) { LOGGER.log(Level.SEVERE, "Failed to serialize HTTPRestErrorResponse to JSON", ex); - return "{\"error\":{\"code\":500,\"status\":\"INTERNAL\",\"message\":\"Internal Server Error\",\"details\":[]}}"; + return "{\"error\":{\"code\":500,\"status\":\"INTERNAL\",\"message\":\"Internal" + + " Server Error\",\"details\":[]}}"; } } @@ -1165,6 +1362,7 @@ public String toString() { return "HTTPRestErrorResponse{error=" + error + '}'; } - private record ErrorBody(int code, String status, String message, List details) {} + private record ErrorBody( + int code, String status, String message, List details) {} } } diff --git a/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/package-info.java b/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/package-info.java index d6b28295c..f8fe9f0e3 100644 --- a/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/package-info.java +++ b/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/package-info.java @@ -14,4 +14,3 @@ package org.a2aproject.sdk.transport.rest.handler; import org.jspecify.annotations.NullMarked; - diff --git a/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java b/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java index 6569b9fa5..979ddf34b 100644 --- a/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java +++ b/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java @@ -2,6 +2,10 @@ import static org.a2aproject.sdk.common.MediaType.APPLICATION_JSON; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.protobuf.InvalidProtocolBufferException; +import jakarta.enterprise.inject.Instance; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -13,12 +17,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; - -import jakarta.enterprise.inject.Instance; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.protobuf.InvalidProtocolBufferException; import org.a2aproject.sdk.jsonrpc.common.wrappers.ListTasksResult; import org.a2aproject.sdk.server.AgentCardCacheMetadata; import org.a2aproject.sdk.server.AgentCardValidator; @@ -54,7 +52,9 @@ @Timeout(value = 1, unit = TimeUnit.MINUTES) public class RestHandlerTest extends AbstractA2ARequestHandlerTest { - private final ServerCallContext callContext = new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "1.0"); + private final ServerCallContext callContext = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "1.0"); private static AgentCardCacheMetadata createCacheMetadata() { return createCacheMetadata(CARD); @@ -66,10 +66,12 @@ private static AgentCardCacheMetadata createCacheMetadata(AgentCard card) { @Test public void testGetTaskSuccess() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - RestHandler.HTTPRestResponse response = handler.getTask(callContext, "", MINIMAL_TASK.id(), "0"); + RestHandler.HTTPRestResponse response = + handler.getTask(callContext, "", MINIMAL_TASK.id(), "0"); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -84,36 +86,53 @@ public void testGetTaskSuccess() { @Test public void testGetTaskNotFound() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); - RestHandler.HTTPRestResponse response = handler.getTask(callContext, "", "nonexistent", "0"); + RestHandler.HTTPRestResponse response = + handler.getTask(callContext, "", "nonexistent", "0"); - assertProblemDetail(response, 404, - "TASK_NOT_FOUND", "Task not found"); + assertProblemDetail(response, 404, "TASK_NOT_FOUND", "Task not found"); } @Test public void testGetTaskNegativeHistoryLengthReturns422() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); - RestHandler.HTTPRestResponse response = handler.getTask(callContext, "", MINIMAL_TASK.id(), "-1"); + RestHandler.HTTPRestResponse response = + handler.getTask(callContext, "", MINIMAL_TASK.id(), "-1"); - assertProblemDetail(response, 422, - "INVALID_PARAMS", "Invalid history length"); + assertProblemDetail(response, 422, "INVALID_PARAMS", "Invalid history length"); Assertions.assertEquals(Map.of(), response.getHeaders()); Assertions.assertFalse(response instanceof RestHandler.HTTPRestStreamingResponse); - JsonObject error = JsonParser.parseString(response.getBody()).getAsJsonObject().getAsJsonObject("error"); + JsonObject error = + JsonParser.parseString(response.getBody()) + .getAsJsonObject() + .getAsJsonObject("error"); Assertions.assertEquals("INVALID_ARGUMENT", error.get("status").getAsString()); - Assertions.assertEquals(new JsonObject(), error.getAsJsonArray("details").get(0).getAsJsonObject().get("metadata")); + Assertions.assertEquals( + new JsonObject(), + error.getAsJsonArray("details").get(0).getAsJsonObject().get("metadata")); } @Test public void testListTasksStatusWireString() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - RestHandler.HTTPRestResponse response = handler.listTasks(callContext, "", null, "TASK_STATE_SUBMITTED", null, null, - null, null, null); + RestHandler.HTTPRestResponse response = + handler.listTasks( + callContext, + "", + null, + "TASK_STATE_SUBMITTED", + null, + null, + null, + null, + null); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -122,47 +141,63 @@ public void testListTasksStatusWireString() { @Test public void testListTasksInvalidStatus() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); - RestHandler.HTTPRestResponse response = handler.listTasks(callContext, "", null, "not-a-status", null, null, - null, null, null); + RestHandler.HTTPRestResponse response = + handler.listTasks( + callContext, "", null, "not-a-status", null, null, null, null, null); - assertProblemDetail(response, 422, - "INVALID_PARAMS", "Invalid params"); + assertProblemDetail(response, 422, "INVALID_PARAMS", "Invalid params"); Assertions.assertEquals(Map.of(), response.getHeaders()); Assertions.assertFalse(response instanceof RestHandler.HTTPRestStreamingResponse); - JsonObject error = JsonParser.parseString(response.getBody()).getAsJsonObject().getAsJsonObject("error"); + JsonObject error = + JsonParser.parseString(response.getBody()) + .getAsJsonObject() + .getAsJsonObject("error"); Assertions.assertEquals("INVALID_ARGUMENT", error.get("status").getAsString()); - JsonObject metadata = error.getAsJsonArray("details").get(0).getAsJsonObject().getAsJsonObject("metadata"); + JsonObject metadata = + error.getAsJsonArray("details") + .get(0) + .getAsJsonObject() + .getAsJsonObject("metadata"); Assertions.assertEquals("status", metadata.get("parameter").getAsString()); - Assertions.assertEquals("Must be one of: " + java.util.Arrays.stream(TaskState.values()) - .map(Enum::name).collect(java.util.stream.Collectors.joining(", ")), metadata.get("reason").getAsString()); + Assertions.assertEquals( + "Must be one of: " + + java.util.Arrays.stream(TaskState.values()) + .map(Enum::name) + .collect(java.util.stream.Collectors.joining(", ")), + metadata.get("reason").getAsString()); } @Test public void testSendMessage() throws InvalidProtocolBufferException { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; - String requestBody = """ - { - "message": + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; + String requestBody = + """ { - "messageId": "message-1234", - "contextId": "context-1234", - "role": "ROLE_USER", - "parts": [{ - "text": "tell me a joke" - }], - "metadata": { + "message": + { + "messageId": "message-1234", + "contextId": "context-1234", + "role": "ROLE_USER", + "parts": [{ + "text": "tell me a joke" + }], + "metadata": { + } + }, + "configuration": + { + "returnImmediately": false } - }, - "configuration": - { - "returnImmediately": false - } - }"""; + }\ + """; RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", requestBody); Assertions.assertEquals(200, response.getStatusCode(), response.toString()); @@ -172,32 +207,35 @@ public void testSendMessage() throws InvalidProtocolBufferException { @Test public void testSendMessageInvalidBody() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); String invalidBody = "invalid json"; RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", invalidBody); - assertProblemDetail(response, 400, - "JSON_PARSE", "Failed to parse json"); + assertProblemDetail(response, 400, "JSON_PARSE", "Failed to parse json"); } @Test public void testSendMessageWrongValueBody() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); - String requestBody = """ + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + String requestBody = + """ + { + "message": { - "message": - { - "messageId": "message-1234", - "contextId": "context-1234", - "role": "user", - "parts": [{ - "text": "tell me a joke" - }], - "metadata": { - } + "messageId": "message-1234", + "contextId": "context-1234", + "role": "user", + "parts": [{ + "text": "tell me a joke" + }], + "metadata": { } - }"""; + } + }\ + """; RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", requestBody); Assertions.assertEquals(422, response.getStatusCode()); @@ -205,36 +243,47 @@ public void testSendMessageWrongValueBody() { JsonObject body = JsonParser.parseString(response.getBody()).getAsJsonObject(); JsonObject error = body.getAsJsonObject("error"); Assertions.assertEquals(422, error.get("code").getAsInt()); - Assertions.assertEquals("INVALID_PARAMS", error.getAsJsonArray("details").get(0).getAsJsonObject().get("reason").getAsString()); - Assertions.assertTrue(error.get("message").getAsString().startsWith("Failed to parse request body:"), + Assertions.assertEquals( + "INVALID_PARAMS", + error.getAsJsonArray("details") + .get(0) + .getAsJsonObject() + .get("reason") + .getAsString()); + Assertions.assertTrue( + error.get("message").getAsString().startsWith("Failed to parse request body:"), "message should indicate parse failure: " + error.get("message").getAsString()); } @Test public void testSendMessageEmptyBody() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", ""); - assertProblemDetail(response, 400, - "INVALID_REQUEST", "Request body is required"); + assertProblemDetail(response, 400, "INVALID_REQUEST", "Request body is required"); } @Test public void testCancelTaskSuccess() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorCancel = (context, agentEmitter) -> { - // We need to cancel the task or the EventConsumer never finds a 'final' event. - // Looking at the Python implementation, they typically use AgentExecutors that - // don't support cancellation. So my theory is the Agent updates the task to the CANCEL status - Task task = context.getTask(); - agentEmitter.cancel(); - }; + agentExecutorCancel = + (context, agentEmitter) -> { + // We need to cancel the task or the EventConsumer never finds a 'final' event. + // Looking at the Python implementation, they typically use AgentExecutors that + // don't support cancellation. So my theory is the Agent updates the task to the + // CANCEL status + Task task = context.getTask(); + agentEmitter.cancel(); + }; String requestBody = String.format("{\"id\":\"%s\"}", MINIMAL_TASK.id()); - RestHandler.HTTPRestResponse response = handler.cancelTask(callContext, "", requestBody, MINIMAL_TASK.id()); + RestHandler.HTTPRestResponse response = + handler.cancelTask(callContext, "", requestBody, MINIMAL_TASK.id()); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -243,40 +292,45 @@ public void testCancelTaskSuccess() { @Test public void testCancelTaskNotFound() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); String requestBody = "{\"id\":\"nonexistent\"}"; - RestHandler.HTTPRestResponse response = handler.cancelTask(callContext, "", requestBody, "nonexistent"); + RestHandler.HTTPRestResponse response = + handler.cancelTask(callContext, "", requestBody, "nonexistent"); - assertProblemDetail(response, 404, - "TASK_NOT_FOUND", "Task not found"); + assertProblemDetail(response, 404, "TASK_NOT_FOUND", "Task not found"); } @Test public void testCancelTaskWithMetadata() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorCancel = (context, agentEmitter) -> { - // Verify metadata is accessible in the context - Task task = context.getTask(); + agentExecutorCancel = + (context, agentEmitter) -> { + // Verify metadata is accessible in the context + Task task = context.getTask(); - // Cancel the task so EventConsumer finds a final event - agentEmitter.cancel(); - }; + // Cancel the task so EventConsumer finds a final event + agentEmitter.cancel(); + }; // Request body with metadata - String requestBody = """ - { - "metadata": { - "reason": "user_requested", - "source": "web_ui", - "priority": "high" + String requestBody = + """ + { + "metadata": { + "reason": "user_requested", + "source": "web_ui", + "priority": "high" + } } - } - """; + """; - RestHandler.HTTPRestResponse response = handler.cancelTask(callContext, "", requestBody, MINIMAL_TASK.id()); + RestHandler.HTTPRestResponse response = + handler.cancelTask(callContext, "", requestBody, MINIMAL_TASK.id()); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -285,22 +339,26 @@ public void testCancelTaskWithMetadata() { @Test public void testCancelTaskWithEmptyMetadata() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorCancel = (context, agentEmitter) -> { - Task task = context.getTask(); - agentEmitter.cancel(); - }; + agentExecutorCancel = + (context, agentEmitter) -> { + Task task = context.getTask(); + agentEmitter.cancel(); + }; // Request body with empty metadata object - String requestBody = """ - { - "metadata": {} - } - """; + String requestBody = + """ + { + "metadata": {} + } + """; - RestHandler.HTTPRestResponse response = handler.cancelTask(callContext, "", requestBody, MINIMAL_TASK.id()); + RestHandler.HTTPRestResponse response = + handler.cancelTask(callContext, "", requestBody, MINIMAL_TASK.id()); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -309,18 +367,21 @@ public void testCancelTaskWithEmptyMetadata() { @Test public void testCancelTaskWithNoMetadata() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorCancel = (context, agentEmitter) -> { - Task task = context.getTask(); - agentEmitter.cancel(); - }; + agentExecutorCancel = + (context, agentEmitter) -> { + Task task = context.getTask(); + agentEmitter.cancel(); + }; // Request body without metadata field String requestBody = "{}"; - RestHandler.HTTPRestResponse response = handler.cancelTask(callContext, "", requestBody, MINIMAL_TASK.id()); + RestHandler.HTTPRestResponse response = + handler.cancelTask(callContext, "", requestBody, MINIMAL_TASK.id()); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -329,16 +390,19 @@ public void testCancelTaskWithNoMetadata() { @Test public void testCancelTaskWithNullBody() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - agentExecutorCancel = (context, agentEmitter) -> { - Task task = context.getTask(); - agentEmitter.cancel(); - }; + agentExecutorCancel = + (context, agentEmitter) -> { + Task task = context.getTask(); + agentEmitter.cancel(); + }; // Null body should still work - metadata defaults to empty map - RestHandler.HTTPRestResponse response = handler.cancelTask(callContext, "", null, MINIMAL_TASK.id()); + RestHandler.HTTPRestResponse response = + handler.cancelTask(callContext, "", null, MINIMAL_TASK.id()); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -347,31 +411,37 @@ public void testCancelTaskWithNullBody() { @Test public void testSendStreamingMessageSuccess() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; - String requestBody = """ - { - "message": { - "role": "ROLE_USER", - "parts": [ - { - "text": "tell me some jokes" + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; + String requestBody = + """ + { + "message": { + "role": "ROLE_USER", + "parts": [ + { + "text": "tell me some jokes" + } + ], + "messageId": "message-1234", + "contextId": "context-1234" + }, + "configuration": { + "acceptedOutputModes": ["text"] } - ], - "messageId": "message-1234", - "contextId": "context-1234" - }, - "configuration": { - "acceptedOutputModes": ["text"] - } - }"""; - - RestHandler.HTTPRestResponse response = handler.sendStreamingMessage(callContext, "", requestBody); + }\ + """; + + RestHandler.HTTPRestResponse response = + handler.sendStreamingMessage(callContext, "", requestBody); Assertions.assertEquals(200, response.getStatusCode(), response.toString()); Assertions.assertInstanceOf(RestHandler.HTTPRestStreamingResponse.class, response); - RestHandler.HTTPRestStreamingResponse streamingResponse = (RestHandler.HTTPRestStreamingResponse) response; + RestHandler.HTTPRestStreamingResponse streamingResponse = + (RestHandler.HTTPRestStreamingResponse) response; Assertions.assertNotNull(streamingResponse.getPublisher()); Assertions.assertEquals("text/event-stream", streamingResponse.getContentType()); } @@ -379,41 +449,49 @@ public void testSendStreamingMessageSuccess() { @Test public void testSendStreamingMessageNotSupported() { AgentCard card = createAgentCard(false, true); - RestHandler handler = new RestHandler(card, createCacheMetadata(card), requestHandler, internalExecutor); - - String requestBody = """ - { - "contextId": "ctx123", - "role": "ROLE_USER", - "parts": [{ - "text": "Hello" - }] - } - """; + RestHandler handler = + new RestHandler(card, createCacheMetadata(card), requestHandler, internalExecutor); - RestHandler.HTTPRestResponse response = handler.sendStreamingMessage(callContext, "", requestBody); + String requestBody = + """ + { + "contextId": "ctx123", + "role": "ROLE_USER", + "parts": [{ + "text": "Hello" + }] + } + """; - assertProblemDetail(response, 400, - "UNSUPPORTED_OPERATION", - "Streaming is not supported by the agent"); + RestHandler.HTTPRestResponse response = + handler.sendStreamingMessage(callContext, "", requestBody); + + assertProblemDetail( + response, 400, "UNSUPPORTED_OPERATION", "Streaming is not supported by the agent"); } @Test public void testPushNotificationConfigSuccess() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - String requestBody = """ - { - "id": "default-config-id", - "taskId": "%s", - "url": "https://example.com/callback", - "authentication": { - "scheme": "jwt" - } - }""".formatted(MINIMAL_TASK.id()); + String requestBody = + """ + { + "id": "default-config-id", + "taskId": "%s", + "url": "https://example.com/callback", + "authentication": { + "scheme": "jwt" + } + }\ + """ + .formatted(MINIMAL_TASK.id()); - RestHandler.HTTPRestResponse response = handler.createTaskPushNotificationConfiguration(callContext, "", requestBody, MINIMAL_TASK.id()); + RestHandler.HTTPRestResponse response = + handler.createTaskPushNotificationConfiguration( + callContext, "", requestBody, MINIMAL_TASK.id()); Assertions.assertEquals(201, response.getStatusCode(), response.toString()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -423,61 +501,82 @@ public void testPushNotificationConfigSuccess() { @Test public void testPushNotificationConfigNotSupported() { AgentCard card = createAgentCard(true, false); - RestHandler handler = new RestHandler(card, createCacheMetadata(card), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(card, createCacheMetadata(card), requestHandler, internalExecutor); - String requestBody = """ - { - "id": "default-config-id", - "taskId": "%s", - "url": "http://example.com" - } - """.formatted(MINIMAL_TASK.id()); + String requestBody = + """ + { + "id": "default-config-id", + "taskId": "%s", + "url": "http://example.com" + } + """ + .formatted(MINIMAL_TASK.id()); - RestHandler.HTTPRestResponse response = handler.createTaskPushNotificationConfiguration(callContext, "", requestBody, MINIMAL_TASK.id()); + RestHandler.HTTPRestResponse response = + handler.createTaskPushNotificationConfiguration( + callContext, "", requestBody, MINIMAL_TASK.id()); - assertProblemDetail(response, 400, + assertProblemDetail( + response, + 400, "PUSH_NOTIFICATION_NOT_SUPPORTED", "Push Notification is not supported"); } @Test public void testGetPushNotificationConfig() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); // First, create a push notification config - String createRequestBody = """ - { - "id": "default-config-id", - "taskId": "%s", - "url": "https://example.com/callback", - "authentication": { - "scheme": "jwt" - } - }""".formatted(MINIMAL_TASK.id()); - RestHandler.HTTPRestResponse response = handler.createTaskPushNotificationConfiguration(callContext, "", createRequestBody, MINIMAL_TASK.id()); + String createRequestBody = + """ + { + "id": "default-config-id", + "taskId": "%s", + "url": "https://example.com/callback", + "authentication": { + "scheme": "jwt" + } + }\ + """ + .formatted(MINIMAL_TASK.id()); + RestHandler.HTTPRestResponse response = + handler.createTaskPushNotificationConfiguration( + callContext, "", createRequestBody, MINIMAL_TASK.id()); Assertions.assertEquals(201, response.getStatusCode(), response.toString()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); // Now get it - response = handler.getTaskPushNotificationConfiguration(callContext, "", MINIMAL_TASK.id(), "default-config-id"); + response = + handler.getTaskPushNotificationConfiguration( + callContext, "", MINIMAL_TASK.id(), "default-config-id"); Assertions.assertEquals(200, response.getStatusCode(), response.toString()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); } @Test public void testDeletePushNotificationConfig() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - RestHandler.HTTPRestResponse response = handler.deleteTaskPushNotificationConfiguration(callContext, "", MINIMAL_TASK.id(), "default-config-id"); + RestHandler.HTTPRestResponse response = + handler.deleteTaskPushNotificationConfiguration( + callContext, "", MINIMAL_TASK.id(), "default-config-id"); Assertions.assertEquals(204, response.getStatusCode()); } @Test public void testListPushNotificationConfigs() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - RestHandler.HTTPRestResponse response = handler.listTaskPushNotificationConfigurations(callContext, "", MINIMAL_TASK.id(), "0", ""); + RestHandler.HTTPRestResponse response = + handler.listTaskPushNotificationConfigurations( + callContext, "", MINIMAL_TASK.id(), "0", ""); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -486,34 +585,45 @@ public void testListPushNotificationConfigs() { @Test public void testPushNotificationConfigParseError_DoesNotLogSensitiveData() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); - String requestBody = """ - { - "id": "default-config-id", - "taskId": "%s", - "url": "https://webhook.example.com", - "token": "secret-token-12345", - "authentication": { - "scheme": "Bearer", - "credentials": "oauth-secret-67890" - }, - "unknownField": "trigger-parse-error" - }""".formatted(MINIMAL_TASK.id()); + String requestBody = + """ + { + "id": "default-config-id", + "taskId": "%s", + "url": "https://webhook.example.com", + "token": "secret-token-12345", + "authentication": { + "scheme": "Bearer", + "credentials": "oauth-secret-67890" + }, + "unknownField": "trigger-parse-error" + }\ + """ + .formatted(MINIMAL_TASK.id()); // Request body with sensitive data and an unknown field to trigger parse error - java.util.logging.Logger logger = java.util.logging.Logger.getLogger(RestHandler.class.getName()); - LogCaptureAssertions.assertSensitiveDataNotLogged(logger, - () -> Assertions.assertEquals(422, - handler.createTaskPushNotificationConfiguration(callContext, "", requestBody, MINIMAL_TASK.id()) - .getStatusCode()), - "secret-token-12345", "oauth-secret-67890"); + java.util.logging.Logger logger = + java.util.logging.Logger.getLogger(RestHandler.class.getName()); + LogCaptureAssertions.assertSensitiveDataNotLogged( + logger, + () -> + Assertions.assertEquals( + 422, + handler.createTaskPushNotificationConfiguration( + callContext, "", requestBody, MINIMAL_TASK.id()) + .getStatusCode()), + "secret-token-12345", + "oauth-secret-67890"); } @Test public void testHttpStatusCodeMapping() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); // Test 400 for invalid request RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", ""); @@ -526,73 +636,81 @@ public void testHttpStatusCodeMapping() { @Test public void testStreamingDoesNotBlockMainThread() throws Exception { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); // Track if the main thread gets blocked during streaming AtomicBoolean eventReceived = new AtomicBoolean(false); CountDownLatch streamStarted = new CountDownLatch(1); CountDownLatch eventProcessed = new CountDownLatch(1); - agentExecutorExecute = (context, agentEmitter) -> { - // Wait a bit to ensure the main thread continues - try { - Thread.sleep(100); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - agentEmitter.sendMessage(context.getMessage()); - }; - - String requestBody = """ - { - "message": { - "role": "ROLE_USER", - "parts": [ - { - "text": "tell me some jokes" + agentExecutorExecute = + (context, agentEmitter) -> { + // Wait a bit to ensure the main thread continues + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + agentEmitter.sendMessage(context.getMessage()); + }; + + String requestBody = + """ + { + "message": { + "role": "ROLE_USER", + "parts": [ + { + "text": "tell me some jokes" + } + ], + "messageId": "message-1234", + "contextId": "context-1234" + }, + "configuration": { + "acceptedOutputModes": ["text"] } - ], - "messageId": "message-1234", - "contextId": "context-1234" - }, - "configuration": { - "acceptedOutputModes": ["text"] - } - }"""; + }\ + """; // Start streaming - RestHandler.HTTPRestResponse response = handler.sendStreamingMessage(callContext, "", requestBody); + RestHandler.HTTPRestResponse response = + handler.sendStreamingMessage(callContext, "", requestBody); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertInstanceOf(RestHandler.HTTPRestStreamingResponse.class, response); - RestHandler.HTTPRestStreamingResponse streamingResponse = (RestHandler.HTTPRestStreamingResponse) response; + RestHandler.HTTPRestStreamingResponse streamingResponse = + (RestHandler.HTTPRestStreamingResponse) response; Flow.Publisher publisher = streamingResponse.getPublisher(); - publisher.subscribe(new Flow.Subscriber() { - @Override - public void onSubscribe(Flow.Subscription subscription) { - streamStarted.countDown(); - subscription.request(1); - } + publisher.subscribe( + new Flow.Subscriber() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + streamStarted.countDown(); + subscription.request(1); + } - @Override - public void onNext(String item) { - eventReceived.set(true); - eventProcessed.countDown(); - } + @Override + public void onNext(String item) { + eventReceived.set(true); + eventProcessed.countDown(); + } - @Override - public void onError(Throwable throwable) { - eventProcessed.countDown(); - } + @Override + public void onError(Throwable throwable) { + eventProcessed.countDown(); + } - @Override - public void onComplete() { - eventProcessed.countDown(); - } - }); + @Override + public void onComplete() { + eventProcessed.countDown(); + } + }); // The main thread should not be blocked - we should be able to continue immediately - Assertions.assertTrue(streamStarted.await(100, TimeUnit.MILLISECONDS), + Assertions.assertTrue( + streamStarted.await(100, TimeUnit.MILLISECONDS), "Streaming subscription should start quickly without blocking main thread"); // This proves the main thread is not blocked - we can do other work @@ -600,7 +718,8 @@ public void onComplete() { Thread.sleep(50); // Wait for the actual event processing to complete - Assertions.assertTrue(eventProcessed.await(2, TimeUnit.SECONDS), + Assertions.assertTrue( + eventProcessed.await(2, TimeUnit.SECONDS), "Event should be processed within reasonable time"); // Verify we received the event @@ -610,137 +729,173 @@ public void onComplete() { @Test public void testExtensionSupportRequiredErrorOnSendMessage() { // Create AgentCard with a required extension - AgentCard cardWithExtension = AgentCard.builder() - .name("test-card") - .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extensions(List.of( - AgentExtension.builder() - .uri("https://example.com/test-extension") - .required(true) - .build() - )) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); - - RestHandler handler = new RestHandler(cardWithExtension, createCacheMetadata(cardWithExtension), requestHandler, internalExecutor); - - String requestBody = """ - { - "message": { - "messageId": "message-1234", - "contextId": "context-1234", - "role": "ROLE_USER", - "parts": [{ - "text": "tell me a joke" - }], - "metadata": {} - }, - "configuration": { - "returnImmediately": false - } - }"""; + AgentCard cardWithExtension = + AgentCard.builder() + .name("test-card") + .description("Test card with required extension") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("HTTP+JSON", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extensions( + List.of( + AgentExtension.builder() + .uri( + "https://example.com/test-extension") + .required(true) + .build())) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + + RestHandler handler = + new RestHandler( + cardWithExtension, + createCacheMetadata(cardWithExtension), + requestHandler, + internalExecutor); + + String requestBody = + """ + { + "message": { + "messageId": "message-1234", + "contextId": "context-1234", + "role": "ROLE_USER", + "parts": [{ + "text": "tell me a joke" + }], + "metadata": {} + }, + "configuration": { + "returnImmediately": false + } + }\ + """; RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", requestBody); - assertProblemDetail(response, 400, + assertProblemDetail( + response, + 400, "EXTENSION_SUPPORT_REQUIRED", - "Required extension 'https://example.com/test-extension' was not requested by the client"); + "Required extension 'https://example.com/test-extension' was not requested by the" + + " client"); } @Test public void testExtensionSupportRequiredErrorOnSendStreamingMessage() { // Create AgentCard with a required extension - AgentCard cardWithExtension = AgentCard.builder() - .name("test-card") - .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extensions(List.of( - AgentExtension.builder() - .uri("https://example.com/streaming-extension") - .required(true) - .build() - )) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard cardWithExtension = + AgentCard.builder() + .name("test-card") + .description("Test card with required extension") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("HTTP+JSON", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extensions( + List.of( + AgentExtension.builder() + .uri( + "https://example.com/streaming-extension") + .required(true) + .build())) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + + RestHandler handler = + new RestHandler( + cardWithExtension, + createCacheMetadata(cardWithExtension), + requestHandler, + internalExecutor); + + String requestBody = + """ + { + "message": { + "role": "ROLE_USER", + "parts": [{ + "text": "tell me some jokes" + }], + "messageId": "message-1234", + "contextId": "context-1234" + }, + "configuration": { + "acceptedOutputModes": ["text"] + } + }\ + """; - RestHandler handler = new RestHandler(cardWithExtension, createCacheMetadata(cardWithExtension), requestHandler, internalExecutor); - - String requestBody = """ - { - "message": { - "role": "ROLE_USER", - "parts": [{ - "text": "tell me some jokes" - }], - "messageId": "message-1234", - "contextId": "context-1234" - }, - "configuration": { - "acceptedOutputModes": ["text"] - } - }"""; - - RestHandler.HTTPRestResponse response = handler.sendStreamingMessage(callContext, "", requestBody); + RestHandler.HTTPRestResponse response = + handler.sendStreamingMessage(callContext, "", requestBody); // Streaming responses embed errors in the stream with status 200 Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertInstanceOf(RestHandler.HTTPRestStreamingResponse.class, response); - + // Subscribe to publisher and verify error in stream - RestHandler.HTTPRestStreamingResponse streamingResponse = (RestHandler.HTTPRestStreamingResponse) response; + RestHandler.HTTPRestStreamingResponse streamingResponse = + (RestHandler.HTTPRestStreamingResponse) response; Flow.Publisher publisher = streamingResponse.getPublisher(); - + AtomicBoolean errorFound = new AtomicBoolean(false); CountDownLatch latch = new CountDownLatch(1); - - publisher.subscribe(new Flow.Subscriber() { - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscription.request(1); - } - @Override - public void onNext(String item) { - JsonObject body = JsonParser.parseString(item).getAsJsonObject(); - if (body.has("error")) { - JsonObject error = body.getAsJsonObject("error"); - var details = error.has("details") ? error.getAsJsonArray("details") : null; - if (details != null && !details.isEmpty()) { - String reason = details.get(0).getAsJsonObject().get("reason").getAsString(); - if ("EXTENSION_SUPPORT_REQUIRED".equals(reason) && - item.contains("https://example.com/streaming-extension")) { - errorFound.set(true); + publisher.subscribe( + new Flow.Subscriber() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(1); + } + + @Override + public void onNext(String item) { + JsonObject body = JsonParser.parseString(item).getAsJsonObject(); + if (body.has("error")) { + JsonObject error = body.getAsJsonObject("error"); + var details = + error.has("details") ? error.getAsJsonArray("details") : null; + if (details != null && !details.isEmpty()) { + String reason = + details.get(0) + .getAsJsonObject() + .get("reason") + .getAsString(); + if ("EXTENSION_SUPPORT_REQUIRED".equals(reason) + && item.contains( + "https://example.com/streaming-extension")) { + errorFound.set(true); + } + } } + latch.countDown(); } - } - latch.countDown(); - } - @Override - public void onError(Throwable throwable) { - latch.countDown(); - } + @Override + public void onError(Throwable throwable) { + latch.countDown(); + } - @Override - public void onComplete() { - latch.countDown(); - } - }); + @Override + public void onComplete() { + latch.countDown(); + } + }); try { Assertions.assertTrue(latch.await(1, TimeUnit.SECONDS)); @@ -753,59 +908,73 @@ public void onComplete() { @Test public void testRequiredExtensionProvidedSuccess() { // Create AgentCard with a required extension - AgentCard cardWithExtension = AgentCard.builder() - .name("test-card") - .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extensions(List.of( - AgentExtension.builder() - .uri("https://example.com/required-extension") - .required(true) - .build() - )) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); - - RestHandler handler = new RestHandler(cardWithExtension, createCacheMetadata(cardWithExtension), requestHandler, internalExecutor); + AgentCard cardWithExtension = + AgentCard.builder() + .name("test-card") + .description("Test card with required extension") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("HTTP+JSON", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extensions( + List.of( + AgentExtension.builder() + .uri( + "https://example.com/required-extension") + .required(true) + .build())) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + + RestHandler handler = + new RestHandler( + cardWithExtension, + createCacheMetadata(cardWithExtension), + requestHandler, + internalExecutor); // Create context WITH the required extension Set requestedExtensions = new HashSet<>(); requestedExtensions.add("https://example.com/required-extension"); - ServerCallContext contextWithExtension = new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("foo", "bar"), - requestedExtensions, - "1.0" - ); - - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; - - String requestBody = """ - { - "message": { - "messageId": "message-1234", - "contextId": "context-1234", - "role": "ROLE_USER", - "parts": [{ - "text": "tell me a joke" - }], - "metadata": {} - }, - "configuration": { - "returnImmediately": false - } - }"""; - - RestHandler.HTTPRestResponse response = handler.sendMessage(contextWithExtension, "", requestBody); + ServerCallContext contextWithExtension = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + requestedExtensions, + "1.0"); + + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; + + String requestBody = + """ + { + "message": { + "messageId": "message-1234", + "contextId": "context-1234", + "role": "ROLE_USER", + "parts": [{ + "text": "tell me a joke" + }], + "metadata": {} + }, + "configuration": { + "returnImmediately": false + } + }\ + """; + + RestHandler.HTTPRestResponse response = + handler.sendMessage(contextWithExtension, "", requestBody); // Should succeed without error Assertions.assertEquals(200, response.getStatusCode()); @@ -816,49 +985,64 @@ public void testRequiredExtensionProvidedSuccess() { @Test public void testVersionNotSupportedErrorOnSendMessage() { // Create AgentCard with protocol version 1.0 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); - - RestHandler handler = new RestHandler(agentCard, createCacheMetadata(agentCard), requestHandler, internalExecutor); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("HTTP+JSON", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + + RestHandler handler = + new RestHandler( + agentCard, + createCacheMetadata(agentCard), + requestHandler, + internalExecutor); // Create context with incompatible version 2.0 - ServerCallContext contextWithVersion = new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("foo", "bar"), - new HashSet<>(), - "2.0" // Incompatible version - ); - - String requestBody = """ - { - "message": { - "messageId": "message-1234", - "contextId": "context-1234", - "role": "ROLE_USER", - "parts": [{ - "text": "tell me a joke" - }], - "metadata": {} - }, - "configuration": { - "returnImmediately": false - } - }"""; - - RestHandler.HTTPRestResponse response = handler.sendMessage(contextWithVersion, "", requestBody); - - assertProblemDetail(response, 400, + ServerCallContext contextWithVersion = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + new HashSet<>(), + "2.0" // Incompatible version + ); + + String requestBody = + """ + { + "message": { + "messageId": "message-1234", + "contextId": "context-1234", + "role": "ROLE_USER", + "parts": [{ + "text": "tell me a joke" + }], + "metadata": {} + }, + "configuration": { + "returnImmediately": false + } + }\ + """; + + RestHandler.HTTPRestResponse response = + handler.sendMessage(contextWithVersion, "", requestBody); + + assertProblemDetail( + response, + 400, "VERSION_NOT_SUPPORTED", "Protocol version '2.0' is not supported. Supported versions: [1.0]"); } @@ -866,90 +1050,111 @@ public void testVersionNotSupportedErrorOnSendMessage() { @Test public void testVersionNotSupportedErrorOnSendStreamingMessage() { // Create AgentCard with protocol version 1.0 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); - - RestHandler handler = new RestHandler(agentCard, createCacheMetadata(agentCard), requestHandler, internalExecutor); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("HTTP+JSON", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + + RestHandler handler = + new RestHandler( + agentCard, + createCacheMetadata(agentCard), + requestHandler, + internalExecutor); // Create context with incompatible version 2.0 - ServerCallContext contextWithVersion = new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("foo", "bar"), - new HashSet<>(), - "2.0" // Incompatible version - ); - - String requestBody = """ - { - "message": { - "role": "ROLE_USER", - "parts": [{ - "text": "tell me some jokes" - }], - "messageId": "message-1234", - "contextId": "context-1234" - }, - "configuration": { - "acceptedOutputModes": ["text"] - } - }"""; - - RestHandler.HTTPRestResponse response = handler.sendStreamingMessage(contextWithVersion, "", requestBody); + ServerCallContext contextWithVersion = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + new HashSet<>(), + "2.0" // Incompatible version + ); + + String requestBody = + """ + { + "message": { + "role": "ROLE_USER", + "parts": [{ + "text": "tell me some jokes" + }], + "messageId": "message-1234", + "contextId": "context-1234" + }, + "configuration": { + "acceptedOutputModes": ["text"] + } + }\ + """; + + RestHandler.HTTPRestResponse response = + handler.sendStreamingMessage(contextWithVersion, "", requestBody); // Streaming responses embed errors in the stream with status 200 Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertInstanceOf(RestHandler.HTTPRestStreamingResponse.class, response); // Subscribe to publisher and verify error in stream - RestHandler.HTTPRestStreamingResponse streamingResponse = (RestHandler.HTTPRestStreamingResponse) response; + RestHandler.HTTPRestStreamingResponse streamingResponse = + (RestHandler.HTTPRestStreamingResponse) response; Flow.Publisher publisher = streamingResponse.getPublisher(); AtomicBoolean errorFound = new AtomicBoolean(false); CountDownLatch latch = new CountDownLatch(1); - publisher.subscribe(new Flow.Subscriber<>() { - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscription.request(Long.MAX_VALUE); - } + publisher.subscribe( + new Flow.Subscriber<>() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } - @Override - public void onNext(String item) { - JsonObject body = JsonParser.parseString(item).getAsJsonObject(); - if (body.has("error")) { - JsonObject error = body.getAsJsonObject("error"); - var details = error.has("details") ? error.getAsJsonArray("details") : null; - if (details != null && !details.isEmpty()) { - String reason = details.get(0).getAsJsonObject().get("reason").getAsString(); - if ("VERSION_NOT_SUPPORTED".equals(reason) && - error.has("message") && error.get("message").getAsString().contains("2.0")) { - errorFound.set(true); + @Override + public void onNext(String item) { + JsonObject body = JsonParser.parseString(item).getAsJsonObject(); + if (body.has("error")) { + JsonObject error = body.getAsJsonObject("error"); + var details = + error.has("details") ? error.getAsJsonArray("details") : null; + if (details != null && !details.isEmpty()) { + String reason = + details.get(0) + .getAsJsonObject() + .get("reason") + .getAsString(); + if ("VERSION_NOT_SUPPORTED".equals(reason) + && error.has("message") + && error.get("message").getAsString().contains("2.0")) { + errorFound.set(true); + } + } } } - } - } - @Override - public void onError(Throwable throwable) { - latch.countDown(); - } + @Override + public void onError(Throwable throwable) { + latch.countDown(); + } - @Override - public void onComplete() { - latch.countDown(); - } - }); + @Override + public void onComplete() { + latch.countDown(); + } + }); try { Assertions.assertTrue(latch.await(1, TimeUnit.SECONDS)); @@ -962,51 +1167,65 @@ public void onComplete() { @Test public void testCompatibleVersionSuccess() { // Create AgentCard with protocol version 1.0 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); - - RestHandler handler = new RestHandler(agentCard, createCacheMetadata(agentCard), requestHandler, internalExecutor); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("HTTP+JSON", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + + RestHandler handler = + new RestHandler( + agentCard, + createCacheMetadata(agentCard), + requestHandler, + internalExecutor); // Create context with compatible version 1.1 - ServerCallContext contextWithVersion = new ServerCallContext( - UnauthenticatedUser.INSTANCE, - Map.of("foo", "bar"), - new HashSet<>(), - "1.1" // Compatible version (same major version) - ); - - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; - - String requestBody = """ - { - "message": { - "messageId": "message-1234", - "contextId": "context-1234", - "role": "ROLE_USER", - "parts": [{ - "text": "tell me a joke" - }], - "metadata": {} - }, - "configuration": { - "returnImmediately": false - } - }"""; - - RestHandler.HTTPRestResponse response = handler.sendMessage(contextWithVersion, "", requestBody); + ServerCallContext contextWithVersion = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of("foo", "bar"), + new HashSet<>(), + "1.1" // Compatible version (same major version) + ); + + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; + + String requestBody = + """ + { + "message": { + "messageId": "message-1234", + "contextId": "context-1234", + "role": "ROLE_USER", + "parts": [{ + "text": "tell me a joke" + }], + "metadata": {} + }, + "configuration": { + "returnImmediately": false + } + }\ + """; + + RestHandler.HTTPRestResponse response = + handler.sendMessage(contextWithVersion, "", requestBody); // Should succeed without error Assertions.assertEquals(200, response.getStatusCode()); @@ -1017,47 +1236,61 @@ public void testCompatibleVersionSuccess() { @Test public void testNoVersionDefaultsTo0_3_RejectedByV10OnlyServer() { // Per spec Section 3.6.2: missing A2A-Version defaults to 0.3 - AgentCard agentCard = AgentCard.builder() - .name("test-card") - .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) - .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(false) - .build()) - .defaultInputModes(List.of("text")) - .defaultOutputModes(List.of("text")) - .skills(List.of()) - .build(); + AgentCard agentCard = + AgentCard.builder() + .name("test-card") + .description("Test card with version 1.0") + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("HTTP+JSON", "http://localhost:9999"))) + .version("1.0.0") + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(false) + .build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of()) + .build(); + + RestHandler handler = + new RestHandler( + agentCard, + createCacheMetadata(agentCard), + requestHandler, + internalExecutor); + + agentExecutorExecute = + (context, agentEmitter) -> { + agentEmitter.sendMessage(context.getMessage()); + }; - RestHandler handler = new RestHandler(agentCard, createCacheMetadata(agentCard), requestHandler, internalExecutor); + // Context with no version — defaults to 0.3, incompatible with v1.0-only server + ServerCallContext noVersionContext = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>()); - agentExecutorExecute = (context, agentEmitter) -> { - agentEmitter.sendMessage(context.getMessage()); - }; + String requestBody = + """ + { + "message": { + "messageId": "message-1234", + "contextId": "context-1234", + "role": "ROLE_USER", + "parts": [{ + "text": "tell me a joke" + }], + "metadata": {} + }, + "configuration": { + "returnImmediately": false + } + }\ + """; - // Context with no version — defaults to 0.3, incompatible with v1.0-only server - ServerCallContext noVersionContext = new ServerCallContext( - UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>()); - - String requestBody = """ - { - "message": { - "messageId": "message-1234", - "contextId": "context-1234", - "role": "ROLE_USER", - "parts": [{ - "text": "tell me a joke" - }], - "metadata": {} - }, - "configuration": { - "returnImmediately": false - } - }"""; - - RestHandler.HTTPRestResponse response = handler.sendMessage(noVersionContext, "", requestBody); + RestHandler.HTTPRestResponse response = + handler.sendMessage(noVersionContext, "", requestBody); // Should return error (0.3 is not supported by v1.0-only server) Assertions.assertEquals(400, response.getStatusCode()); @@ -1067,33 +1300,44 @@ public void testNoVersionDefaultsTo0_3_RejectedByV10OnlyServer() { @Test public void testListTasksNegativeTimestampReturns422() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); // Negative timestamp should return 422 (Invalid params) - RestHandler.HTTPRestResponse response = handler.listTasks(callContext, "", null, null, null, null, - null, "-1", null); + RestHandler.HTTPRestResponse response = + handler.listTasks(callContext, "", null, null, null, null, null, "-1", null); - assertProblemDetail(response, 422, - "INVALID_PARAMS", "Invalid params"); + assertProblemDetail(response, 422, "INVALID_PARAMS", "Invalid params"); } @Test public void testListTasksUnixMillisecondsTimestamp() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); // Unix milliseconds timestamp are no longer accepted - RestHandler.HTTPRestResponse response = handler.listTasks(callContext, "", null, null, null, null, - null, "1234567", null); + RestHandler.HTTPRestResponse response = + handler.listTasks(callContext, "", null, null, null, null, null, "1234567", null); Assertions.assertEquals(422, response.getStatusCode()); } @Test public void testListTasksProtobufEnumStatus() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); // Protobuf enum format (TASK_STATE_SUBMITTED) should be accepted - RestHandler.HTTPRestResponse response = handler.listTasks(callContext, "", null, "TASK_STATE_SUBMITTED", null, null, - null, null, null); + RestHandler.HTTPRestResponse response = + handler.listTasks( + callContext, + "", + null, + "TASK_STATE_SUBMITTED", + null, + null, + null, + null, + null); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -1102,12 +1346,22 @@ public void testListTasksProtobufEnumStatus() { @Test public void testListTasksEnumConstantStatus() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); taskStore.save(MINIMAL_TASK, false); // Enum constant format (TASK_STATE_SUBMITTED) should be accepted - RestHandler.HTTPRestResponse response = handler.listTasks(callContext, "", null, "TASK_STATE_SUBMITTED", null, null, - null, null, null); + RestHandler.HTTPRestResponse response = + handler.listTasks( + callContext, + "", + null, + "TASK_STATE_SUBMITTED", + null, + null, + null, + null, + null); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -1116,11 +1370,21 @@ public void testListTasksEnumConstantStatus() { @Test public void testListTasksEmptyResultIncludesAllFields() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); // Query for a context that doesn't exist - should return empty result with all fields - RestHandler.HTTPRestResponse response = handler.listTasks(callContext, "", "nonexistent-context-id", null, null, null, - null, null, null); + RestHandler.HTTPRestResponse response = + handler.listTasks( + callContext, + "", + "nonexistent-context-id", + null, + null, + null, + null, + null, + null); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); @@ -1128,11 +1392,15 @@ public void testListTasksEmptyResultIncludesAllFields() { String body = response.getBody(); // Verify all required fields are present (not missing) Assertions.assertTrue(body.contains("\"tasks\""), "Response should contain tasks field"); - Assertions.assertTrue(body.contains("\"totalSize\""), "Response should contain totalSize field"); - Assertions.assertTrue(body.contains("\"pageSize\""), "Response should contain pageSize field"); - Assertions.assertTrue(body.contains("\"nextPageToken\""), "Response should contain nextPageToken field"); + Assertions.assertTrue( + body.contains("\"totalSize\""), "Response should contain totalSize field"); + Assertions.assertTrue( + body.contains("\"pageSize\""), "Response should contain pageSize field"); + Assertions.assertTrue( + body.contains("\"nextPageToken\""), "Response should contain nextPageToken field"); // Verify empty array, not null - Assertions.assertTrue(body.contains("\"tasks\":[]") || body.contains("\"tasks\": []"), + Assertions.assertTrue( + body.contains("\"tasks\":[]") || body.contains("\"tasks\": []"), "tasks should be empty array"); } @@ -1140,27 +1408,44 @@ public void testListTasksEmptyResultIncludesAllFields() { void constructorDoesNotResolveAgentCardInstances() { Instance throwOnGet = TestInstances.throwOnGet(); - Assertions.assertDoesNotThrow(() -> new RestHandler(throwOnGet, throwOnGet, - createCacheMetadata(), requestHandler, internalExecutor, null)); - } - - private static void assertProblemDetail(RestHandler.HTTPRestResponse response, - int expectedStatus, String expectedReason, String expectedMessage) { + Assertions.assertDoesNotThrow( + () -> + new RestHandler( + throwOnGet, + throwOnGet, + createCacheMetadata(), + requestHandler, + internalExecutor, + null)); + } + + private static void assertProblemDetail( + RestHandler.HTTPRestResponse response, + int expectedStatus, + String expectedReason, + String expectedMessage) { Assertions.assertEquals(expectedStatus, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); JsonObject body = JsonParser.parseString(response.getBody()).getAsJsonObject(); Assertions.assertTrue(body.has("error"), "error wrapper should be present"); JsonObject error = body.getAsJsonObject("error"); - Assertions.assertEquals(expectedStatus, error.get("code").getAsInt(), "code field mismatch"); - Assertions.assertEquals(expectedMessage, error.get("message").getAsString(), "message field mismatch"); + Assertions.assertEquals( + expectedStatus, error.get("code").getAsInt(), "code field mismatch"); + Assertions.assertEquals( + expectedMessage, error.get("message").getAsString(), "message field mismatch"); Assertions.assertTrue(error.has("status"), "status field should be present"); Assertions.assertTrue(error.has("details"), "details field should be present"); var details = error.getAsJsonArray("details"); Assertions.assertFalse(details.isEmpty(), "details array should not be empty"); JsonObject detail = details.get(0).getAsJsonObject(); - Assertions.assertEquals("type.googleapis.com/google.rpc.ErrorInfo", detail.get("@type").getAsString(), "@type field mismatch"); - Assertions.assertEquals(expectedReason, detail.get("reason").getAsString(), "reason field mismatch"); - Assertions.assertEquals("a2a-protocol.org", detail.get("domain").getAsString(), "domain field mismatch"); + Assertions.assertEquals( + "type.googleapis.com/google.rpc.ErrorInfo", + detail.get("@type").getAsString(), + "@type field mismatch"); + Assertions.assertEquals( + expectedReason, detail.get("reason").getAsString(), "reason field mismatch"); + Assertions.assertEquals( + "a2a-protocol.org", detail.get("domain").getAsString(), "domain field mismatch"); } @Test @@ -1168,10 +1453,13 @@ public void testSendMessageSanitizesInternalError() { // A non-A2AError exception must not leak its message to the client RequestHandler mocked = Mockito.mock(RequestHandler.class); Mockito.doThrow(new RuntimeException("sensitive detail: /var/lib/secret/config.db")) - .when(mocked).onMessageSend(Mockito.any(), Mockito.any()); + .when(mocked) + .onMessageSend(Mockito.any(), Mockito.any()); - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), mocked, internalExecutor); - String requestBody = """ + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), mocked, internalExecutor); + String requestBody = + """ { "message": { "messageId": "message-1234", @@ -1180,7 +1468,8 @@ public void testSendMessageSanitizesInternalError() { "parts": [{"text": "hello"}], "metadata": {} } - }"""; + }\ + """; RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", requestBody); @@ -1188,7 +1477,8 @@ public void testSendMessageSanitizesInternalError() { JsonObject error = body.getAsJsonObject("error"); Assertions.assertEquals(500, error.get("code").getAsInt()); Assertions.assertEquals("Internal error", error.get("message").getAsString()); - Assertions.assertFalse(error.get("message").getAsString().contains("sensitive"), + Assertions.assertFalse( + error.get("message").getAsString().contains("sensitive"), "Internal exception message must not be leaked to the client"); } @@ -1197,7 +1487,8 @@ public void testVersionNotSupportedErrorOnGetTask() { RestHandler handler = versionTestHandler(); taskStore.save(MINIMAL_TASK, false); - assertVersionRejected(handler.getTask(incompatibleVersionContext(), "", MINIMAL_TASK.id(), null)); + assertVersionRejected( + handler.getTask(incompatibleVersionContext(), "", MINIMAL_TASK.id(), null)); } @Test @@ -1205,8 +1496,17 @@ public void testVersionNotSupportedErrorOnListTasks() { RestHandler handler = versionTestHandler(); taskStore.save(MINIMAL_TASK, false); - assertVersionRejected(handler.listTasks(incompatibleVersionContext(), "", - MINIMAL_TASK.contextId(), null, null, null, null, null, null)); + assertVersionRejected( + handler.listTasks( + incompatibleVersionContext(), + "", + MINIMAL_TASK.contextId(), + null, + null, + null, + null, + null, + null)); } @Test @@ -1218,7 +1518,8 @@ public void testVersionNotSupportedErrorOnCancelTask() { // waits forever for a final event rather than failing the assertion. agentExecutorCancel = (context, agentEmitter) -> agentEmitter.cancel(); - assertVersionRejected(handler.cancelTask(incompatibleVersionContext(), "", "{}", MINIMAL_TASK.id())); + assertVersionRejected( + handler.cancelTask(incompatibleVersionContext(), "", "{}", MINIMAL_TASK.id())); } @Test @@ -1226,15 +1527,19 @@ public void testVersionNotSupportedErrorOnCreateTaskPushNotificationConfiguratio RestHandler handler = versionTestHandler(); taskStore.save(MINIMAL_TASK, false); - String requestBody = """ - { - "id": "config-1", - "taskId": "%s", - "url": "http://example.com" - }""".formatted(MINIMAL_TASK.id()); + String requestBody = + """ + { + "id": "config-1", + "taskId": "%s", + "url": "http://example.com" + }\ + """ + .formatted(MINIMAL_TASK.id()); - assertVersionRejected(handler.createTaskPushNotificationConfiguration( - incompatibleVersionContext(), "", requestBody, MINIMAL_TASK.id())); + assertVersionRejected( + handler.createTaskPushNotificationConfiguration( + incompatibleVersionContext(), "", requestBody, MINIMAL_TASK.id())); } @Test @@ -1242,8 +1547,9 @@ public void testVersionNotSupportedErrorOnGetTaskPushNotificationConfiguration() RestHandler handler = versionTestHandler(); taskStore.save(MINIMAL_TASK, false); - assertVersionRejected(handler.getTaskPushNotificationConfiguration( - incompatibleVersionContext(), "", MINIMAL_TASK.id(), "config-1")); + assertVersionRejected( + handler.getTaskPushNotificationConfiguration( + incompatibleVersionContext(), "", MINIMAL_TASK.id(), "config-1")); } @Test @@ -1251,8 +1557,9 @@ public void testVersionNotSupportedErrorOnListTaskPushNotificationConfigurations RestHandler handler = versionTestHandler(); taskStore.save(MINIMAL_TASK, false); - assertVersionRejected(handler.listTaskPushNotificationConfigurations( - incompatibleVersionContext(), "", MINIMAL_TASK.id(), "10", "")); + assertVersionRejected( + handler.listTaskPushNotificationConfigurations( + incompatibleVersionContext(), "", MINIMAL_TASK.id(), "10", "")); } @Test @@ -1260,8 +1567,9 @@ public void testVersionNotSupportedErrorOnDeleteTaskPushNotificationConfiguratio RestHandler handler = versionTestHandler(); taskStore.save(MINIMAL_TASK, false); - assertVersionRejected(handler.deleteTaskPushNotificationConfiguration( - incompatibleVersionContext(), "", MINIMAL_TASK.id(), "config-1")); + assertVersionRejected( + handler.deleteTaskPushNotificationConfiguration( + incompatibleVersionContext(), "", MINIMAL_TASK.id(), "config-1")); } @Test @@ -1270,22 +1578,36 @@ public void testVersionNotSupportedErrorOnGetExtendedAgentCard() { AgentCard extended = AgentCard.builder(card).description("extended").build(); Instance extendedInstance = new FixedInstance<>(extended); - RestHandler handler = new RestHandler(new FixedInstance<>(card), extendedInstance, - createCacheMetadata(card), requestHandler, internalExecutor, null); + RestHandler handler = + new RestHandler( + new FixedInstance<>(card), + extendedInstance, + createCacheMetadata(card), + requestHandler, + internalExecutor, + null); assertVersionRejected(handler.getExtendedAgentCard(incompatibleVersionContext(), "")); } @Test public void testExtendedAgentCardWithRouterKnownTenant() { - AgentCard cardWithExtCapability = AgentCard.builder(CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); + AgentCard cardWithExtCapability = + AgentCard.builder(CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); AgentCard tenantCard = AgentCard.builder(cardWithExtCapability).name("acme-card").build(); - AgentCardRouter router = tenant -> "acme".equals(tenant) ? tenantCard : cardWithExtCapability; - - RestHandler handler = new RestHandler(new FixedInstance<>(cardWithExtCapability), null, - createCacheMetadata(cardWithExtCapability), requestHandler, internalExecutor, - new FixedInstance<>(router)); + AgentCardRouter router = + tenant -> "acme".equals(tenant) ? tenantCard : cardWithExtCapability; + + RestHandler handler = + new RestHandler( + new FixedInstance<>(cardWithExtCapability), + null, + createCacheMetadata(cardWithExtCapability), + requestHandler, + internalExecutor, + new FixedInstance<>(router)); RestHandler.HTTPRestResponse response = handler.getExtendedAgentCard(callContext, "acme"); @@ -1296,39 +1618,59 @@ public void testExtendedAgentCardWithRouterKnownTenant() { @Test public void testExtendedAgentCardWithRouterReturnsNull() { - AgentCard cardWithExtCapability = AgentCard.builder(CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); - AgentCardRouter router = new AgentCardRouter() { - @Override - public AgentCard resolveExtendedCard(String tenant) { - return null; - } + AgentCard cardWithExtCapability = + AgentCard.builder(CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); + AgentCardRouter router = + new AgentCardRouter() { + @Override + public AgentCard resolveExtendedCard(String tenant) { + return null; + } - @Override - public AgentCard resolvePublicCard(String tenant) { - return cardWithExtCapability; - } - }; + @Override + public AgentCard resolvePublicCard(String tenant) { + return cardWithExtCapability; + } + }; - RestHandler handler = new RestHandler(new FixedInstance<>(cardWithExtCapability), null, - createCacheMetadata(cardWithExtCapability), requestHandler, internalExecutor, - new FixedInstance<>(router)); + RestHandler handler = + new RestHandler( + new FixedInstance<>(cardWithExtCapability), + null, + createCacheMetadata(cardWithExtCapability), + requestHandler, + internalExecutor, + new FixedInstance<>(router)); RestHandler.HTTPRestResponse response = handler.getExtendedAgentCard(callContext, "acme"); - assertProblemDetail(response, 400, - "EXTENDED_AGENT_CARD_NOT_CONFIGURED", "Extended Card not configured"); + assertProblemDetail( + response, + 400, + "EXTENDED_AGENT_CARD_NOT_CONFIGURED", + "Extended Card not configured"); } @Test public void testExtendedAgentCardWithoutRouter() { - AgentCard cardWithExtCapability = AgentCard.builder(CARD) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); - AgentCard extended = AgentCard.builder(cardWithExtCapability).description("extended").build(); + AgentCard cardWithExtCapability = + AgentCard.builder(CARD) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); + AgentCard extended = + AgentCard.builder(cardWithExtCapability).description("extended").build(); Instance extendedInstance = new FixedInstance<>(extended); - RestHandler handler = new RestHandler(new FixedInstance<>(cardWithExtCapability), extendedInstance, - createCacheMetadata(cardWithExtCapability), requestHandler, internalExecutor, null); + RestHandler handler = + new RestHandler( + new FixedInstance<>(cardWithExtCapability), + extendedInstance, + createCacheMetadata(cardWithExtCapability), + requestHandler, + internalExecutor, + null); RestHandler.HTTPRestResponse response = handler.getExtendedAgentCard(callContext, "acme"); @@ -1343,13 +1685,27 @@ public void testExtendedAgentCardWithoutRouter() { @Test public void testGetPublicAgentCard_withRouterKnownTenant_returns200() { AgentCard tenantCard = AgentCard.builder(CARD).name("acme-public").build(); - AgentCardRouter router = new AgentCardRouter() { - @Override public AgentCard resolveExtendedCard(String t) { return null; } - @Override public AgentCard resolvePublicCard(String t) { return "acme".equals(t) ? tenantCard : null; } - }; + AgentCardRouter router = + new AgentCardRouter() { + @Override + public AgentCard resolveExtendedCard(String t) { + return null; + } + + @Override + public AgentCard resolvePublicCard(String t) { + return "acme".equals(t) ? tenantCard : null; + } + }; - RestHandler handler = new RestHandler(new FixedInstance<>(CARD), null, - createCacheMetadata(CARD), requestHandler, internalExecutor, new FixedInstance<>(router)); + RestHandler handler = + new RestHandler( + new FixedInstance<>(CARD), + null, + createCacheMetadata(CARD), + requestHandler, + internalExecutor, + new FixedInstance<>(router)); RestHandler.HTTPRestResponse response = handler.getAgentCard("acme"); @@ -1359,13 +1715,27 @@ public void testGetPublicAgentCard_withRouterKnownTenant_returns200() { @Test public void testGetPublicAgentCard_withRouterUnknownTenant_returns404() { - AgentCardRouter router = new AgentCardRouter() { - @Override public AgentCard resolveExtendedCard(String t) { return null; } - @Override public AgentCard resolvePublicCard(String t) { return null; } - }; + AgentCardRouter router = + new AgentCardRouter() { + @Override + public AgentCard resolveExtendedCard(String t) { + return null; + } + + @Override + public AgentCard resolvePublicCard(String t) { + return null; + } + }; - RestHandler handler = new RestHandler(new FixedInstance<>(CARD), null, - createCacheMetadata(CARD), requestHandler, internalExecutor, new FixedInstance<>(router)); + RestHandler handler = + new RestHandler( + new FixedInstance<>(CARD), + null, + createCacheMetadata(CARD), + requestHandler, + internalExecutor, + new FixedInstance<>(router)); RestHandler.HTTPRestResponse response = handler.getAgentCard("unknown"); @@ -1375,8 +1745,14 @@ public void testGetPublicAgentCard_withRouterUnknownTenant_returns404() { @Test public void testGetPublicAgentCard_noRouterWithTenant_returnsDefaultCard() { // Single-tenant server: no AgentCardRouter configured; tenant segment is ignored. - RestHandler handler = new RestHandler(new FixedInstance<>(CARD), null, - createCacheMetadata(CARD), requestHandler, internalExecutor, null); + RestHandler handler = + new RestHandler( + new FixedInstance<>(CARD), + null, + createCacheMetadata(CARD), + requestHandler, + internalExecutor, + null); RestHandler.HTTPRestResponse response = handler.getAgentCard("any-tenant"); @@ -1386,8 +1762,14 @@ public void testGetPublicAgentCard_noRouterWithTenant_returnsDefaultCard() { @Test public void testGetPublicAgentCard_noTenant_returnsDefaultCard() { - RestHandler handler = new RestHandler(new FixedInstance<>(CARD), null, - createCacheMetadata(CARD), requestHandler, internalExecutor, null); + RestHandler handler = + new RestHandler( + new FixedInstance<>(CARD), + null, + createCacheMetadata(CARD), + requestHandler, + internalExecutor, + null); RestHandler.HTTPRestResponse response = handler.getAgentCard((String) null); @@ -1416,13 +1798,16 @@ private static AgentCard versionTestCard() { return AgentCard.builder() .name("test-card") .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) + .supportedInterfaces( + Collections.singletonList( + new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") - .capabilities(AgentCapabilities.builder() - .streaming(true) - .pushNotifications(true) - .extendedAgentCard(true) - .build()) + .capabilities( + AgentCapabilities.builder() + .streaming(true) + .pushNotifications(true) + .extendedAgentCard(true) + .build()) .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) .skills(List.of()) @@ -1439,7 +1824,8 @@ private RestHandler versionTestHandler() { * {@link #versionTestCard()} and is therefore incompatible under section 3.6.2. */ private static ServerCallContext incompatibleVersionContext() { - return new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "2.0"); + return new ServerCallContext( + UnauthenticatedUser.INSTANCE, Map.of("foo", "bar"), new HashSet<>(), "2.0"); } /** @@ -1449,7 +1835,10 @@ private static ServerCallContext incompatibleVersionContext() { * @param response the response an operation returned */ private static void assertVersionRejected(RestHandler.HTTPRestResponse response) { - assertProblemDetail(response, 400, "VERSION_NOT_SUPPORTED", + assertProblemDetail( + response, + 400, + "VERSION_NOT_SUPPORTED", "Protocol version '2.0' is not supported. Supported versions: [1.0]"); } @@ -1460,101 +1849,158 @@ private static void assertVersionRejected(RestHandler.HTTPRestResponse response) * @param response the streaming response under test * @throws InterruptedException if the wait for termination is interrupted */ - private static void assertVersionRejectedInStream(RestHandler.HTTPRestStreamingResponse response) - throws InterruptedException { + private static void assertVersionRejectedInStream( + RestHandler.HTTPRestStreamingResponse response) throws InterruptedException { AtomicBoolean errorFound = new AtomicBoolean(false); CountDownLatch latch = new CountDownLatch(1); - response.getPublisher().subscribe(new Flow.Subscriber<>() { - @Override - public void onSubscribe(Flow.Subscription subscription) { - subscription.request(Long.MAX_VALUE); - } - - @Override - public void onNext(String item) { - JsonObject body = JsonParser.parseString(item).getAsJsonObject(); - if (!body.has("error")) { - return; - } - JsonObject error = body.getAsJsonObject("error"); - var details = error.has("details") ? error.getAsJsonArray("details") : null; - if (details != null && !details.isEmpty() - && "VERSION_NOT_SUPPORTED".equals(details.get(0).getAsJsonObject().get("reason").getAsString()) - && error.get("message").getAsString().contains("2.0")) { - errorFound.set(true); - } - } - - @Override - public void onError(Throwable throwable) { - latch.countDown(); - } - - @Override - public void onComplete() { - latch.countDown(); - } - }); - - Assertions.assertTrue(latch.await(2, TimeUnit.SECONDS), "Expected the stream to terminate within timeout"); - Assertions.assertTrue(errorFound.get(), "Expected a VERSION_NOT_SUPPORTED event in the stream"); + response.getPublisher() + .subscribe( + new Flow.Subscriber<>() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(String item) { + JsonObject body = JsonParser.parseString(item).getAsJsonObject(); + if (!body.has("error")) { + return; + } + JsonObject error = body.getAsJsonObject("error"); + var details = + error.has("details") + ? error.getAsJsonArray("details") + : null; + if (details != null + && !details.isEmpty() + && "VERSION_NOT_SUPPORTED" + .equals( + details.get(0) + .getAsJsonObject() + .get("reason") + .getAsString()) + && error.get("message").getAsString().contains("2.0")) { + errorFound.set(true); + } + } + + @Override + public void onError(Throwable throwable) { + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } + }); + + Assertions.assertTrue( + latch.await(2, TimeUnit.SECONDS), + "Expected the stream to terminate within timeout"); + Assertions.assertTrue( + errorFound.get(), "Expected a VERSION_NOT_SUPPORTED event in the stream"); } @Test public void testGetAgentCardReturns404WhenNoPublicCard() { - RestHandler handler = new RestHandler(null, createCacheMetadata(CARD), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(null, createCacheMetadata(CARD), requestHandler, internalExecutor); RestHandler.HTTPRestResponse response = handler.getAgentCard(); Assertions.assertEquals(404, response.getStatusCode()); } @Test public void testGetAgentCardWithTenantReturns404WhenNoPublicCard() { - RestHandler handler = new RestHandler(null, createCacheMetadata(CARD), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(null, createCacheMetadata(CARD), requestHandler, internalExecutor); RestHandler.HTTPRestResponse response = handler.getAgentCard("some-tenant"); Assertions.assertEquals(404, response.getStatusCode()); } + @Test public void testUnknownOperationTenantIsInvalidParams() { AgentCardRouter router = tenant -> null; - RestHandler handler = new RestHandler(new FixedInstance<>(CARD), null, createCacheMetadata(), - requestHandler, internalExecutor, new FixedInstance<>(router)); + RestHandler handler = + new RestHandler( + new FixedInstance<>(CARD), + null, + createCacheMetadata(), + requestHandler, + internalExecutor, + new FixedInstance<>(router)); - assertProblemDetail(handler.sendMessage(callContext, "unknown", "{broken"), 422, - "INVALID_PARAMS", "No public agent card registered for tenant: unknown"); + assertProblemDetail( + handler.sendMessage(callContext, "unknown", "{broken"), + 422, + "INVALID_PARAMS", + "No public agent card registered for tenant: unknown"); } @Test public void testInvalidTenantPrecedesVersionAndStreamingBody() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); String message = "Tenant contains invalid characters. Only a-zA-Z0-9_-. are allowed"; - assertProblemDetail(handler.sendMessage(incompatibleVersionContext(), "bad/tenant", "{broken"), - 422, "INVALID_PARAMS", message); - RestHandler.HTTPRestStreamingResponse response = Assertions.assertInstanceOf( - RestHandler.HTTPRestStreamingResponse.class, - handler.sendStreamingMessage(callContext, "bad/tenant", "{broken")); + assertProblemDetail( + handler.sendMessage(incompatibleVersionContext(), "bad/tenant", "{broken"), + 422, + "INVALID_PARAMS", + message); + RestHandler.HTTPRestStreamingResponse response = + Assertions.assertInstanceOf( + RestHandler.HTTPRestStreamingResponse.class, + handler.sendStreamingMessage(callContext, "bad/tenant", "{broken")); assertSingleStreamProblem(response, 422, "INVALID_PARAMS", message); } @Test public void testDisabledStreamingPrecedesMalformedBody() { - AgentCard card = AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().build()).build(); - RestHandler handler = new RestHandler(card, createCacheMetadata(card), requestHandler, internalExecutor); - assertProblemDetail(handler.sendStreamingMessage(callContext, "", "{broken"), 400, - "UNSUPPORTED_OPERATION", "Streaming is not supported by the agent"); + AgentCard card = + AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().build()).build(); + RestHandler handler = + new RestHandler(card, createCacheMetadata(card), requestHandler, internalExecutor); + assertProblemDetail( + handler.sendStreamingMessage(callContext, "", "{broken"), + 400, + "UNSUPPORTED_OPERATION", + "Streaming is not supported by the agent"); } - private static void assertSingleStreamProblem(RestHandler.HTTPRestStreamingResponse response, - int status, String reason, String message) { + private static void assertSingleStreamProblem( + RestHandler.HTTPRestStreamingResponse response, + int status, + String reason, + String message) { CompletableFuture event = new CompletableFuture<>(); - response.getPublisher().subscribe(new Flow.Subscriber<>() { - @Override public void onSubscribe(Flow.Subscription subscription) { subscription.request(Long.MAX_VALUE); } - @Override public void onNext(String item) { event.complete(item); } - @Override public void onError(Throwable failure) { event.completeExceptionally(failure); } - @Override public void onComplete() { } - }); - assertProblemDetail(new RestHandler.HTTPRestResponse(status, APPLICATION_JSON, event.join()), - status, reason, message); + response.getPublisher() + .subscribe( + new Flow.Subscriber<>() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(String item) { + event.complete(item); + } + + @Override + public void onError(Throwable failure) { + event.completeExceptionally(failure); + } + + @Override + public void onComplete() {} + }); + assertProblemDetail( + new RestHandler.HTTPRestResponse(status, APPLICATION_JSON, event.join()), + status, + reason, + message); } @Test @@ -1563,10 +2009,17 @@ public void testBoundarySlashesCanonicalizedForRoutingAndDispatch() { Mockito.when(router.resolvePublicCard("acme")).thenReturn(CARD); RequestHandler delegate = Mockito.mock(RequestHandler.class); Mockito.when(delegate.onGetTask(Mockito.any(), Mockito.any())).thenReturn(MINIMAL_TASK); - RestHandler handler = new RestHandler(new FixedInstance<>(CARD), null, createCacheMetadata(), - delegate, internalExecutor, new FixedInstance<>(router)); - - Assertions.assertEquals(200, handler.getTask(callContext, "/acme/", "task", null).getStatusCode()); + RestHandler handler = + new RestHandler( + new FixedInstance<>(CARD), + null, + createCacheMetadata(), + delegate, + internalExecutor, + new FixedInstance<>(router)); + + Assertions.assertEquals( + 200, handler.getTask(callContext, "/acme/", "task", null).getStatusCode()); ArgumentCaptor params = ArgumentCaptor.forClass(TaskQueryParams.class); Mockito.verify(delegate).onGetTask(params.capture(), Mockito.same(callContext)); Assertions.assertEquals("acme", params.getValue().tenant()); @@ -1578,47 +2031,96 @@ public void testBoundarySlashesCanonicalizedForRoutingAndDispatch() { @Test public void testPublicDiscoveryValidatesDefaultAndRouterCardsOnce() { AgentCard routed = AgentCard.builder(CARD).name("routed").build(); - AgentCardRouter router = new AgentCardRouter() { - @Override public AgentCard resolvePublicCard(String tenant) { return routed; } - @Override public AgentCard resolveExtendedCard(String tenant) { return null; } - }; - RestHandler handler = new RestHandler(new FixedInstance<>(CARD), null, createCacheMetadata(), - requestHandler, internalExecutor, new FixedInstance<>(router)); - try (MockedStatic validator = Mockito.mockStatic(AgentCardValidator.class, Mockito.CALLS_REAL_METHODS)) { + AgentCardRouter router = + new AgentCardRouter() { + @Override + public AgentCard resolvePublicCard(String tenant) { + return routed; + } + + @Override + public AgentCard resolveExtendedCard(String tenant) { + return null; + } + }; + RestHandler handler = + new RestHandler( + new FixedInstance<>(CARD), + null, + createCacheMetadata(), + requestHandler, + internalExecutor, + new FixedInstance<>(router)); + try (MockedStatic validator = + Mockito.mockStatic(AgentCardValidator.class, Mockito.CALLS_REAL_METHODS)) { for (int i = 0; i < 2; i++) { Assertions.assertEquals(200, handler.getAgentCard().getStatusCode()); RestHandler.HTTPRestResponse response = handler.getAgentCard("acme"); Assertions.assertEquals(200, response.getStatusCode()); - Assertions.assertEquals("routed", JsonParser.parseString(response.getBody()).getAsJsonObject().get("name").getAsString()); + Assertions.assertEquals( + "routed", + JsonParser.parseString(response.getBody()) + .getAsJsonObject() + .get("name") + .getAsString()); } - validator.verify(() -> AgentCardValidator.validateTransportConfiguration(CARD), Mockito.times(1)); - validator.verify(() -> AgentCardValidator.validateTransportConfiguration(routed), Mockito.times(1)); + validator.verify( + () -> AgentCardValidator.validateTransportConfiguration(CARD), + Mockito.times(1)); + validator.verify( + () -> AgentCardValidator.validateTransportConfiguration(routed), + Mockito.times(1)); } } @Test public void testDiscoveryValidationFailureCanRetry() { - AgentCardRouter router = new AgentCardRouter() { - @Override public AgentCard resolvePublicCard(String tenant) { return CARD; } - @Override public AgentCard resolveExtendedCard(String tenant) { return null; } - }; - RestHandler handler = new RestHandler(new FixedInstance<>(CARD), null, createCacheMetadata(), - requestHandler, internalExecutor, new FixedInstance<>(router)); - try (MockedStatic validator = Mockito.mockStatic(AgentCardValidator.class, Mockito.CALLS_REAL_METHODS)) { - validator.when(() -> AgentCardValidator.validateTransportConfiguration(CARD)) - .thenThrow(new IllegalArgumentException("invalid transport")).thenCallRealMethod(); + AgentCardRouter router = + new AgentCardRouter() { + @Override + public AgentCard resolvePublicCard(String tenant) { + return CARD; + } + + @Override + public AgentCard resolveExtendedCard(String tenant) { + return null; + } + }; + RestHandler handler = + new RestHandler( + new FixedInstance<>(CARD), + null, + createCacheMetadata(), + requestHandler, + internalExecutor, + new FixedInstance<>(router)); + try (MockedStatic validator = + Mockito.mockStatic(AgentCardValidator.class, Mockito.CALLS_REAL_METHODS)) { + validator + .when(() -> AgentCardValidator.validateTransportConfiguration(CARD)) + .thenThrow(new IllegalArgumentException("invalid transport")) + .thenCallRealMethod(); validator.clearInvocations(); assertProblemDetail(handler.getAgentCard("acme"), 500, "INTERNAL", "Internal error"); Assertions.assertEquals(200, handler.getAgentCard("acme").getStatusCode()); Assertions.assertEquals(200, handler.getAgentCard("acme").getStatusCode()); - validator.verify(() -> AgentCardValidator.validateTransportConfiguration(CARD), Mockito.times(2)); + validator.verify( + () -> AgentCardValidator.validateTransportConfiguration(CARD), + Mockito.times(2)); } } @Test public void testPublicDiscoveryNeverExposesOnlyExtendedCard() { - RestHandler handler = new RestHandler(FixedInstance.empty(), new FixedInstance<>(CARD), createCacheMetadata(), - requestHandler, internalExecutor, null); + RestHandler handler = + new RestHandler( + FixedInstance.empty(), + new FixedInstance<>(CARD), + createCacheMetadata(), + requestHandler, + internalExecutor, + null); RestHandler.HTTPRestResponse response = handler.getAgentCard(); Assertions.assertEquals(404, response.getStatusCode()); Assertions.assertEquals("text/plain", response.getContentType()); @@ -1627,22 +2129,40 @@ public void testPublicDiscoveryNeverExposesOnlyExtendedCard() { @Test public void testDiscoveryInvalidTenantRetainsSanitized500() { - RestHandler handler = new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); + RestHandler handler = + new RestHandler(CARD, createCacheMetadata(), requestHandler, internalExecutor); assertProblemDetail(handler.getAgentCard("bad/tenant"), 500, "INTERNAL", "Internal error"); - assertProblemDetail(handler.getExtendedAgentCard(callContext, "bad/tenant"), 500, "INTERNAL", "Internal error"); + assertProblemDetail( + handler.getExtendedAgentCard(callContext, "bad/tenant"), + 500, + "INTERNAL", + "Internal error"); } @Test public void testExtendedDiscoveryDistinguishesCapabilityAndMissingCard() { - AgentCard disabled = AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().build()).build(); - RestHandler handler = new RestHandler(disabled, createCacheMetadata(disabled), requestHandler, internalExecutor); - assertProblemDetail(handler.getExtendedAgentCard(callContext, ""), 400, - "UNSUPPORTED_OPERATION", "This operation is not supported"); - AgentCard enabled = AgentCard.builder(disabled) - .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()).build(); - handler = new RestHandler(enabled, createCacheMetadata(enabled), requestHandler, internalExecutor); - assertProblemDetail(handler.getExtendedAgentCard(callContext, ""), 400, - "EXTENDED_AGENT_CARD_NOT_CONFIGURED", "Extended Card not configured"); + AgentCard disabled = + AgentCard.builder(CARD).capabilities(AgentCapabilities.builder().build()).build(); + RestHandler handler = + new RestHandler( + disabled, createCacheMetadata(disabled), requestHandler, internalExecutor); + assertProblemDetail( + handler.getExtendedAgentCard(callContext, ""), + 400, + "UNSUPPORTED_OPERATION", + "This operation is not supported"); + AgentCard enabled = + AgentCard.builder(disabled) + .capabilities(AgentCapabilities.builder().extendedAgentCard(true).build()) + .build(); + handler = + new RestHandler( + enabled, createCacheMetadata(enabled), requestHandler, internalExecutor); + assertProblemDetail( + handler.getExtendedAgentCard(callContext, ""), + 400, + "EXTENDED_AGENT_CARD_NOT_CONFIGURED", + "Extended Card not configured"); } private RequestHandler validationHandler; @@ -1651,13 +2171,30 @@ public void testExtendedDiscoveryDistinguishesCapabilityAndMissingCard() { private void validationFixture(int conflict) { validationHandler = Mockito.mock(RequestHandler.class); - AgentCard card = AgentCard.builder(CARD) - .capabilities(AgentCapabilities.builder().streaming(conflict != 3).pushNotifications(conflict != 3) - .extensions(conflict == 2 ? List.of(AgentExtension.builder().uri("urn:test:required") - .required(true).build()) : List.of()).build()).build(); - validationRestHandler = new RestHandler(card, createCacheMetadata(card), validationHandler, Runnable::run); - validationContext = new ServerCallContext(UnauthenticatedUser.INSTANCE, Map.of(), new HashSet<>(), - conflict == 1 ? "2.0" : "1.0"); + AgentCard card = + AgentCard.builder(CARD) + .capabilities( + AgentCapabilities.builder() + .streaming(conflict != 3) + .pushNotifications(conflict != 3) + .extensions( + conflict == 2 + ? List.of( + AgentExtension.builder() + .uri("urn:test:required") + .required(true) + .build()) + : List.of()) + .build()) + .build(); + validationRestHandler = + new RestHandler(card, createCacheMetadata(card), validationHandler, Runnable::run); + validationContext = + new ServerCallContext( + UnauthenticatedUser.INSTANCE, + Map.of(), + new HashSet<>(), + conflict == 1 ? "2.0" : "1.0"); } private void assertRawInputError(RestHandler.HTTPRestResponse response, String message) { @@ -1665,31 +2202,40 @@ private void assertRawInputError(RestHandler.HTTPRestResponse response, String m } private void assertRawVersionError(RestHandler.HTTPRestResponse response) { - assertRawInputError(response, 400, "VERSION_NOT_SUPPORTED", + assertRawInputError( + response, + 400, + "VERSION_NOT_SUPPORTED", "Protocol version '2.0' is not supported. Supported versions: [1.0]"); } - private void assertRawInputError(RestHandler.HTTPRestResponse response, int status, String reason, String message) { + private void assertRawInputError( + RestHandler.HTTPRestResponse response, int status, String reason, String message) { Assertions.assertEquals(status, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); Assertions.assertFalse(response instanceof RestHandler.HTTPRestStreamingResponse); Assertions.assertEquals(Map.of(), response.getHeaders()); - JsonObject expected = JsonParser.parseString(""" - {"error":{"code":422,"status":"INVALID_ARGUMENT","message":"", "details":[{ - "@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_PARAMS", - "domain":"a2a-protocol.org","metadata":{}}]}} - """).getAsJsonObject(); + JsonObject expected = + JsonParser.parseString( + """ + {"error":{"code":422,"status":"INVALID_ARGUMENT","message":"", "details":[{ + "@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"INVALID_PARAMS", + "domain":"a2a-protocol.org","metadata":{}}]}} + """) + .getAsJsonObject(); JsonObject error = expected.getAsJsonObject("error"); error.addProperty("code", status); - error.addProperty("status", reason.equals("VERSION_NOT_SUPPORTED") ? "UNIMPLEMENTED" : "INVALID_ARGUMENT"); + error.addProperty( + "status", + reason.equals("VERSION_NOT_SUPPORTED") ? "UNIMPLEMENTED" : "INVALID_ARGUMENT"); error.addProperty("message", message); error.getAsJsonArray("details").get(0).getAsJsonObject().addProperty("reason", reason); Assertions.assertEquals(expected, JsonParser.parseString(response.getBody())); Mockito.verifyNoInteractions(validationHandler); } - private void assertBadTaskPath(String taskId, - Function operation) { + private void assertBadTaskPath( + String taskId, Function operation) { for (int conflict = 0; conflict < 4; conflict++) { validationFixture(conflict); assertRawInputError(operation.apply(taskId), "bad task id"); @@ -1700,11 +2246,14 @@ private void assertBadTaskPath(String taskId, @NullAndEmptySource @ValueSource(strings = " ") public void testGetTaskRawInputValidationOrder(String taskId) { - assertBadTaskPath(taskId, id -> validationRestHandler.getTask(validationContext, "", id, "bad")); + assertBadTaskPath( + taskId, id -> validationRestHandler.getTask(validationContext, "", id, "bad")); for (String history : List.of("", "bad", "2147483648")) { for (int conflict = 0; conflict < 3; conflict++) { validationFixture(conflict); - assertRawInputError(validationRestHandler.getTask(validationContext, "", "task", history), "bad historyLength"); + assertRawInputError( + validationRestHandler.getTask(validationContext, "", "task", history), + "bad historyLength"); } } validationFixture(1); @@ -1716,33 +2265,44 @@ public void testGetTaskRawInputValidationOrder(String taskId) { @NullAndEmptySource @ValueSource(strings = " ") public void testCancelTaskPathValidationOrder(String taskId) { - assertBadTaskPath(taskId, id -> validationRestHandler.cancelTask(validationContext, "", "{broken", id)); + assertBadTaskPath( + taskId, + id -> validationRestHandler.cancelTask(validationContext, "", "{broken", id)); } @ParameterizedTest @NullAndEmptySource @ValueSource(strings = " ") public void testSubscribeToTaskPathValidationOrder(String taskId) { - assertBadTaskPath(taskId, id -> validationRestHandler.subscribeToTask(validationContext, "", id)); + assertBadTaskPath( + taskId, id -> validationRestHandler.subscribeToTask(validationContext, "", id)); } @ParameterizedTest @NullAndEmptySource @ValueSource(strings = " ") public void testCreatePushConfigPathValidationOrder(String taskId) { - assertBadTaskPath(taskId, - id -> validationRestHandler.createTaskPushNotificationConfiguration(validationContext, "", "{broken", id)); + assertBadTaskPath( + taskId, + id -> + validationRestHandler.createTaskPushNotificationConfiguration( + validationContext, "", "{broken", id)); } @ParameterizedTest @NullAndEmptySource @ValueSource(strings = " ") public void testGetPushConfigPathValidationOrder(String id) { - assertBadTaskPath(id, - taskId -> validationRestHandler.getTaskPushNotificationConfiguration(validationContext, "", taskId, id)); + assertBadTaskPath( + id, + taskId -> + validationRestHandler.getTaskPushNotificationConfiguration( + validationContext, "", taskId, id)); for (int conflict = 0; conflict < 4; conflict++) { validationFixture(conflict); - assertRawInputError(validationRestHandler.getTaskPushNotificationConfiguration(validationContext, "", "task", id), + assertRawInputError( + validationRestHandler.getTaskPushNotificationConfiguration( + validationContext, "", "task", id), "bad configuration id"); } } @@ -1751,11 +2311,16 @@ public void testGetPushConfigPathValidationOrder(String id) { @NullAndEmptySource @ValueSource(strings = " ") public void testDeletePushConfigPathValidationOrder(String id) { - assertBadTaskPath(id, - taskId -> validationRestHandler.deleteTaskPushNotificationConfiguration(validationContext, "", taskId, id)); + assertBadTaskPath( + id, + taskId -> + validationRestHandler.deleteTaskPushNotificationConfiguration( + validationContext, "", taskId, id)); for (int conflict = 0; conflict < 4; conflict++) { validationFixture(conflict); - assertRawInputError(validationRestHandler.deleteTaskPushNotificationConfiguration(validationContext, "", "task", id), + assertRawInputError( + validationRestHandler.deleteTaskPushNotificationConfiguration( + validationContext, "", "task", id), "bad config id"); } } @@ -1764,13 +2329,18 @@ public void testDeletePushConfigPathValidationOrder(String id) { @NullAndEmptySource @ValueSource(strings = " ") public void testListPushConfigsRawInputValidationOrder(String taskId) { - assertBadTaskPath(taskId, - id -> validationRestHandler.listTaskPushNotificationConfigurations(validationContext, "", id, "bad", null)); + assertBadTaskPath( + taskId, + id -> + validationRestHandler.listTaskPushNotificationConfigurations( + validationContext, "", id, "bad", null)); for (String page : List.of("", "bad", "2147483648")) { for (int conflict = 0; conflict < 4; conflict++) { validationFixture(conflict); - assertRawInputError(validationRestHandler.listTaskPushNotificationConfigurations( - validationContext, "", "task", page, null), "bad pageSize"); + assertRawInputError( + validationRestHandler.listTaskPushNotificationConfigurations( + validationContext, "", "task", page, null), + "bad pageSize"); } } } @@ -1780,14 +2350,31 @@ public void testListTasksRawQueryValidationOrder() { for (String field : List.of("pageSize", "historyLength")) { for (int conflict = 0; conflict < 3; conflict++) { validationFixture(conflict); - assertRawInputError(validationRestHandler.listTasks(validationContext, "", null, "invalid", - field.equals("pageSize") ? "bad" : null, null, "also-bad", "invalid", null), + assertRawInputError( + validationRestHandler.listTasks( + validationContext, + "", + null, + "invalid", + field.equals("pageSize") ? "bad" : null, + null, + "also-bad", + "invalid", + null), "Invalid number format in parameters"); } validationFixture(1); - assertRawVersionError(validationRestHandler.listTasks(validationContext, "", null, null, - field.equals("pageSize") ? "-1" : null, null, - field.equals("historyLength") ? "-1" : null, null, null)); + assertRawVersionError( + validationRestHandler.listTasks( + validationContext, + "", + null, + null, + field.equals("pageSize") ? "-1" : null, + null, + field.equals("historyLength") ? "-1" : null, + null, + null)); Mockito.verifyNoInteractions(validationHandler); } for (String number : new String[] {null, "", "2"}) { @@ -1795,17 +2382,32 @@ public void testListTasksRawQueryValidationOrder() { validationFixture(0); Mockito.when(validationHandler.onListTasks(Mockito.any(), Mockito.any())) .thenReturn(new ListTasksResult(List.of())); - RestHandler.HTTPRestResponse response = validationRestHandler.listTasks(validationContext, "", null, - "task_state_submitted", number, null, number, null, raw); + RestHandler.HTTPRestResponse response = + validationRestHandler.listTasks( + validationContext, + "", + null, + "task_state_submitted", + number, + null, + number, + null, + raw); Assertions.assertEquals(200, response.getStatusCode()); Assertions.assertEquals(APPLICATION_JSON, response.getContentType()); ArgumentCaptor params = ArgumentCaptor.forClass(ListTasksParams.class); Mockito.verify(validationHandler).onListTasks(params.capture(), Mockito.any()); Assertions.assertEquals(TaskState.TASK_STATE_SUBMITTED, params.getValue().status()); - Assertions.assertEquals(number == null || number.isEmpty() ? null : Integer.valueOf(number), params.getValue().pageSize()); - Assertions.assertEquals(number == null || number.isEmpty() ? null : Integer.valueOf(number), params.getValue().historyLength()); - Assertions.assertEquals(raw == null || raw.isEmpty() ? null : Boolean.valueOf(raw), params.getValue().includeArtifacts()); + Assertions.assertEquals( + number == null || number.isEmpty() ? null : Integer.valueOf(number), + params.getValue().pageSize()); + Assertions.assertEquals( + number == null || number.isEmpty() ? null : Integer.valueOf(number), + params.getValue().historyLength()); + Assertions.assertEquals( + raw == null || raw.isEmpty() ? null : Boolean.valueOf(raw), + params.getValue().includeArtifacts()); } } } @@ -1814,27 +2416,40 @@ public void testListTasksRawQueryValidationOrder() { public void testRawQueryDefaultsAndSuccessfulConversions() { for (String history : new String[] {null, "0", "2"}) { validationFixture(0); - Mockito.when(validationHandler.onGetTask(Mockito.any(), Mockito.any())).thenReturn(MINIMAL_TASK); - Assertions.assertEquals(200, validationRestHandler.getTask(validationContext, "", "task", history).getStatusCode()); - ArgumentCaptor params = - ArgumentCaptor.forClass(TaskQueryParams.class); + Mockito.when(validationHandler.onGetTask(Mockito.any(), Mockito.any())) + .thenReturn(MINIMAL_TASK); + Assertions.assertEquals( + 200, + validationRestHandler + .getTask(validationContext, "", "task", history) + .getStatusCode()); + ArgumentCaptor params = ArgumentCaptor.forClass(TaskQueryParams.class); Mockito.verify(validationHandler).onGetTask(params.capture(), Mockito.any()); - Assertions.assertEquals(history == null ? null : Integer.valueOf(history), params.getValue().historyLength()); + Assertions.assertEquals( + history == null ? null : Integer.valueOf(history), + params.getValue().historyLength()); } for (String page : new String[] {null, "0", "2"}) { for (String token : new String[] {null, "", "next"}) { validationFixture(0); - Mockito.when(validationHandler.onListTaskPushNotificationConfigs(Mockito.any(), Mockito.any())) + Mockito.when( + validationHandler.onListTaskPushNotificationConfigs( + Mockito.any(), Mockito.any())) .thenReturn(new ListTaskPushNotificationConfigsResult(List.of())); - Assertions.assertEquals(200, validationRestHandler.listTaskPushNotificationConfigurations( - validationContext, "", "task", page, token).getStatusCode()); + Assertions.assertEquals( + 200, + validationRestHandler + .listTaskPushNotificationConfigurations( + validationContext, "", "task", page, token) + .getStatusCode()); ArgumentCaptor params = ArgumentCaptor.forClass(ListTaskPushNotificationConfigsParams.class); - Mockito.verify(validationHandler).onListTaskPushNotificationConfigs(params.capture(), Mockito.any()); - Assertions.assertEquals(page == null ? 0 : Integer.parseInt(page), params.getValue().pageSize()); + Mockito.verify(validationHandler) + .onListTaskPushNotificationConfigs(params.capture(), Mockito.any()); + Assertions.assertEquals( + page == null ? 0 : Integer.parseInt(page), params.getValue().pageSize()); Assertions.assertEquals(token == null ? "" : token, params.getValue().pageToken()); } } } - } diff --git a/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestTestTransportMetadata.java b/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestTestTransportMetadata.java index 69ede73d9..78bf4b526 100644 --- a/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestTestTransportMetadata.java +++ b/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestTestTransportMetadata.java @@ -17,5 +17,4 @@ public class RestTestTransportMetadata implements TransportMetadata { public String getTransportProtocol() { return TransportProtocol.HTTP_JSON.asString(); } - } From 20233ec067d892382fc573fb1f3b82752b8986a0 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Mon, 2 Nov 2026 21:52:57 +0000 Subject: [PATCH 3/3] docs: clarify Spotless fixes and supported JDKs This fixes #139 --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 71e2159e4..9e84c263f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,21 +73,21 @@ To set up your development environment you need to: ## Code Formatting -CI checks formatting with Spotless using JDK 25. Before submitting a PR, apply the configured formatting from the -repository root: +Run Spotless from the repository root using JDK 21 or later. CI uses JDK 25; the SDK itself still supports Java 17. + +**Fix formatting errors** (updates files in place): ```bash mvn -N -Dskip.spotless=false spotless:apply ``` -To check formatting without changing files: +**Check formatting** without changing files (the command fails if formatting errors remain): ```bash mvn -N -Dskip.spotless=false spotless:check ``` -Run these commands with JDK 25. The root configuration covers sources and POMs throughout the repository; `-N` avoids -repeating the same work in each module. Released documentation snapshots are excluded. +The root configuration covers sources and POMs throughout the repository; `-N` avoids repeating the same work in each module. Released documentation snapshots are excluded. ## Contributing Guidelines