Skip to content

Cross Language Unified Event#631

Merged
wenjin272 merged 13 commits intoapache:mainfrom
addu390:feature/custom-event-def-v3
May 7, 2026
Merged

Cross Language Unified Event#631
wenjin272 merged 13 commits intoapache:mainfrom
addu390:feature/custom-event-def-v3

Conversation

@addu390
Copy link
Copy Markdown
Contributor

@addu390 addu390 commented Apr 15, 2026

Linked issue: #424

Purpose of change

Add unified event support. Users can create events with a type string and attributes map instead of defining subclasses. Enables string-based event routing and JSON-based cross-language transport. Migrates/removes backward compatible with existing subclassed events.

Tests

New unit tests in Java (EventTest, EventLogRecordJsonSerdeTest) and Python (test_event, test_decorators, test_agent_plan, test_local_execution_environment) covering unified event creation, serialization, routing, and backward compatibility.

API

Yes. Event class (Java/Python) gains type, attributes, getType()/get_type(). @Action annotation gains listenEventTypes().

More details about the design in the issue #424

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

@github-actions github-actions Bot added doc-needed Your PR changes impact docs. fixVersion/0.3.0 The feature or bug should be implemented/fixed in the 0.3.0 version. priority/major Default priority of the PR or issue. labels Apr 15, 2026
@addu390 addu390 changed the title Feature/custom event def v3 Cross Language Unified Event Apr 15, 2026
@github-actions github-actions Bot added doc-needed Your PR changes impact docs. and removed doc-needed Your PR changes impact docs. labels Apr 15, 2026
@addu390 addu390 marked this pull request as ready for review April 16, 2026 00:24
@addu390
Copy link
Copy Markdown
Contributor Author

addu390 commented Apr 16, 2026

Hi @wenjin272, I closed the PR #561 and opened this.
I'll clean-up the PR a bit more, but up for a review otherwise.

Do take a first pass when you get a chance.

Copy link
Copy Markdown
Collaborator

@wenjin272 wenjin272 left a comment

Choose a reason for hiding this comment

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

Hi, @addu390, thanks for you work. Refactoring the Event will have a significant impact on the framework; therefore, this work requires careful design and attention to numerous details. Thanks Again.

I think though some details still need to be addressed, the current Python event design aligns with my expectations, including

  • action listen to event.type
  • json based DeSer
  • remove PythonEvent in java, unified in Event
  • Subclasses of Event provide from_event method to validate and reconstruct specific event object.

However, the Java event design does not appear to be aligned with the Python approach. In fact, after completing the Event refactoring, we will continue to support cross-language usage of Actions. This means a Python event sent by a Python action may be delivered to a Java action. Therefore, Python events and Java events must maintain consistency in both design and usage.

By the way, there are some conflicts between the PR and the main branch, likely because the main branch was recently reformatted.

Comment thread api/src/main/java/org/apache/flink/agents/api/annotation/Action.java Outdated
Comment thread api/src/main/java/org/apache/flink/agents/api/Event.java Outdated
private final String type;
private final Map<String, Object> attributes;
/** The timestamp of the source record. */
private Long sourceTimestamp;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we can remove this field to keep the align between python Event and java Event

Copy link
Copy Markdown
Contributor Author

@addu390 addu390 Apr 17, 2026

Choose a reason for hiding this comment

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

sourceTimestamp seems to be used in runtime for timestamp propagation (ActionExecutionOperator, RunnerContextImpl, JavaActionTask, etc.). Safe to remove?

I could however, specifically not use in the cross-language event contract, without any repercussions.

Comment thread python/flink_agents/api/events/chat_event.py Outdated
Comment thread python/flink_agents/api/events/chat_event.py
Comment thread python/flink_agents/api/events/event.py Outdated
Comment thread python/flink_agents/api/tests/test_decorators.py Outdated
Comment thread python/flink_agents/runtime/flink_runner_context.py Outdated
event = Event.from_json(event_json)
event_class = Event._type_registry.get(event.type)
if event_class is not None and hasattr(event_class, "from_event"):
return event_class.from_event(event)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Similar to my comment in Event, the framework does not need to be aware of the subclass types of Event. This would require the framework to handle many details of serialization and deserialization.
Fro subclass types of Event, user need use SubClass.from_event() to reconstruct the specific event object in action, like what you do in built-in tool call action and chat action.

@addu390
Copy link
Copy Markdown
Contributor Author

addu390 commented Apr 17, 2026

@wenjin272 Agreed on consistency between Java and Python, the reasoning was that it was quite a larger change, so, was considering having a follow-up PR.
However, It's a fair call-out, let me actually just do that too in this PR, doing so covers most of the review comments too.

@addu390
Copy link
Copy Markdown
Contributor Author

addu390 commented Apr 19, 2026

@wenjin272 Thanks again for the review! I’ve addressed the broader concern of consistency between Python and Java implementations, along side other review comments. Please take another look.

Copy link
Copy Markdown
Collaborator

@wenjin272 wenjin272 left a comment

Choose a reason for hiding this comment

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

Hi, @addu390. I left some comments, mainly because Java's Event lacks the fromEvent method.

@SuppressWarnings("unchecked")
public List<ChatMessage> getMessages() {
return messages;
return (List<ChatMessage>) getAttr("messages");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In the python ChatRequestEvent, we deserialize the messages field in from_event method. But the java Event is missing corresponding method. This may cause ClassCastException.

Comment thread api/src/main/java/org/apache/flink/agents/api/Event.java
Comment thread api/src/main/java/org/apache/flink/agents/api/Event.java Outdated
Comment thread api/src/main/java/org/apache/flink/agents/api/EventContext.java Outdated
@addu390 addu390 requested a review from wenjin272 April 22, 2026 15:47
@addu390
Copy link
Copy Markdown
Contributor Author

addu390 commented Apr 23, 2026

@wenjin272 I have addressed the comments related to fromEvent, can you take an other pass when you get chance?

@wenjin272
Copy link
Copy Markdown
Collaborator

@wenjin272 I have addressed the comments related to fromEvent, can you take an other pass when you get chance?

Sorry for the delay in reviewing — I'll get to this PR today or tomorrow.

Copy link
Copy Markdown
Collaborator

@wenjin272 wenjin272 left a comment

Choose a reason for hiding this comment

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

Hi, @addu390, thanks for your work.

Personally, I think this PR is nearly complete. I left one remaining comment. However, this PR has far-reaching implications, so I’ll also ask others to review it.

The current commit history is somewhat disorganized, which may make it difficult for others to review. If possible, please reorganize the commits. For example, organize the changes into two commits: one for event refactoring and another for test and example adaptations. Python and Java can also be separated.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this should be changed to ChatRequestEvent.fromEvent(event). The same applies to other actions, including built-in actions, as well as actions in tests and examples. I've checked, and it seems that everything on the Python side has been handled.

@addu390 addu390 force-pushed the feature/custom-event-def-v3 branch from 42660b0 to 71bce68 Compare May 2, 2026 15:37
@addu390 addu390 requested a review from wenjin272 May 2, 2026 15:58
@addu390 addu390 force-pushed the feature/custom-event-def-v3 branch 2 times, most recently from 591b89e to 1389e87 Compare May 2, 2026 16:23
@addu390
Copy link
Copy Markdown
Contributor Author

addu390 commented May 2, 2026

@wenjin272 Thanks again for the quick reviews, I have addressed the comments and organized the commits, do have a look again! And agreed on the far-reaching implications, @xintongsong could you do a review as well?

Copy link
Copy Markdown
Collaborator

@wenjin272 wenjin272 left a comment

Choose a reason for hiding this comment

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

Hi, @addu390. Sorry for the delayed reply due to the holiday. I left a few comments regarding the details.

This PR introduces significant changes to the framework, please take a look at your convenience @xintongsong.

@@ -41,7 +41,7 @@ public static Action getToolCallAction() throws Exception {
ToolCallAction.class,
"processToolRequest",
new Class[] {ToolRequestEvent.class, RunnerContext.class}),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here, we constrain the first parameter of processToolRequest to be of type ToolRequestEvent. However, in cross-language invocations, only generic Event objects are passed rather than specific subclasses, which results in an argument type mismatch error.

I think this can be aligned with ChatModelAction by changing the parameter type to Event and calling ToolRequestEvent.fromEvent(event) within the function to convert the Event into a ToolRequestEvent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's the pattern. I thought I fixed this in all places, I'll do another full sweep again.

}
}
}
return new ChatRequestEvent(model, messages, event.getAttr("output_schema"));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Here, we do not copy the event's id. Therefore, ChatRequestEvent generates a new random id, which causes the same ChatRequestEvent to have different id on the sender and receiver sides.

This issue also exists for other built-in Event subclasses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The purpose of ActionState is to enable action-level recovery after a job failover. However, the HashMap storing attributes does not guarantee strict consistency in key order before and after a JVM restart, which may lead to inconsistent UUIDs.

After the Event refactoring, it seems that there is no distinction made for InputEvent here. I think we can unify it into

return String.valueOf(
                    UUID.nameUUIDFromBytes(MAPPER.writeValueAsBytes(event.getAttributes())));

And the MAPPER needs to have sorting enabled.

private static final ObjectMapper MAPPER =
            JsonMapper.builder()
                    .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
                    .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
                    .build();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nice find! Now that all event data lives in attributes, can unify the branches and use a sorted ObjectMapper for key ordering. Updated.

@github-actions github-actions Bot added doc-included Your PR already contains the necessary documentation updates. and removed doc-needed Your PR changes impact docs. labels May 6, 2026
Copy link
Copy Markdown
Contributor

@xintongsong xintongsong left a comment

Choose a reason for hiding this comment

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

@addu390 @wenjin272

Thank you both for working on this. I have no more comments except for the following two.

{{< tab "Python" >}}
```python
@action(InputEvent)
@action("_input_event")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In the documentation, we should also use InputEvent.EVENT_TYPE rather than the string literal. Please also check for other occurrences in the documentation.

@@ -41,7 +41,7 @@ public static Action getToolCallAction() throws Exception {
ToolCallAction.class,
"processToolRequest",
new Class[] {ToolRequestEvent.class, RunnerContext.class}),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1

Comment thread python/flink_agents/api/events/event.py Outdated
"""

id: UUID = Field(default=None)
type: str | None = Field(default=None)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we allow type to be None?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not anymore, I'll remove the None support.

@addu390 addu390 requested a review from xintongsong May 6, 2026 17:21
@addu390 addu390 requested a review from wenjin272 May 6, 2026 17:21
@addu390
Copy link
Copy Markdown
Contributor Author

addu390 commented May 6, 2026

@wenjin272 @xintongsong Did a full cross-language consistency sweep, all @Action methods use base Event + fromEvent(), updated docs and JSON fixtures to match. And I resolved a few older comments, so, it's easier to track the open reviews/discussions.

@wenjin272 wenjin272 force-pushed the feature/custom-event-def-v3 branch from f4ee31e to 6b9ecb6 Compare May 7, 2026 09:05
addu390 and others added 12 commits May 7, 2026 17:06
- Standardize Event on type strings and attributes map for cross-language interop
- Add Event.fromEvent() base method; subclasses override for typed reconstruction
- Store built-in event fields in attributes map with snake_case keys
- Add @JsonIgnore on getters to prevent duplicate serialization
- Move @JsonCreator type validation into Event constructor
- Remove listenEvents() from @action annotation, use listenEventTypes() exclusively
- Replace instanceof casts with EVENT_TYPE checks and fromEvent() in built-in actions
- Remove EventContext.eventClass; simplify event log deserialization to base Event
- Remove PythonEvent wrapper; use unified Event with JSON serde

Co-authored-by: Cursor <cursoragent@cursor.com>
- Update all @action annotations from listenEvents to listenEventTypes with EVENT_TYPE
- Replace direct event casts with fromEvent() in e2e agents and test assertions
- Update JSON test resources for new event format (type strings, attributes map)
- Update documentation examples to use string-based event types

Co-authored-by: Cursor <cursoragent@cursor.com>
- Standardize Event on type strings and attributes dict for cross-language interop
- Remove _type_registry auto-dispatch; require explicit SubClass.from_event() in actions
- Add get_attr()/set_attr() to base Event; update property accessors to use them
- Enforce string-only @action decorator arguments
- Handle JSON round-trip for Row, MemoryRef, OutputSchema, and UUID keys
- Remove cloudpickle dependency from event serialization path

Co-authored-by: Cursor <cursoragent@cursor.com>
- Update all @action decorators to use EVENT_TYPE string constants
- Replace direct event attribute access with SubClass.from_event() pattern
- Add return type annotations to staticmethod actions for linting compliance
- Update test assertions and JSON resources for new event format

Co-authored-by: Cursor <cursoragent@cursor.com>
@wenjin272 wenjin272 force-pushed the feature/custom-event-def-v3 branch from 6b9ecb6 to 3f52c6b Compare May 7, 2026 09:06
@wenjin272
Copy link
Copy Markdown
Collaborator

wenjin272 commented May 7, 2026

Hi, @addu390. Some @Action methods in the documentation were not using base Event + fromEvent(). Since they were scattered across various sections, I fixed them directly. Also fixed the conflicts with the main branch.

Thanks for your contribution, merged.

@wenjin272 wenjin272 merged commit 071c3a0 into apache:main May 7, 2026
67 of 69 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-included Your PR already contains the necessary documentation updates. fixVersion/0.3.0 The feature or bug should be implemented/fixed in the 0.3.0 version. priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants