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 8df595122..3f1c120cc 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 @@ -163,13 +163,13 @@ public void shouldSelectCorrectInterfaceWithClientPreference() throws A2AClientE } @Test - public void shouldPreserveEmptyTenant() throws A2AClientException { + public void shouldHaveNullTenantWhenNotSet() throws A2AClientException { ClientBuilder builder = Client .builder(card) .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder()); AgentInterface selectedInterface = builder.findBestClientTransport(); - Assertions.assertEquals("", selectedInterface.tenant()); + Assertions.assertNull(selectedInterface.tenant()); } } 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 81b476088..d7b51224f 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 @@ -8,12 +8,15 @@ * Mapper between {@link org.a2aproject.sdk.spec.AgentInterface} and {@link org.a2aproject.sdk.grpc.AgentInterface}. */ @Mapper(config = A2AProtoMapperConfig.class, - collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED) + 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)") org.a2aproject.sdk.grpc.AgentInterface toProto(org.a2aproject.sdk.spec.AgentInterface domain); + @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") org.a2aproject.sdk.spec.AgentInterface fromProto(org.a2aproject.sdk.grpc.AgentInterface 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 18fc3ae9d..a464dfab2 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 @@ -9,7 +9,7 @@ /** * Mapper between {@link org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest} and {@link org.a2aproject.sdk.spec.DeleteTaskPushNotificationConfigParams}. */ -@Mapper(config = A2AProtoMapperConfig.class) +@Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class) public interface DeleteTaskPushNotificationConfigParamsMapper { DeleteTaskPushNotificationConfigParamsMapper INSTANCE = A2AMappers.getMapper(DeleteTaskPushNotificationConfigParamsMapper.class); @@ -20,7 +20,7 @@ public interface DeleteTaskPushNotificationConfigParamsMapper { @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "taskId", source = "taskId") @Mapping(target = "id", source = "id") - @Mapping(target = "tenant", source = "tenant") + @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") DeleteTaskPushNotificationConfigParams fromProto(org.a2aproject.sdk.grpc.DeleteTaskPushNotificationConfigRequest proto); /** @@ -28,6 +28,6 @@ public interface DeleteTaskPushNotificationConfigParamsMapper { */ @Mapping(target = "taskId", source = "taskId") @Mapping(target = "id", source = "id") - @Mapping(target = "tenant", source = "tenant") + @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 9febadf56..e277b7ce0 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 @@ -9,7 +9,7 @@ /** * Mapper between {@link org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest} and {@link org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams}. */ -@Mapper(config = A2AProtoMapperConfig.class) +@Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class) public interface GetTaskPushNotificationConfigParamsMapper { GetTaskPushNotificationConfigParamsMapper INSTANCE = A2AMappers.getMapper(GetTaskPushNotificationConfigParamsMapper.class); @@ -20,7 +20,7 @@ public interface GetTaskPushNotificationConfigParamsMapper { @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "taskId", source = "taskId") @Mapping(target = "id", source = "id") - @Mapping(target = "tenant", source = "tenant") + @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") GetTaskPushNotificationConfigParams fromProto(org.a2aproject.sdk.grpc.GetTaskPushNotificationConfigRequest proto); /** @@ -28,6 +28,6 @@ public interface GetTaskPushNotificationConfigParamsMapper { */ @Mapping(target = "taskId", source = "taskId") @Mapping(target = "id", source = "id", conditionExpression = "java(domain.id() != null)") - @Mapping(target = "tenant", source = "tenant") + @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/ListTaskPushNotificationConfigsParamsMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/ListTaskPushNotificationConfigsParamsMapper.java index 635600bf2..c69fa1925 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 @@ -9,7 +9,7 @@ /** * Mapper between {@link org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest} and {@link org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams}. */ -@Mapper(config = A2AProtoMapperConfig.class) +@Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class) public interface ListTaskPushNotificationConfigsParamsMapper { ListTaskPushNotificationConfigsParamsMapper INSTANCE = A2AMappers.getMapper(ListTaskPushNotificationConfigsParamsMapper.class); @@ -19,13 +19,13 @@ public interface ListTaskPushNotificationConfigsParamsMapper { */ @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "id", source = "taskId") - @Mapping(target = "tenant", source = "tenant") + @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") ListTaskPushNotificationConfigsParams fromProto(org.a2aproject.sdk.grpc.ListTaskPushNotificationConfigsRequest proto); /** * Converts domain ListTaskPushNotificationConfigsParams to proto ListTaskPushNotificationConfigsRequest. */ @Mapping(target = "taskId", source = "id") - @Mapping(target = "tenant", source = "tenant") + @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/MessageSendParamsMapper.java b/spec-grpc/src/main/java/org/a2aproject/sdk/grpc/mapper/MessageSendParamsMapper.java index 78bec7580..0325ca527 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 @@ -22,6 +22,7 @@ public interface MessageSendParamsMapper { */ @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)") org.a2aproject.sdk.grpc.SendMessageRequest toProto(MessageSendParams domain); /** @@ -31,5 +32,6 @@ public interface MessageSendParamsMapper { */ @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataFromProto") + @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") MessageSendParams fromProto(org.a2aproject.sdk.grpc.SendMessageRequest proto); } 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 b42d9379a..d6f3a913d 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 @@ -24,6 +24,7 @@ public interface TaskIdParamsMapper { @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "id", source = "id") @Mapping(target = "metadata", source = "metadata", qualifiedByName = "metadataFromProto") + @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") CancelTaskParams fromProtoCancelTaskRequest(org.a2aproject.sdk.grpc.CancelTaskRequest proto); /** @@ -33,6 +34,7 @@ public interface TaskIdParamsMapper { @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)") org.a2aproject.sdk.grpc.CancelTaskRequest toProtoCancelTaskRequest(CancelTaskParams domain); @@ -42,7 +44,7 @@ public interface TaskIdParamsMapper { */ @BeanMapping(builder = @Builder(buildMethod = "build")) @Mapping(target = "id", source = "id") - @Mapping(target = "tenant", source = "tenant") + @Mapping(target = "tenant", source = "tenant", qualifiedByName = "emptyToNull") TaskIdParams fromProtoSubscribeToTaskRequest(org.a2aproject.sdk.grpc.SubscribeToTaskRequest proto); /** @@ -51,5 +53,6 @@ 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); } 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 fe5916a47..d95147b75 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 @@ -11,7 +11,7 @@ *
* Extracts task ID from resource name format "tasks/{id}" using {@link ResourceNameParser}.
*/
-@Mapper(config = A2AProtoMapperConfig.class)
+@Mapper(config = A2AProtoMapperConfig.class, uses = A2ACommonFieldMapper.class)
public interface TaskQueryParamsMapper {
TaskQueryParamsMapper INSTANCE = A2AMappers.getMapper(TaskQueryParamsMapper.class);
@@ -28,6 +28,6 @@ public interface TaskQueryParamsMapper {
@BeanMapping(builder = @Builder(buildMethod = "build"))
@Mapping(target = "id", source = "id")
@Mapping(target = "historyLength", source = "historyLength")
- @Mapping(target = "tenant", source = "tenant")
+ @Mapping(target = "tenant", source = "tenant", conditionExpression = "java(domain.tenant() != null)")
org.a2aproject.sdk.grpc.GetTaskRequest toProto(TaskQueryParams domain);
}
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 c14147eee..3ad2d4717 100644
--- a/spec/src/main/java/org/a2aproject/sdk/spec/AgentInterface.java
+++ b/spec/src/main/java/org/a2aproject/sdk/spec/AgentInterface.java
@@ -1,6 +1,7 @@
package org.a2aproject.sdk.spec;
import org.a2aproject.sdk.util.Assert;
+import org.jspecify.annotations.Nullable;
/**
* Declares a combination of a target URL and protocol binding for accessing an agent.
@@ -19,13 +20,13 @@
* @param protocolBinding the protocol binding supported at this URL (e.g., "JSONRPC", "GRPC", "HTTP+JSON") (required)
* @param url the endpoint URL where this interface is available; must be a valid absolute HTTPS URL in production
* (required)
- * @param tenant the tenant to be set in the request when calling the agent.
+ * @param tenant optional tenant to be set in the request when calling the agent.
* @param protocolVersion the version of the A2A protocol this interface exposes (e.g., "1.0", "0.3") (required)
* @see AgentCard
* @see TransportProtocol
* @see A2A Protocol Specification
*/
-public record AgentInterface(String protocolBinding, String url, 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";
@@ -42,7 +43,6 @@ public record AgentInterface(String protocolBinding, String url, String tenant,
public AgentInterface {
Assert.checkNotNullParam("protocolBinding", protocolBinding);
Assert.checkNotNullParam("url", url);
- Assert.checkNotNullParam("tenant", tenant);
if (protocolVersion == null || protocolVersion.isEmpty()) {
protocolVersion = CURRENT_PROTOCOL_VERSION;
@@ -67,6 +67,6 @@ public AgentInterface(String protocolBinding, String url, String tenant) {
* @param url the endpoint URL (see class-level JavaDoc)
*/
public AgentInterface(String protocolBinding, String url) {
- this(protocolBinding, url, "", CURRENT_PROTOCOL_VERSION);
+ this(protocolBinding, url, null, CURRENT_PROTOCOL_VERSION);
}
}
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 d950f1793..e823af312 100644
--- a/spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java
+++ b/spec/src/main/java/org/a2aproject/sdk/spec/CancelTaskParams.java
@@ -1,7 +1,6 @@
package org.a2aproject.sdk.spec;
import org.a2aproject.sdk.util.Assert;
-import org.a2aproject.sdk.util.Utils;
import java.util.Collections;
import java.util.Map;
import org.jspecify.annotations.Nullable;
@@ -17,7 +16,7 @@
* @param metadata optional arbitrary key-value metadata (e.g. cancellation reason)
* @see A2A Protocol Specification
*/
-public record CancelTaskParams(String id, String tenant, Map Strips any trailing slash from {@code baseUrl} and ensures {@code cardPath} starts with
+ *
+ * Strips any trailing slash from {@code baseUrl} and ensures {@code cardPath} starts with
* a leading slash before concatenating, so both {@code http://host/base/} and
* {@code http://host/base} produce the same result.
*
@@ -206,7 +207,8 @@ public static String buildCardUrl(String baseUrl, String cardPath) {
* Strips any trailing slash and the standard well-known suffix from {@code baseUrl} so that
* {@link #buildCardUrl} can append the desired path without doubling it.
*
- * Only {@link #DEFAULT_AGENT_CARD_PATH} is stripped; custom paths are never inferred
+ *
+ * Only {@link #DEFAULT_AGENT_CARD_PATH} is stripped; custom paths are never inferred
* from the URL structure.
*
* @param baseUrl the URL to strip
@@ -222,7 +224,8 @@ public static String stripWellKnownSuffix(String baseUrl) {
/**
* Builds a base URL by combining a raw base URL string with an optional tenant path.
*
- * Normalizes trailing slashes on the base URL and validates/normalizes the tenant path.
+ *
+ * Normalizes trailing slashes on the base URL and validates/normalizes the tenant path.
*
* @param baseUrl the base URL string, must not be null
* @param tenant the tenant path override, may be null for no tenant
@@ -299,17 +302,18 @@ private static void validateTenant(String tenant) {
*
* If the provided {@code tenant} parameter is null or blank, the {@code agentTenant} is returned instead.
*
- * @param agentTenant the default tenant from the agent card, must not be null
+ * @param agentTenant the default tenant from the agent card, may be null or blank
* @param tenant the tenant override from the request, may be null or blank
* @return the normalized tenant path
* @throws IllegalArgumentException if the tenant is invalid or unsafe
*/
- private static String extractTenant(String agentTenant, @Nullable String tenant) {
- checkNotNullParam("agentTenant", agentTenant);
-
+ private static String extractTenant(@Nullable String agentTenant, @Nullable String tenant) {
String tenantPath = tenant;
if (tenantPath == null || tenantPath.isBlank()) {
- return agentTenant;
+ tenantPath = agentTenant;
+ }
+ if (tenantPath == null || tenantPath.isBlank()) {
+ return "";
}
// Normalize slashes