diff --git a/.github/workflows/run-tck.yml b/.github/workflows/run-tck.yml index d2cba0890..291802da4 100644 --- a/.github/workflows/run-tck.yml +++ b/.github/workflows/run-tck.yml @@ -11,7 +11,7 @@ on: env: # Tag of the TCK - TCK_VERSION: v0.2.3 + TCK_VERSION: v0.2.5 # Tells uv to not need a venv, and instead use system UV_SYSTEM_PYTHON: 1 diff --git a/README.md b/README.md index fe7d3bbc0..2186dde3b 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ public class WeatherAgentCardProducer { .tags(Collections.singletonList("weather")) .examples(List.of("weather in LA, CA")) .build())) + .protocolVersion("0.2.5") .build(); } } @@ -255,18 +256,21 @@ Map metadata = ... CancelTaskResponse response = client.cancelTask(new TaskIdParams("task-1234", metadata)); ``` -#### Get the push notification configuration for a task +#### Get a push notification configuration for a task ```java // Get task push notification configuration GetTaskPushNotificationConfigResponse response = client.getTaskPushNotificationConfig("task-1234"); -// You can also specify additional properties using a map +// The push notification configuration ID can also be optionally specified +GetTaskPushNotificationConfigResponse response = client.getTaskPushNotificationConfig("task-1234", "config-4567"); + +// Additional properties can be specified using a map Map metadata = ... -GetTaskPushNotificationConfigResponse response = client.getTaskPushNotificationConfig(new TaskIdParams("task-1234", metadata)); +GetTaskPushNotificationConfigResponse response = client.getTaskPushNotificationConfig(new GetTaskPushNotificationConfigParams("task-1234", "config-1234", metadata)); ``` -#### Set the push notification configuration for a task +#### Set a push notification configuration for a task ```java // Set task push notification configuration @@ -277,6 +281,26 @@ PushNotificationConfig pushNotificationConfig = new PushNotificationConfig.Build SetTaskPushNotificationResponse response = client.setTaskPushNotificationConfig("task-1234", pushNotificationConfig); ``` +#### List the push notification configurations for a task + +```java +ListTaskPushNotificationConfigResponse response = client.listTaskPushNotificationConfig("task-1234"); + +// Additional properties can be specified using a map +Map metadata = ... +ListTaskPushNotificationConfigResponse response = client.listTaskPushNotificationConfig(new ListTaskPushNotificationConfigParams("task-123", metadata)); +``` + +#### Delete a push notification configuration for a task + +```java +DeleteTaskPushNotificationConfigResponse response = client.deleteTaskPushNotificationConfig("task-1234", "config-4567"); + +// Additional properties can be specified using a map +Map metadata = ... +DeleteTaskPushNotificationConfigResponse response = client.deleteTaskPushNotificationConfig(new DeleteTaskPushNotificationConfigParams("task-1234", "config-4567", metadata)); +``` + #### Send a streaming message ```java diff --git a/client/pom.xml b/client/pom.xml index 64f983e42..9ec3b9f5b 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT a2a-java-sdk-client diff --git a/client/src/main/java/io/a2a/client/A2AClient.java b/client/src/main/java/io/a2a/client/A2AClient.java index ea08baea4..80f8c401c 100644 --- a/client/src/main/java/io/a2a/client/A2AClient.java +++ b/client/src/main/java/io/a2a/client/A2AClient.java @@ -21,6 +21,10 @@ import io.a2a.spec.AgentCard; import io.a2a.spec.CancelTaskRequest; import io.a2a.spec.CancelTaskResponse; +import io.a2a.spec.DeleteTaskPushNotificationConfigParams; +import io.a2a.spec.DeleteTaskPushNotificationConfigRequest; +import io.a2a.spec.DeleteTaskPushNotificationConfigResponse; +import io.a2a.spec.GetTaskPushNotificationConfigParams; import io.a2a.spec.GetTaskPushNotificationConfigRequest; import io.a2a.spec.GetTaskPushNotificationConfigResponse; import io.a2a.spec.GetTaskRequest; @@ -28,6 +32,9 @@ import io.a2a.spec.JSONRPCError; import io.a2a.spec.JSONRPCMessage; import io.a2a.spec.JSONRPCResponse; +import io.a2a.spec.ListTaskPushNotificationConfigParams; +import io.a2a.spec.ListTaskPushNotificationConfigRequest; +import io.a2a.spec.ListTaskPushNotificationConfigResponse; import io.a2a.spec.MessageSendParams; import io.a2a.spec.PushNotificationConfig; import io.a2a.spec.SendMessageRequest; @@ -52,6 +59,8 @@ public class A2AClient { private static final TypeReference CANCEL_TASK_RESPONSE_REFERENCE = new TypeReference<>() {}; private static final TypeReference GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; private static final TypeReference SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; + private static final TypeReference LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; + private static final TypeReference DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE = new TypeReference<>() {}; private final A2AHttpClient httpClient; private final String agentUrl; private AgentCard agentCard; @@ -164,7 +173,7 @@ public SendMessageResponse sendMessage(String requestId, MessageSendParams messa String httpResponseBody = sendPostRequest(sendMessageRequest); return unmarshalResponse(httpResponseBody, SEND_MESSAGE_RESPONSE_REFERENCE); } catch (IOException | InterruptedException e) { - throw new A2AServerException("Failed to send message: " + e); + throw new A2AServerException("Failed to send message: " + e, e.getCause()); } } @@ -216,7 +225,7 @@ public GetTaskResponse getTask(String requestId, TaskQueryParams taskQueryParams String httpResponseBody = sendPostRequest(getTaskRequest); return unmarshalResponse(httpResponseBody, GET_TASK_RESPONSE_REFERENCE); } catch (IOException | InterruptedException e) { - throw new A2AServerException("Failed to get task: " + e); + throw new A2AServerException("Failed to get task: " + e, e.getCause()); } } @@ -266,45 +275,57 @@ public CancelTaskResponse cancelTask(String requestId, TaskIdParams taskIdParams String httpResponseBody = sendPostRequest(cancelTaskRequest); return unmarshalResponse(httpResponseBody, CANCEL_TASK_RESPONSE_REFERENCE); } catch (IOException | InterruptedException e) { - throw new A2AServerException("Failed to cancel task: " + e); + throw new A2AServerException("Failed to cancel task: " + e, e.getCause()); } } /** * Get the push notification configuration for a task. * - * @param id the task ID + * @param taskId the task ID + * @return the response containing the push notification configuration + * @throws A2AServerException if getting the push notification configuration fails for any reason + */ + public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(String taskId) throws A2AServerException { + return getTaskPushNotificationConfig(null, new GetTaskPushNotificationConfigParams(taskId)); + } + + /** + * Get the push notification configuration for a task. + * + * @param taskId the task ID + * @param pushNotificationConfigId the push notification configuration ID * @return the response containing the push notification configuration * @throws A2AServerException if getting the push notification configuration fails for any reason */ - public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(String id) throws A2AServerException { - return getTaskPushNotificationConfig(null, new TaskIdParams(id)); + public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(String taskId, String pushNotificationConfigId) throws A2AServerException { + return getTaskPushNotificationConfig(null, new GetTaskPushNotificationConfigParams(taskId, pushNotificationConfigId)); } /** * Get the push notification configuration for a task. * - * @param taskIdParams the params for the task + * @param getTaskPushNotificationConfigParams the params for the task * @return the response containing the push notification configuration * @throws A2AServerException if getting the push notification configuration fails for any reason */ - public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(TaskIdParams taskIdParams) throws A2AServerException { - return getTaskPushNotificationConfig(null, taskIdParams); + public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(GetTaskPushNotificationConfigParams getTaskPushNotificationConfigParams) throws A2AServerException { + return getTaskPushNotificationConfig(null, getTaskPushNotificationConfigParams); } /** * Get the push notification configuration for a task. * * @param requestId the request ID to use - * @param taskIdParams the params for the task + * @param getTaskPushNotificationConfigParams the params for the task * @return the response containing the push notification configuration * @throws A2AServerException if getting the push notification configuration fails for any reason */ - public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(String requestId, TaskIdParams taskIdParams) throws A2AServerException { + public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(String requestId, GetTaskPushNotificationConfigParams getTaskPushNotificationConfigParams) throws A2AServerException { GetTaskPushNotificationConfigRequest.Builder getTaskPushNotificationRequestBuilder = new GetTaskPushNotificationConfigRequest.Builder() .jsonrpc(JSONRPCMessage.JSONRPC_VERSION) .method(GetTaskPushNotificationConfigRequest.METHOD) - .params(taskIdParams); + .params(getTaskPushNotificationConfigParams); if (requestId != null) { getTaskPushNotificationRequestBuilder.id(requestId); @@ -316,7 +337,7 @@ public GetTaskPushNotificationConfigResponse getTaskPushNotificationConfig(Strin String httpResponseBody = sendPostRequest(getTaskPushNotificationRequest); return unmarshalResponse(httpResponseBody, GET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); } catch (IOException | InterruptedException e) { - throw new A2AServerException("Failed to get task push notification config: " + e); + throw new A2AServerException("Failed to get task push notification config: " + e, e.getCause()); } } @@ -359,7 +380,137 @@ public SetTaskPushNotificationConfigResponse setTaskPushNotificationConfig(Strin String httpResponseBody = sendPostRequest(setTaskPushNotificationRequest); return unmarshalResponse(httpResponseBody, SET_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); } catch (IOException | InterruptedException e) { - throw new A2AServerException("Failed to set task push notification config: " + e); + throw new A2AServerException("Failed to set task push notification config: " + e, e.getCause()); + } + } + + /** + * Retrieves the push notification configurations for a specified task. + * + * @param requestId the request ID to use + * @param taskId the task ID to use + * @return the response containing the push notification configuration + * @throws A2AServerException if getting the push notification configuration fails for any reason + */ + public ListTaskPushNotificationConfigResponse listTaskPushNotificationConfig(String requestId, String taskId) throws A2AServerException { + return listTaskPushNotificationConfig(requestId, new ListTaskPushNotificationConfigParams(taskId)); + } + + /** + * Retrieves the push notification configurations for a specified task. + * + * @param taskId the task ID to use + * @return the response containing the push notification configuration + * @throws A2AServerException if getting the push notification configuration fails for any reason + */ + public ListTaskPushNotificationConfigResponse listTaskPushNotificationConfig(String taskId) throws A2AServerException { + return listTaskPushNotificationConfig(null, new ListTaskPushNotificationConfigParams(taskId)); + } + + /** + * Retrieves the push notification configurations for a specified task. + * + * @param listTaskPushNotificationConfigParams the params for retrieving the push notification configuration + * @return the response containing the push notification configuration + * @throws A2AServerException if getting the push notification configuration fails for any reason + */ + public ListTaskPushNotificationConfigResponse listTaskPushNotificationConfig(ListTaskPushNotificationConfigParams listTaskPushNotificationConfigParams) throws A2AServerException { + return listTaskPushNotificationConfig(null, listTaskPushNotificationConfigParams); + } + + /** + * Retrieves the push notification configurations for a specified task. + * + * @param requestId the request ID to use + * @param listTaskPushNotificationConfigParams the params for retrieving the push notification configuration + * @return the response containing the push notification configuration + * @throws A2AServerException if getting the push notification configuration fails for any reason + */ + public ListTaskPushNotificationConfigResponse listTaskPushNotificationConfig(String requestId, + ListTaskPushNotificationConfigParams listTaskPushNotificationConfigParams) throws A2AServerException { + ListTaskPushNotificationConfigRequest.Builder listTaskPushNotificationRequestBuilder = new ListTaskPushNotificationConfigRequest.Builder() + .jsonrpc(JSONRPCMessage.JSONRPC_VERSION) + .method(ListTaskPushNotificationConfigRequest.METHOD) + .params(listTaskPushNotificationConfigParams); + + if (requestId != null) { + listTaskPushNotificationRequestBuilder.id(requestId); + } + + ListTaskPushNotificationConfigRequest listTaskPushNotificationRequest = listTaskPushNotificationRequestBuilder.build(); + + try { + String httpResponseBody = sendPostRequest(listTaskPushNotificationRequest); + return unmarshalResponse(httpResponseBody, LIST_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); + } catch (IOException | InterruptedException e) { + throw new A2AServerException("Failed to list task push notification config: " + e, e.getCause()); + } + } + + /** + * Delete the push notification configuration for a specified task. + * + * @param requestId the request ID to use + * @param taskId the task ID + * @param pushNotificationConfigId the push notification config ID + * @return the response + * @throws A2AServerException if deleting the push notification configuration fails for any reason + */ + public DeleteTaskPushNotificationConfigResponse deleteTaskPushNotificationConfig(String requestId, String taskId, + String pushNotificationConfigId) throws A2AServerException { + return deleteTaskPushNotificationConfig(requestId, new DeleteTaskPushNotificationConfigParams(taskId, pushNotificationConfigId)); + } + + /** + * Delete the push notification configuration for a specified task. + * + * @param taskId the task ID + * @param pushNotificationConfigId the push notification config ID + * @return the response + * @throws A2AServerException if deleting the push notification configuration fails for any reason + */ + public DeleteTaskPushNotificationConfigResponse deleteTaskPushNotificationConfig(String taskId, + String pushNotificationConfigId) throws A2AServerException { + return deleteTaskPushNotificationConfig(null, new DeleteTaskPushNotificationConfigParams(taskId, pushNotificationConfigId)); + } + + /** + * Delete the push notification configuration for a specified task. + * + * @param deleteTaskPushNotificationConfigParams the params for deleting the push notification configuration + * @return the response + * @throws A2AServerException if deleting the push notification configuration fails for any reason + */ + public DeleteTaskPushNotificationConfigResponse deleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigParams deleteTaskPushNotificationConfigParams) throws A2AServerException { + return deleteTaskPushNotificationConfig(null, deleteTaskPushNotificationConfigParams); + } + + /** + * Delete the push notification configuration for a specified task. + * + * @param requestId the request ID to use + * @param deleteTaskPushNotificationConfigParams the params for deleting the push notification configuration + * @return the response + * @throws A2AServerException if deleting the push notification configuration fails for any reason + */ + public DeleteTaskPushNotificationConfigResponse deleteTaskPushNotificationConfig(String requestId, + DeleteTaskPushNotificationConfigParams deleteTaskPushNotificationConfigParams) throws A2AServerException { + DeleteTaskPushNotificationConfigRequest.Builder deleteTaskPushNotificationRequestBuilder = new DeleteTaskPushNotificationConfigRequest.Builder() + .jsonrpc(JSONRPCMessage.JSONRPC_VERSION) + .method(DeleteTaskPushNotificationConfigRequest.METHOD) + .params(deleteTaskPushNotificationConfigParams); + + if (requestId != null) { + deleteTaskPushNotificationRequestBuilder.id(requestId); + } + + DeleteTaskPushNotificationConfigRequest deleteTaskPushNotificationRequest = deleteTaskPushNotificationRequestBuilder.build(); + + try { + String httpResponseBody = sendPostRequest(deleteTaskPushNotificationRequest); + return unmarshalResponse(httpResponseBody, DELETE_TASK_PUSH_NOTIFICATION_CONFIG_RESPONSE_REFERENCE); + } catch (IOException | InterruptedException e) { + throw new A2AServerException("Failed to delete task push notification config: " + e, e.getCause()); } } @@ -416,9 +567,9 @@ public void sendStreamingMessage(String requestId, MessageSendParams messageSend })); } catch (IOException e) { - throw new A2AServerException("Failed to send streaming message request: " + e); + throw new A2AServerException("Failed to send streaming message request: " + e, e.getCause()); } catch (InterruptedException e) { - throw new A2AServerException("Send streaming message request timed out: " + e); + throw new A2AServerException("Send streaming message request timed out: " + e, e.getCause()); } } @@ -475,9 +626,9 @@ public void resubscribeToTask(String requestId, TaskIdParams taskIdParams, Consu })); } catch (IOException e) { - throw new A2AServerException("Failed to send task resubscription request: " + e); + throw new A2AServerException("Failed to send task resubscription request: " + e, e.getCause()); } catch (InterruptedException e) { - throw new A2AServerException("Task resubscription request timed out: " + e); + throw new A2AServerException("Task resubscription request timed out: " + e, e.getCause()); } } @@ -503,7 +654,7 @@ private T unmarshalResponse(String response, TypeRef T value = Utils.unmarshalFrom(response, typeReference); JSONRPCError error = value.getError(); if (error != null) { - throw new A2AServerException(error.getMessage() + (error.getData() != null ? ": " + error.getData() : "")); + throw new A2AServerException(error.getMessage() + (error.getData() != null ? ": " + error.getData() : ""), error); } return value; } diff --git a/client/src/test/java/io/a2a/client/A2AClientTest.java b/client/src/test/java/io/a2a/client/A2AClientTest.java index e99734ff1..eda03e02a 100644 --- a/client/src/test/java/io/a2a/client/A2AClientTest.java +++ b/client/src/test/java/io/a2a/client/A2AClientTest.java @@ -46,6 +46,7 @@ import io.a2a.spec.FilePart; import io.a2a.spec.FileWithBytes; import io.a2a.spec.FileWithUri; +import io.a2a.spec.GetTaskPushNotificationConfigParams; import io.a2a.spec.GetTaskPushNotificationConfigResponse; import io.a2a.spec.GetTaskResponse; import io.a2a.spec.Message; @@ -328,7 +329,7 @@ public void testA2AClientGetTaskPushNotificationConfig() throws Exception { A2AClient client = new A2AClient("http://localhost:4001"); GetTaskPushNotificationConfigResponse response = client.getTaskPushNotificationConfig("1", - new TaskIdParams("de38c76d-d54c-436c-8b9f-4c2703648d64", new HashMap<>())); + new GetTaskPushNotificationConfigParams("de38c76d-d54c-436c-8b9f-4c2703648d64", null, new HashMap<>())); assertEquals("2.0", response.getJsonrpc()); assertEquals(1, response.getId()); assertInstanceOf(TaskPushNotificationConfig.class, response.getResult()); @@ -442,6 +443,7 @@ public void testA2AClientGetAgentCard() throws Exception { assertEquals(outputModes, skills.get(1).outputModes()); assertTrue(agentCard.supportsAuthenticatedExtendedCard()); assertEquals("https://georoute-agent.example.com/icon.png", agentCard.iconUrl()); + assertEquals("0.2.5", agentCard.protocolVersion()); } @Test @@ -514,6 +516,7 @@ public void testA2AClientGetAuthenticatedExtendedAgentCard() throws Exception { assertEquals(List.of("extended"), skills.get(2).tags()); assertTrue(agentCard.supportsAuthenticatedExtendedCard()); assertEquals("https://georoute-agent.example.com/icon.png", agentCard.iconUrl()); + assertEquals("0.2.5", agentCard.protocolVersion()); } @Test diff --git a/client/src/test/java/io/a2a/client/JsonMessages.java b/client/src/test/java/io/a2a/client/JsonMessages.java index c7ebd7780..fecf216d0 100644 --- a/client/src/test/java/io/a2a/client/JsonMessages.java +++ b/client/src/test/java/io/a2a/client/JsonMessages.java @@ -67,7 +67,8 @@ public class JsonMessages { ] } ], - "supportsAuthenticatedExtendedCard": true + "supportsAuthenticatedExtendedCard": true, + "protocolVersion": "0.2.5" }"""; static final String AUTHENTICATION_EXTENDED_AGENT_CARD = """ @@ -137,7 +138,8 @@ public class JsonMessages { "tags": ["extended"] } ], - "supportsAuthenticatedExtendedCard": true + "supportsAuthenticatedExtendedCard": true, + "protocolVersion": "0.2.5" }"""; diff --git a/common/pom.xml b/common/pom.xml index 9b3d81137..254a6da14 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT a2a-java-sdk-common diff --git a/examples/helloworld/client/pom.xml b/examples/helloworld/client/pom.xml index 26f701934..3584a4202 100644 --- a/examples/helloworld/client/pom.xml +++ b/examples/helloworld/client/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT a2a-java-sdk-examples-client diff --git a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java index 8583ef27a..597113380 100644 --- a/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java +++ b/examples/helloworld/client/src/main/java/io/a2a/examples/helloworld/HelloWorldRunner.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.github.a2asdk:a2a-java-sdk-client:0.2.3.Beta2-SNAPSHOT +//DEPS io.github.a2asdk:a2a-java-sdk-client:0.2.5.Beta2-SNAPSHOT //SOURCES HelloWorldClient.java /** diff --git a/examples/helloworld/pom.xml b/examples/helloworld/pom.xml index 3b7bd849d..8253fcc83 100644 --- a/examples/helloworld/pom.xml +++ b/examples/helloworld/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT ../../pom.xml diff --git a/examples/helloworld/server/pom.xml b/examples/helloworld/server/pom.xml index 3f5f74a1e..979b4bf99 100644 --- a/examples/helloworld/server/pom.xml +++ b/examples/helloworld/server/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-examples-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT a2a-java-sdk-examples-server diff --git a/examples/helloworld/server/src/main/java/io/a2a/examples/helloworld/AgentCardProducer.java b/examples/helloworld/server/src/main/java/io/a2a/examples/helloworld/AgentCardProducer.java index f07a6e527..06450c935 100644 --- a/examples/helloworld/server/src/main/java/io/a2a/examples/helloworld/AgentCardProducer.java +++ b/examples/helloworld/server/src/main/java/io/a2a/examples/helloworld/AgentCardProducer.java @@ -37,6 +37,7 @@ public AgentCard agentCard() { .tags(Collections.singletonList("hello world")) .examples(List.of("hi", "hello world")) .build())) + .protocolVersion("0.2.5") .build(); } } diff --git a/pom.xml b/pom.xml index 9fd7cfeaf..c7e409f2a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT pom diff --git a/reference-impl/pom.xml b/reference-impl/pom.xml index 15a62e8b0..c2397b698 100644 --- a/reference-impl/pom.xml +++ b/reference-impl/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT a2a-java-reference-server 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 1500dfe4c..81a0df3b3 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 @@ -22,6 +22,7 @@ import io.a2a.server.util.async.Internal; import io.a2a.spec.AgentCard; import io.a2a.spec.CancelTaskRequest; +import io.a2a.spec.DeleteTaskPushNotificationConfigRequest; import io.a2a.spec.GetTaskPushNotificationConfigRequest; import io.a2a.spec.GetTaskRequest; import io.a2a.spec.IdJsonMappingException; @@ -35,6 +36,7 @@ import io.a2a.spec.JSONRPCErrorResponse; import io.a2a.spec.JSONRPCRequest; import io.a2a.spec.JSONRPCResponse; +import io.a2a.spec.ListTaskPushNotificationConfigRequest; import io.a2a.spec.MethodNotFoundError; import io.a2a.spec.MethodNotFoundJsonMappingException; import io.a2a.spec.NonStreamingJSONRPCRequest; @@ -187,11 +189,15 @@ private JSONRPCResponse processNonStreamingRequest(NonStreamingJSONRPCRequest } else if (request instanceof CancelTaskRequest) { return jsonRpcHandler.onCancelTask((CancelTaskRequest) request); } else if (request instanceof SetTaskPushNotificationConfigRequest) { - return jsonRpcHandler.setPushNotification((SetTaskPushNotificationConfigRequest) request); + return jsonRpcHandler.setPushNotificationConfig((SetTaskPushNotificationConfigRequest) request); } else if (request instanceof GetTaskPushNotificationConfigRequest) { - return jsonRpcHandler.getPushNotification((GetTaskPushNotificationConfigRequest) request); + return jsonRpcHandler.getPushNotificationConfig((GetTaskPushNotificationConfigRequest) request); } else if (request instanceof SendMessageRequest) { return jsonRpcHandler.onMessageSend((SendMessageRequest) request); + } else if (request instanceof ListTaskPushNotificationConfigRequest) { + return jsonRpcHandler.listPushNotificationConfig((ListTaskPushNotificationConfigRequest) request); + } else if (request instanceof DeleteTaskPushNotificationConfigRequest) { + return jsonRpcHandler.deletePushNotificationConfig((DeleteTaskPushNotificationConfigRequest) request); } else { return generateErrorResponse(request, new UnsupportedOperationError()); } diff --git a/reference-impl/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java b/reference-impl/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java index bc6a342a6..5af126bf5 100644 --- a/reference-impl/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java +++ b/reference-impl/src/test/java/io/a2a/server/apps/quarkus/A2ATestRoutes.java @@ -11,6 +11,7 @@ import jakarta.inject.Singleton; import io.a2a.server.apps.common.TestUtilsBean; +import io.a2a.spec.PushNotificationConfig; import io.a2a.spec.Task; import io.a2a.spec.TaskArtifactUpdateEvent; import io.a2a.spec.TaskStatusUpdateEvent; @@ -138,6 +139,44 @@ public void getStreamingSubscribedCount(RoutingContext rc) { .end(String.valueOf(streamingSubscribedCount.get())); } + @Route(path = "/test/task/:taskId/config/:configId", methods = {Route.HttpMethod.DELETE}, type = Route.HandlerType.BLOCKING) + public void deleteTaskPushNotificationConfig(@Param String taskId, @Param String configId, RoutingContext rc) { + try { + Task task = testUtilsBean.getTask(taskId); + if (task == null) { + rc.response() + .setStatusCode(404) + .end(); + return; + } + testUtilsBean.deleteTaskPushNotificationConfig(taskId, configId); + rc.response() + .setStatusCode(200) + .end(); + } catch (Throwable t) { + errorResponse(t, rc); + } + } + + @Route(path = "/test/task/:taskId", methods = {Route.HttpMethod.POST}, type = Route.HandlerType.BLOCKING) + public void saveTaskPushNotificationConfig(@Param String taskId, @Body String body, RoutingContext rc) { + try { + PushNotificationConfig notificationConfig = Utils.OBJECT_MAPPER.readValue(body, PushNotificationConfig.class); + if (notificationConfig == null) { + rc.response() + .setStatusCode(404) + .end(); + return; + } + testUtilsBean.saveTaskPushNotificationConfig(taskId, notificationConfig); + rc.response() + .setStatusCode(200) + .end(); + } catch (Throwable t) { + errorResponse(t, rc); + } + } + private void errorResponse(Throwable t, RoutingContext rc) { t.printStackTrace(); rc.response() diff --git a/sdk-server-common/pom.xml b/sdk-server-common/pom.xml index f35b91280..27b38fe99 100644 --- a/sdk-server-common/pom.xml +++ b/sdk-server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT a2a-java-sdk-server-common diff --git a/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java b/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java index 5d6cbec84..b4dbb2feb 100644 --- a/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java +++ b/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/DefaultRequestHandler.java @@ -16,20 +16,27 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; +import io.a2a.server.agentexecution.AgentExecutor; +import io.a2a.server.agentexecution.RequestContext; +import io.a2a.server.agentexecution.SimpleRequestContextBuilder; import io.a2a.server.events.EnhancedRunnable; import io.a2a.server.events.EventConsumer; import io.a2a.server.events.EventQueue; import io.a2a.server.events.QueueManager; import io.a2a.server.events.TaskQueueExistsException; -import io.a2a.server.tasks.PushNotifier; +import io.a2a.server.tasks.PushNotificationConfigStore; +import io.a2a.server.tasks.PushNotificationSender; import io.a2a.server.tasks.ResultAggregator; import io.a2a.server.tasks.TaskManager; import io.a2a.server.tasks.TaskStore; import io.a2a.server.util.async.Internal; +import io.a2a.spec.DeleteTaskPushNotificationConfigParams; import io.a2a.spec.Event; import io.a2a.spec.EventKind; +import io.a2a.spec.GetTaskPushNotificationConfigParams; import io.a2a.spec.InternalError; import io.a2a.spec.JSONRPCError; +import io.a2a.spec.ListTaskPushNotificationConfigParams; import io.a2a.spec.Message; import io.a2a.spec.MessageSendParams; import io.a2a.spec.PushNotificationConfig; @@ -43,9 +50,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import io.a2a.server.agentexecution.AgentExecutor; -import io.a2a.server.agentexecution.RequestContext; -import io.a2a.server.agentexecution.SimpleRequestContextBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,7 +61,8 @@ public class DefaultRequestHandler implements RequestHandler { private final AgentExecutor agentExecutor; private final TaskStore taskStore; private final QueueManager queueManager; - private final PushNotifier pushNotifier; + private final PushNotificationConfigStore pushConfigStore; + private final PushNotificationSender pushSender; private final Supplier requestContextBuilder; private final ConcurrentMap> runningAgents = new ConcurrentHashMap<>(); @@ -66,11 +71,13 @@ public class DefaultRequestHandler implements RequestHandler { @Inject public DefaultRequestHandler(AgentExecutor agentExecutor, TaskStore taskStore, - QueueManager queueManager, PushNotifier pushNotifier, @Internal Executor executor) { + QueueManager queueManager, PushNotificationConfigStore pushConfigStore, + PushNotificationSender pushSender, @Internal Executor executor) { this.agentExecutor = agentExecutor; this.taskStore = taskStore; this.queueManager = queueManager; - this.pushNotifier = pushNotifier; + this.pushConfigStore = pushConfigStore; + this.pushSender = pushSender; this.executor = executor; // TODO In Python this is also a constructor parameter defaulting to this SimpleRequestContextBuilder // implementation if the parameter is null. Skip that for now, since otherwise I get CDI errors, and @@ -226,20 +233,20 @@ public Flow.Publisher onMessageSendStream(MessageSendParams } catch (TaskQueueExistsException e) { // TODO Log } - if (pushNotifier != null && + if (pushConfigStore != null && params.configuration() != null && params.configuration().pushNotification() != null) { - pushNotifier.setInfo( + pushConfigStore.setInfo( createdTask.getId(), params.configuration().pushNotification()); } } - if (pushNotifier != null && taskId.get() != null) { + if (pushSender != null && taskId.get() != null) { EventKind latest = resultAggregator.getCurrentResult(); if (latest instanceof Task latestTask) { - pushNotifier.sendNotification(latestTask); + pushSender.sendNotification(latestTask); } } @@ -254,7 +261,7 @@ public Flow.Publisher onMessageSendStream(MessageSendParams @Override public TaskPushNotificationConfig onSetTaskPushNotificationConfig(TaskPushNotificationConfig params) throws JSONRPCError { - if (pushNotifier == null) { + if (pushConfigStore == null) { throw new UnsupportedOperationError(); } Task task = taskStore.get(params.taskId()); @@ -262,14 +269,14 @@ public TaskPushNotificationConfig onSetTaskPushNotificationConfig(TaskPushNotifi throw new TaskNotFoundError(); } - pushNotifier.setInfo(params.taskId(), params.pushNotificationConfig()); + pushConfigStore.setInfo(params.taskId(), params.pushNotificationConfig()); return params; } @Override - public TaskPushNotificationConfig onGetTaskPushNotificationConfig(TaskIdParams params) throws JSONRPCError { - if (pushNotifier == null) { + public TaskPushNotificationConfig onGetTaskPushNotificationConfig(GetTaskPushNotificationConfigParams params) throws JSONRPCError { + if (pushConfigStore == null) { throw new UnsupportedOperationError(); } Task task = taskStore.get(params.id()); @@ -277,12 +284,24 @@ public TaskPushNotificationConfig onGetTaskPushNotificationConfig(TaskIdParams p throw new TaskNotFoundError(); } - PushNotificationConfig pushNotificationConfig = pushNotifier.getInfo(params.id()); - if (pushNotificationConfig == null) { + List pushNotificationConfigList = pushConfigStore.getInfo(params.id()); + if (pushNotificationConfigList == null || pushNotificationConfigList.isEmpty()) { throw new InternalError("No push notification config found"); } - return new TaskPushNotificationConfig(params.id(), pushNotificationConfig); + return new TaskPushNotificationConfig(params.id(), getPushNotificationConfig(pushNotificationConfigList, params.pushNotificationConfigId())); + } + + private PushNotificationConfig getPushNotificationConfig(List notificationConfigList, + String configId) { + if (configId != null) { + for (PushNotificationConfig notificationConfig : notificationConfigList) { + if (configId.equals(notificationConfig.id())) { + return notificationConfig; + } + } + } + return notificationConfigList.get(0); } @Override @@ -305,8 +324,44 @@ public Flow.Publisher onResubscribeToTask(TaskIdParams param return convertingProcessor(results, e -> (StreamingEventKind) e); } + @Override + public List onListTaskPushNotificationConfig(ListTaskPushNotificationConfigParams params) throws JSONRPCError { + if (pushConfigStore == null) { + throw new UnsupportedOperationError(); + } + + Task task = taskStore.get(params.id()); + if (task == null) { + throw new TaskNotFoundError(); + } + + List pushNotificationConfigList = pushConfigStore.getInfo(params.id()); + List taskPushNotificationConfigList = new ArrayList<>(); + if (pushNotificationConfigList != null) { + for (PushNotificationConfig pushNotificationConfig : pushNotificationConfigList) { + TaskPushNotificationConfig taskPushNotificationConfig = new TaskPushNotificationConfig(params.id(), pushNotificationConfig); + taskPushNotificationConfigList.add(taskPushNotificationConfig); + } + } + return taskPushNotificationConfigList; + } + + @Override + public void onDeleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigParams params) { + if (pushConfigStore == null) { + throw new UnsupportedOperationError(); + } + + Task task = taskStore.get(params.id()); + if (task == null) { + throw new TaskNotFoundError(); + } + + pushConfigStore.deleteInfo(params.id(), params.pushNotificationConfigId()); + } + private boolean shouldAddPushInfo(MessageSendParams params) { - return pushNotifier != null && params.configuration() != null && params.configuration().pushNotification() != null; + return pushConfigStore != null && params.configuration() != null && params.configuration().pushNotification() != null; } private EnhancedRunnable registerAndExecuteAgentAsync(String taskId, RequestContext requestContext, EventQueue queue) { @@ -357,7 +412,7 @@ private MessageSendSetup initMessageSend(MessageSendParams params) { if (shouldAddPushInfo(params)) { LOGGER.debug("Adding push info"); - pushNotifier.setInfo(task.getId(), params.configuration().pushNotification()); + pushConfigStore.setInfo(task.getId(), params.configuration().pushNotification()); } } diff --git a/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/JSONRPCHandler.java b/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/JSONRPCHandler.java index fac014b0e..6a394e023 100644 --- a/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/JSONRPCHandler.java +++ b/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/JSONRPCHandler.java @@ -1,16 +1,18 @@ package io.a2a.server.requesthandlers; import static io.a2a.server.util.async.AsyncUtils.createTubeConfig; - -import java.util.concurrent.Flow; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import java.util.List; +import java.util.concurrent.Flow; + import io.a2a.server.PublicAgentCard; import io.a2a.spec.AgentCard; import io.a2a.spec.CancelTaskRequest; import io.a2a.spec.CancelTaskResponse; +import io.a2a.spec.DeleteTaskPushNotificationConfigRequest; +import io.a2a.spec.DeleteTaskPushNotificationConfigResponse; import io.a2a.spec.EventKind; import io.a2a.spec.GetTaskPushNotificationConfigRequest; import io.a2a.spec.GetTaskPushNotificationConfigResponse; @@ -19,6 +21,9 @@ import io.a2a.spec.InternalError; import io.a2a.spec.InvalidRequestError; import io.a2a.spec.JSONRPCError; +import io.a2a.spec.ListTaskPushNotificationConfigRequest; +import io.a2a.spec.ListTaskPushNotificationConfigResponse; +import io.a2a.spec.PushNotificationNotSupportedError; import io.a2a.spec.SendMessageRequest; import io.a2a.spec.SendMessageResponse; import io.a2a.spec.SendStreamingMessageRequest; @@ -113,7 +118,11 @@ public Flow.Publisher onResubscribeToTask(TaskResu } } - public GetTaskPushNotificationConfigResponse getPushNotification(GetTaskPushNotificationConfigRequest request) { + public GetTaskPushNotificationConfigResponse getPushNotificationConfig(GetTaskPushNotificationConfigRequest request) { + if (!agentCard.capabilities().pushNotifications()) { + return new GetTaskPushNotificationConfigResponse(request.getId(), + new PushNotificationNotSupportedError()); + } try { TaskPushNotificationConfig config = requestHandler.onGetTaskPushNotificationConfig(request.getParams()); return new GetTaskPushNotificationConfigResponse(request.getId(), config); @@ -124,10 +133,10 @@ public GetTaskPushNotificationConfigResponse getPushNotification(GetTaskPushNoti } } - public SetTaskPushNotificationConfigResponse setPushNotification(SetTaskPushNotificationConfigRequest request) { + public SetTaskPushNotificationConfigResponse setPushNotificationConfig(SetTaskPushNotificationConfigRequest request) { if (!agentCard.capabilities().pushNotifications()) { return new SetTaskPushNotificationConfigResponse(request.getId(), - new InvalidRequestError("Push notifications are not supported by the agent")); + new PushNotificationNotSupportedError()); } try { TaskPushNotificationConfig config = requestHandler.onSetTaskPushNotificationConfig(request.getParams()); @@ -150,6 +159,36 @@ public GetTaskResponse onGetTask(GetTaskRequest request) { } } + public ListTaskPushNotificationConfigResponse listPushNotificationConfig(ListTaskPushNotificationConfigRequest request) { + if ( !agentCard.capabilities().pushNotifications()) { + return new ListTaskPushNotificationConfigResponse(request.getId(), + new PushNotificationNotSupportedError()); + } + try { + List pushNotificationConfigList = requestHandler.onListTaskPushNotificationConfig(request.getParams()); + return new ListTaskPushNotificationConfigResponse(request.getId(), pushNotificationConfigList); + } catch (JSONRPCError e) { + return new ListTaskPushNotificationConfigResponse(request.getId(), e); + } catch (Throwable t) { + return new ListTaskPushNotificationConfigResponse(request.getId(), new InternalError(t.getMessage())); + } + } + + public DeleteTaskPushNotificationConfigResponse deletePushNotificationConfig(DeleteTaskPushNotificationConfigRequest request) { + if ( !agentCard.capabilities().pushNotifications()) { + return new DeleteTaskPushNotificationConfigResponse(request.getId(), + new PushNotificationNotSupportedError()); + } + try { + requestHandler.onDeleteTaskPushNotificationConfig(request.getParams()); + return new DeleteTaskPushNotificationConfigResponse(request.getId()); + } catch (JSONRPCError e) { + return new DeleteTaskPushNotificationConfigResponse(request.getId(), e); + } catch (Throwable t) { + return new DeleteTaskPushNotificationConfigResponse(request.getId(), new InternalError(t.getMessage())); + } + } + public AgentCard getAgentCard() { return agentCard; } diff --git a/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/RequestHandler.java b/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/RequestHandler.java index e2902bca0..66d07b55b 100644 --- a/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/RequestHandler.java +++ b/sdk-server-common/src/main/java/io/a2a/server/requesthandlers/RequestHandler.java @@ -1,9 +1,13 @@ package io.a2a.server.requesthandlers; +import java.util.List; import java.util.concurrent.Flow; +import io.a2a.spec.DeleteTaskPushNotificationConfigParams; import io.a2a.spec.EventKind; +import io.a2a.spec.GetTaskPushNotificationConfigParams; import io.a2a.spec.JSONRPCError; +import io.a2a.spec.ListTaskPushNotificationConfigParams; import io.a2a.spec.MessageSendParams; import io.a2a.spec.StreamingEventKind; import io.a2a.spec.Task; @@ -22,7 +26,11 @@ public interface RequestHandler { TaskPushNotificationConfig onSetTaskPushNotificationConfig(TaskPushNotificationConfig params) throws JSONRPCError; - TaskPushNotificationConfig onGetTaskPushNotificationConfig(TaskIdParams params) throws JSONRPCError; + TaskPushNotificationConfig onGetTaskPushNotificationConfig(GetTaskPushNotificationConfigParams params) throws JSONRPCError; Flow.Publisher onResubscribeToTask(TaskIdParams params) throws JSONRPCError; + + List onListTaskPushNotificationConfig(ListTaskPushNotificationConfigParams params) throws JSONRPCError; + + void onDeleteTaskPushNotificationConfig(DeleteTaskPushNotificationConfigParams params) throws JSONRPCError; } diff --git a/sdk-server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java b/sdk-server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java new file mode 100644 index 000000000..33ac4445c --- /dev/null +++ b/sdk-server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java @@ -0,0 +1,96 @@ +package io.a2a.server.tasks; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import io.a2a.http.A2AHttpClient; +import io.a2a.http.JdkA2AHttpClient; +import io.a2a.spec.PushNotificationConfig; +import io.a2a.spec.Task; +import io.a2a.util.Utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApplicationScoped +public class BasePushNotificationSender implements PushNotificationSender { + + private static final Logger LOGGER = LoggerFactory.getLogger(BasePushNotificationSender.class); + + private final A2AHttpClient httpClient; + private final PushNotificationConfigStore configStore; + + @Inject + public BasePushNotificationSender(PushNotificationConfigStore configStore) { + this.httpClient = new JdkA2AHttpClient(); + this.configStore = configStore; + } + + public BasePushNotificationSender(PushNotificationConfigStore configStore, A2AHttpClient httpClient) { + this.configStore = configStore; + this.httpClient = httpClient; + } + + @Override + public void sendNotification(Task task) { + List pushConfigs = configStore.getInfo(task.getId()); + if (pushConfigs == null || pushConfigs.isEmpty()) { + return; + } + + List> dispatchResults = pushConfigs + .stream() + .map(pushConfig -> dispatch(task, pushConfig)) + .toList(); + CompletableFuture allFutures = CompletableFuture.allOf(dispatchResults.toArray(new CompletableFuture[0])); + CompletableFuture dispatchResult = allFutures.thenApply(v -> dispatchResults.stream() + .allMatch(CompletableFuture::join)); + try { + boolean allSent = dispatchResult.get(); + if (! allSent) { + LOGGER.warn("Some push notifications failed to send for taskId: " + task.getId()); + } + } catch (InterruptedException | ExecutionException e) { + LOGGER.warn("Some push notifications failed to send for taskId " + task.getId() + ": {}", e.getMessage(), e); + } + } + + private CompletableFuture dispatch(Task task, PushNotificationConfig pushInfo) { + return CompletableFuture.supplyAsync(() -> dispatchNotification(task, pushInfo)); + } + + private boolean dispatchNotification(Task task, PushNotificationConfig pushInfo) { + String url = pushInfo.url(); + + // TODO auth + + String body; + try { + body = Utils.OBJECT_MAPPER.writeValueAsString(task); + } catch (JsonProcessingException e) { + LOGGER.debug("Error writing value as string: {}", e.getMessage(), e); + return false; + } catch (Throwable throwable) { + LOGGER.debug("Error writing value as string: {}", throwable.getMessage(), throwable); + return false; + } + + try { + httpClient.createPost() + .url(url) + .body(body) + .post(); + } catch (IOException | InterruptedException e) { + LOGGER.debug("Error pushing data to " + url + ": {}", e.getMessage(), e); + return false; + } + return true; + } +} diff --git a/sdk-server-common/src/main/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStore.java b/sdk-server-common/src/main/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStore.java new file mode 100644 index 000000000..e66fc1669 --- /dev/null +++ b/sdk-server-common/src/main/java/io/a2a/server/tasks/InMemoryPushNotificationConfigStore.java @@ -0,0 +1,77 @@ +package io.a2a.server.tasks; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import io.a2a.spec.PushNotificationConfig; + +/** + * In-memory implementation of the PushNotificationConfigStore interface. + * + * Stores push notification configurations in memory + */ +@ApplicationScoped +public class InMemoryPushNotificationConfigStore implements PushNotificationConfigStore { + + private final Map> pushNotificationInfos = Collections.synchronizedMap(new HashMap<>()); + + @Inject + public InMemoryPushNotificationConfigStore() { + } + + @Override + public void setInfo(String taskId, PushNotificationConfig notificationConfig) { + List notificationConfigList = pushNotificationInfos.getOrDefault(taskId, new ArrayList<>()); + PushNotificationConfig.Builder builder = new PushNotificationConfig.Builder(notificationConfig); + if (notificationConfig.id() == null) { + builder.id(taskId); + } + notificationConfig = builder.build(); + + Iterator notificationConfigIterator = notificationConfigList.iterator(); + while (notificationConfigIterator.hasNext()) { + PushNotificationConfig config = notificationConfigIterator.next(); + if (config.id().equals(notificationConfig.id())) { + notificationConfigIterator.remove(); + break; + } + } + notificationConfigList.add(notificationConfig); + pushNotificationInfos.put(taskId, notificationConfigList); + } + + @Override + public List getInfo(String taskId) { + return pushNotificationInfos.get(taskId); + } + + @Override + public void deleteInfo(String taskId, String configId) { + if (configId == null) { + configId = taskId; + } + List notificationConfigList = pushNotificationInfos.get(taskId); + if (notificationConfigList == null || notificationConfigList.isEmpty()) { + return; + } + + Iterator notificationConfigIterator = notificationConfigList.iterator(); + while (notificationConfigIterator.hasNext()) { + PushNotificationConfig config = notificationConfigIterator.next(); + if (configId.equals(config.id())) { + notificationConfigIterator.remove(); + break; + } + } + if (notificationConfigList.isEmpty()) { + pushNotificationInfos.remove(taskId); + } + } +} diff --git a/sdk-server-common/src/main/java/io/a2a/server/tasks/InMemoryPushNotifier.java b/sdk-server-common/src/main/java/io/a2a/server/tasks/InMemoryPushNotifier.java deleted file mode 100644 index 6fb1fb39a..000000000 --- a/sdk-server-common/src/main/java/io/a2a/server/tasks/InMemoryPushNotifier.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.a2a.server.tasks; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; - -import com.fasterxml.jackson.core.JsonProcessingException; -import io.a2a.http.A2AHttpClient; -import io.a2a.http.JdkA2AHttpClient; -import io.a2a.spec.PushNotificationConfig; -import io.a2a.spec.Task; -import io.a2a.util.Utils; - -@ApplicationScoped -public class InMemoryPushNotifier implements PushNotifier { - private final A2AHttpClient httpClient; - private final Map pushNotificationInfos = Collections.synchronizedMap(new HashMap<>()); - - @Inject - public InMemoryPushNotifier() { - this.httpClient = new JdkA2AHttpClient(); - } - - public InMemoryPushNotifier(A2AHttpClient httpClient) { - this.httpClient = httpClient; - } - - @Override - public void setInfo(String taskId, PushNotificationConfig notificationConfig) { - pushNotificationInfos.put(taskId, notificationConfig); - } - - @Override - public PushNotificationConfig getInfo(String taskId) { - return pushNotificationInfos.get(taskId); - } - - @Override - public void deleteInfo(String taskId) { - pushNotificationInfos.remove(taskId); - } - - @Override - public void sendNotification(Task task) { - PushNotificationConfig pushInfo = pushNotificationInfos.get(task.getId()); - if (pushInfo == null) { - return; - } - String url = pushInfo.url(); - - // TODO auth - - String body; - try { - body = Utils.OBJECT_MAPPER.writeValueAsString(task); - } catch (JsonProcessingException e) { - e.printStackTrace(); - throw new RuntimeException("Error writing value as string: " + e.getMessage(), e); - } catch (Throwable throwable) { - throwable.printStackTrace(); - throw new RuntimeException("Error writing value as string: " + throwable.getMessage(), throwable); - } - - try { - httpClient.createPost() - .url(url) - .body(body) - .post(); - } catch (IOException | InterruptedException e) { - throw new RuntimeException("Error pushing data to " + url + ": " + e.getMessage(), e); - } - - } -} diff --git a/sdk-server-common/src/main/java/io/a2a/server/tasks/PushNotificationConfigStore.java b/sdk-server-common/src/main/java/io/a2a/server/tasks/PushNotificationConfigStore.java new file mode 100644 index 000000000..68f132620 --- /dev/null +++ b/sdk-server-common/src/main/java/io/a2a/server/tasks/PushNotificationConfigStore.java @@ -0,0 +1,33 @@ +package io.a2a.server.tasks; + +import java.util.List; + +import io.a2a.spec.PushNotificationConfig; + +/** + * Interface for storing and retrieving push notification configurations for tasks. + */ +public interface PushNotificationConfigStore { + + /** + * Sets or updates the push notification configuration for a task. + * @param taskId the task ID + * @param notificationConfig the push notification configuration + */ + void setInfo(String taskId, PushNotificationConfig notificationConfig); + + /** + * Retrieves the push notification configuration for a task. + * @param taskId the task ID + * @return the push notification configurations for a task + */ + List getInfo(String taskId); + + /** + * Deletes the push notification configuration for a task. + * @param taskId the task ID + * @param configId the push notification configuration + */ + void deleteInfo(String taskId, String configId); + +} diff --git a/sdk-server-common/src/main/java/io/a2a/server/tasks/PushNotificationSender.java b/sdk-server-common/src/main/java/io/a2a/server/tasks/PushNotificationSender.java new file mode 100644 index 000000000..81d577f46 --- /dev/null +++ b/sdk-server-common/src/main/java/io/a2a/server/tasks/PushNotificationSender.java @@ -0,0 +1,15 @@ +package io.a2a.server.tasks; + +import io.a2a.spec.Task; + +/** + * Interface for sending push notifications for tasks. + */ +public interface PushNotificationSender { + + /** + * Sends a push notification containing the latest task state. + * @param task the task + */ + void sendNotification(Task task); +} diff --git a/sdk-server-common/src/main/java/io/a2a/server/tasks/PushNotifier.java b/sdk-server-common/src/main/java/io/a2a/server/tasks/PushNotifier.java deleted file mode 100644 index 2dfc7dff0..000000000 --- a/sdk-server-common/src/main/java/io/a2a/server/tasks/PushNotifier.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.a2a.server.tasks; - -import io.a2a.spec.PushNotificationConfig; -import io.a2a.spec.Task; - -public interface PushNotifier { - void setInfo(String taskId, PushNotificationConfig notificationConfig); - - PushNotificationConfig getInfo(String taskId); - - void deleteInfo(String taskId); - - void sendNotification(Task task); -} diff --git a/sdk-server-common/src/test/java/io/a2a/server/requesthandlers/JSONRPCHandlerTest.java b/sdk-server-common/src/test/java/io/a2a/server/requesthandlers/JSONRPCHandlerTest.java index 49913c0b6..80eaacf9e 100644 --- a/sdk-server-common/src/test/java/io/a2a/server/requesthandlers/JSONRPCHandlerTest.java +++ b/sdk-server-common/src/test/java/io/a2a/server/requesthandlers/JSONRPCHandlerTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import jakarta.enterprise.context.Dependent; import java.io.IOException; import java.util.ArrayList; @@ -21,9 +22,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import io.a2a.spec.InternalError; -import jakarta.enterprise.context.Dependent; - import io.a2a.http.A2AHttpClient; import io.a2a.http.A2AHttpResponse; import io.a2a.server.agentexecution.AgentExecutor; @@ -31,9 +29,11 @@ import io.a2a.server.events.EventConsumer; import io.a2a.server.events.EventQueue; import io.a2a.server.events.InMemoryQueueManager; -import io.a2a.server.tasks.InMemoryPushNotifier; +import io.a2a.server.tasks.BasePushNotificationSender; +import io.a2a.server.tasks.InMemoryPushNotificationConfigStore; import io.a2a.server.tasks.InMemoryTaskStore; -import io.a2a.server.tasks.PushNotifier; +import io.a2a.server.tasks.PushNotificationConfigStore; +import io.a2a.server.tasks.PushNotificationSender; import io.a2a.server.tasks.ResultAggregator; import io.a2a.server.tasks.TaskStore; import io.a2a.server.tasks.TaskUpdater; @@ -42,7 +42,11 @@ import io.a2a.spec.Artifact; import io.a2a.spec.CancelTaskRequest; import io.a2a.spec.CancelTaskResponse; +import io.a2a.spec.DeleteTaskPushNotificationConfigParams; +import io.a2a.spec.DeleteTaskPushNotificationConfigRequest; +import io.a2a.spec.DeleteTaskPushNotificationConfigResponse; import io.a2a.spec.Event; +import io.a2a.spec.GetTaskPushNotificationConfigParams; import io.a2a.spec.GetTaskPushNotificationConfigRequest; import io.a2a.spec.GetTaskPushNotificationConfigResponse; import io.a2a.spec.GetTaskRequest; @@ -50,9 +54,13 @@ import io.a2a.spec.InternalError; import io.a2a.spec.InvalidRequestError; import io.a2a.spec.JSONRPCError; +import io.a2a.spec.ListTaskPushNotificationConfigParams; +import io.a2a.spec.ListTaskPushNotificationConfigRequest; +import io.a2a.spec.ListTaskPushNotificationConfigResponse; import io.a2a.spec.Message; import io.a2a.spec.MessageSendParams; import io.a2a.spec.PushNotificationConfig; +import io.a2a.spec.PushNotificationNotSupportedError; import io.a2a.spec.SendMessageRequest; import io.a2a.spec.SendMessageResponse; import io.a2a.spec.SendStreamingMessageRequest; @@ -75,6 +83,7 @@ import io.a2a.util.Utils; import io.quarkus.arc.profile.IfBuildProfile; import mutiny.zero.ZeroPublisher; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -130,9 +139,10 @@ public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPC taskStore = new InMemoryTaskStore(); queueManager = new InMemoryQueueManager(); httpClient = new TestHttpClient(); - PushNotifier pushNotifier = new InMemoryPushNotifier(httpClient); + PushNotificationConfigStore pushConfigStore = new InMemoryPushNotificationConfigStore(); + PushNotificationSender pushSender = new BasePushNotificationSender(pushConfigStore, httpClient); - requestHandler = new DefaultRequestHandler(executor, taskStore, queueManager, pushNotifier, internalExecutor); + requestHandler = new DefaultRequestHandler(executor, taskStore, queueManager, pushConfigStore, pushSender, internalExecutor); } @AfterEach @@ -612,7 +622,7 @@ public void onComplete() { @Test - public void testSetPushNotificationSuccess() { + public void testSetPushNotificationConfigSuccess() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler); taskStore.save(MINIMAL_TASK); @@ -620,12 +630,12 @@ public void testSetPushNotificationSuccess() { new TaskPushNotificationConfig( MINIMAL_TASK.getId(), new PushNotificationConfig.Builder().url("http://example.com").build()); SetTaskPushNotificationConfigRequest request = new SetTaskPushNotificationConfigRequest("1", taskPushConfig); - SetTaskPushNotificationConfigResponse response = handler.setPushNotification(request); + SetTaskPushNotificationConfigResponse response = handler.setPushNotificationConfig(request); assertSame(taskPushConfig, response.getResult()); } @Test - public void testGetPushNotificationSuccess() { + public void testGetPushNotificationConfigSuccess() { JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler); taskStore.save(MINIMAL_TASK); agentExecutorExecute = (context, eventQueue) -> { @@ -638,13 +648,15 @@ public void testGetPushNotificationSuccess() { MINIMAL_TASK.getId(), new PushNotificationConfig.Builder().url("http://example.com").build()); SetTaskPushNotificationConfigRequest request = new SetTaskPushNotificationConfigRequest("1", taskPushConfig); - handler.setPushNotification(request); + handler.setPushNotificationConfig(request); GetTaskPushNotificationConfigRequest getRequest = - new GetTaskPushNotificationConfigRequest("111", new TaskIdParams(MINIMAL_TASK.getId())); - GetTaskPushNotificationConfigResponse getResponse = handler.getPushNotification(getRequest); + new GetTaskPushNotificationConfigRequest("111", new GetTaskPushNotificationConfigParams(MINIMAL_TASK.getId())); + GetTaskPushNotificationConfigResponse getResponse = handler.getPushNotificationConfig(getRequest); - assertEquals(taskPushConfig, getResponse.getResult()); + TaskPushNotificationConfig expectedConfig = new TaskPushNotificationConfig(MINIMAL_TASK.getId(), + new PushNotificationConfig.Builder().id(MINIMAL_TASK.getId()).url("http://example.com").build()); + assertEquals(expectedConfig, getResponse.getResult()); } @Test @@ -681,7 +693,7 @@ public void testOnMessageStreamNewMessageSendPushNotificationSuccess() throws Ex MINIMAL_TASK.getId(), new PushNotificationConfig.Builder().url("http://example.com").build()); SetTaskPushNotificationConfigRequest stpnRequest = new SetTaskPushNotificationConfigRequest("1", config); - SetTaskPushNotificationConfigResponse stpnResponse = handler.setPushNotification(stpnRequest); + SetTaskPushNotificationConfigResponse stpnResponse = handler.setPushNotificationConfig(stpnRequest); assertNull(stpnResponse.getError()); Message msg = new Message.Builder(MESSAGE) @@ -1036,24 +1048,23 @@ public void testPushNotificationsNotSupportedError() { SetTaskPushNotificationConfigRequest request = new SetTaskPushNotificationConfigRequest.Builder() .params(config) .build(); - SetTaskPushNotificationConfigResponse response = handler.setPushNotification(request); - assertInstanceOf(InvalidRequestError.class, response.getError()); - assertEquals("Push notifications are not supported by the agent", response.getError().getMessage()); + SetTaskPushNotificationConfigResponse response = handler.setPushNotificationConfig(request); + assertInstanceOf(PushNotificationNotSupportedError.class, response.getError()); } @Test - public void testOnGetPushNotificationNoPushNotifier() { + public void testOnGetPushNotificationNoPushNotifierConfig() { // Create request handler without a push notifier DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, internalExecutor); + new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); AgentCard card = createAgentCard(false, true, false); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler); taskStore.save(MINIMAL_TASK); GetTaskPushNotificationConfigRequest request = - new GetTaskPushNotificationConfigRequest("id", new TaskIdParams(MINIMAL_TASK.getId())); - GetTaskPushNotificationConfigResponse response = handler.getPushNotification(request); + new GetTaskPushNotificationConfigRequest("id", new GetTaskPushNotificationConfigParams(MINIMAL_TASK.getId())); + GetTaskPushNotificationConfigResponse response = handler.getPushNotificationConfig(request); assertNotNull(response.getError()); assertInstanceOf(UnsupportedOperationError.class, response.getError()); @@ -1061,10 +1072,10 @@ public void testOnGetPushNotificationNoPushNotifier() { } @Test - public void testOnSetPushNotificationNoPushNotifier() { + public void testOnSetPushNotificationNoPushNotifierConfig() { // Create request handler without a push notifier DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, internalExecutor); + new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); AgentCard card = createAgentCard(false, true, false); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler); @@ -1080,7 +1091,7 @@ public void testOnSetPushNotificationNoPushNotifier() { SetTaskPushNotificationConfigRequest request = new SetTaskPushNotificationConfigRequest.Builder() .params(config) .build(); - SetTaskPushNotificationConfigResponse response = handler.setPushNotification(request); + SetTaskPushNotificationConfigResponse response = handler.setPushNotificationConfig(request); assertInstanceOf(UnsupportedOperationError.class, response.getError()); assertEquals("This operation is not supported", response.getError().getMessage()); @@ -1153,7 +1164,7 @@ public void testDefaultRequestHandlerWithCustomComponents() { @Test public void testOnMessageSendErrorHandling() { DefaultRequestHandler requestHandler = - new DefaultRequestHandler(executor, taskStore, queueManager, null, internalExecutor); + new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); AgentCard card = createAgentCard(false, true, false); JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler); @@ -1241,6 +1252,175 @@ public void onComplete() { assertInstanceOf(InternalError.class, results.get(0).getError()); } + @Test + public void testListPushNotificationConfig() { + JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler); + taskStore.save(MINIMAL_TASK); + agentExecutorExecute = (context, eventQueue) -> { + eventQueue.enqueueEvent(context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + new TaskPushNotificationConfig( + MINIMAL_TASK.getId(), new PushNotificationConfig.Builder() + .url("http://example.com") + .id(MINIMAL_TASK.getId()) + .build()); + SetTaskPushNotificationConfigRequest request = new SetTaskPushNotificationConfigRequest("1", taskPushConfig); + handler.setPushNotificationConfig(request); + + ListTaskPushNotificationConfigRequest listRequest = + new ListTaskPushNotificationConfigRequest("111", new ListTaskPushNotificationConfigParams(MINIMAL_TASK.getId())); + ListTaskPushNotificationConfigResponse listResponse = handler.listPushNotificationConfig(listRequest); + + assertEquals("111", listResponse.getId()); + assertEquals(1, listResponse.getResult().size()); + assertEquals(taskPushConfig, listResponse.getResult().get(0)); + } + + @Test + public void testListPushNotificationConfigNotSupported() { + AgentCard card = createAgentCard(true, false, true); + JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler); + taskStore.save(MINIMAL_TASK); + agentExecutorExecute = (context, eventQueue) -> { + eventQueue.enqueueEvent(context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + new TaskPushNotificationConfig( + MINIMAL_TASK.getId(), new PushNotificationConfig.Builder() + .url("http://example.com") + .id(MINIMAL_TASK.getId()) + .build()); + SetTaskPushNotificationConfigRequest request = new SetTaskPushNotificationConfigRequest("1", taskPushConfig); + handler.setPushNotificationConfig(request); + + ListTaskPushNotificationConfigRequest listRequest = + new ListTaskPushNotificationConfigRequest("111", new ListTaskPushNotificationConfigParams(MINIMAL_TASK.getId())); + ListTaskPushNotificationConfigResponse listResponse = handler.listPushNotificationConfig(listRequest); + + assertEquals("111", listResponse.getId()); + assertNull(listResponse.getResult()); + assertInstanceOf(PushNotificationNotSupportedError.class, listResponse.getError()); + } + + @Test + public void testListPushNotificationConfigNoPushConfigStore() { + DefaultRequestHandler requestHandler = + new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); + JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler); + taskStore.save(MINIMAL_TASK); + agentExecutorExecute = (context, eventQueue) -> { + eventQueue.enqueueEvent(context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + ListTaskPushNotificationConfigRequest listRequest = + new ListTaskPushNotificationConfigRequest("111", new ListTaskPushNotificationConfigParams(MINIMAL_TASK.getId())); + ListTaskPushNotificationConfigResponse listResponse = handler.listPushNotificationConfig(listRequest); + + assertEquals("111", listResponse.getId()); + assertNull(listResponse.getResult()); + assertInstanceOf(UnsupportedOperationError.class, listResponse.getError()); + } + + @Test + public void testListPushNotificationConfigTaskNotFound() { + JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler); + agentExecutorExecute = (context, eventQueue) -> { + eventQueue.enqueueEvent(context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + ListTaskPushNotificationConfigRequest listRequest = + new ListTaskPushNotificationConfigRequest("111", new ListTaskPushNotificationConfigParams(MINIMAL_TASK.getId())); + ListTaskPushNotificationConfigResponse listResponse = handler.listPushNotificationConfig(listRequest); + + assertEquals("111", listResponse.getId()); + assertNull(listResponse.getResult()); + assertInstanceOf(TaskNotFoundError.class, listResponse.getError()); + } + + @Test + public void testDeletePushNotificationConfig() { + JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler); + taskStore.save(MINIMAL_TASK); + agentExecutorExecute = (context, eventQueue) -> { + eventQueue.enqueueEvent(context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + new TaskPushNotificationConfig( + MINIMAL_TASK.getId(), new PushNotificationConfig.Builder() + .url("http://example.com") + .id(MINIMAL_TASK.getId()) + .build()); + SetTaskPushNotificationConfigRequest request = new SetTaskPushNotificationConfigRequest("1", taskPushConfig); + handler.setPushNotificationConfig(request); + + DeleteTaskPushNotificationConfigRequest deleteRequest = + new DeleteTaskPushNotificationConfigRequest("111", new DeleteTaskPushNotificationConfigParams(MINIMAL_TASK.getId(), MINIMAL_TASK.getId())); + DeleteTaskPushNotificationConfigResponse deleteResponse = handler.deletePushNotificationConfig(deleteRequest); + + assertEquals("111", deleteResponse.getId()); + assertNull(deleteResponse.getError()); + assertNull(deleteResponse.getResult()); + } + + @Test + public void testDeletePushNotificationConfigNotSupported() { + AgentCard card = createAgentCard(true, false, true); + JSONRPCHandler handler = new JSONRPCHandler(card, requestHandler); + taskStore.save(MINIMAL_TASK); + agentExecutorExecute = (context, eventQueue) -> { + eventQueue.enqueueEvent(context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + new TaskPushNotificationConfig( + MINIMAL_TASK.getId(), new PushNotificationConfig.Builder() + .url("http://example.com") + .id(MINIMAL_TASK.getId()) + .build()); + SetTaskPushNotificationConfigRequest request = new SetTaskPushNotificationConfigRequest("1", taskPushConfig); + handler.setPushNotificationConfig(request); + + DeleteTaskPushNotificationConfigRequest deleteRequest = + new DeleteTaskPushNotificationConfigRequest("111", new DeleteTaskPushNotificationConfigParams(MINIMAL_TASK.getId(), MINIMAL_TASK.getId())); + DeleteTaskPushNotificationConfigResponse deleteResponse = handler.deletePushNotificationConfig(deleteRequest); + + assertEquals("111", deleteResponse.getId()); + assertNull(deleteResponse.getResult()); + assertInstanceOf(PushNotificationNotSupportedError.class, deleteResponse.getError()); + } + + @Test + public void testDeletePushNotificationConfigNoPushConfigStore() { + DefaultRequestHandler requestHandler = + new DefaultRequestHandler(executor, taskStore, queueManager, null, null, internalExecutor); + JSONRPCHandler handler = new JSONRPCHandler(CARD, requestHandler); + taskStore.save(MINIMAL_TASK); + agentExecutorExecute = (context, eventQueue) -> { + eventQueue.enqueueEvent(context.getTask() != null ? context.getTask() : context.getMessage()); + }; + + TaskPushNotificationConfig taskPushConfig = + new TaskPushNotificationConfig( + MINIMAL_TASK.getId(), new PushNotificationConfig.Builder() + .url("http://example.com") + .id(MINIMAL_TASK.getId()) + .build()); + SetTaskPushNotificationConfigRequest request = new SetTaskPushNotificationConfigRequest("1", taskPushConfig); + handler.setPushNotificationConfig(request); + + DeleteTaskPushNotificationConfigRequest deleteRequest = + new DeleteTaskPushNotificationConfigRequest("111", new DeleteTaskPushNotificationConfigParams(MINIMAL_TASK.getId(), MINIMAL_TASK.getId())); + DeleteTaskPushNotificationConfigResponse deleteResponse = handler.deletePushNotificationConfig(deleteRequest); + + assertEquals("111", deleteResponse.getId()); + assertNull(deleteResponse.getResult()); + assertInstanceOf(UnsupportedOperationError.class, deleteResponse.getError()); + } + private static AgentCard createAgentCard(boolean streaming, boolean pushNotifications, boolean stateTransitionHistory) { return new AgentCard.Builder() .name("test-card") @@ -1256,6 +1436,7 @@ private static AgentCard createAgentCard(boolean streaming, boolean pushNotifica .defaultInputModes(new ArrayList<>()) .defaultOutputModes(new ArrayList<>()) .skills(new ArrayList<>()) + .protocolVersion("0.2.5") .build(); } diff --git a/spec/pom.xml b/spec/pom.xml index cddec7a1c..c2decdd38 100644 --- a/spec/pom.xml +++ b/spec/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT a2a-java-sdk-spec diff --git a/spec/src/main/java/io/a2a/spec/AgentCard.java b/spec/src/main/java/io/a2a/spec/AgentCard.java index 8429b16f5..e394fb626 100644 --- a/spec/src/main/java/io/a2a/spec/AgentCard.java +++ b/spec/src/main/java/io/a2a/spec/AgentCard.java @@ -17,7 +17,8 @@ public record AgentCard(String name, String description, String url, AgentProvid String version, String documentationUrl, AgentCapabilities capabilities, List defaultInputModes, List defaultOutputModes, List skills, boolean supportsAuthenticatedExtendedCard, Map securitySchemes, - List>> security, String iconUrl) { + List>> security, String iconUrl, List additionalInterfaces, + String preferredTransport, String protocolVersion) { private static final String TEXT_MODE = "text"; @@ -30,6 +31,7 @@ public record AgentCard(String name, String description, String url, AgentProvid Assert.checkNotNullParam("skills", skills); Assert.checkNotNullParam("url", url); Assert.checkNotNullParam("version", version); + Assert.checkNotNullParam("protocolVersion", protocolVersion); } public static class Builder { @@ -47,6 +49,9 @@ public static class Builder { private Map securitySchemes; private List>> security; private String iconUrl; + private List additionalInterfaces; + String preferredTransport; + String protocolVersion; public Builder name(String name) { this.name = name; @@ -118,10 +123,26 @@ public Builder iconUrl(String iconUrl) { return this; } + public Builder additionalInterfaces(List additionalInterfaces) { + this.additionalInterfaces = additionalInterfaces; + return this; + } + + public Builder preferredTransport(String preferredTransport) { + this.preferredTransport = preferredTransport; + return this; + } + + public Builder protocolVersion(String protocolVersion) { + this.protocolVersion = protocolVersion; + return this; + } + public AgentCard build() { return new AgentCard(name, description, url, provider, version, documentationUrl, capabilities, defaultInputModes, defaultOutputModes, skills, - supportsAuthenticatedExtendedCard, securitySchemes, security, iconUrl); + supportsAuthenticatedExtendedCard, securitySchemes, security, iconUrl, + additionalInterfaces, preferredTransport, protocolVersion); } } } diff --git a/spec/src/main/java/io/a2a/spec/AgentInterface.java b/spec/src/main/java/io/a2a/spec/AgentInterface.java new file mode 100644 index 000000000..ab2b7307d --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/AgentInterface.java @@ -0,0 +1,18 @@ +package io.a2a.spec; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import io.a2a.util.Assert; + +/** + * Provides a declaration of the target url and the supported transport to interact with the agent. + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AgentInterface(String transport, String url) { + + public AgentInterface { + Assert.checkNotNullParam("transport", transport); + Assert.checkNotNullParam("url", url); + } +} diff --git a/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java b/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java index 02bd63461..9ef775118 100644 --- a/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java +++ b/spec/src/main/java/io/a2a/spec/CancelTaskResponse.java @@ -15,7 +15,7 @@ public final class CancelTaskResponse extends JSONRPCResponse { @JsonCreator public CancelTaskResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, @JsonProperty("result") Task result, @JsonProperty("error") JSONRPCError error) { - super(jsonrpc, id, result, error); + super(jsonrpc, id, result, error, Task.class); } public CancelTaskResponse(Object id, JSONRPCError error) { diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java new file mode 100644 index 000000000..a64421a4c --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigParams.java @@ -0,0 +1,50 @@ +package io.a2a.spec; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import io.a2a.util.Assert; + +/** + * Parameters for removing pushNotificationConfiguration associated with a Task. + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +public record DeleteTaskPushNotificationConfigParams(String id, String pushNotificationConfigId, Map metadata) { + + public DeleteTaskPushNotificationConfigParams { + Assert.checkNotNullParam("id", id); + Assert.checkNotNullParam("pushNotificationConfigId", pushNotificationConfigId); + } + + public DeleteTaskPushNotificationConfigParams(String id, String pushNotificationConfigId) { + this(id, pushNotificationConfigId, null); + } + + public static class Builder { + String id; + String pushNotificationConfigId; + Map metadata; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder pushNotificationConfigId(String pushNotificationConfigId) { + this.pushNotificationConfigId = pushNotificationConfigId; + return this; + } + + public Builder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public DeleteTaskPushNotificationConfigParams build() { + return new DeleteTaskPushNotificationConfigParams(id, pushNotificationConfigId, metadata); + } + } +} diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java new file mode 100644 index 000000000..99f50ebfd --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigRequest.java @@ -0,0 +1,77 @@ +package io.a2a.spec; + +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.a2a.util.Assert; +import io.a2a.util.Utils; + +/** + * A delete task push notification config request. + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class DeleteTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { + + public static final String METHOD = "tasks/pushNotificationConfig/delete"; + + @JsonCreator + public DeleteTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, + @JsonProperty("method") String method, + @JsonProperty("params") DeleteTaskPushNotificationConfigParams params) { + if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); + } + Assert.checkNotNullParam("method", method); + if (! method.equals(METHOD)) { + throw new IllegalArgumentException("Invalid DeleteTaskPushNotificationConfigRequest method"); + } + Assert.isNullOrStringOrInteger(id); + this.jsonrpc = Utils.defaultIfNull(jsonrpc, JSONRPC_VERSION); + this.id = id; + this.method = method; + this.params = params; + } + + public DeleteTaskPushNotificationConfigRequest(String id, DeleteTaskPushNotificationConfigParams params) { + this(null, id, METHOD, params); + } + + public static class Builder { + private String jsonrpc; + private Object id; + private String method; + private DeleteTaskPushNotificationConfigParams params; + + public Builder jsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; + return this; + } + + public Builder id(Object id) { + this.id = id; + return this; + } + + public Builder method(String method) { + this.method = method; + return this; + } + + public Builder params(DeleteTaskPushNotificationConfigParams params) { + this.params = params; + return this; + } + + public DeleteTaskPushNotificationConfigRequest build() { + if (id == null) { + id = UUID.randomUUID().toString(); + } + return new DeleteTaskPushNotificationConfigRequest(jsonrpc, id, method, params); + } + } +} diff --git a/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java new file mode 100644 index 000000000..0f65b5ad5 --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/DeleteTaskPushNotificationConfigResponse.java @@ -0,0 +1,32 @@ +package io.a2a.spec; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +/** + * A response for a delete task push notification config request. + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonSerialize(using = JSONRPCVoidResponseSerializer.class) +public final class DeleteTaskPushNotificationConfigResponse extends JSONRPCResponse { + + @JsonCreator + public DeleteTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, + @JsonProperty("result") Void result, + @JsonProperty("error") JSONRPCError error) { + super(jsonrpc, id, result, error, Void.class); + } + + public DeleteTaskPushNotificationConfigResponse(Object id, JSONRPCError error) { + this(null, id, null, error); + } + + public DeleteTaskPushNotificationConfigResponse(Object id) { + this(null, id, null, null); + } + +} diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java new file mode 100644 index 000000000..d8952f87d --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigParams.java @@ -0,0 +1,53 @@ +package io.a2a.spec; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import io.a2a.util.Assert; + +/** + * Parameters for fetching a pushNotificationConfiguration associated with a Task. + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +public record GetTaskPushNotificationConfigParams(String id, String pushNotificationConfigId, Map metadata) { + + public GetTaskPushNotificationConfigParams { + Assert.checkNotNullParam("id", id); + } + + public GetTaskPushNotificationConfigParams(String id) { + this(id, null, null); + } + + public GetTaskPushNotificationConfigParams(String id, String pushNotificationConfigId) { + this(id, pushNotificationConfigId, null); + } + + public static class Builder { + String id; + String pushNotificationConfigId; + Map metadata; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder pushNotificationConfigId(String pushNotificationConfigId) { + this.pushNotificationConfigId = pushNotificationConfigId; + return this; + } + + public Builder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public GetTaskPushNotificationConfigParams build() { + return new GetTaskPushNotificationConfigParams(id, pushNotificationConfigId, metadata); + } + } +} diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java index ab39015bb..b353e0cc8 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigRequest.java @@ -14,13 +14,13 @@ */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) -public final class GetTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { +public final class GetTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { public static final String METHOD = "tasks/pushNotificationConfig/get"; @JsonCreator public GetTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, - @JsonProperty("method") String method, @JsonProperty("params") TaskIdParams params) { + @JsonProperty("method") String method, @JsonProperty("params") GetTaskPushNotificationConfigParams params) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } @@ -35,7 +35,7 @@ public GetTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String json this.params = params; } - public GetTaskPushNotificationConfigRequest(String id, TaskIdParams params) { + public GetTaskPushNotificationConfigRequest(String id, GetTaskPushNotificationConfigParams params) { this(null, id, METHOD, params); } @@ -43,7 +43,7 @@ public static class Builder { private String jsonrpc; private Object id; private String method; - private TaskIdParams params; + private GetTaskPushNotificationConfigParams params; public GetTaskPushNotificationConfigRequest.Builder jsonrpc(String jsonrpc) { this.jsonrpc = jsonrpc; @@ -60,7 +60,7 @@ public GetTaskPushNotificationConfigRequest.Builder method(String method) { return this; } - public GetTaskPushNotificationConfigRequest.Builder params(TaskIdParams params) { + public GetTaskPushNotificationConfigRequest.Builder params(GetTaskPushNotificationConfigParams params) { this.params = params; return this; } diff --git a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java index c4340188f..116799a9e 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskPushNotificationConfigResponse.java @@ -16,7 +16,7 @@ public final class GetTaskPushNotificationConfigResponse extends JSONRPCResponse public GetTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, @JsonProperty("result") TaskPushNotificationConfig result, @JsonProperty("error") JSONRPCError error) { - super(jsonrpc, id, result, error); + super(jsonrpc, id, result, error, TaskPushNotificationConfig.class); } public GetTaskPushNotificationConfigResponse(Object id, JSONRPCError error) { diff --git a/spec/src/main/java/io/a2a/spec/GetTaskResponse.java b/spec/src/main/java/io/a2a/spec/GetTaskResponse.java index e51cb66aa..0d27a8e68 100644 --- a/spec/src/main/java/io/a2a/spec/GetTaskResponse.java +++ b/spec/src/main/java/io/a2a/spec/GetTaskResponse.java @@ -15,7 +15,7 @@ public final class GetTaskResponse extends JSONRPCResponse { @JsonCreator public GetTaskResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, @JsonProperty("result") Task result, @JsonProperty("error") JSONRPCError error) { - super(jsonrpc, id, result, error); + super(jsonrpc, id, result, error, Task.class); } public GetTaskResponse(Object id, JSONRPCError error) { diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java b/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java index 95ac5d341..ea7846655 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCErrorResponse.java @@ -17,7 +17,7 @@ public final class JSONRPCErrorResponse extends JSONRPCResponse { @JsonCreator public JSONRPCErrorResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, @JsonProperty("result") Void result, @JsonProperty("error") JSONRPCError error) { - super(jsonrpc, id, result, error); + super(jsonrpc, id, result, error, Void.class); Assert.checkNotNullParam("error", error); } diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java b/spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java index fe21ffab8..cf0134efe 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCRequestDeserializerBase.java @@ -4,13 +4,10 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import java.io.IOException; - public abstract class JSONRPCRequestDeserializerBase extends StdDeserializer> { public JSONRPCRequestDeserializerBase() { @@ -21,35 +18,6 @@ public JSONRPCRequestDeserializerBase(Class vc) { super(vc); } - @Override - public JSONRPCRequest deserialize(JsonParser jsonParser, DeserializationContext context) - throws IOException, JsonProcessingException { - JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser); - String jsonrpc = getAndValidateJsonrpc(treeNode, jsonParser); - String method = getAndValidateMethod(treeNode, jsonParser); - Object id = getAndValidateId(treeNode, jsonParser); - JsonNode paramsNode = treeNode.get("params"); - - switch (method) { - case GetTaskRequest.METHOD: - return new GetTaskRequest(jsonrpc, id, method, getAndValidateParams(paramsNode, jsonParser, treeNode, TaskQueryParams.class)); - case CancelTaskRequest.METHOD: - return new CancelTaskRequest(jsonrpc, id, method, getAndValidateParams(paramsNode, jsonParser, treeNode, TaskIdParams.class)); - case SetTaskPushNotificationConfigRequest.METHOD: - return new SetTaskPushNotificationConfigRequest(jsonrpc, id, method, getAndValidateParams(paramsNode, jsonParser, treeNode, TaskPushNotificationConfig.class)); - case GetTaskPushNotificationConfigRequest.METHOD: - return new GetTaskPushNotificationConfigRequest(jsonrpc, id, method, getAndValidateParams(paramsNode, jsonParser, treeNode, TaskIdParams.class)); - case SendMessageRequest.METHOD: - return new SendMessageRequest(jsonrpc, id, method, getAndValidateParams(paramsNode, jsonParser, treeNode, MessageSendParams.class)); - case TaskResubscriptionRequest.METHOD: - return new TaskResubscriptionRequest(jsonrpc, id, method, getAndValidateParams(paramsNode, jsonParser, treeNode, TaskIdParams.class)); - case SendStreamingMessageRequest.METHOD: - return new SendStreamingMessageRequest(jsonrpc, id, method, getAndValidateParams(paramsNode, jsonParser, treeNode, MessageSendParams.class)); - default: - throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(treeNode, jsonParser)); - } - } - protected T getAndValidateParams(JsonNode paramsNode, JsonParser jsonParser, JsonNode node, Class paramsType) throws JsonMappingException { if (paramsNode == null) { return null; @@ -112,7 +80,9 @@ protected static boolean isValidMethodName(String methodName) { || methodName.equals(SetTaskPushNotificationConfigRequest.METHOD) || methodName.equals(TaskResubscriptionRequest.METHOD) || methodName.equals(SendMessageRequest.METHOD) - || methodName.equals(SendStreamingMessageRequest.METHOD)); + || methodName.equals(SendStreamingMessageRequest.METHOD) + || methodName.equals(ListTaskPushNotificationConfigRequest.METHOD) + || methodName.equals(DeleteTaskPushNotificationConfigRequest.METHOD)); } } diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java index 1be348043..3a382b1a7 100644 --- a/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java +++ b/spec/src/main/java/io/a2a/spec/JSONRPCResponse.java @@ -2,6 +2,7 @@ import static io.a2a.util.Utils.defaultIfNull; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -14,7 +15,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) public abstract sealed class JSONRPCResponse implements JSONRPCMessage permits SendStreamingMessageResponse, GetTaskResponse, CancelTaskResponse, SetTaskPushNotificationConfigResponse, GetTaskPushNotificationConfigResponse, - SendMessageResponse, JSONRPCErrorResponse { + SendMessageResponse, DeleteTaskPushNotificationConfigResponse, ListTaskPushNotificationConfigResponse, JSONRPCErrorResponse { protected String jsonrpc; protected Object id; @@ -24,14 +25,14 @@ public abstract sealed class JSONRPCResponse implements JSONRPCMessage permit public JSONRPCResponse() { } - public JSONRPCResponse(String jsonrpc, Object id, T result, JSONRPCError error) { + public JSONRPCResponse(String jsonrpc, Object id, T result, JSONRPCError error, Class resultType) { if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); } if (error != null && result != null) { throw new IllegalArgumentException("Invalid JSON-RPC error response"); } - if (error == null && result == null) { + if (error == null && result == null && ! Void.class.equals(resultType)) { throw new IllegalArgumentException("Invalid JSON-RPC success response"); } Assert.isNullOrStringOrInteger(id); diff --git a/spec/src/main/java/io/a2a/spec/JSONRPCVoidResponseSerializer.java b/spec/src/main/java/io/a2a/spec/JSONRPCVoidResponseSerializer.java new file mode 100644 index 000000000..1d5049410 --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/JSONRPCVoidResponseSerializer.java @@ -0,0 +1,34 @@ +package io.a2a.spec; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +public class JSONRPCVoidResponseSerializer extends StdSerializer> { + + private static final JSONRPCErrorSerializer JSON_RPC_ERROR_SERIALIZER = new JSONRPCErrorSerializer(); + + public JSONRPCVoidResponseSerializer() { + this(null); + } + + public JSONRPCVoidResponseSerializer(Class> vc) { + super(vc); + } + + @Override + public void serialize(JSONRPCResponse value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartObject(); + gen.writeStringField("jsonrpc", value.getJsonrpc()); + gen.writeObjectField("id", value.getId()); + if (value.getError() != null) { + gen.writeFieldName("error"); + JSON_RPC_ERROR_SERIALIZER.serialize(value.getError(), gen, provider); + } else { + gen.writeNullField("result"); + } + gen.writeEndObject(); + } +} diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java new file mode 100644 index 000000000..5ebb12f76 --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigParams.java @@ -0,0 +1,24 @@ +package io.a2a.spec; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +import io.a2a.util.Assert; + +/** + * Parameters for getting list of pushNotificationConfigurations associated with a Task. + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ListTaskPushNotificationConfigParams(String id, Map metadata) { + + public ListTaskPushNotificationConfigParams { + Assert.checkNotNullParam("id", id); + } + + public ListTaskPushNotificationConfigParams(String id) { + this(id, null); + } +} diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java new file mode 100644 index 000000000..90ba0f1f5 --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigRequest.java @@ -0,0 +1,77 @@ +package io.a2a.spec; + +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.a2a.util.Assert; +import io.a2a.util.Utils; + +/** + * A list task push notification config request. + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ListTaskPushNotificationConfigRequest extends NonStreamingJSONRPCRequest { + + public static final String METHOD = "tasks/pushNotificationConfig/list"; + + @JsonCreator + public ListTaskPushNotificationConfigRequest(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, + @JsonProperty("method") String method, + @JsonProperty("params") ListTaskPushNotificationConfigParams params) { + if (jsonrpc != null && ! jsonrpc.equals(JSONRPC_VERSION)) { + throw new IllegalArgumentException("Invalid JSON-RPC protocol version"); + } + Assert.checkNotNullParam("method", method); + if (! method.equals(METHOD)) { + throw new IllegalArgumentException("Invalid ListTaskPushNotificationConfigRequest method"); + } + Assert.isNullOrStringOrInteger(id); + this.jsonrpc = Utils.defaultIfNull(jsonrpc, JSONRPC_VERSION); + this.id = id; + this.method = method; + this.params = params; + } + + public ListTaskPushNotificationConfigRequest(String id, ListTaskPushNotificationConfigParams params) { + this(null, id, METHOD, params); + } + + public static class Builder { + private String jsonrpc; + private Object id; + private String method; + private ListTaskPushNotificationConfigParams params; + + public Builder jsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; + return this; + } + + public Builder id(Object id) { + this.id = id; + return this; + } + + public Builder method(String method) { + this.method = method; + return this; + } + + public Builder params(ListTaskPushNotificationConfigParams params) { + this.params = params; + return this; + } + + public ListTaskPushNotificationConfigRequest build() { + if (id == null) { + id = UUID.randomUUID().toString(); + } + return new ListTaskPushNotificationConfigRequest(jsonrpc, id, method, params); + } + } +} diff --git a/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java new file mode 100644 index 000000000..cc610416e --- /dev/null +++ b/spec/src/main/java/io/a2a/spec/ListTaskPushNotificationConfigResponse.java @@ -0,0 +1,32 @@ +package io.a2a.spec; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A response for a list task push notification config request. + */ +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ListTaskPushNotificationConfigResponse extends JSONRPCResponse> { + + @JsonCreator + public ListTaskPushNotificationConfigResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, + @JsonProperty("result") List result, + @JsonProperty("error") JSONRPCError error) { + super(jsonrpc, id, result, error, (Class>) (Class) List.class); + } + + public ListTaskPushNotificationConfigResponse(Object id, JSONRPCError error) { + this(null, id, null, error); + } + + public ListTaskPushNotificationConfigResponse(Object id, List result) { + this(null, id, result, null); + } + +} diff --git a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java index f79800215..1c0a696e7 100644 --- a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java +++ b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequest.java @@ -12,5 +12,5 @@ @JsonDeserialize(using = NonStreamingJSONRPCRequestDeserializer.class) public abstract sealed class NonStreamingJSONRPCRequest extends JSONRPCRequest permits GetTaskRequest, CancelTaskRequest, SetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigRequest, - SendMessageRequest { + SendMessageRequest, DeleteTaskPushNotificationConfigRequest, ListTaskPushNotificationConfigRequest { } diff --git a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java index f2978b66f..c97c524c5 100644 --- a/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java +++ b/spec/src/main/java/io/a2a/spec/NonStreamingJSONRPCRequestDeserializer.java @@ -1,12 +1,12 @@ package io.a2a.spec; +import java.io.IOException; + import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonNode; -import java.io.IOException; - public class NonStreamingJSONRPCRequestDeserializer extends JSONRPCRequestDeserializerBase> { public NonStreamingJSONRPCRequestDeserializer() { @@ -38,10 +38,16 @@ public NonStreamingJSONRPCRequest deserialize(JsonParser jsonParser, Deserial getAndValidateParams(paramsNode, jsonParser, treeNode, TaskPushNotificationConfig.class)); case GetTaskPushNotificationConfigRequest.METHOD: return new GetTaskPushNotificationConfigRequest(jsonrpc, id, method, - getAndValidateParams(paramsNode, jsonParser, treeNode, TaskIdParams.class)); + getAndValidateParams(paramsNode, jsonParser, treeNode, GetTaskPushNotificationConfigParams.class)); case SendMessageRequest.METHOD: return new SendMessageRequest(jsonrpc, id, method, getAndValidateParams(paramsNode, jsonParser, treeNode, MessageSendParams.class)); + case ListTaskPushNotificationConfigRequest.METHOD: + return new ListTaskPushNotificationConfigRequest(jsonrpc, id, method, + getAndValidateParams(paramsNode, jsonParser, treeNode, ListTaskPushNotificationConfigParams.class)); + case DeleteTaskPushNotificationConfigRequest.METHOD: + return new DeleteTaskPushNotificationConfigRequest(jsonrpc, id, method, + getAndValidateParams(paramsNode, jsonParser, treeNode, DeleteTaskPushNotificationConfigParams.class)); default: throw new MethodNotFoundJsonMappingException("Invalid method", getIdIfPossible(treeNode, jsonParser)); } diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java b/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java index 19e9d3491..34270637f 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationConfig.java @@ -21,6 +21,16 @@ public static class Builder { private PushNotificationAuthenticationInfo authentication; private String id; + public Builder() { + } + + public Builder(PushNotificationConfig notificationConfig) { + this.url = notificationConfig.url; + this.token = notificationConfig.token; + this.authentication = notificationConfig.authentication; + this.id = notificationConfig.id; + } + public Builder url(String url) { this.url = url; return this; diff --git a/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java b/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java index b97094cc0..d639b7bab 100644 --- a/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java +++ b/spec/src/main/java/io/a2a/spec/PushNotificationNotSupportedError.java @@ -13,6 +13,10 @@ public class PushNotificationNotSupportedError extends JSONRPCError { public final static Integer DEFAULT_CODE = -32003; + public PushNotificationNotSupportedError() { + this(null, null, null); + } + @JsonCreator public PushNotificationNotSupportedError( @JsonProperty("code") Integer code, diff --git a/spec/src/main/java/io/a2a/spec/SendMessageResponse.java b/spec/src/main/java/io/a2a/spec/SendMessageResponse.java index fa95bad36..901beba90 100644 --- a/spec/src/main/java/io/a2a/spec/SendMessageResponse.java +++ b/spec/src/main/java/io/a2a/spec/SendMessageResponse.java @@ -18,11 +18,7 @@ public final class SendMessageResponse extends JSONRPCResponse { @JsonCreator public SendMessageResponse(@JsonProperty("jsonrpc") String jsonrpc, @JsonProperty("id") Object id, @JsonProperty("result") EventKind result, @JsonProperty("error") JSONRPCError error) { - this.jsonrpc = defaultIfNull(jsonrpc, JSONRPC_VERSION); - Assert.isNullOrStringOrInteger(id); - this.id = id; - this.result = result; - this.error = error; + super(jsonrpc, id, result, error, EventKind.class); } public SendMessageResponse(Object id, EventKind result) { diff --git a/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java b/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java index b3597bfb6..f3bcb9676 100644 --- a/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java +++ b/spec/src/main/java/io/a2a/spec/SendStreamingMessageResponse.java @@ -18,11 +18,7 @@ public final class SendStreamingMessageResponse extends JSONRPCResponse io.github.a2asdk a2a-java-sdk-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT a2a-tck-server diff --git a/tck/src/main/java/io/a2a/tck/server/AgentCardProducer.java b/tck/src/main/java/io/a2a/tck/server/AgentCardProducer.java index 7abf29a12..610443ac8 100644 --- a/tck/src/main/java/io/a2a/tck/server/AgentCardProducer.java +++ b/tck/src/main/java/io/a2a/tck/server/AgentCardProducer.java @@ -37,6 +37,7 @@ public AgentCard agentCard() { .tags(Collections.singletonList("hello world")) .examples(List.of("hi", "hello world")) .build())) + .protocolVersion("0.2.5") .build(); } } diff --git a/tests/server-common/pom.xml b/tests/server-common/pom.xml index 0e205b547..a66e91d08 100644 --- a/tests/server-common/pom.xml +++ b/tests/server-common/pom.xml @@ -7,7 +7,7 @@ io.github.a2asdk a2a-java-sdk-parent - 0.2.3.Beta2-SNAPSHOT + 0.2.5.Beta1-SNAPSHOT ../../pom.xml a2a-java-sdk-tests-server-common diff --git a/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java b/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java index 8624f8e27..3671da130 100644 --- a/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java +++ b/tests/server-common/src/test/java/io/a2a/server/apps/common/AbstractA2AServerTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.fail; import static org.wildfly.common.Assert.assertNotNull; import static org.wildfly.common.Assert.assertTrue; @@ -28,11 +29,16 @@ import jakarta.ws.rs.core.MediaType; import com.fasterxml.jackson.core.JsonProcessingException; + +import io.a2a.client.A2AClient; +import io.a2a.spec.A2AServerException; import io.a2a.spec.AgentCard; import io.a2a.spec.Artifact; import io.a2a.spec.CancelTaskRequest; import io.a2a.spec.CancelTaskResponse; +import io.a2a.spec.DeleteTaskPushNotificationConfigResponse; import io.a2a.spec.Event; +import io.a2a.spec.GetTaskPushNotificationConfigParams; import io.a2a.spec.GetTaskPushNotificationConfigRequest; import io.a2a.spec.GetTaskPushNotificationConfigResponse; import io.a2a.spec.GetTaskRequest; @@ -42,6 +48,7 @@ import io.a2a.spec.JSONParseError; import io.a2a.spec.JSONRPCError; import io.a2a.spec.JSONRPCErrorResponse; +import io.a2a.spec.ListTaskPushNotificationConfigResponse; import io.a2a.spec.Message; import io.a2a.spec.MessageSendParams; import io.a2a.spec.MethodNotFoundError; @@ -69,6 +76,7 @@ import io.a2a.util.Utils; import io.restassured.RestAssured; import io.restassured.specification.RequestSpecification; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -111,9 +119,11 @@ public abstract class AbstractA2AServerTest { public static final String APPLICATION_JSON = "application/json"; private final int serverPort; + private A2AClient client; protected AbstractA2AServerTest(int serverPort) { this.serverPort = serverPort; + this.client = new A2AClient("http://localhost:" + serverPort); } @Test @@ -338,6 +348,7 @@ public void testSetPushNotificationSuccess() throws Exception { assertEquals("http://example.com", config.pushNotificationConfig().url()); } catch (Exception e) { } finally { + deletePushNotificationConfigInStore(MINIMAL_TASK.getId(), MINIMAL_TASK.getId()); deleteTaskInTaskStore(MINIMAL_TASK.getId()); } } @@ -363,7 +374,7 @@ public void testGetPushNotificationSuccess() throws Exception { assertNotNull(setTaskPushNotificationResponse); GetTaskPushNotificationConfigRequest request = - new GetTaskPushNotificationConfigRequest("111", new TaskIdParams(MINIMAL_TASK.getId())); + new GetTaskPushNotificationConfigRequest("111", new GetTaskPushNotificationConfigParams(MINIMAL_TASK.getId())); GetTaskPushNotificationConfigResponse response = given() .contentType(MediaType.APPLICATION_JSON) .body(request) @@ -380,6 +391,7 @@ public void testGetPushNotificationSuccess() throws Exception { assertEquals("http://example.com", config.pushNotificationConfig().url()); } catch (Exception e) { } finally { + deletePushNotificationConfigInStore(MINIMAL_TASK.getId(), MINIMAL_TASK.getId()); deleteTaskInTaskStore(MINIMAL_TASK.getId()); } } @@ -842,6 +854,220 @@ private void testSendStreamingMessage(String mediaType) throws Exception { } + @Test + public void testListPushNotificationConfigWithConfigId() throws Exception { + saveTaskInTaskStore(MINIMAL_TASK); + PushNotificationConfig notificationConfig1 = + new PushNotificationConfig.Builder() + .url("http://example.com") + .id("config1") + .build(); + PushNotificationConfig notificationConfig2 = + new PushNotificationConfig.Builder() + .url("http://example.com") + .id("config2") + .build(); + savePushNotificationConfigInStore(MINIMAL_TASK.getId(), notificationConfig1); + savePushNotificationConfigInStore(MINIMAL_TASK.getId(), notificationConfig2); + + try { + ListTaskPushNotificationConfigResponse listResponse = client.listTaskPushNotificationConfig("111", MINIMAL_TASK.getId()); + assertEquals("111", listResponse.getId()); + assertEquals(2, listResponse.getResult().size()); + assertEquals(new TaskPushNotificationConfig(MINIMAL_TASK.getId(), notificationConfig1), listResponse.getResult().get(0)); + assertEquals(new TaskPushNotificationConfig(MINIMAL_TASK.getId(), notificationConfig2), listResponse.getResult().get(1)); + } catch (Exception e) { + fail(); + } finally { + deletePushNotificationConfigInStore(MINIMAL_TASK.getId(), "config1"); + deletePushNotificationConfigInStore(MINIMAL_TASK.getId(), "config2"); + deleteTaskInTaskStore(MINIMAL_TASK.getId()); + } + } + + @Test + public void testListPushNotificationConfigWithoutConfigId() throws Exception { + saveTaskInTaskStore(MINIMAL_TASK); + PushNotificationConfig notificationConfig1 = + new PushNotificationConfig.Builder() + .url("http://1.example.com") + .build(); + PushNotificationConfig notificationConfig2 = + new PushNotificationConfig.Builder() + .url("http://2.example.com") + .build(); + savePushNotificationConfigInStore(MINIMAL_TASK.getId(), notificationConfig1); + + // will overwrite the previous one + savePushNotificationConfigInStore(MINIMAL_TASK.getId(), notificationConfig2); + try { + ListTaskPushNotificationConfigResponse listResponse = client.listTaskPushNotificationConfig("111", MINIMAL_TASK.getId()); + assertEquals("111", listResponse.getId()); + assertEquals(1, listResponse.getResult().size()); + + PushNotificationConfig expectedNotificationConfig = new PushNotificationConfig.Builder() + .url("http://2.example.com") + .id(MINIMAL_TASK.getId()) + .build(); + assertEquals(new TaskPushNotificationConfig(MINIMAL_TASK.getId(), expectedNotificationConfig), + listResponse.getResult().get(0)); + } catch (Exception e) { + fail(); + } finally { + deletePushNotificationConfigInStore(MINIMAL_TASK.getId(), MINIMAL_TASK.getId()); + deleteTaskInTaskStore(MINIMAL_TASK.getId()); + } + } + + @Test + public void testListPushNotificationConfigTaskNotFound() { + try { + client.listTaskPushNotificationConfig("111", "non-existent-task"); + fail(); + } catch (A2AServerException e) { + assertInstanceOf(TaskNotFoundError.class, e.getCause()); + } + } + + @Test + public void testListPushNotificationConfigEmptyList() throws Exception { + saveTaskInTaskStore(MINIMAL_TASK); + try { + ListTaskPushNotificationConfigResponse listResponse = client.listTaskPushNotificationConfig("111", MINIMAL_TASK.getId()); + assertEquals("111", listResponse.getId()); + assertEquals(0, listResponse.getResult().size()); + } catch (Exception e) { + fail(); + } finally { + deleteTaskInTaskStore(MINIMAL_TASK.getId()); + } + } + + @Test + public void testDeletePushNotificationConfigWithValidConfigId() throws Exception { + saveTaskInTaskStore(MINIMAL_TASK); + saveTaskInTaskStore(new Task.Builder() + .id("task-456") + .contextId("session-xyz") + .status(new TaskStatus(TaskState.SUBMITTED)) + .build()); + + PushNotificationConfig notificationConfig1 = + new PushNotificationConfig.Builder() + .url("http://example.com") + .id("config1") + .build(); + PushNotificationConfig notificationConfig2 = + new PushNotificationConfig.Builder() + .url("http://example.com") + .id("config2") + .build(); + savePushNotificationConfigInStore(MINIMAL_TASK.getId(), notificationConfig1); + savePushNotificationConfigInStore(MINIMAL_TASK.getId(), notificationConfig2); + savePushNotificationConfigInStore("task-456", notificationConfig1); + + try { + // specify the config ID to delete + DeleteTaskPushNotificationConfigResponse deleteResponse = client.deleteTaskPushNotificationConfig(MINIMAL_TASK.getId(), + "config1"); + assertNull(deleteResponse.getError()); + assertNull(deleteResponse.getResult()); + + // should now be 1 left + ListTaskPushNotificationConfigResponse listResponse = client.listTaskPushNotificationConfig(MINIMAL_TASK.getId()); + assertEquals(1, listResponse.getResult().size()); + + // should remain unchanged, this is a different task + listResponse = client.listTaskPushNotificationConfig("task-456"); + assertEquals(1, listResponse.getResult().size()); + } catch (Exception e) { + fail(); + } finally { + deletePushNotificationConfigInStore(MINIMAL_TASK.getId(), "config1"); + deletePushNotificationConfigInStore(MINIMAL_TASK.getId(), "config2"); + deletePushNotificationConfigInStore("task-456", "config1"); + deleteTaskInTaskStore(MINIMAL_TASK.getId()); + deleteTaskInTaskStore("task-456"); + } + } + + @Test + public void testDeletePushNotificationConfigWithNonExistingConfigId() throws Exception { + saveTaskInTaskStore(MINIMAL_TASK); + PushNotificationConfig notificationConfig1 = + new PushNotificationConfig.Builder() + .url("http://example.com") + .id("config1") + .build(); + PushNotificationConfig notificationConfig2 = + new PushNotificationConfig.Builder() + .url("http://example.com") + .id("config2") + .build(); + savePushNotificationConfigInStore(MINIMAL_TASK.getId(), notificationConfig1); + savePushNotificationConfigInStore(MINIMAL_TASK.getId(), notificationConfig2); + + try { + DeleteTaskPushNotificationConfigResponse deleteResponse = client.deleteTaskPushNotificationConfig(MINIMAL_TASK.getId(), + "non-existent-config-id"); + assertNull(deleteResponse.getError()); + assertNull(deleteResponse.getResult()); + + // should remain unchanged + ListTaskPushNotificationConfigResponse listResponse = client.listTaskPushNotificationConfig(MINIMAL_TASK.getId()); + assertEquals(2, listResponse.getResult().size()); + } catch (Exception e) { + fail(); + } finally { + deletePushNotificationConfigInStore(MINIMAL_TASK.getId(), "config1"); + deletePushNotificationConfigInStore(MINIMAL_TASK.getId(), "config2"); + deleteTaskInTaskStore(MINIMAL_TASK.getId()); + } + } + + @Test + public void testDeletePushNotificationConfigTaskNotFound() { + try { + client.deleteTaskPushNotificationConfig("non-existent-task", "non-existent-config-id"); + fail(); + } catch (A2AServerException e) { + assertInstanceOf(TaskNotFoundError.class, e.getCause()); + } + } + + @Test + public void testDeletePushNotificationConfigSetWithoutConfigId() throws Exception { + saveTaskInTaskStore(MINIMAL_TASK); + PushNotificationConfig notificationConfig1 = + new PushNotificationConfig.Builder() + .url("http://1.example.com") + .build(); + PushNotificationConfig notificationConfig2 = + new PushNotificationConfig.Builder() + .url("http://2.example.com") + .build(); + savePushNotificationConfigInStore(MINIMAL_TASK.getId(), notificationConfig1); + + // this one will overwrite the previous one + savePushNotificationConfigInStore(MINIMAL_TASK.getId(), notificationConfig2); + + try { + DeleteTaskPushNotificationConfigResponse deleteResponse = client.deleteTaskPushNotificationConfig(MINIMAL_TASK.getId(), + MINIMAL_TASK.getId()); + assertNull(deleteResponse.getError()); + assertNull(deleteResponse.getResult()); + + // should now be 0 + ListTaskPushNotificationConfigResponse listResponse = client.listTaskPushNotificationConfig(MINIMAL_TASK.getId()); + assertEquals(0, listResponse.getResult().size()); + } catch (Exception e) { + fail(); + } finally { + deletePushNotificationConfigInStore(MINIMAL_TASK.getId(), MINIMAL_TASK.getId()); + deleteTaskInTaskStore(MINIMAL_TASK.getId()); + } + } + private SendStreamingMessageResponse extractJsonResponseFromSseLine(String line) throws JsonProcessingException { line = extractSseData(line); if (line != null) { @@ -1025,6 +1251,36 @@ private int getStreamingSubscribedCount() { } } + protected void deletePushNotificationConfigInStore(String taskId, String configId) throws Exception { + HttpClient client = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(("http://localhost:" + serverPort + "/test/task/" + taskId + "/config/" + configId))) + .DELETE() + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() != 200) { + throw new RuntimeException(response.statusCode() + ": Deleting task failed!" + response.body()); + } + } + + protected void savePushNotificationConfigInStore(String taskId, PushNotificationConfig notificationConfig) throws Exception { + HttpClient client = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + serverPort + "/test/task/" + taskId)) + .POST(HttpRequest.BodyPublishers.ofString(Utils.OBJECT_MAPPER.writeValueAsString(notificationConfig))) + .header("Content-Type", APPLICATION_JSON) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() != 200) { + throw new RuntimeException(response.statusCode() + ": Creating task push notification config failed! " + response.body()); + } + } + private static class BreakException extends RuntimeException { } diff --git a/tests/server-common/src/test/java/io/a2a/server/apps/common/AgentCardProducer.java b/tests/server-common/src/test/java/io/a2a/server/apps/common/AgentCardProducer.java index f68f44967..3354c1522 100644 --- a/tests/server-common/src/test/java/io/a2a/server/apps/common/AgentCardProducer.java +++ b/tests/server-common/src/test/java/io/a2a/server/apps/common/AgentCardProducer.java @@ -32,6 +32,7 @@ public AgentCard agentCard() { .defaultInputModes(Collections.singletonList("text")) .defaultOutputModes(Collections.singletonList("text")) .skills(new ArrayList<>()) + .protocolVersion("0.2.5") .build(); } } diff --git a/tests/server-common/src/test/java/io/a2a/server/apps/common/TestUtilsBean.java b/tests/server-common/src/test/java/io/a2a/server/apps/common/TestUtilsBean.java index 8cd333a55..c65766dd8 100644 --- a/tests/server-common/src/test/java/io/a2a/server/apps/common/TestUtilsBean.java +++ b/tests/server-common/src/test/java/io/a2a/server/apps/common/TestUtilsBean.java @@ -2,14 +2,13 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.core.Response; import io.a2a.server.events.QueueManager; +import io.a2a.server.tasks.PushNotificationConfigStore; import io.a2a.server.tasks.TaskStore; import io.a2a.spec.Event; +import io.a2a.spec.PushNotificationConfig; import io.a2a.spec.Task; -import io.quarkus.arc.profile.IfBuildProfile; /** * Contains utilities to interact with the server side for the tests. @@ -28,6 +27,9 @@ public class TestUtilsBean { @Inject QueueManager queueManager; + @Inject + PushNotificationConfigStore pushNotificationConfigStore; + public void saveTask(Task task) { taskStore.save(task); } @@ -47,4 +49,12 @@ public void ensureQueue(String taskId) { public void enqueueEvent(String taskId, Event event) { queueManager.get(taskId).enqueueEvent(event); } + + public void deleteTaskPushNotificationConfig(String taskId, String configId) { + pushNotificationConfigStore.deleteInfo(taskId, configId); + } + + public void saveTaskPushNotificationConfig(String taskId, PushNotificationConfig notificationConfig) { + pushNotificationConfigStore.setInfo(taskId, notificationConfig); + } }