From 5e1e8e9ef8c684aec313f2420e4defdd4881129b Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Wed, 15 Apr 2026 16:47:37 +0200 Subject: [PATCH 1/5] chore: updating the workflows Signed-off-by: Emmanuel Hugonnet --- .github/workflows/build-and-test.yml | 6 +- .../workflows/build-with-release-profile.yml | 71 ++++++++----------- .../workflows/cloud-deployment-example.yml | 8 +-- 3 files changed, 37 insertions(+), 48 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e2fb7253d..69cc8a178 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -18,7 +18,7 @@ jobs: matrix: java-version: ['17', '21', '25'] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up JDK ${{ matrix.java-version }} uses: actions/setup-java@v4 with: @@ -29,7 +29,7 @@ jobs: run: mvn -B package --file pom.xml -fae - name: Upload Test Reports if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: surefire-reports-java-${{ matrix.java-version }} path: | @@ -39,7 +39,7 @@ jobs: if-no-files-found: warn - name: Upload Build Logs if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: build-logs-java-${{ matrix.java-version }} path: | diff --git a/.github/workflows/build-with-release-profile.yml b/.github/workflows/build-with-release-profile.yml index 129833307..70737fca9 100644 --- a/.github/workflows/build-with-release-profile.yml +++ b/.github/workflows/build-with-release-profile.yml @@ -1,12 +1,13 @@ -name: Build with '-Prelease' - -# Simply runs the build with -Prelease to avoid nasty surprises when running the release-to-maven-central workflow. +name: Build with '-Prelease' (Trigger) +# Trigger workflow for release profile build verification. +# This workflow runs on PRs and uploads the PR info for the workflow_run job. +# The actual build with secrets happens in build-with-release-profile-run.yml +# See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests on: - # Handle all branches for now + pull_request: # Changed from pull_request_target for security push: - pull_request_target: workflow_dispatch: # Only run the latest job @@ -15,7 +16,7 @@ concurrency: cancel-in-progress: true jobs: - build: + trigger: # Only run this job for the main repository, not for forks if: github.repository == 'a2aproject/a2a-java' runs-on: ubuntu-latest @@ -23,39 +24,27 @@ jobs: contents: read steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - cache: maven - - # Use secrets to import GPG key - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 - with: - gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} - passphrase: ${{ secrets.GPG_SIGNING_PASSPHRASE }} - - # Create settings.xml for Maven since it needs the 'central-a2asdk-temp' server. - # Populate wqith username and password from secrets - - name: Create settings.xml + - name: Prepare PR info run: | - mkdir -p ~/.m2 - echo "central-a2asdk-temp${{ secrets.CENTRAL_TOKEN_USERNAME }}${{ secrets.CENTRAL_TOKEN_PASSWORD }}" > ~/.m2/settings.xml - - # Build with the same settings as the deploy job - # -s uses the settings file we created. - - name: Build with same arguments as deploy job - run: > - mvn -B install - -s ~/.m2/settings.xml - -P release - -DskipTests - -Drelease.auto.publish=true - env: - # GPG passphrase is set as an environment variable for the gpg plugin to use - GPG_PASSPHRASE: ${{ secrets.GPG_SIGNING_PASSPHRASE }} \ No newline at end of file + mkdir -p pr_info + + # Store PR number for workflow_run job + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo ${{ github.event.number }} > pr_info/pr_number + echo ${{ github.event.pull_request.head.sha }} > pr_info/pr_sha + echo ${{ github.event.pull_request.head.ref }} > pr_info/pr_ref + else + # For push events, store the commit sha + echo ${{ github.sha }} > pr_info/pr_sha + echo ${{ github.ref }} > pr_info/pr_ref + fi + + echo "Event: ${{ github.event_name }}" + cat pr_info/* + + - name: Upload PR info + uses: actions/upload-artifact@v6 + with: + name: pr-info + path: pr_info/ + retention-days: 1 diff --git a/.github/workflows/cloud-deployment-example.yml b/.github/workflows/cloud-deployment-example.yml index f52ea5111..57a97a638 100644 --- a/.github/workflows/cloud-deployment-example.yml +++ b/.github/workflows/cloud-deployment-example.yml @@ -16,8 +16,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v4 - + uses: actions/checkout@v6 - name: Set up JDK 17 uses: actions/setup-java@v5 with: @@ -27,7 +26,7 @@ jobs: - name: Install Kind run: | - curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.20.0/kind-linux-amd64 + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.31.0/kind-linux-amd64 chmod +x ./kind sudo mv ./kind /usr/local/bin/kind kind version @@ -58,7 +57,8 @@ jobs: mvn test-compile exec:java \ -Dexec.mainClass="io.a2a.examples.cloud.A2ACloudExampleClient" \ -Dexec.classpathScope=test \ - -Dagent.url=http://localhost:8080 + -Dagent.url=http://localhost:8080 \ + -Dci.mode=true - name: Show diagnostics on failure if: failure() From 972a1fd05afc3ff131ca6628ac2a4d228e631046 Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Wed, 15 Apr 2026 17:31:15 +0200 Subject: [PATCH 2/5] chore: fixing javadoc Signed-off-by: Emmanuel Hugonnet --- spec/src/main/java/io/a2a/json/JsonUtil.java | 1 - .../java/io/a2a/spec/AgentCapabilities.java | 5 +++++ spec/src/main/java/io/a2a/spec/AgentCard.java | 19 +++++++++++++++++++ .../java/io/a2a/spec/AgentCardSignature.java | 4 ++++ .../main/java/io/a2a/spec/AgentExtension.java | 5 +++++ .../main/java/io/a2a/spec/AgentInterface.java | 4 +++- .../main/java/io/a2a/spec/AgentProvider.java | 3 +++ .../src/main/java/io/a2a/spec/AgentSkill.java | 9 +++++++++ spec/src/main/java/io/a2a/spec/Artifact.java | 7 +++++++ .../java/io/a2a/spec/AuthenticationInfo.java | 3 +++ .../a2a/spec/AuthorizationCodeOAuthFlow.java | 5 +++++ .../a2a/spec/ClientCredentialsOAuthFlow.java | 4 ++++ ...eleteTaskPushNotificationConfigParams.java | 4 ++++ .../main/java/io/a2a/spec/FileWithBytes.java | 4 ++++ .../main/java/io/a2a/spec/FileWithUri.java | 4 ++++ .../GetTaskPushNotificationConfigParams.java | 4 ++++ .../java/io/a2a/spec/ImplicitOAuthFlow.java | 4 ++++ .../main/java/io/a2a/spec/JSONRPCRequest.java | 2 ++ .../java/io/a2a/spec/JSONRPCResponse.java | 2 ++ .../ListTaskPushNotificationConfigParams.java | 3 +++ .../io/a2a/spec/MessageSendConfiguration.java | 7 ++++++- .../java/io/a2a/spec/MessageSendParams.java | 4 ++++ .../a2a/spec/NonStreamingJSONRPCRequest.java | 2 ++ .../src/main/java/io/a2a/spec/OAuthFlows.java | 5 +++++ .../java/io/a2a/spec/PasswordOAuthFlow.java | 4 ++++ .../PushNotificationAuthenticationInfo.java | 3 +++ .../io/a2a/spec/PushNotificationConfig.java | 5 +++++ .../io/a2a/spec/StreamingJSONRPCRequest.java | 3 ++- .../main/java/io/a2a/spec/TaskIdParams.java | 3 +++ .../a2a/spec/TaskPushNotificationConfig.java | 3 +++ spec/src/main/java/io/a2a/util/Utils.java | 5 ++--- 31 files changed, 133 insertions(+), 7 deletions(-) diff --git a/spec/src/main/java/io/a2a/json/JsonUtil.java b/spec/src/main/java/io/a2a/json/JsonUtil.java index 56dd3f310..5e252a7ae 100644 --- a/spec/src/main/java/io/a2a/json/JsonUtil.java +++ b/spec/src/main/java/io/a2a/json/JsonUtil.java @@ -87,7 +87,6 @@ private static GsonBuilder createBaseGsonBuilder() { *

* Used throughout the SDK for consistent JSON serialization and deserialization. * - * @see GsonFactory#createGson() */ public static final Gson OBJECT_MAPPER = createBaseGsonBuilder() .registerTypeHierarchyAdapter(Part.class, new PartTypeAdapter()) diff --git a/spec/src/main/java/io/a2a/spec/AgentCapabilities.java b/spec/src/main/java/io/a2a/spec/AgentCapabilities.java index 1de51d5f9..2b4fbcee5 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCapabilities.java +++ b/spec/src/main/java/io/a2a/spec/AgentCapabilities.java @@ -4,6 +4,11 @@ /** * Defines optional capabilities supported by an agent. + * + * @param streaming whether the agent supports streaming responses + * @param pushNotifications whether the agent supports push notifications + * @param stateTransitionHistory whether the agent supports state transition history + * @param extensions optional list of protocol extensions supported by the agent */ public record AgentCapabilities(boolean streaming, boolean pushNotifications, boolean stateTransitionHistory, List extensions) { diff --git a/spec/src/main/java/io/a2a/spec/AgentCard.java b/spec/src/main/java/io/a2a/spec/AgentCard.java index 2574f5425..7963f6c57 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCard.java +++ b/spec/src/main/java/io/a2a/spec/AgentCard.java @@ -10,6 +10,25 @@ * The AgentCard is a self-describing manifest for an agent. It provides essential * metadata including the agent's identity, capabilities, skills, supported * communication methods, and security requirements. + * + * @param name the human-readable name of the agent + * @param description a human-readable description of the agent + * @param url the URL of the agent's primary endpoint + * @param provider the organization or individual providing the agent + * @param version the version of the agent + * @param documentationUrl optional URL to the agent's documentation + * @param capabilities the capabilities supported by the agent + * @param defaultInputModes the default input content modes supported by the agent + * @param defaultOutputModes the default output content modes supported by the agent + * @param skills the list of skills the agent can perform + * @param supportsAuthenticatedExtendedCard whether the agent supports an authenticated extended card + * @param securitySchemes the security scheme definitions available for this agent + * @param security the security requirements for accessing the agent + * @param iconUrl optional URL to the agent's icon + * @param additionalInterfaces additional transport/URL combinations for interacting with the agent + * @param preferredTransport the preferred transport protocol + * @param protocolVersion the A2A protocol version supported by the agent + * @param signatures optional JWS signatures of the agent card */ public record AgentCard(String name, String description, String url, AgentProvider provider, String version, String documentationUrl, AgentCapabilities capabilities, diff --git a/spec/src/main/java/io/a2a/spec/AgentCardSignature.java b/spec/src/main/java/io/a2a/spec/AgentCardSignature.java index 70a92cd57..35714554e 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCardSignature.java +++ b/spec/src/main/java/io/a2a/spec/AgentCardSignature.java @@ -8,6 +8,10 @@ /** * Represents a JWS signature of an AgentCard. * This follows the JSON format of an RFC 7515 JSON Web Signature (JWS). + * + * @param header the JWS unprotected header + * @param protectedHeader the JWS protected header (base64url-encoded) + * @param signature the JWS signature value (base64url-encoded) */ public record AgentCardSignature(Map header, @SerializedName("protected")String protectedHeader, String signature) { diff --git a/spec/src/main/java/io/a2a/spec/AgentExtension.java b/spec/src/main/java/io/a2a/spec/AgentExtension.java index 053855976..76f1c9579 100644 --- a/spec/src/main/java/io/a2a/spec/AgentExtension.java +++ b/spec/src/main/java/io/a2a/spec/AgentExtension.java @@ -6,6 +6,11 @@ /** * A declaration of a protocol extension supported by an Agent. + * + * @param description a human-readable description of the extension + * @param params optional parameters for configuring the extension + * @param required whether the extension is required for the agent to function + * @param uri the URI identifying the extension */ public record AgentExtension (String description, Map params, boolean required, String uri) { diff --git a/spec/src/main/java/io/a2a/spec/AgentInterface.java b/spec/src/main/java/io/a2a/spec/AgentInterface.java index 0b2e8d8b0..ff30c913d 100644 --- a/spec/src/main/java/io/a2a/spec/AgentInterface.java +++ b/spec/src/main/java/io/a2a/spec/AgentInterface.java @@ -5,8 +5,10 @@ /** * Declares a combination of a target URL and a transport protocol for interacting with the agent. + * + * @param transport the transport protocol identifier (e.g., "jsonrpc", "grpc") + * @param url the endpoint URL for this transport */ - public record AgentInterface(String transport, String url) { public AgentInterface { Assert.checkNotNullParam("transport", transport); diff --git a/spec/src/main/java/io/a2a/spec/AgentProvider.java b/spec/src/main/java/io/a2a/spec/AgentProvider.java index 1d50b699e..eea07b24e 100644 --- a/spec/src/main/java/io/a2a/spec/AgentProvider.java +++ b/spec/src/main/java/io/a2a/spec/AgentProvider.java @@ -4,6 +4,9 @@ /** * Represents the service provider of an agent. + * + * @param organization the name of the organization providing the agent + * @param url the URL of the provider's website or documentation */ public record AgentProvider(String organization, String url) { diff --git a/spec/src/main/java/io/a2a/spec/AgentSkill.java b/spec/src/main/java/io/a2a/spec/AgentSkill.java index b397f6248..4ec0c6911 100644 --- a/spec/src/main/java/io/a2a/spec/AgentSkill.java +++ b/spec/src/main/java/io/a2a/spec/AgentSkill.java @@ -7,6 +7,15 @@ /** * The set of skills, or distinct capabilities, that the agent can perform. + * + * @param id a unique identifier for the skill + * @param name the human-readable name of the skill + * @param description a human-readable description of the skill + * @param tags tags for categorizing or discovering the skill + * @param examples example prompts or use cases for the skill + * @param inputModes the content modes accepted as input by the skill + * @param outputModes the content modes produced as output by the skill + * @param security optional security requirements specific to this skill */ public record AgentSkill(String id, String name, String description, List tags, List examples, List inputModes, List outputModes, diff --git a/spec/src/main/java/io/a2a/spec/Artifact.java b/spec/src/main/java/io/a2a/spec/Artifact.java index 69d2f0581..bda976da3 100644 --- a/spec/src/main/java/io/a2a/spec/Artifact.java +++ b/spec/src/main/java/io/a2a/spec/Artifact.java @@ -7,6 +7,13 @@ /** * Represents a file, data structure, or other resource generated by an agent during a task. + * + * @param artifactId a unique identifier for the artifact within the task + * @param name optional human-readable name of the artifact + * @param description optional human-readable description of the artifact + * @param parts the content parts that make up the artifact + * @param metadata optional additional metadata associated with the artifact + * @param extensions optional list of protocol extension identifiers */ public record Artifact(String artifactId, String name, String description, List> parts, Map metadata, List extensions) { diff --git a/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java b/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java index 4f24e3c4c..3ecc368a4 100644 --- a/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java +++ b/spec/src/main/java/io/a2a/spec/AuthenticationInfo.java @@ -6,6 +6,9 @@ /** * The authentication info for an agent. + * + * @param schemes the list of authentication scheme identifiers + * @param credentials optional credentials string for the authentication scheme */ public record AuthenticationInfo(List schemes, String credentials) { diff --git a/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java b/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java index cc5e0ee54..886360b7a 100644 --- a/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/AuthorizationCodeOAuthFlow.java @@ -7,6 +7,11 @@ /** * Defines configuration details for the OAuth 2.0 Authorization Code flow. + * + * @param authorizationUrl the URL for the authorization endpoint + * @param refreshUrl optional URL for obtaining refresh tokens + * @param scopes the available scopes mapped to their descriptions + * @param tokenUrl the URL for the token endpoint */ public record AuthorizationCodeOAuthFlow(String authorizationUrl, String refreshUrl, Map scopes, String tokenUrl) { diff --git a/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java b/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java index 18056681f..9577649e0 100644 --- a/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/ClientCredentialsOAuthFlow.java @@ -8,6 +8,10 @@ /** * Defines configuration details for the OAuth 2.0 Client Credentials flow. + * + * @param refreshUrl optional URL for obtaining refresh tokens + * @param scopes the available scopes mapped to their descriptions + * @param tokenUrl the URL for the token endpoint */ public record ClientCredentialsOAuthFlow(String refreshUrl, Map scopes, String tokenUrl) { diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java index 0cb34a38d..4e300a59f 100644 --- a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java @@ -7,6 +7,10 @@ /** * Parameters for removing pushNotificationConfiguration associated with a Task. + * + * @param id the task ID + * @param pushNotificationConfigId the ID of the push notification configuration to delete + * @param metadata optional additional metadata */ public record DeleteTaskPushNotificationConfigParams(String id, String pushNotificationConfigId, Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/FileWithBytes.java b/spec/src/main/java/io/a2a/spec/FileWithBytes.java index 01ccef127..0a5df369b 100644 --- a/spec/src/main/java/io/a2a/spec/FileWithBytes.java +++ b/spec/src/main/java/io/a2a/spec/FileWithBytes.java @@ -2,6 +2,10 @@ /** * Represents a file with its content provided directly as a base64-encoded string. + * + * @param mimeType the MIME type of the file content + * @param name optional name of the file + * @param bytes the base64-encoded file content */ public record FileWithBytes(String mimeType, String name, String bytes) implements FileContent { } diff --git a/spec/src/main/java/io/a2a/spec/FileWithUri.java b/spec/src/main/java/io/a2a/spec/FileWithUri.java index e1edd4bd2..45514ae04 100644 --- a/spec/src/main/java/io/a2a/spec/FileWithUri.java +++ b/spec/src/main/java/io/a2a/spec/FileWithUri.java @@ -2,6 +2,10 @@ /** * Represents a file with its content located at a specific URI. + * + * @param mimeType the MIME type of the file content + * @param name optional name of the file + * @param uri the URI pointing to the file content */ public record FileWithUri(String mimeType, String name, String uri) implements FileContent { } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java index 2836e2065..200a3c3d5 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java @@ -8,6 +8,10 @@ /** * Parameters for fetching a pushNotificationConfiguration associated with a Task. + * + * @param id the task ID + * @param pushNotificationConfigId optional ID of a specific push notification configuration to retrieve + * @param metadata optional additional metadata */ public record GetTaskPushNotificationConfigParams(String id, @Nullable String pushNotificationConfigId, @Nullable Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java b/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java index cd2ef6235..ec76ab318 100644 --- a/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/ImplicitOAuthFlow.java @@ -7,6 +7,10 @@ /** * Defines configuration details for the OAuth 2.0 Implicit flow. + * + * @param authorizationUrl the URL for the authorization endpoint + * @param refreshUrl optional URL for obtaining refresh tokens + * @param scopes the available scopes mapped to their descriptions */ public record ImplicitOAuthFlow(String authorizationUrl, String refreshUrl, Map scopes) { diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java index 45e2b6883..69683a764 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCRequest.java @@ -6,6 +6,8 @@ /** * Represents a JSONRPC request. + * + * @param the type of the request parameters */ public abstract sealed class JSONRPCRequest implements JSONRPCMessage permits NonStreamingJSONRPCRequest, StreamingJSONRPCRequest { diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java index d4330d843..395483596 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java @@ -6,6 +6,8 @@ /** * Represents a JSONRPC response. + * + * @param the type of the response result */ public abstract sealed class JSONRPCResponse implements JSONRPCMessage permits SendStreamingMessageResponse, GetTaskResponse, CancelTaskResponse, SetTaskPushNotificationConfigResponse, GetTaskPushNotificationConfigResponse, diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java index 2d04f36ce..179e86172 100644 --- a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java @@ -6,6 +6,9 @@ /** * Parameters for getting list of pushNotificationConfigurations associated with a Task. + * + * @param id the task ID + * @param metadata optional additional metadata */ public record ListTaskPushNotificationConfigParams(String id, Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java b/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java index d44ce494f..cd4888ff4 100644 --- a/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java +++ b/spec/src/main/java/io/a2a/spec/MessageSendConfiguration.java @@ -6,7 +6,12 @@ import org.jspecify.annotations.Nullable; /** - * Defines configuration options for a `message/send` or `message/stream` request. + * Defines configuration options for a {@code message/send} or {@code message/stream} request. + * + * @param acceptedOutputModes the output content modes the client accepts + * @param historyLength optional maximum number of history messages to include + * @param pushNotificationConfig optional push notification configuration for task updates + * @param blocking whether the request should block until the task completes */ public record MessageSendConfiguration(List acceptedOutputModes, Integer historyLength, PushNotificationConfig pushNotificationConfig, Boolean blocking) { diff --git a/spec/src/main/java/io/a2a/spec/MessageSendParams.java b/spec/src/main/java/io/a2a/spec/MessageSendParams.java index 5914ef462..1f81def3d 100644 --- a/spec/src/main/java/io/a2a/spec/MessageSendParams.java +++ b/spec/src/main/java/io/a2a/spec/MessageSendParams.java @@ -7,6 +7,10 @@ /** * 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. + * + * @param message the message to send to the agent + * @param configuration optional configuration options for this send request + * @param metadata optional additional metadata */ public record MessageSendParams(Message message, MessageSendConfiguration configuration, Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java index e969ce08e..66562662d 100644 --- a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java @@ -2,6 +2,8 @@ /** * Represents a non-streaming JSON-RPC request. + * + * @param the type of the request parameters */ public abstract sealed class NonStreamingJSONRPCRequest extends JSONRPCRequest permits GetTaskRequest, CancelTaskRequest, SetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, diff --git a/spec/src/main/java/io/a2a/spec/OAuthFlows.java b/spec/src/main/java/io/a2a/spec/OAuthFlows.java index 849f84d41..31312b41b 100644 --- a/spec/src/main/java/io/a2a/spec/OAuthFlows.java +++ b/spec/src/main/java/io/a2a/spec/OAuthFlows.java @@ -2,6 +2,11 @@ /** * Defines the configuration for the supported OAuth 2.0 flows. + * + * @param authorizationCode configuration for the Authorization Code flow + * @param clientCredentials configuration for the Client Credentials flow + * @param implicit configuration for the Implicit flow + * @param password configuration for the Resource Owner Password flow */ public record OAuthFlows(AuthorizationCodeOAuthFlow authorizationCode, ClientCredentialsOAuthFlow clientCredentials, ImplicitOAuthFlow implicit, PasswordOAuthFlow password) { diff --git a/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java b/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java index e5de924cb..58d4b81dd 100644 --- a/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java +++ b/spec/src/main/java/io/a2a/spec/PasswordOAuthFlow.java @@ -6,6 +6,10 @@ /** * Defines configuration details for the OAuth 2.0 Resource Owner Password flow. + * + * @param refreshUrl optional URL for obtaining refresh tokens + * @param scopes the available scopes mapped to their descriptions + * @param tokenUrl the URL for the token endpoint */ public record PasswordOAuthFlow(String refreshUrl, Map scopes, String tokenUrl) { diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java b/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java index 6263ac990..66d01e523 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationAuthenticationInfo.java @@ -5,6 +5,9 @@ /** * Defines authentication details for a push notification endpoint. + * + * @param schemes the list of authentication scheme identifiers + * @param credentials optional credentials string for the authentication scheme */ public record PushNotificationAuthenticationInfo(List schemes, String credentials) { diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java b/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java index b5a9e1131..18eda5f08 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java @@ -4,6 +4,11 @@ /** * Defines the configuration for setting up push notifications for task updates. + * + * @param url the URL of the push notification endpoint + * @param token optional authentication token for the push notification endpoint + * @param authentication optional authentication details for the push notification endpoint + * @param id optional identifier for this push notification configuration */ public record PushNotificationConfig(String url, String token, PushNotificationAuthenticationInfo authentication, String id) { diff --git a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java index 9cbb5e4c6..e0b2a6255 100644 --- a/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/StreamingJSONRPCRequest.java @@ -2,8 +2,9 @@ /** * Represents a streaming JSON-RPC request. + * + * @param the type of the request parameters */ - public abstract sealed class StreamingJSONRPCRequest extends JSONRPCRequest permits TaskResubscriptionRequest, SendStreamingMessageRequest { diff --git a/spec/src/main/java/io/a2a/spec/TaskIdParams.java b/spec/src/main/java/io/a2a/spec/TaskIdParams.java index 096c9a8a0..7a9e9b159 100644 --- a/spec/src/main/java/io/a2a/spec/TaskIdParams.java +++ b/spec/src/main/java/io/a2a/spec/TaskIdParams.java @@ -6,6 +6,9 @@ /** * Defines parameters containing a task ID, used for simple task operations. + * + * @param id the task ID + * @param metadata optional additional metadata */ public record TaskIdParams(String id, Map metadata) { diff --git a/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java b/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java index 23a7fc0c4..5dfdede6c 100644 --- a/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java +++ b/spec/src/main/java/io/a2a/spec/TaskPushNotificationConfig.java @@ -4,6 +4,9 @@ /** * A container associating a push notification configuration with a specific task. + * + * @param taskId the ID of the task this configuration is associated with + * @param pushNotificationConfig the push notification configuration for the task */ public record TaskPushNotificationConfig(String taskId, PushNotificationConfig pushNotificationConfig) { diff --git a/spec/src/main/java/io/a2a/util/Utils.java b/spec/src/main/java/io/a2a/util/Utils.java index f374f302e..004b1cdc4 100644 --- a/spec/src/main/java/io/a2a/util/Utils.java +++ b/spec/src/main/java/io/a2a/util/Utils.java @@ -24,13 +24,12 @@ *

* Key capabilities: *

    - *
  • JSON processing with pre-configured {@link Gson}
  • + *
  • JSON processing with pre-configured {@link io.a2a.json.JsonUtil#OBJECT_MAPPER}
  • *
  • Null-safe value defaults via {@link #defaultIfNull(Object, Object)}
  • *
  • Artifact streaming support via {@link #appendArtifactToTask(Task, TaskArtifactUpdateEvent, String)}
  • *
  • Type-safe exception rethrowing via {@link #rethrow(Throwable)}
  • *
* - * @see Gson for JSON processing * @see TaskArtifactUpdateEvent for streaming artifact updates */ public class Utils { @@ -41,7 +40,7 @@ public class Utils { /** * Deserializes JSON string into a typed object using Gson. *

- * This method uses the pre-configured {@link #OBJECT_MAPPER} to parse JSON. + * This method uses the pre-configured {@link io.a2a.json.JsonUtil#OBJECT_MAPPER} to parse JSON. * * @param the target type * @param data JSON string to deserialize From c17f7fb8183a4a20e12499f8d8cdd4097aed4b03 Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Wed, 15 Apr 2026 19:11:43 +0200 Subject: [PATCH 3/5] chore: Updating kafka version Signed-off-by: Emmanuel Hugonnet --- examples/cloud-deployment/k8s/02-kafka.yaml | 4 ++-- examples/cloud-deployment/scripts/deploy.sh | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/examples/cloud-deployment/k8s/02-kafka.yaml b/examples/cloud-deployment/k8s/02-kafka.yaml index 044aeb1ac..a4ad8eb0f 100644 --- a/examples/cloud-deployment/k8s/02-kafka.yaml +++ b/examples/cloud-deployment/k8s/02-kafka.yaml @@ -33,8 +33,8 @@ metadata: strimzi.io/kraft: enabled spec: kafka: - version: 4.0.0 - metadataVersion: 4.0-IV0 + version: 4.2.0 + metadataVersion: 4.2-IV0 listeners: - name: plain port: 9092 diff --git a/examples/cloud-deployment/scripts/deploy.sh b/examples/cloud-deployment/scripts/deploy.sh index e267f3302..448457221 100755 --- a/examples/cloud-deployment/scripts/deploy.sh +++ b/examples/cloud-deployment/scripts/deploy.sh @@ -177,6 +177,11 @@ if ! kubectl get namespace kafka > /dev/null 2>&1; then fi if ! kubectl get crd kafkas.kafka.strimzi.io > /dev/null 2>&1; then +# Keep this around in case we need to hardcode operator version again in the future +# echo "Installing Strimzi operator... at https://github.com/strimzi/strimzi-kafka-operator/releases/download/0.50.1/strimzi-cluster-operator-0.50.1.yaml" +# curl -sL 'https://github.com/strimzi/strimzi-kafka-operator/releases/download/0.50.1/strimzi-cluster-operator-0.50.1.yaml' \ +# | sed 's/namespace: .*/namespace: kafka/' \ +# | kubectl apply -f - -n kafka echo "Installing Strimzi operator..." kubectl create -f 'https://strimzi.io/install/latest?namespace=kafka' -n kafka @@ -212,6 +217,22 @@ echo "" echo "Deploying PostgreSQL..." kubectl apply -f ../k8s/01-postgres.yaml echo "Waiting for PostgreSQL to be ready..." + +# Wait for pod to be created (StatefulSet takes time to create pod) +for i in {1..30}; do + if kubectl get pod -l app=postgres -n a2a-demo 2>/dev/null | grep -q postgres; then + echo "PostgreSQL pod found, waiting for ready state..." + break + fi + if [ $i -eq 30 ]; then + echo -e "${RED}ERROR: PostgreSQL pod not created after 30 seconds${NC}" + kubectl get statefulset -n a2a-demo + exit 1 + fi + sleep 1 +done + +# Now wait for pod to be ready kubectl wait --for=condition=Ready pod -l app=postgres -n a2a-demo --timeout=120s echo -e "${GREEN}✓ PostgreSQL deployed${NC}" From ef7013dedbb39a13b5d374c538611fc0942bb9e2 Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Thu, 16 Apr 2026 13:57:01 +0200 Subject: [PATCH 4/5] fix; Fixing the last issues to be able to pass the TCK again Signed-off-by: Emmanuel Hugonnet --- .../server/apps/quarkus/A2AServerRoutes.java | 3 + spec/src/main/java/io/a2a/json/JsonUtil.java | 108 +++++++++++++++++- spec/src/main/java/io/a2a/spec/Message.java | 13 +++ .../java/io/a2a/spec/MessageSendParams.java | 6 + .../java/io/a2a/spec/SendMessageRequest.java | 16 +++ .../a2a/spec/SendStreamingMessageRequest.java | 14 +++ .../spec/JSONRPCErrorSerializationTest.java | 34 ++++++ .../a2a/tck/server/AgentExecutorProducer.java | 2 +- .../jsonrpc/handler/JSONRPCHandler.java | 9 +- 9 files changed, 197 insertions(+), 8 deletions(-) diff --git a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java index 7a4f8e76f..bc19159d0 100644 --- a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java +++ b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java @@ -104,6 +104,9 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { com.google.gson.JsonElement jsonrpcElement = node.get("jsonrpc"); if (jsonrpcElement == null || !jsonrpcElement.isJsonPrimitive() || !JSONRPCMessage.JSONRPC_VERSION.equals(jsonrpcElement.getAsString())) { + if(node.has("id")) { + throw new InvalidRequestError(null, "Invalid JSON-RPC request: missing or invalid 'jsonrpc' field", Map.of("id", node.get("id").getAsString())); + } throw new InvalidRequestError("Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); } diff --git a/spec/src/main/java/io/a2a/json/JsonUtil.java b/spec/src/main/java/io/a2a/json/JsonUtil.java index 5e252a7ae..ab8a67f84 100644 --- a/spec/src/main/java/io/a2a/json/JsonUtil.java +++ b/spec/src/main/java/io/a2a/json/JsonUtil.java @@ -23,10 +23,13 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.ToNumberPolicy; import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import io.a2a.spec.APIKeySecurityScheme; import io.a2a.spec.EventKind; +import io.a2a.spec.JSONRPCResponse; import io.a2a.spec.ContentTypeNotSupportedError; import io.a2a.spec.DataPart; import io.a2a.spec.FileContent; @@ -76,7 +79,10 @@ private static GsonBuilder createBaseGsonBuilder() { .registerTypeAdapter(Message.Role.class, new RoleTypeAdapter()) .registerTypeAdapter(Part.Kind.class, new PartKindTypeAdapter()) .registerTypeHierarchyAdapter(FileContent.class, new FileContentTypeAdapter()) - .registerTypeHierarchyAdapter(SecurityScheme.class, new SecuritySchemeTypeAdapter()); + .registerTypeHierarchyAdapter(SecurityScheme.class, new SecuritySchemeTypeAdapter()) + .registerTypeAdapter(void.class, new VoidTypeAdapter()) + .registerTypeAdapter(Void.class, new VoidTypeAdapter()) + .registerTypeAdapterFactory(new JSONRPCResponseTypeAdapterFactory()); } /** @@ -724,8 +730,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() == com.google.gson.stream.JsonToken.NULL) { in.nextNull(); return null; @@ -874,8 +879,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; @@ -900,4 +904,98 @@ FileContent read(JsonReader in) throws java.io.IOException { } } + static class VoidTypeAdapter extends TypeAdapter { + + + @Override + @SuppressWarnings("resource") + public void write(final JsonWriter out, final Void value) throws java.io.IOException { + out.nullValue(); + } + + @Override + public @Nullable Void read(final JsonReader in) throws java.io.IOException { + in.skipValue(); + return null; + } + + } + + /** + * Gson TypeAdapterFactory for serializing {@link JSONRPCResponse} subclasses. + *

+ * JSON-RPC 2.0 requires that: + *

    + *
  • {@code result} MUST be present (possibly null) on success, and MUST NOT be present on error
  • + *
  • {@code error} MUST be present on error, and MUST NOT be present on success
  • + *
+ * Gson's default null-field-skipping behavior would omit {@code "result": null} for Void responses, + * so this factory writes the fields explicitly to comply with the spec. + */ + static class JSONRPCResponseTypeAdapterFactory implements TypeAdapterFactory { + + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public @Nullable TypeAdapter create(Gson gson, TypeToken type) { + if (!JSONRPCResponse.class.isAssignableFrom(type.getRawType())) { + return null; + } + + TypeAdapter delegateAdapter = gson.getDelegateAdapter(this, type); + TypeAdapter errorAdapter = gson.getAdapter(JSONRPCError.class); + + return new TypeAdapter() { + @Override + public void write(JsonWriter out, T value) throws java.io.IOException { + if (value == null) { + out.nullValue(); + return; + } + + JSONRPCResponse response = (JSONRPCResponse) value; + + out.beginObject(); + out.name("jsonrpc").value(response.getJsonrpc()); + + Object id = response.getId(); + out.name("id"); + if (id == null) { + out.nullValue(); + } else if (id instanceof Number n) { + out.value(n.longValue()); + } else { + out.value(id.toString()); + } + + JSONRPCError error = response.getError(); + if (error != null) { + out.name("error"); + errorAdapter.write(out, error); + } else { + out.name("result"); + Object result = response.getResult(); + if (result == null) { + // 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(); + out.setSerializeNulls(true); + out.nullValue(); + out.setSerializeNulls(prev); + } else { + TypeAdapter resultAdapter = gson.getAdapter(result.getClass()); + resultAdapter.write(out, result); + } + } + + out.endObject(); + } + + @Override + public T read(JsonReader in) throws java.io.IOException { + return delegateAdapter.read(in); + } + }; + } + } } diff --git a/spec/src/main/java/io/a2a/spec/Message.java b/spec/src/main/java/io/a2a/spec/Message.java index 9d08cb56c..050e44c53 100644 --- a/spec/src/main/java/io/a2a/spec/Message.java +++ b/spec/src/main/java/io/a2a/spec/Message.java @@ -55,6 +55,19 @@ public Message(Role role, List> parts, this.kind = kind; } + public void check() { + Assert.checkNotNullParam("kind", kind); + Assert.checkNotNullParam("parts", parts); + if (parts.isEmpty()) { + throw new IllegalArgumentException("Parts cannot be empty"); + } + Assert.checkNotNullParam("role", role); + if (!kind.equals(MESSAGE)) { + throw new IllegalArgumentException("Invalid Message"); + } + Assert.checkNotNullParam("messageId", messageId); + } + public Role getRole() { return role; } diff --git a/spec/src/main/java/io/a2a/spec/MessageSendParams.java b/spec/src/main/java/io/a2a/spec/MessageSendParams.java index 1f81def3d..de7e4bfc7 100644 --- a/spec/src/main/java/io/a2a/spec/MessageSendParams.java +++ b/spec/src/main/java/io/a2a/spec/MessageSendParams.java @@ -1,5 +1,6 @@ package io.a2a.spec; + import java.util.Map; import io.a2a.util.Assert; @@ -19,6 +20,11 @@ public record MessageSendParams(Message message, MessageSendConfiguration config Assert.checkNotNullParam("message", message); } + public void check() { + Assert.checkNotNullParam("message", message); + message.check(); + } + public static class Builder { Message message; MessageSendConfiguration configuration; diff --git a/spec/src/main/java/io/a2a/spec/SendMessageRequest.java b/spec/src/main/java/io/a2a/spec/SendMessageRequest.java index a58ce0890..a8ec457e5 100644 --- a/spec/src/main/java/io/a2a/spec/SendMessageRequest.java +++ b/spec/src/main/java/io/a2a/spec/SendMessageRequest.java @@ -44,6 +44,22 @@ public SendMessageRequest(String jsonrpc, Object id, String method, MessageSendP this.params = params; } + public void check() { + if (jsonrpc == null || jsonrpc.isEmpty()) { + throw new IllegalArgumentException("JSON-RPC protocol version cannot be null or empty"); + } + 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 SendMessageRequest method"); + } + Assert.checkNotNullParam("params", params); + Assert.isNullOrStringOrInteger(id); + params.check(); + } + public SendMessageRequest(Object id, MessageSendParams params) { this(JSONRPC_VERSION, id, METHOD, params); } diff --git a/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java b/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java index de3abf950..d0eba2dbc 100644 --- a/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java +++ b/spec/src/main/java/io/a2a/spec/SendStreamingMessageRequest.java @@ -1,5 +1,6 @@ package io.a2a.spec; +import static io.a2a.spec.JSONRPCMessage.JSONRPC_VERSION; import static io.a2a.util.Utils.defaultIfNull; import io.a2a.util.Assert; @@ -33,6 +34,19 @@ public SendStreamingMessageRequest(Object id, MessageSendParams params) { this(null, id, METHOD, params); } + public void check() { + 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 SendStreamingMessageRequest method"); + } + Assert.checkNotNullParam("params", params); + Assert.isNullOrStringOrInteger(id); + params.check(); + } + public static class Builder { private String jsonrpc; private Object id; diff --git a/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java b/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java index 9c83f0806..e95be5a5a 100644 --- a/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java +++ b/spec/src/test/java/io/a2a/spec/JSONRPCErrorSerializationTest.java @@ -3,9 +3,12 @@ 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 io.a2a.json.JsonProcessingException; import io.a2a.json.JsonUtil; @@ -44,5 +47,36 @@ record ErrorCase(int code, Class clazz) {} } } + @Test + @SuppressWarnings("unchecked") + void deleteTaskPushNotificationConfigSuccessResponseSerializesResultAsNull() throws JsonProcessingException { + DeleteTaskPushNotificationConfigResponse response = + new DeleteTaskPushNotificationConfigResponse("req-123"); + + String json = JsonUtil.toJson(response); + Map map = JsonUtil.fromJson(json, Map.class); + + assertEquals("2.0", map.get("jsonrpc")); + 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"); + } + + @Test + @SuppressWarnings("unchecked") + void deleteTaskPushNotificationConfigErrorResponseSerializesErrorWithoutResult() throws JsonProcessingException { + DeleteTaskPushNotificationConfigResponse response = + new DeleteTaskPushNotificationConfigResponse("req-456", new TaskNotFoundError()); + + String json = JsonUtil.toJson(response); + Map map = JsonUtil.fromJson(json, Map.class); + + 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"); + } + } diff --git a/tck/src/main/java/io/a2a/tck/server/AgentExecutorProducer.java b/tck/src/main/java/io/a2a/tck/server/AgentExecutorProducer.java index 5c085f981..560c4d7f2 100644 --- a/tck/src/main/java/io/a2a/tck/server/AgentExecutorProducer.java +++ b/tck/src/main/java/io/a2a/tck/server/AgentExecutorProducer.java @@ -39,7 +39,7 @@ public void execute(RequestContext context, EventQueue eventQueue) throws JSONRP } // Sleep to allow task state persistence before TCK resubscribe test - if (context.getMessage().getMessageId().startsWith("test-resubscribe-message-id")) { + if (context.getMessage().getMessageId() != null && context.getMessage().getMessageId().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 { diff --git a/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java b/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java index ef577aaa5..853d6978b 100644 --- a/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java +++ b/transport/jsonrpc/src/main/java/io/a2a/transport/jsonrpc/handler/JSONRPCHandler.java @@ -1,6 +1,7 @@ package io.a2a.transport.jsonrpc.handler; import static io.a2a.server.util.async.AsyncUtils.createTubeConfig; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -46,6 +47,7 @@ import io.a2a.spec.TaskPushNotificationConfig; import io.a2a.spec.TaskResubscriptionRequest; import io.a2a.server.util.async.Internal; +import io.a2a.spec.InvalidParamsError; import mutiny.zero.ZeroPublisher; @ApplicationScoped @@ -78,11 +80,14 @@ public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, RequestHandler reque public SendMessageResponse onMessageSend(SendMessageRequest request, ServerCallContext context) { try { + request.check(); EventKind taskOrMessage = requestHandler.onMessageSend(request.getParams(), context); return new SendMessageResponse(request.getId(), taskOrMessage); } catch (JSONRPCError e) { return new SendMessageResponse(request.getId(), e); - } catch (Throwable t) { + } catch (IllegalArgumentException t) { + return new SendMessageResponse(request.getId(), new InvalidParamsError(t.getMessage())); + }catch (Throwable t) { return new SendMessageResponse(request.getId(), new InternalError(t.getMessage())); } } @@ -96,8 +101,8 @@ public Flow.Publisher onMessageSendStream( request.getId(), new InvalidRequestError("Streaming is not supported by the agent"))); } - try { + request.check(); Flow.Publisher publisher = requestHandler.onMessageSendStream(request.getParams(), context); // We can't use the convertingProcessor convenience method since that propagates any errors as an error handled From 54fa0327ce32cbde29f000648432599d705e0550 Mon Sep 17 00:00:00 2001 From: Emmanuel Hugonnet Date: Thu, 16 Apr 2026 18:26:07 +0200 Subject: [PATCH 5/5] fix: Fixing the missing id in the jsonrpc response Extract request id before jsonrpc validation so error responses include top-level id. Signed-off-by: Emmanuel Hugonnet --- .../server/apps/quarkus/A2AServerRoutes.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java index bc19159d0..1f14a61ae 100644 --- a/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java +++ b/reference/jsonrpc/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java @@ -100,17 +100,7 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { throw new JSONParseError(e.getMessage()); } - // Validate jsonrpc field - com.google.gson.JsonElement jsonrpcElement = node.get("jsonrpc"); - if (jsonrpcElement == null || !jsonrpcElement.isJsonPrimitive() - || !JSONRPCMessage.JSONRPC_VERSION.equals(jsonrpcElement.getAsString())) { - if(node.has("id")) { - throw new InvalidRequestError(null, "Invalid JSON-RPC request: missing or invalid 'jsonrpc' field", Map.of("id", node.get("id").getAsString())); - } - throw new InvalidRequestError("Invalid JSON-RPC request: missing or invalid 'jsonrpc' field"); - } - - // Validate id field (must be string, number, or null — not an object or array) + // 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("Invalid JSON-RPC request: 'id' must be a string, number, or null"); @@ -120,6 +110,13 @@ public void invokeJSONRPCHandler(@Body String body, RoutingContext rc) { requestId = idPrimitive.isNumber() ? idPrimitive.getAsLong() : idPrimitive.getAsString(); } + // Validate jsonrpc field + com.google.gson.JsonElement jsonrpcElement = node.get("jsonrpc"); + if (jsonrpcElement == null || !jsonrpcElement.isJsonPrimitive() + || !JSONRPCMessage.JSONRPC_VERSION.equals(jsonrpcElement.getAsString())) { + throw new InvalidRequestError("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()) {