Skip to content

Conversation

@jmesnil
Copy link
Collaborator

@jmesnil jmesnil commented Nov 27, 2025

Expand PushNotificationSender to support all StreamingEventKind payload types
as defined in the A2A specification, not just Task objects.

Fixes: #490

  • Follow the CONTRIBUTING Guide.
  • Make your Pull Request title in the https://www.conventionalcommits.org/ specification.
    • Important Prefixes for release-please:
      • fix: which represents bug fixes, and correlates to a SemVer patch.
      • feat: represents a new feature, and correlates to a SemVer minor.
      • feat!:, or fix!:, refactor!:, etc., which represent a breaking change (indicated by the !) and will result in a SemVer major.
  • Ensure the tests pass
  • Appropriate READMEs were updated (if necessary)

Fixes #490 🦕

The payload of push notifications is encapsulated by the
`kind` of payload as specified in [§ 4.3.3. Push Notification Payload](https://a2a-protocol.org/latest/specification/#433-push-notification-payload)

This commit only works for `task` payload and will required more work to support additional payloads (tracked by a2aproject#490).

This fixes a2aproject#491
Signed-off-by: Jeff Mesnil <jmesnil@ibm.com>
@jmesnil
Copy link
Collaborator Author

jmesnil commented Nov 27, 2025

This PR builds on top of #492 and should be merged after.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @jmesnil, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the push notification system by generalizing its payload handling. Instead of being limited to only sending task-related notifications, the system can now dispatch any type of streaming event defined by the A2A specification. This change provides greater flexibility and aligns the notification mechanism with the broader event-driven architecture, allowing for more comprehensive and diverse real-time updates.

Highlights

  • Expanded Push Notification Payload Types: The PushNotificationSender now supports all StreamingEventKind payload types as defined in the A2A specification, moving beyond just Task objects. This includes Message, TaskStatusUpdateEvent, and TaskArtifactUpdateEvent.
  • Updated PushNotificationSender Interface and Implementation: The sendNotification method in the PushNotificationSender interface and its BasePushNotificationSender implementation have been updated to accept a generic StreamingEventKind object. The implementation now uses a switch expression to determine the taskId based on the specific event kind.
  • New Serialization/Deserialization Utilities: New utility methods, unmarshalStreamingEventKindFrom and marshalFrom, have been added to Utils.java to handle the serialization and deserialization of StreamingEventKind objects. These methods wrap the event payload in a JSON field named after its kind.
  • Enhanced Test Coverage: Integration tests and unit tests for the PushNotificationSender have been updated and expanded to verify the correct handling and dispatching of Message, TaskStatusUpdateEvent, and TaskArtifactUpdateEvent payloads.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request successfully expands the PushNotificationSender to support various StreamingEventKind payload types, not just Task objects. The changes are well-structured, introducing new utility methods for marshalling and unmarshalling different event kinds, and adding comprehensive tests for the new functionality. I've included a few review comments to enhance robustness, particularly around type handling in switch statements and in test mocks, to prevent potential runtime errors and silent failures.

Comment on lines +54 to +60
String taskId = switch (kind.getKind()) {
case TASK -> ((Task)kind).getId();
case MESSAGE -> ((Message)kind).getTaskId();
case STATUS_UPDATE -> ((TaskStatusUpdateEvent)kind).getTaskId();
case ARTIFACT_UPDATE -> ((TaskArtifactUpdateEvent)kind).getTaskId();
default -> null;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The switch statement on kind.getKind() uses a default -> null case, which will cause push notifications for any new or unhandled StreamingEventKind types to be silently dropped. This could lead to hard-to-debug issues.

Since StreamingEventKind is a sealed interface, you can use a pattern-matching switch statement on the kind object itself. This is safer as the compiler will enforce that all permitted subtypes are handled, eliminating the need for a default case and preventing silent failures.

        String taskId = switch (kind) {
            case Task t -> t.getId();
            case Message m -> m.getTaskId();
            case TaskStatusUpdateEvent e -> e.getTaskId();
            case TaskArtifactUpdateEvent e -> e.getTaskId();
        };

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good suggestion but this requires Java 21+ while the project targets Java 17.

Expand PushNotificationSender to support all StreamingEventKind payload types
as defined in the A2A specification, not just Task objects.

Fixes: a2aproject#490
Signed-off-by: Jeff Mesnil <jmesnil@ibm.com>
@jmesnil jmesnil force-pushed the 490_push_notifications_payload branch from c254a4a to 238aa55 Compare November 27, 2025 14:29
@jmesnil
Copy link
Collaborator Author

jmesnil commented Nov 27, 2025

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request successfully expands the PushNotificationSender to support multiple payload types as defined by StreamingEventKind, which is a great feature enhancement. The changes are logical and well-tested, with new test cases for Message, TaskStatusUpdateEvent, and TaskArtifactUpdateEvent. The introduction of Utils.marshalFrom and Utils.unmarshalStreamingEventKindFrom centralizes the custom serialization logic, which is a solid design choice. I have a few suggestions to improve code clarity, reduce duplication in tests, and address the use of a deprecated API.

Comment on lines +99 to +120
public static <K extends StreamingEventKind> K unmarshalStreamingEventKindFrom(String data) throws JsonProcessingException {
JsonNode root = OBJECT_MAPPER.readTree(data);

// Validate that there is exactly one field at the root level
if (root.size() != 1) {
throw new JsonParseException(null,
String.format("Expected exactly one field at root level, but found %d fields", root.size()));
}

// Get the "kind" field to determine which type to deserialize to
Map.Entry<String, JsonNode> entry = root.fields().next();
String kind = entry.getKey();
JsonNode payload = entry.getValue();

return (K) switch (kind) {
case TASK -> OBJECT_MAPPER.treeToValue(payload, Task.TYPE_REFERENCE);
case MESSAGE -> OBJECT_MAPPER.treeToValue(payload, Message.TYPE_REFERENCE);
case STATUS_UPDATE -> OBJECT_MAPPER.treeToValue(payload, TaskStatusUpdateEvent.TYPE_REFERENCE);
case ARTIFACT_UPDATE -> OBJECT_MAPPER.treeToValue(payload, TaskArtifactUpdateEvent.TYPE_REFERENCE);
default -> throw new JsonParseException(null, "Unexpected kind of payload: " + kind);
};
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

JsonParseException is deprecated in recent versions of Jackson. It's better to use a non-deprecated exception class like JsonMappingException to report issues with the JSON structure. This applies to both the root field validation and the unexpected kind handling.

    public static <K extends StreamingEventKind> K unmarshalStreamingEventKindFrom(String data) throws JsonProcessingException {
        JsonNode root = OBJECT_MAPPER.readTree(data);

        // Validate that there is exactly one field at the root level
        if (root.size() != 1) {
            throw new com.fasterxml.jackson.databind.JsonMappingException(null,
                String.format("Expected exactly one field at root level, but found %d fields", root.size()));
        }

        // Get the "kind" field to determine which type to deserialize to
        Map.Entry<String, JsonNode> entry = root.fields().next();
        String kind = entry.getKey();
        JsonNode payload = entry.getValue();

        return (K) switch (kind) {
            case TASK -> OBJECT_MAPPER.treeToValue(payload, Task.TYPE_REFERENCE);
            case MESSAGE -> OBJECT_MAPPER.treeToValue(payload, Message.TYPE_REFERENCE);
            case STATUS_UPDATE -> OBJECT_MAPPER.treeToValue(payload, TaskStatusUpdateEvent.TYPE_REFERENCE);
            case ARTIFACT_UPDATE -> OBJECT_MAPPER.treeToValue(payload, TaskArtifactUpdateEvent.TYPE_REFERENCE);
            default -> throw new com.fasterxml.jackson.databind.JsonMappingException(null, "Unexpected kind of payload: " + kind);
        };
    }

Comment on lines +54 to +62
String taskId = switch (kind.getKind()) {
case TASK -> ((Task)kind).getId();
case MESSAGE -> ((Message)kind).getTaskId();
case STATUS_UPDATE -> ((TaskStatusUpdateEvent)kind).getTaskId();
case ARTIFACT_UPDATE -> ((TaskArtifactUpdateEvent)kind).getTaskId();
default -> null;
};
if (taskId == null) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The switch statement with casting can be simplified using pattern matching for switch, which is available in modern Java versions. This would make the code more concise and type-safe. Since StreamingEventKind is a sealed interface, the switch expression is exhaustive, so the default case and the subsequent null check for taskId are no longer necessary.

Suggested change
String taskId = switch (kind.getKind()) {
case TASK -> ((Task)kind).getId();
case MESSAGE -> ((Message)kind).getTaskId();
case STATUS_UPDATE -> ((TaskStatusUpdateEvent)kind).getTaskId();
case ARTIFACT_UPDATE -> ((TaskArtifactUpdateEvent)kind).getTaskId();
default -> null;
};
if (taskId == null) {
return;
String taskId = switch (kind) {
case Task task -> task.getId();
case Message message -> message.getTaskId();
case TaskStatusUpdateEvent event -> event.getTaskId();
case TaskArtifactUpdateEvent event -> event.getTaskId();
};

Comment on lines +96 to +109
switch (kind.getKind()) {
case TASK:
tasks.add((Task) kind);
break;
case MESSAGE:
messages.add((Message) kind);
break;
case STATUS_UPDATE:
statusUpdateEvents.add((TaskStatusUpdateEvent)kind);
break;
case ARTIFACT_UPDATE:
artifactUpdateEvents.add((TaskArtifactUpdateEvent) kind);
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This switch statement can be modernized using pattern matching. This improves type safety and readability by avoiding explicit casts and break statements. Since StreamingEventKind is a sealed interface, the compiler will also ensure all cases are handled if you use a pattern switch.

                    switch (kind) {
                        case Task task -> tasks.add(task);
                        case Message message -> messages.add(message);
                        case TaskStatusUpdateEvent event -> statusUpdateEvents.add(event);
                        case TaskArtifactUpdateEvent event -> artifactUpdateEvents.add(event);
                    }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it requires Java 21+, the project targets 17+

Comment on lines +381 to +388
assertEquals(1, testHttpClient.headers.size());
Map<String, String> sentHeaders = testHttpClient.headers.get(0);
assertEquals(2, sentHeaders.size());
assertTrue(sentHeaders.containsKey(A2AHeaders.X_A2A_NOTIFICATION_TOKEN));
assertEquals(config.token(), sentHeaders.get(A2AHeaders.X_A2A_NOTIFICATION_TOKEN));
// Content-Type header should always be present
assertTrue(sentHeaders.containsKey(CONTENT_TYPE));
assertEquals(APPLICATION_JSON, sentHeaders.get(CONTENT_TYPE));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The header verification logic is duplicated across testSendNotificationWithMessage, testSendNotificationWithStatusUpdateEvent (lines 424-431), and testSendNotificationWithArtifactUpdateEvent (lines 473-480). Extracting this logic into a private helper method would reduce code duplication and improve test maintainability. For example:

private void assertNotificationHeaders(PushNotificationConfig config) {
    assertEquals(1, testHttpClient.headers.size());
    Map<String, String> sentHeaders = testHttpClient.headers.get(0);
    assertEquals(2, sentHeaders.size());
    assertTrue(sentHeaders.containsKey(A2AHeaders.X_A2A_NOTIFICATION_TOKEN));
    assertEquals(config.token(), sentHeaders.get(A2AHeaders.X_A2A_NOTIFICATION_TOKEN));
    assertTrue(sentHeaders.containsKey(CONTENT_TYPE));
    assertEquals(APPLICATION_JSON, sentHeaders.get(CONTENT_TYPE));
}

You could then call assertNotificationHeaders(config) from each test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to keep the assertions in each test

@jmesnil
Copy link
Collaborator Author

jmesnil commented Nov 27, 2025

/gemini summary

@gemini-code-assist
Copy link
Contributor

Summary of Changes

This pull request significantly enhances the push notification system by generalizing its payload handling. Instead of being limited to only sending task-related notifications, the system can now dispatch any type of streaming event defined by the A2A specification. This change provides greater flexibility and aligns the notification mechanism with the broader event-driven architecture, allowing for more comprehensive and diverse real-time updates.

Highlights

  • Expanded Push Notification Payload Types: The PushNotificationSender now supports all StreamingEventKind payload types as defined in the A2A specification, moving beyond just Task objects. This includes Message, TaskStatusUpdateEvent, and TaskArtifactUpdateEvent.
  • Updated PushNotificationSender Interface and Implementation: The sendNotification method in the PushNotificationSender interface and its BasePushNotificationSender implementation have been updated to accept a generic StreamingEventKind object. The implementation now uses a switch expression to determine the taskId based on the specific event kind.
  • New Serialization/Deserialization Utilities: New utility methods, unmarshalStreamingEventKindFrom and marshalFrom, have been added to Utils.java to handle the serialization and deserialization of StreamingEventKind objects. These methods wrap the event payload in a JSON field named after its kind.
  • Enhanced Test Coverage: Integration tests and unit tests for the PushNotificationSender have been updated and expanded to verify the correct handling and dispatching of Message, TaskStatusUpdateEvent, and TaskArtifactUpdateEvent payloads.
Changelog
  • extras/push-notification-config-store-database-jpa/src/test/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/JpaDatabasePushNotificationConfigStoreIntegrationTest.java
    • Imported StreamingEventKind to support broader event types.
    • Updated mockPushNotificationSender.getCapturedTasks() to getCapturedEvents() and changed the queue type from Queue<Task> to Queue<StreamingEventKind>.
    • Added filtering and casting logic to correctly extract and verify Task objects from the StreamingEventKind queue.
  • extras/push-notification-config-store-database-jpa/src/test/java/io/a2a/extras/pushnotificationconfigstore/database/jpa/MockPushNotificationSender.java
    • Replaced Task import with StreamingEventKind.
    • Modified the internal queue capturedTasks to capturedEvents of type Queue<StreamingEventKind>.
    • Updated the sendNotification method to accept StreamingEventKind and getCapturedTasks to getCapturedEvents to return the new queue type.
  • server-common/src/main/java/io/a2a/server/tasks/BasePushNotificationSender.java
    • Added static imports for Message.MESSAGE, Task.TASK, TaskArtifactUpdateEvent.ARTIFACT_UPDATE, and TaskStatusUpdateEvent.STATUS_UPDATE.
    • Updated the sendNotification method signature to accept StreamingEventKind instead of Task.
    • Implemented a switch expression to dynamically extract the taskId from different StreamingEventKind types.
    • Modified the dispatch and dispatchNotification methods to handle StreamingEventKind.
    • Replaced direct Task serialization with Utils.marshalFrom(kind) to handle various StreamingEventKind types.
  • server-common/src/main/java/io/a2a/server/tasks/PushNotificationSender.java
    • Updated the sendNotification method signature to accept StreamingEventKind instead of Task.
    • Adjusted Javadoc for the sendNotification method to reflect the new generic payload type.
  • server-common/src/test/java/io/a2a/server/requesthandlers/AbstractA2ARequestHandlerTest.java
    • Added imports for Map, StreamingEventKind, and JUnit annotations.
    • Modified the mock post method in TestHttpClient to unmarshal the request body as StreamingEventKind and then conditionally add it to the tasks list if it's an instance of Task.
  • server-common/src/test/java/io/a2a/server/tasks/PushNotificationSenderTest.java
    • Added static imports for various StreamingEventKind types and TaskState.WORKING.
    • Imported Artifact, Message, StreamingEventKind, TaskArtifactUpdateEvent, TaskStatusUpdateEvent, TextPart, and UpdateEvent.
    • Expanded TestHttpClient to include separate lists for messages, statusUpdateEvents, and artifactUpdateEvents.
    • Updated the post method in TestHttpClient to use Utils.unmarshalStreamingEventKindFrom and a switch statement to correctly categorize and store different StreamingEventKind payloads.
    • Added new test methods (testSendNotificationWithMessage, testSendNotificationWithStatusUpdateEvent, testSendNotificationWithArtifactUpdateEvent) to validate the handling of these new payload types.
  • spec/src/main/java/io/a2a/spec/TaskArtifactUpdateEvent.java
    • Added public static final TypeReference<TaskArtifactUpdateEvent> TYPE_REFERENCE for Jackson deserialization.
  • spec/src/main/java/io/a2a/spec/TaskStatusUpdateEvent.java
    • Added public static final TypeReference<TaskStatusUpdateEvent> TYPE_REFERENCE for Jackson deserialization.
  • spec/src/main/java/io/a2a/util/Utils.java
    • Added static imports for Message.MESSAGE, Task.TASK, TaskArtifactUpdateEvent.ARTIFACT_UPDATE, and TaskStatusUpdateEvent.STATUS_UPDATE.
    • Introduced unmarshalStreamingEventKindFrom method to deserialize JSON into appropriate StreamingEventKind subtypes based on a 'kind' field, including validation for single root field.
    • Introduced marshalFrom method to serialize StreamingEventKind objects by wrapping them in a JSON field named after their kind.
Activity
  • jmesnil commented that this PR builds on top of fix: Encapsulate the payload of Push notifications #492 and should be merged after it.
  • gemini-code-assist[bot] provided an initial summary of the changes.
  • gemini-code-assist[bot] raised a high-priority concern regarding the default -> null case in BasePushNotificationSender's switch statement, suggesting pattern matching for switch (Java 21+). jmesnil responded that the project targets Java 17.
  • gemini-code-assist[bot] raised a high-priority concern about an unsafe cast to Task in AbstractA2ARequestHandlerTest's mock post method, suggesting an instanceof check.
  • gemini-code-assist[bot] noted a medium-priority indentation issue in PushNotificationSenderTest.
  • gemini-code-assist[bot] suggested a medium-priority change to use assertFalse() instead of assertEquals(false, ...) for Boolean checks in PushNotificationSenderTest.
  • gemini-code-assist[bot] raised a high-priority concern about using the deprecated JsonParseException in Utils.unmarshalStreamingEventKindFrom, suggesting JsonMappingException instead.
  • gemini-code-assist[bot] suggested a medium-priority improvement to use pattern matching for switch in BasePushNotificationSender for taskId (Java 21+). jmesnil responded that the project targets Java 17.
  • gemini-code-assist[bot] suggested a medium-priority improvement to use pattern matching for switch in TestHttpClient's post method (Java 21+). jmesnil responded that the project targets Java 17.
  • gemini-code-assist[bot] suggested a medium-priority refactoring to extract duplicated header verification logic into a helper method in PushNotificationSenderTest. jmesnil preferred to keep the assertions in each test.

Copy link
Collaborator

@kabir kabir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might have to do something in DefaultRequestHandler/ResultAggregator to send these other events. At the moment, it seems to just get the latest version of the Task and send that.
But I am not sure exactly what should be sent when :-) e.g. if we get a TaskStatusUpdateEvent do we just send that, or do we also send the Task?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat]: Push Notification Payload can also be message, statusUpdate or artifactUpdate

2 participants