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..9e84c263f 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 + +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 +``` + +**Check formatting** without changing files (the command fails if formatting errors remain): + +```bash +mvn -N -Dskip.spotless=false spotless:check +``` + +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/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 53af4bcee..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 @@ -636,6 +705,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 @@ -646,16 +721,18 @@ true false - - - - + + + true 4 + + **/target/** + pom.xml **/pom.xml @@ -678,7 +755,7 @@ - docs/**/*.md + docs/content/dev/**/*.md CODE_OF_CONDUCT.md CONTRIBUTING.md CONTRIBUTING_INTEGRATIONS.md @@ -686,14 +763,7 @@ RELEASE.md SECURITY.md - - COMMONMARK - ALL,TOC - YamlFrontMatter,Emoji - - 120 - - + @@ -705,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(); } - }