diff --git a/client/src/main/java/io/a2a/client/A2ACardResolver.java b/client/src/main/java/io/a2a/client/A2ACardResolver.java index 1266f7219..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; @@ -18,14 +20,16 @@ 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 + * @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); } @@ -34,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); } @@ -45,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; } diff --git a/client/src/test/java/io/a2a/client/A2ACardResolverTest.java b/client/src/test/java/io/a2a/client/A2ACardResolverTest.java index 8265b9514..8d9ff0f5b 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,37 @@ 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); + // 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" + 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); + // 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" + 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)); + // 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" + 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)); + // 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" + A2ACardResolver.DEFAULT_AGENT_CARD_PATH, client.url); + assertEquals("http://example.com" + AGENT_CARD_PATH, client.url); } 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) { 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; }