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 dee737d4b..2b2f3eb2e 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
@@ -50,10 +50,8 @@ public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificatio
@Override
public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificationConfig, @Nullable String protocolVersion) {
String taskId = Assert.checkNotNullParam("taskId", notificationConfig.taskId());
- // Ensure config has an ID - default to taskId if not provided (mirroring InMemoryPushNotificationConfigStore behavior)
- if (notificationConfig.id().isEmpty()) {
- // This means the taskId and configId are same. This will not allow having multiple configs for a single Task.
- // The configId is a required field in the spec and should not be empty
+ // Default missing config IDs to the task ID, matching the in-memory store.
+ if (notificationConfig.id() == null || notificationConfig.id().isEmpty()) {
notificationConfig = TaskPushNotificationConfig.builder(notificationConfig).id(taskId).build();
}
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 089a418b7..9da2b36bf 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
@@ -163,6 +163,24 @@ public void testSetInfoWithoutConfigId() {
assertEquals(updatedConfig.url(), configResult.configs().get(0).url());
}
+ @Test
+ @Transactional
+ public void testSetInfoWithNullConfigId() {
+ String taskId = "task_null_config_id";
+ 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));
+ assertEquals(1, configResult.configs().size());
+ assertEquals(taskId, configResult.configs().get(0).id());
+ }
+
@Test
@Transactional
public void testGetInfoExistingConfig() {
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java b/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java
index d234e7764..bdb540000 100644
--- a/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java
@@ -1126,7 +1126,8 @@ public TaskPushNotificationConfig onGetTaskPushNotificationConfig(
throw new InternalError("No push notification config found");
}
- String configId = params.id();
+ String requestedConfigId = params.id();
+ String configId = requestedConfigId == null || requestedConfigId.isEmpty() ? params.taskId() : requestedConfigId;
return getTaskPushNotificationConfig(listTaskPushNotificationConfigsResult, configId);
}
diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java
index 854bd7fbd..403b99c0e 100644
--- a/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java
+++ b/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStore.java
@@ -48,11 +48,13 @@ public InMemoryPushNotificationConfigStore() {
public TaskPushNotificationConfig setInfo(TaskPushNotificationConfig notificationConfig) {
String taskId = Assert.checkNotNullParam("taskId", notificationConfig.taskId());
TaskPushNotificationConfig.Builder builder = TaskPushNotificationConfig.builder(notificationConfig);
- if (notificationConfig.id().isEmpty()) {
+ String requestedConfigId = notificationConfig.id();
+ boolean configIdIsMissing = requestedConfigId == null || requestedConfigId.isEmpty();
+ String configId = configIdIsMissing ? taskId : requestedConfigId;
+ if (configIdIsMissing) {
builder.id(taskId);
}
TaskPushNotificationConfig config = builder.build();
- String configId = config.id();
int maxPerTask = PushNotificationConfigStore.maxPushConfigsPerTask(configProvider);
pushNotificationInfos.compute(taskId, (key, list) -> {
diff --git a/server-common/src/test/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandlerTest.java b/server-common/src/test/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandlerTest.java
index da0603961..fa9bf6814 100644
--- a/server-common/src/test/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandlerTest.java
+++ b/server-common/src/test/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandlerTest.java
@@ -45,6 +45,7 @@
import org.a2aproject.sdk.spec.CancelTaskParams;
import org.a2aproject.sdk.spec.Event;
import org.a2aproject.sdk.spec.EventKind;
+import org.a2aproject.sdk.spec.GetTaskPushNotificationConfigParams;
import org.a2aproject.sdk.spec.InvalidParamsError;
import org.a2aproject.sdk.spec.ListTasksParams;
import org.a2aproject.sdk.spec.ListTaskPushNotificationConfigsParams;
@@ -989,6 +990,26 @@ void testVersionStored_OnCreateTaskPushNotificationConfig() throws Exception {
"Protocol version should be stored for the push notification config");
}
+ @Test
+ void testGetTaskPushNotificationConfigDefaultsMissingIdToTaskId() throws Exception {
+ String taskId = "get-default-config-id";
+ taskStore.save(Task.builder()
+ .id(taskId)
+ .contextId("ctx-get-default-config-id")
+ .status(new TaskStatus(TaskState.TASK_STATE_WORKING))
+ .build(), false);
+ requestHandler.onCreateTaskPushNotificationConfig(TaskPushNotificationConfig.builder()
+ .taskId(taskId)
+ .url("http://example.com/get-default-config-id")
+ .build(), NULL_CONTEXT);
+
+ TaskPushNotificationConfig result = requestHandler.onGetTaskPushNotificationConfig(
+ new GetTaskPushNotificationConfigParams(taskId), NULL_CONTEXT);
+
+ assertEquals(taskId, result.id());
+ assertEquals("http://example.com/get-default-config-id", result.url());
+ }
+
/**
* Verify that onMessageSend stores the protocol version when the request
* includes a push notification config (new task path).
diff --git a/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java b/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java
index 9a1a0f706..7adbe6e31 100644
--- a/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java
+++ b/server-common/src/test/java/org/a2aproject/sdk/server/tasks/InMemoryPushNotificationConfigStoreTest.java
@@ -151,10 +151,10 @@ public void testSetInfoAppendsToExistingConfig() {
}
@Test
- public void testSetInfoWithoutConfigId() {
+ public void testSetInfoWithEmptyConfigId() {
String taskId = "task1";
TaskPushNotificationConfig initialConfig = TaskPushNotificationConfig.builder()
- .id("") // No ID set
+ .id("")
.url("http://initial.url/callback")
.taskId(taskId)
.build();
@@ -167,7 +167,7 @@ public void testSetInfoWithoutConfigId() {
assertEquals(taskId, configResult.configs().get(0).id());
TaskPushNotificationConfig updatedConfig = TaskPushNotificationConfig.builder()
- .id("") // No ID set
+ .id("")
.url("http://initial.url/callback_new")
.taskId(taskId)
.build();
@@ -180,6 +180,19 @@ public void testSetInfoWithoutConfigId() {
assertEquals(updatedConfig.url(), configResult.configs().get(0).url());
}
+ @Test
+ public void testSetInfoWithNullConfigId() {
+ String taskId = "task_with_null_config_id";
+ TaskPushNotificationConfig config = TaskPushNotificationConfig.builder()
+ .url("http://initial.url/callback")
+ .taskId(taskId)
+ .build();
+
+ TaskPushNotificationConfig result = configStore.setInfo(config);
+
+ assertEquals(taskId, result.id(), "Config ID should default to taskId when null");
+ }
+
@Test
public void testGetInfoExistingConfig() {
String taskId = "task_get_exist";
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 eeb950cd8..584b7de23 100644
--- a/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java
+++ b/spec/src/main/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParams.java
@@ -18,7 +18,7 @@
* @see TaskPushNotificationConfig for the returned configuration structure
* @see A2A Protocol Specification
*/
-public record GetTaskPushNotificationConfigParams(String taskId, String id, @Nullable String tenant) {
+public record GetTaskPushNotificationConfigParams(String taskId, @Nullable String id, @Nullable String tenant) {
/**
* Compact constructor that validates required fields.
@@ -26,21 +26,29 @@ public record GetTaskPushNotificationConfigParams(String taskId, String id, @Nul
* @param taskId the taskId parameter (see class-level JavaDoc)
* @param id the id parameter (see class-level JavaDoc)
* @param tenant the tenant parameter (see class-level JavaDoc)
- * @throws IllegalArgumentException if taskId or tenant is null
+ * @throws IllegalArgumentException if taskId is null
*/
public GetTaskPushNotificationConfigParams {
Assert.checkNotNullParam("taskId", taskId);
- Assert.checkNotNullParam("id", id);
Utils.validateTenant(tenant);
}
+ /**
+ * Convenience constructor for retrieving the configuration that uses the task ID as its default ID.
+ *
+ * @param taskId the task identifier (required)
+ */
+ public GetTaskPushNotificationConfigParams(String taskId) {
+ this(taskId, null, null);
+ }
+
/**
* Convenience constructor for creating parameters without tenant.
*
* @param taskId the task identifier (required)
* @param id optional configuration ID to retrieve
*/
- public GetTaskPushNotificationConfigParams(String taskId, String id) {
+ public GetTaskPushNotificationConfigParams(String taskId, @Nullable String id) {
this(taskId, id, null);
}
@@ -84,7 +92,7 @@ public Builder taskId(String taskId) {
* @param id the configuration ID
* @return this builder for method chaining
*/
- public Builder id(String id) {
+ public Builder id(@Nullable String id) {
this.id = id;
return this;
}
@@ -108,7 +116,7 @@ public Builder tenant(@Nullable String tenant) {
public GetTaskPushNotificationConfigParams build() {
return new GetTaskPushNotificationConfigParams(
Assert.checkNotNullParam("taskId", taskId),
- Assert.checkNotNullParam("id", id),
+ id,
tenant);
}
}
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 dc3fa7a0f..e98924bee 100644
--- a/spec/src/main/java/org/a2aproject/sdk/spec/TaskPushNotificationConfig.java
+++ b/spec/src/main/java/org/a2aproject/sdk/spec/TaskPushNotificationConfig.java
@@ -21,7 +21,8 @@
* Used for managing task-specific push notification settings via the push notification
* management methods ({@code tasks/pushNotificationConfig/set}, {@code tasks/pushNotificationConfig/get}, etc.).
*
- * @param id unique identifier (e.g. UUID) for this push notification configuration
+ * @param id optional unique identifier (e.g. UUID) for this push notification configuration.
+ * When omitted while creating a configuration, the server assigns one.
* @param taskId the unique identifier of the task to receive push notifications for
* @param url the HTTP/HTTPS endpoint URL to receive push notifications (required)
* @param token optional bearer token for simple authentication
@@ -31,14 +32,14 @@
* @see MessageSendConfiguration for configuring push notifications on message send
* @see A2A Protocol Specification
*/
-public record TaskPushNotificationConfig(String id, @Nullable String taskId, String url, @Nullable String token,
+public record TaskPushNotificationConfig(@Nullable String id, @Nullable String taskId, String url, @Nullable String token,
@Nullable AuthenticationInfo authentication, @Nullable String tenant) {
/**
* Compact constructor for validation.
* Validates that required parameters are not null.
*
- * @param id the configuration identifier
+ * @param id the optional configuration identifier
* @param taskId the task identifier
* @param url the notification endpoint URL
* @param token optional bearer token
@@ -46,7 +47,6 @@ public record TaskPushNotificationConfig(String id, @Nullable String taskId, Str
* @param tenant the tenant identifier
*/
public TaskPushNotificationConfig {
- Assert.checkNotNullParam("id", id);
Assert.checkNotNullParam("url", url);
Utils.validateTenant(tenant);
}
@@ -105,10 +105,10 @@ private Builder(TaskPushNotificationConfig config) {
/**
* Sets the configuration identifier.
*
- * @param id the configuration ID
+ * @param id the optional configuration ID
* @return this builder
*/
- public Builder id(String id) {
+ public Builder id(@Nullable String id) {
this.id = id;
return this;
}
@@ -172,11 +172,11 @@ public Builder tenant(String tenant) {
* Builds the {@link TaskPushNotificationConfig}.
*
* @return a new push notification configuration
- * @throws IllegalArgumentException if id or url is null
+ * @throws IllegalArgumentException if url is null
*/
public TaskPushNotificationConfig build() {
return new TaskPushNotificationConfig(
- Assert.checkNotNullParam("id", id),
+ id,
taskId,
Assert.checkNotNullParam("url", url),
token,
diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java
new file mode 100644
index 000000000..030bf209e
--- /dev/null
+++ b/spec/src/test/java/org/a2aproject/sdk/spec/GetTaskPushNotificationConfigParamsTest.java
@@ -0,0 +1,26 @@
+package org.a2aproject.sdk.spec;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class GetTaskPushNotificationConfigParamsTest {
+
+ @Test
+ void testConstructionAllowsOmittedConfigurationId() {
+ GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams("task-1");
+
+ assertEquals("task-1", params.taskId());
+ assertNull(params.id());
+ }
+
+ @Test
+ void testBuilderAllowsOmittedConfigurationId() {
+ GetTaskPushNotificationConfigParams params = GetTaskPushNotificationConfigParams.builder()
+ .taskId("task-1")
+ .build();
+
+ assertNull(params.id());
+ }
+}
diff --git a/spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java b/spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java
new file mode 100644
index 000000000..bf5216182
--- /dev/null
+++ b/spec/src/test/java/org/a2aproject/sdk/spec/TaskPushNotificationConfigTest.java
@@ -0,0 +1,20 @@
+package org.a2aproject.sdk.spec;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+class TaskPushNotificationConfigTest {
+
+ @Test
+ void builderAllowsAnOmittedConfigurationId() {
+ TaskPushNotificationConfig config = TaskPushNotificationConfig.builder()
+ .taskId("task-123")
+ .url("https://example.com/callback")
+ .build();
+
+ assertNull(config.id());
+ assertEquals("task-123", config.taskId());
+ }
+}