From 5628827461c005acd0fed27bd402d54cc1e5e750 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Fri, 11 Jul 2025 12:42:50 -0400 Subject: [PATCH 1/5] fix: Make DEFAULT_AGENT_CARD_PATH and AGENT_CARD_TYPE_REFERENCE private final in A2ACardResolver --- .../java/io/a2a/client/A2ACardResolver.java | 5 ++-- .../io/a2a/client/A2ACardResolverTest.java | 26 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/client/src/main/java/io/a2a/client/A2ACardResolver.java b/client/src/main/java/io/a2a/client/A2ACardResolver.java index 1266f7219..70e76c872 100644 --- a/client/src/main/java/io/a2a/client/A2ACardResolver.java +++ b/client/src/main/java/io/a2a/client/A2ACardResolver.java @@ -18,9 +18,10 @@ public class A2ACardResolver { private final String url; private final Map authHeaders; - static String DEFAULT_AGENT_CARD_PATH = "/.well-known/agent.json"; + private static final String DEFAULT_AGENT_CARD_PATH = "/.well-known/agent.json"; + + private static final TypeReference AGENT_CARD_TYPE_REFERENCE = new TypeReference<>() {}; - static final TypeReference AGENT_CARD_TYPE_REFERENCE = new TypeReference<>() {}; /** * @param httpClient the http client to use * @param baseUrl the base URL for the agent whose agent card we want to retrieve diff --git a/client/src/test/java/io/a2a/client/A2ACardResolverTest.java b/client/src/test/java/io/a2a/client/A2ACardResolverTest.java index 8265b9514..47517eecb 100644 --- a/client/src/test/java/io/a2a/client/A2ACardResolverTest.java +++ b/client/src/test/java/io/a2a/client/A2ACardResolverTest.java @@ -1,6 +1,5 @@ package io.a2a.client; -import static io.a2a.client.A2ACardResolver.AGENT_CARD_TYPE_REFERENCE; import static io.a2a.util.Utils.OBJECT_MAPPER; import static io.a2a.util.Utils.unmarshalFrom; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -11,6 +10,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; +import com.fasterxml.jackson.core.type.TypeReference; import io.a2a.http.A2AHttpClient; import io.a2a.http.A2AHttpResponse; import io.a2a.spec.A2AClientError; @@ -19,6 +19,10 @@ import org.junit.jupiter.api.Test; public class A2ACardResolverTest { + + private static final String AGENT_CARD_PATH = "/.well-known/agent.json"; + private static final TypeReference AGENT_CARD_TYPE_REFERENCE = new TypeReference<>() {}; + @Test public void testConstructorStripsSlashes() throws Exception { TestHttpClient client = new TestHttpClient(); @@ -27,33 +31,33 @@ public void testConstructorStripsSlashes() throws Exception { A2ACardResolver resolver = new A2ACardResolver(client, "http://example.com/"); AgentCard card = resolver.getAgentCard(); - assertEquals("http://example.com" + A2ACardResolver.DEFAULT_AGENT_CARD_PATH, client.url); + assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); resolver = new A2ACardResolver(client, "http://example.com"); card = resolver.getAgentCard(); - assertEquals("http://example.com" + A2ACardResolver.DEFAULT_AGENT_CARD_PATH, client.url); + assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); - resolver = new A2ACardResolver(client, "http://example.com/", A2ACardResolver.DEFAULT_AGENT_CARD_PATH); + resolver = new A2ACardResolver(client, "http://example.com/", AGENT_CARD_PATH); card = resolver.getAgentCard(); - assertEquals("http://example.com" + A2ACardResolver.DEFAULT_AGENT_CARD_PATH, client.url); + assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); - resolver = new A2ACardResolver(client, "http://example.com", A2ACardResolver.DEFAULT_AGENT_CARD_PATH); + resolver = new A2ACardResolver(client, "http://example.com", AGENT_CARD_PATH); card = resolver.getAgentCard(); - assertEquals("http://example.com" + A2ACardResolver.DEFAULT_AGENT_CARD_PATH, client.url); + assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); - resolver = new A2ACardResolver(client, "http://example.com/", A2ACardResolver.DEFAULT_AGENT_CARD_PATH.substring(0)); + resolver = new A2ACardResolver(client, "http://example.com/", AGENT_CARD_PATH.substring(0)); card = resolver.getAgentCard(); - assertEquals("http://example.com" + A2ACardResolver.DEFAULT_AGENT_CARD_PATH, client.url); + assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); - resolver = new A2ACardResolver(client, "http://example.com", A2ACardResolver.DEFAULT_AGENT_CARD_PATH.substring(0)); + resolver = new A2ACardResolver(client, "http://example.com", AGENT_CARD_PATH.substring(0)); card = resolver.getAgentCard(); - assertEquals("http://example.com" + A2ACardResolver.DEFAULT_AGENT_CARD_PATH, client.url); + assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); } From 74ae78400256fca75cf916be021c20395f9bc61a Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Fri, 11 Jul 2025 13:02:08 -0400 Subject: [PATCH 2/5] fix: Tweak A2ACardResolver to use java.net.URI --- .../java/io/a2a/client/A2ACardResolver.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/client/src/main/java/io/a2a/client/A2ACardResolver.java b/client/src/main/java/io/a2a/client/A2ACardResolver.java index 70e76c872..88d1e351f 100644 --- a/client/src/main/java/io/a2a/client/A2ACardResolver.java +++ b/client/src/main/java/io/a2a/client/A2ACardResolver.java @@ -3,6 +3,8 @@ import static io.a2a.util.Utils.unmarshalFrom; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; @@ -25,8 +27,9 @@ public class A2ACardResolver { /** * @param httpClient the http client to use * @param baseUrl the base URL for the agent whose agent card we want to retrieve + * @throws A2AClientError if the URL for the agent is invalid */ - public A2ACardResolver(A2AHttpClient httpClient, String baseUrl) { + public A2ACardResolver(A2AHttpClient httpClient, String baseUrl) throws A2AClientError { this(httpClient, baseUrl, null, null); } @@ -35,8 +38,9 @@ public A2ACardResolver(A2AHttpClient httpClient, String baseUrl) { * @param baseUrl the base URL for the agent whose agent card we want to retrieve * @param agentCardPath optional path to the agent card endpoint relative to the base * agent URL, defaults to ".well-known/agent.json" + * @throws A2AClientError if the URL for the agent is invalid */ - public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, String agentCardPath) { + public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, String agentCardPath) throws A2AClientError { this(httpClient, baseUrl, agentCardPath, null); } @@ -46,17 +50,17 @@ public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, String agentCar * @param agentCardPath optional path to the agent card endpoint relative to the base * agent URL, defaults to ".well-known/agent.json" * @param authHeaders the HTTP authentication headers to use. May be {@code null} + * @throws A2AClientError if the URL for the agent is invalid */ - public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, String agentCardPath, Map authHeaders) { + public A2ACardResolver(A2AHttpClient httpClient, String baseUrl, String agentCardPath, + Map authHeaders) throws A2AClientError { this.httpClient = httpClient; - if (!baseUrl.endsWith("/")) { - baseUrl += "/"; - } agentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? DEFAULT_AGENT_CARD_PATH : agentCardPath; - if (agentCardPath.startsWith("/")) { - agentCardPath = agentCardPath.substring(1); + try { + this.url = new URI(baseUrl).resolve(agentCardPath).toString(); + } catch (URISyntaxException e) { + throw new A2AClientError("Invalid agent URL", e); } - this.url = baseUrl + agentCardPath; this.authHeaders = authHeaders; } From 2ea5f23526e27d5dc267b8a338cbcdb27ed09d28 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Fri, 11 Jul 2025 13:21:42 -0400 Subject: [PATCH 3/5] fix: Update A2AServerRoutes#isStreamingRequest so it converts the request body to JSON and checks the method attribute Remove A2AServerRoutes#isNonStreamingRequest since it's unused --- .../server/apps/quarkus/A2AServerRoutes.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/reference-impl/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java b/reference-impl/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java index 7c45c0f9f..acc363868 100644 --- a/reference-impl/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java +++ b/reference-impl/src/main/java/io/a2a/server/apps/quarkus/A2AServerRoutes.java @@ -8,6 +8,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; +import com.fasterxml.jackson.databind.JsonNode; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -212,16 +213,14 @@ private JSONRPCResponse generateErrorResponse(JSONRPCRequest request, JSON } private static boolean isStreamingRequest(String requestBody) { - return requestBody.contains(SendStreamingMessageRequest.METHOD) || - requestBody.contains(TaskResubscriptionRequest.METHOD); - } - - private static boolean isNonStreamingRequest(String requestBody) { - return requestBody.contains(GetTaskRequest.METHOD) || - requestBody.contains(CancelTaskRequest.METHOD) || - requestBody.contains(SendMessageRequest.METHOD) || - requestBody.contains(SetTaskPushNotificationConfigRequest.METHOD) || - requestBody.contains(GetTaskPushNotificationConfigRequest.METHOD); + try { + JsonNode node = Utils.OBJECT_MAPPER.readTree(requestBody); + JsonNode method = node != null ? node.get("method") : null; + return method != null && (SendStreamingMessageRequest.METHOD.equals(method.asText()) + || TaskResubscriptionRequest.METHOD.equals(method.asText())); + } catch (Exception e) { + return false; + } } static void setStreamingMultiSseSupportSubscribedRunnable(Runnable runnable) { From 5bd956a238df731e798bdc2d10bafd38809cab4a Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Fri, 11 Jul 2025 13:37:40 -0400 Subject: [PATCH 4/5] fix: Ensure continueConsuming happens in a background thread in ResultAggregator#consumeAndBreakOnInterrupt --- .../main/java/io/a2a/server/tasks/ResultAggregator.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sdk-server-common/src/main/java/io/a2a/server/tasks/ResultAggregator.java b/sdk-server-common/src/main/java/io/a2a/server/tasks/ResultAggregator.java index 5211d1deb..ad9ed9124 100644 --- a/sdk-server-common/src/main/java/io/a2a/server/tasks/ResultAggregator.java +++ b/sdk-server-common/src/main/java/io/a2a/server/tasks/ResultAggregator.java @@ -4,6 +4,7 @@ import static io.a2a.server.util.async.AsyncUtils.createTubeConfig; import static io.a2a.server.util.async.AsyncUtils.processor; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -100,11 +101,7 @@ public EventTypeAndInterrupt consumeAndBreakOnInterrupt(EventConsumer consumer) // new request is expected in order for the agent to make progress, // so the agent should exit. - // TODO There is the following line in the Python code I don't totally get - // asyncio.create_task(self._continue_consuming(event_stream)) - // I think it means the continueConsuming() call should be done in another thread - continueConsuming(all); - + CompletableFuture.runAsync(() -> continueConsuming(all)); interrupted.set(true); return false; } From 797378e6f2a4b01cb163e54c89689a38f153756a Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Fri, 11 Jul 2025 13:55:53 -0400 Subject: [PATCH 5/5] fix: Update A2ACardResolverTest to make the different test cases more clear and fix some tests --- .../src/test/java/io/a2a/client/A2ACardResolverTest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/test/java/io/a2a/client/A2ACardResolverTest.java b/client/src/test/java/io/a2a/client/A2ACardResolverTest.java index 47517eecb..8d9ff0f5b 100644 --- a/client/src/test/java/io/a2a/client/A2ACardResolverTest.java +++ b/client/src/test/java/io/a2a/client/A2ACardResolverTest.java @@ -39,22 +39,26 @@ public void testConstructorStripsSlashes() throws Exception { assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); + // baseUrl with trailing slash, agentCardParth with leading slash resolver = new A2ACardResolver(client, "http://example.com/", AGENT_CARD_PATH); card = resolver.getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); + // baseUrl without trailing slash, agentCardPath with leading slash resolver = new A2ACardResolver(client, "http://example.com", AGENT_CARD_PATH); card = resolver.getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); - resolver = new A2ACardResolver(client, "http://example.com/", AGENT_CARD_PATH.substring(0)); + // baseUrl with trailing slash, agentCardPath without leading slash + resolver = new A2ACardResolver(client, "http://example.com/", AGENT_CARD_PATH.substring(1)); card = resolver.getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); - resolver = new A2ACardResolver(client, "http://example.com", AGENT_CARD_PATH.substring(0)); + // baseUrl without trailing slash, agentCardPath without leading slash + resolver = new A2ACardResolver(client, "http://example.com", AGENT_CARD_PATH.substring(1)); card = resolver.getAgentCard(); assertEquals("http://example.com" + AGENT_CARD_PATH, client.url);