yunfengzhou-hub opened a new pull request, #1159:
URL: https://github.com/apache/flink-agents/pull/1159

   Linked issue: #1085
   
   ### Purpose of change
   
   A cross-language action is now declared directly on the member that is its 
target. In Java, `@Action` on a `static final` field holding an api-layer 
`Function` descriptor declares that descriptor as the cross-language target; in 
Python, applying `action(...)` to a `Function` descriptor does the same. The 
same annotation on a method, or the same decorator on a function, declares the 
native implementation. No member is a placeholder, and there is no separate 
`target` naming something the annotation merely sits beside. The member name is 
the action name; an optional `name` overrides it. Malformed declarations now 
fail when the plan is built.
   
   #### Runtime flow
   
   Plan construction walks the concrete agent's own declared members. Java 
`AgentPlan` reads `@Action` methods as native implementations and `@Action` 
fields through `readActionDescriptorField` (which requires `static final`, 
non-null, an api-layer `Function`), then lifts the descriptor with 
`toPlanFunction`. Python `action(...)` marks a function native, or wraps a 
descriptor in an immutable `ActionDeclaration` that `from_agent` collects. Both 
resolve the name the same way and reject duplicates and inherited actions as 
they register.
   
   #### Key decisions
   
   **The target is declared, not mirrored.** A Java annotation attribute cannot 
hold an arbitrary object (JLS 9.6.1), which is what forced the old mirror 
`@PythonFunction`; a `static final` field holds a real 
`api.function.PythonFunction`/`JavaFunction` instead, the same object the plan 
already compiles. `PythonFunction.of(module, qualName)` is added as the 
readable factory, mirroring the existing `JavaFunction.forAction`.
   
   **Python wraps rather than mutates.** `action(...)(descriptor)` returns a 
frozen `ActionDeclaration`, so a descriptor shared by two actions keeps each 
one's trigger conditions and name separate.
   
   **Empty name means unset, in both languages.** A Java annotation cannot tell 
its `""` default from an explicit `""`, so the name falls back to the member; 
Python normalizes `""` to `None` to match.
   
   ### Behavioral Semantics
   
   #### Interaction decisions
   
   | Member | `name` | Placement | Result |
   |---|---|---|---|
   | Java `static` method / Python function | unset or `""` | own class | 
native action, name = member |
   | Java `static final` `Function` field / Python descriptor | unset or `""` | 
own class | cross-language action, name = member |
   | either of the above | non-empty | own class | name = override |
   | Java field not `static final` / `null` / not a `Function` | any | own 
class | plan build fails |
   | Python target neither callable nor descriptor, or a descriptor with an 
empty identifier | any | own class | `action()` raises |
   | any action | any | inherited from a superclass | plan build fails |
   | two members resolving to one name | any | own class | plan build fails |
   
   #### Behavioral contracts
   
   1. A Java `@Action` method, or a Python `action`-decorated function, is the 
native implementation; no stub body is required.
   2. A Java `@Action` `static final` field holding an api-layer `Function`, or 
a Python `action(...)` applied to a descriptor, makes that descriptor the 
cross-language target.
   3. Python `action(...)(descriptor)` returns an immutable 
`ActionDeclaration`; a shared descriptor is not mutated, so each declaration 
keeps its own trigger conditions and name.
   4. The action name is the member name unless a non-empty `name` overrides 
it; `""` means "no override" in both languages.
   5. A malformed Java action field, one that is not `static final`, is `null`, 
or is not a `Function`, is rejected at plan construction.
   6. An action inherited from a superclass is rejected; it must sit on the 
concrete agent.
   7. Two actions resolving to the same name are rejected.
   8. A descriptor action compiles to the same exec as before, so the plan wire 
format and the YAML API are unchanged.
   
   #### Failure behavior
   
   Every malformed declaration fails fast at build time; none is silently 
dropped or deferred to dispatch. The behavior change worth naming: an inherited 
action used to be skipped silently, and now raises.
   
   - Java plan construction raises `IllegalStateException` for a 
non-`static-final` / `null` / non-`Function` field, an inherited action, and a 
duplicate name.
   - Python `action()` raises at decoration time for a target that is neither 
callable nor a descriptor, and for a descriptor with an empty required 
identifier.
   - Python `from_agent` raises `RuntimeError` for an inherited action and a 
duplicate name.
   - An empty-string `name` is not a failure; it is normalized to the member 
name.
   
   ### Tests
   
   Both suites run offline. Java: `AgentPlanTest`. Python: 
`test_decorators.py`, `test_agent_plan.py`, 
`test_agent_plan_cross_language.py`. Every contract is pinned on both sides 
where the language exposes it:
   
   | Contract | Java | Python |
   |---|---|---|
   | 1 native member is the implementation | ✅ | ✅ |
   | 2 descriptor member is the target | ✅ | ✅ |
   | 3 immutable `ActionDeclaration`, shared descriptor not mutated | — | ✅ |
   | 4 name default / override / empty | ✅ | ✅ |
   | 5 malformed field rejected | ✅ | ✅ |
   | 6 inherited action rejected | ✅ | ✅ |
   | 7 duplicate name rejected | ✅ | ✅ |
   | 8 exec and wire format unchanged | ✅ | ✅ |
   
   The failure paths carry the risk and are asserted on the raised exception 
and its message, not merely on "did not register."
   
   Not verified: no test drives a live cross-language dispatch. The compiled 
exec is asserted equal to the pre-change form, and actual Python-to-Java 
invocation is covered by the existing end-to-end suites, unchanged here. Java 
also validates a field's shape (`static final`, non-null, is a `Function`) but 
not that the descriptor's `module`/`qualName` are non-empty (the api 
constructors reject `null` only), whereas Python's `action()` rejects empty 
identifiers; an empty-identifier descriptor on the Java side is not caught at 
plan build.
   
   <details>
   <summary>Contracts-to-tests mapping, migration, and implementation 
invariants</summary>
   
   Contract to tests (Java / Python):
   
   1. `testNativeMethodActionCompilesToJavaFunctionExec` / 
`test_native_action_does_not_produce_declaration`
   2. `testFieldDescriptorCompilesToPythonFunctionExec` / 
`test_descriptor_action_compiles_to_plan_java_function`, 
`test_compile_agent_with_descriptor_action_class_attribute`
   3. (Java has no descriptor-mutation surface) / 
`test_action_applied_to_descriptor_returns_immutable_declaration`, 
`test_action_declaration_does_not_mutate_descriptor`, 
`test_action_declaration_keeps_shared_descriptor_declarations_distinct`
   4. `testNameOverrideReplacesMemberName`, 
`testNameOverrideReplacesMethodName` / 
`test_native_action_records_name_override`, 
`test_empty_name_override_is_normalized_to_none`, 
`test_empty_name_override_falls_back_to_attribute_name`, 
`test_compile_agent_with_named_descriptor_action_class_attribute`
   5. `testNonStaticFinalActionFieldIsRejected`, 
`testNullActionFieldIsRejected`, `testNonDescriptorActionFieldIsRejected` / 
`test_action_rejects_non_callable_non_descriptor`
   6. `testActionFieldInheritedFromParentAgentClassIsRejected` / 
`test_descriptor_action_inherited_from_parent_agent_class_is_rejected`
   7. `testDuplicateActionNameIsRejected` / 
`test_duplicate_action_name_is_rejected`
   8. `testNativeMethodActionCompilesToJavaFunctionExec`, 
`testFieldDescriptorCompilesToPythonFunctionExec` / 
`test_descriptor_action_compiles_to_plan_java_function`
   
   Python also pins empty-identifier rejection: 
`test_action_rejects_java_descriptor_with_empty_qualname`, 
`test_action_rejects_java_descriptor_with_empty_method_name`, 
`test_action_rejects_python_descriptor_with_empty_module`, 
`test_action_rejects_python_descriptor_with_empty_qualname`, 
`test_action_descriptor_error_names_override`.
   
   Migration. Java `@Action(EventType.InputEvent) private static final 
PythonFunction handleInput = PythonFunction.of("my_pkg.handlers", 
"handle_input");` replaces the `@Action(target = @PythonFunction(...))` stub 
method. Python `handle_input = 
action(InputEvent.EVENT_TYPE)(JavaFunction.for_action("com.example.MyHandlers", 
"handleInput"))` replaces `action(..., target=...)`.
   
   Implementation invariants, not caller-observable:
   
   - Java `resolveActionName` returns the member name when the override is 
`null` or empty; the field and method branches both funnel through 
`toPlanFunction`.
   - Python `ActionDeclaration` is a frozen dataclass over 
`(trigger_conditions, func, name)`; only the native path sets 
`_action_name`/`_trigger_conditions` on the function.
   - `registerAction` (Java) and `from_agent` (Python) key actions by resolved 
name and reject a collision on insert.
   - `JavaFunction.forAction`/`for_action` and the api 
`PythonFunction`/`JavaFunction` constructors are unchanged; 
`PythonFunction.of()` is additive.
   
   </details>
   
   ### API
   
   Breaking, within the 0.4.0 window; the old placeholder forms are removed 
outright rather than deprecated. Java drops `@Action.target()` and the nested 
`api.annotation.PythonFunction`; Python drops `action(..., target=...)`. 
Migration examples are in the collapsed block under Tests.
   
   Unchanged: native method/function actions; 
`JavaFunction.forAction`/`for_action`; the api `PythonFunction`/`JavaFunction` 
constructors and wire form; trigger-condition (`value=`) semantics; the 
compiled plan and the YAML API. Additive: `PythonFunction.of()` and the `name` 
override.
   
   ### Documentation
   
   - [ ] `doc-needed`
   - [ ] `doc-not-needed`
   - [x] `doc-included`
   
   `docs/content/docs/development/workflow_agent.md` is migrated to the new 
forms, and the "stub that should raise" narrative is replaced by "the declared 
member is the executable target".
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   - [x] Yes
   - [ ] No
   
   Generated-by: Qoder 1.32.0 (Qwen3.8-Max)


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to