diff --git a/.github/workflows/run-tck.yml b/.github/workflows/run-tck.yml index 9c6533ab3..c04eba168 100644 --- a/.github/workflows/run-tck.yml +++ b/.github/workflows/run-tck.yml @@ -12,9 +12,12 @@ on: env: # Tag/branch of the TCK TCK_VERSION: 1.0-dev + TCK_VERSION_0_3: 0.3.0.beta4 # Tells uv to not need a venv, and instead use system UV_SYSTEM_PYTHON: 1 SUT_URL: http://localhost:9999 + # Slow system on CI + TCK_STREAMING_TIMEOUT_0_3: 5.0 # Only run the latest job concurrency: @@ -27,6 +30,7 @@ jobs: strategy: matrix: java-version: [17] + profile: ['', 'multi-mode'] steps: - name: Checkout a2a-java uses: actions/checkout@v6 @@ -54,7 +58,7 @@ jobs: uv pip install -e . working-directory: a2a-tck - name: Start SUT - run: mvn -B quarkus:dev -Dquarkus.console.enabled=false & + run: mvn -B quarkus:dev ${{ matrix.profile && format('-P{0}', matrix.profile) || '' }} -Dquarkus.console.enabled=false & working-directory: tck - name: Wait for SUT to start run: | @@ -97,7 +101,7 @@ jobs: # Extract everything after the first ═══ separator line SUMMARY=$(sed -n '/^═══/,$p' a2a-tck/tck-output.log) if [ -n "$SUMMARY" ]; then - echo '### TCK Results (Java ${{ matrix.java-version }})' >> $GITHUB_STEP_SUMMARY + echo '### TCK 1.0 Results (Java ${{ matrix.java-version }}${{ matrix.profile && format(', {0}', matrix.profile) || '' }})' >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY @@ -112,7 +116,106 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: tck-reports-java-${{ matrix.java-version }} + name: tck-reports-java-${{ matrix.java-version }}${{ matrix.profile && format('-{0}', matrix.profile) || '' }} path: a2a-tck/reports/ retention-days: 14 - if-no-files-found: warn \ No newline at end of file + if-no-files-found: warn + + tck-test-0-3: + runs-on: ubuntu-latest + strategy: + matrix: + java-version: [17] + profile: ['', 'multi-mode'] + steps: + - name: Checkout a2a-java + uses: actions/checkout@v6 + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v5 + with: + java-version: ${{ matrix.java-version }} + distribution: 'temurin' + cache: maven + - name: Build a2a-java SDK + run: mvn -B install -DskipTests + - name: Checkout a2a-tck + uses: actions/checkout@v6 + with: + repository: a2aproject/a2a-tck + path: a2a-tck + ref: ${{ env.TCK_VERSION_0_3 }} + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version-file: "a2a-tck/pyproject.toml" + - name: Install uv and Python dependencies + run: | + pip install uv + uv pip install -e . + working-directory: a2a-tck + - name: Start SUT + run: mvn -B quarkus:dev ${{ matrix.profile && format('-P{0}', matrix.profile) || '' }} -Dquarkus.console.enabled=false & + working-directory: compat-0.3/tck + - name: Wait for SUT to start + run: | + URL="${{ env.SUT_URL }}/.well-known/agent-card.json" + EXPECTED_STATUS=200 + TIMEOUT=120 + RETRY_INTERVAL=2 + START_TIME=$(date +%s) + + while true; do + CURRENT_TIME=$(date +%s) + ELAPSED_TIME=$((CURRENT_TIME - START_TIME)) + + if [ "$ELAPSED_TIME" -ge "$TIMEOUT" ]; then + echo "Timeout: Server did not respond with status $EXPECTED_STATUS within $TIMEOUT seconds." + exit 1 + fi + + HTTP_STATUS=$(curl --output /dev/null --silent --write-out "%{http_code}" "$URL") || true + + if [ "$HTTP_STATUS" -eq "$EXPECTED_STATUS" ]; then + echo "Server is up! Received status $HTTP_STATUS after $ELAPSED_TIME seconds." + break; + fi + + echo "Server not ready (status: $HTTP_STATUS). Retrying in $RETRY_INTERVAL seconds..." + sleep "$RETRY_INTERVAL" + done + - name: Run TCK + id: run-tck + timeout-minutes: 5 + env: + TCK_STREAMING_TIMEOUT: ${{ env.TCK_STREAMING_TIMEOUT_0_3 }} + run: | + set -o pipefail + ./run_tck.py --sut-url ${{ env.SUT_URL }} --category all --transports jsonrpc,grpc,rest --compliance-report report.json 2>&1 | tee tck-output.log + working-directory: a2a-tck + - name: TCK Summary + if: always() && steps.run-tck.outcome != 'skipped' + run: | + if [ -f a2a-tck/tck-output.log ]; then + SUMMARY=$(sed -n '/^═══/,$p' a2a-tck/tck-output.log) + if [ -n "$SUMMARY" ]; then + echo '### TCK 0.3 Results (Java ${{ matrix.java-version }}${{ matrix.profile && format(', {0}', matrix.profile) || '' }})' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + fi + - name: Stop SUT + if: always() + run: | + pkill -f "quarkus:dev" || true + sleep 2 + - name: Upload TCK Reports + if: always() + uses: actions/upload-artifact@v7 + with: + name: tck-0.3-reports-java-${{ matrix.java-version }}${{ matrix.profile && format('-{0}', matrix.profile) || '' }} + path: | + a2a-tck/reports/ + a2a-tck/report.json + retention-days: 14 + if-no-files-found: warn diff --git a/compat-0.3/http-client/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java b/compat-0.3/http-client/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java index f632d3e22..70d4b7b21 100644 --- a/compat-0.3/http-client/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java +++ b/compat-0.3/http-client/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; @@ -93,6 +94,38 @@ public void testGetAgentCardJsonDecodeError() throws Exception { } + @Test + public void testParseV1AgentCardWithUrlAndPreferredTransport() throws Exception { + TestHttpClient client = new TestHttpClient(); + client.body = JsonMessages_v0_3.V1_AGENT_CARD; + + A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, "http://example.com/"); + AgentCard_v0_3 card = resolver.getAgentCard(); + + assertEquals("Test Agent", card.name()); + assertEquals("A test agent", card.description()); + assertEquals("1.0.0", card.version()); + assertEquals("https://agent.example.com/a2a", card.url()); + assertEquals("JSONRPC", card.preferredTransport()); + assertTrue(card.capabilities().streaming()); + } + + @Test + public void testParseV1AgentCardDefaultsPreferredTransportWhenAbsent() throws Exception { + TestHttpClient client = new TestHttpClient(); + client.body = JsonMessages_v0_3.V1_AGENT_CARD_NO_PREFERRED_TRANSPORT; + + A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, "http://example.com/"); + AgentCard_v0_3 card = resolver.getAgentCard(); + + assertEquals("Minimal Agent", card.name()); + assertEquals("https://agent.example.com/a2a", card.url()); + // v0.3 compact constructor defaults null preferredTransport to "JSONRPC" + assertEquals("JSONRPC", card.preferredTransport()); + // v1.0-only fields such as supportedInterfaces are unknown to v0.3 and must be ignored + assertNull(card.additionalInterfaces()); + } + @Test public void testGetAgentCardRequestError() throws Exception { TestHttpClient client = new TestHttpClient(); diff --git a/compat-0.3/http-client/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java b/compat-0.3/http-client/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java index 751dadf6c..9b4d0b16d 100644 --- a/compat-0.3/http-client/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java +++ b/compat-0.3/http-client/src/test/java/org/a2aproject/sdk/compat03/client/http/JsonMessages_v0_3.java @@ -83,6 +83,56 @@ public class JsonMessages_v0_3 { ] }"""; + /** + * 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 = """ + { + "name": "Test Agent", + "description": "A test agent", + "url": "https://agent.example.com/a2a", + "preferredTransport": "JSONRPC", + "version": "1.0.0", + "supportedInterfaces": [ + {"protocolBinding": "JSONRPC", "url": "https://agent.example.com/a2a"}, + {"protocolBinding": "GRPC", "url": "grpc://agent.example.com:9090"} + ], + "capabilities": { + "streaming": true, + "pushNotifications": false, + "stateTransitionHistory": false + }, + "defaultInputModes": ["text/plain"], + "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 = """ + { + "name": "Minimal Agent", + "description": "Minimal agent card", + "url": "https://agent.example.com/a2a", + "version": "2.0.0", + "supportedInterfaces": [ + {"protocolBinding": "JSONRPC", "url": "https://agent.example.com/a2a"} + ], + "capabilities": { + "streaming": false, + "pushNotifications": false, + "stateTransitionHistory": false + }, + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["text/plain"], + "skills": [] + }"""; + static final String AUTHENTICATION_EXTENDED_AGENT_CARD = """ { "name": "GeoSpatial Route Planner Agent Extended", diff --git a/compat-0.3/tck/pom.xml b/compat-0.3/tck/pom.xml index b0cb5c5c8..455cf9096 100644 --- a/compat-0.3/tck/pom.xml +++ b/compat-0.3/tck/pom.xml @@ -49,6 +49,48 @@ + + + multi-mode + + + org.a2aproject.sdk + a2a-java-sdk-reference-jsonrpc + + + org.a2aproject.sdk + a2a-java-sdk-reference-grpc + + + org.a2aproject.sdk + a2a-java-sdk-reference-rest + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-multi-mode-source + generate-sources + + add-source + + + + src/multi-mode/java + + + + + + + + + + diff --git a/compat-0.3/tck/src/multi-mode/java/org/a2aproject/sdk/compat03/tck/server/StubAgentCardProducer.java b/compat-0.3/tck/src/multi-mode/java/org/a2aproject/sdk/compat03/tck/server/StubAgentCardProducer.java new file mode 100644 index 000000000..6595f098d --- /dev/null +++ b/compat-0.3/tck/src/multi-mode/java/org/a2aproject/sdk/compat03/tck/server/StubAgentCardProducer.java @@ -0,0 +1,47 @@ +package org.a2aproject.sdk.compat03.tck.server; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +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; + +/** + * Stub producer that overrides the v1.0 DefaultProducers @DefaultBean when the + * multi-mode profile adds v1.0 reference dependencies to the classpath. + * This will be replaced by a proper translation layer in the future. + */ +@ApplicationScoped +public class StubAgentCardProducer { + + private static final String DEFAULT_SUT_URL = "http://localhost:9999"; + + @Produces + @PublicAgentCard + public AgentCard createStubAgentCard() { + return AgentCard.builder() + .name("stub") + .description("Stub agent card for multi-mode testing") + .version("0.0.0") + .capabilities(AgentCapabilities.builder().build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of(AgentSkill.builder() + .id("stub") + .name("stub") + .description("stub") + .tags(List.of()) + .build())) + .supportedInterfaces(List.of( + new AgentInterface(TransportProtocol.JSONRPC.asString(), DEFAULT_SUT_URL), + new AgentInterface(TransportProtocol.GRPC.asString(), DEFAULT_SUT_URL), + new AgentInterface(TransportProtocol.HTTP_JSON.asString(), DEFAULT_SUT_URL))) + .build(); + } +} 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 d1edf49b8..e8ee10397 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 @@ -184,7 +184,7 @@ public AgentCard getAgentCard() throws A2AClientError, A2AClientJSONError { try { org.a2aproject.sdk.grpc.AgentCard.Builder agentCardBuilder = org.a2aproject.sdk.grpc.AgentCard.newBuilder(); - JSONRPCUtils.parseJsonString(body, agentCardBuilder, ""); + JSONRPCUtils.parseJsonString(body, agentCardBuilder, "", true); return ProtoUtils.FromProto.agentCard(agentCardBuilder); } catch (A2AError | JsonProcessingException e) { throw new A2AClientJSONError("Could not unmarshal agent card response", e); 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 df9be2b6d..565d6255c 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 @@ -30,5 +30,7 @@ public interface AgentCardMapper { @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) org.a2aproject.sdk.spec.AgentCard fromProto(org.a2aproject.sdk.grpc.AgentCard proto); } 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 966c998cc..7af14fa5c 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 @@ -439,8 +439,13 @@ protected static void parseRequestBody(JsonElement jsonRpc, com.google.protobuf. } 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 { try { - JsonFormat.parser().merge(body, builder); + JsonFormat.Parser parser = ignoringUnknownFields ? JsonFormat.parser().ignoringUnknownFields() : JsonFormat.parser(); + parser.merge(body, builder); } catch (InvalidProtocolBufferException e) { log.log(Level.FINE, "Protocol buffer parsing failed for JSON: {0}", body); log.log(Level.FINE, "Proto parsing error details", e); 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 af9a9ce2b..de09aae6a 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/AgentCard.java @@ -56,7 +56,9 @@ public record AgentCard( @Nullable List securityRequirements, @Nullable String iconUrl, List supportedInterfaces, - @Nullable List signatures) { + @Nullable List signatures, + @Nullable String url, + @Nullable String preferredTransport) { /** * Compact constructor that validates required fields. @@ -156,6 +158,8 @@ public static class Builder { private @Nullable String iconUrl; private @Nullable List supportedInterfaces; private @Nullable List signatures; + private @Nullable String url; + private @Nullable String preferredTransport; /** * Creates a new Builder with all fields unset. @@ -186,6 +190,8 @@ private Builder(AgentCard card) { this.iconUrl = card.iconUrl; this.supportedInterfaces = card.supportedInterfaces != null ? new ArrayList<>(card.supportedInterfaces) : Collections.emptyList(); this.signatures = card.signatures != null ? new ArrayList<>(card.signatures) : null; + this.url = card.url; + this.preferredTransport= card.preferredTransport(); } /** @@ -376,6 +382,16 @@ public Builder signatures(List signatures) { return this; } + public Builder preferredTransport(String preferredTransport) { + this.preferredTransport = preferredTransport; + return this; + } + + public Builder url(String url) { + this.url = url; + return this; + } + /** * Builds an immutable {@link AgentCard} from the current builder state. *

@@ -399,7 +415,9 @@ public AgentCard build() { securityRequirements, iconUrl, Assert.checkNotNullParam("supportedInterfaces", supportedInterfaces), - signatures); + signatures, + url, + preferredTransport == null ? TransportProtocol.JSONRPC.name() : preferredTransport); } } } diff --git a/tck/pom.xml b/tck/pom.xml index 41eaec905..07e469690 100644 --- a/tck/pom.xml +++ b/tck/pom.xml @@ -45,6 +45,51 @@ + + + multi-mode + + + org.a2aproject.sdk + a2a-java-sdk-compat-0.3-reference-jsonrpc + ${project.version} + + + org.a2aproject.sdk + a2a-java-sdk-compat-0.3-reference-grpc + ${project.version} + + + org.a2aproject.sdk + a2a-java-sdk-compat-0.3-reference-rest + ${project.version} + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-multi-mode-source + generate-sources + + add-source + + + + src/multi-mode/java + + + + + + + + + + diff --git a/tck/src/multi-mode/java/org/a2aproject/sdk/sut/StubAgentCardProducer_v0_3.java b/tck/src/multi-mode/java/org/a2aproject/sdk/sut/StubAgentCardProducer_v0_3.java new file mode 100644 index 000000000..780aa3277 --- /dev/null +++ b/tck/src/multi-mode/java/org/a2aproject/sdk/sut/StubAgentCardProducer_v0_3.java @@ -0,0 +1,43 @@ +package org.a2aproject.sdk.sut; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; + +import io.quarkus.arc.DefaultBean; + +import org.a2aproject.sdk.compat03.spec.AgentCapabilities_v0_3; +import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3; +import org.a2aproject.sdk.compat03.spec.AgentSkill_v0_3; +import org.a2aproject.sdk.server.PublicAgentCard; + +/** + * Stub producer that satisfies the v0.3 handler CDI requirements when the + * multi-mode profile adds compat-0.3 dependencies to the classpath. + * This will be replaced by a proper translation layer in the future. + */ +@ApplicationScoped +public class StubAgentCardProducer_v0_3 { + + @Produces + @PublicAgentCard + @DefaultBean + public AgentCard_v0_3 createStubAgentCard() { + return new AgentCard_v0_3.Builder() + .name("stub") + .description("Stub agent card for multi-mode testing") + .url("http://localhost:9999") + .version("0.0.0") + .capabilities(new AgentCapabilities_v0_3.Builder().build()) + .defaultInputModes(List.of("text")) + .defaultOutputModes(List.of("text")) + .skills(List.of(new AgentSkill_v0_3.Builder() + .id("stub") + .name("stub") + .description("stub") + .tags(List.of()) + .build())) + .build(); + } +} 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 fa90cbfd8..d6d76dcca 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 @@ -58,7 +58,9 @@ public AgentCard agentCard() { .defaultInputModes(Collections.singletonList("text")) .defaultOutputModes(Collections.singletonList("text")) .skills(new ArrayList<>()) - .supportedInterfaces(Collections.singletonList(new AgentInterface(preferredTransport, transportUrl))); + .supportedInterfaces(Collections.singletonList(new AgentInterface(preferredTransport, transportUrl))) + .url(transportUrl) + .preferredTransport(preferredTransport); // Add security configuration if enabled (for authentication tests) if (securityEnabled) {