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

   Linked issue: Closes #1131
   
   ### Purpose of change
   
   A sub-agent registered in code — a live `SubagentSetup` added via 
`addResource(name, AGENT, ...)` — now reaches a remote TaskManager and is 
rebuilt with its configuration intact, instead of crash-looping the job. On a 
real cluster the TaskManager previously failed in 
`ActionExecutionOperator.open()` with a Jackson `MismatchedInputException` ("no 
delegate- or property-based Creator") and restarted indefinitely; local and 
in-process runs never surfaced it, because the provider returns the live object 
without deserializing and only the cross-process JobManager→TaskManager 
transfer nulls it and forces a `readValue` rebuild.
   
   The cause is the transfer mechanism: a live-instance sub-agent travels 
through `JavaSerializableResourceProvider`, which writes the object into the 
plan JSON and keeps it in an `@JsonIgnore` field. A user subclass normally 
declares only a parameterized constructor, the base's no-arg constructor is not 
inherited, and with no `@JsonCreator` Jackson cannot construct the class on the 
far side. YAML-declared sub-agents were never affected — they already travel as 
a `ResourceDescriptor`. The fix converges the programmatic route onto that YAML 
route in both languages: every registered `SubagentSetup` travels as its 
descriptor (class name + configuration) and is rebuilt from it. Python's 
analogous `PythonSerializableResourceProvider` path is converged the same way.
   
   #### Runtime flow
   
   At compile time on the JobManager, `AgentPlan` turns a live `SubagentSetup` 
into a descriptor — Java via `getDescriptor()`, Python via 
`ResourceDescriptor(clazz=<runtime class>, arguments=value.model_dump())` — and 
stores a descriptor-backed provider, the kind a YAML sub-agent already 
produces; the plan JSON carries the descriptor, not the object. On the 
TaskManager Java reflects over `descriptor.getClazz()` for the 
`(ResourceDescriptor, ResourceContext)` constructor; Python resolves through 
`PythonResourceProvider.get`.
   
   #### Key decisions
   
   **Converge on the descriptor path rather than make Jackson rebuild the live 
object.** Two narrower fixes were rejected: requiring each subclass to add a 
no-arg constructor or `@JsonCreator` pushes an implicit Jackson requirement 
onto every author, and a constructor-injected subclass would rebuild with empty 
config — silently wrong, worse than the crash; a custom deserializer that 
injects base fields would silently drop constructor side-effects and 
unannotated subclass state, rebuilding incompletely while appearing to succeed. 
The descriptor path — already used by YAML sub-agents and every config-style 
resource — removes the failure class, and sub-agents no longer travel through 
`JavaSerializableResourceProvider` at all.
   
   **Check the descriptor's clazz against the concrete type at construction.** 
That name is what the remote rebuild reflects over, so a descriptor naming a 
different class would rebuild the wrong type on a far task; the base 
constructor rejects the mismatch at construction.
   
   ### Behavioral Semantics
   
   #### Interaction decisions
   
   Registration route and language are independent; all resolve through the 
descriptor.
   
   | Registered as | Language | Descriptor comes from | Rebuilt by |
   |---|---|---|---|
   | live `SubagentSetup` | Java | `getDescriptor()` | `(ResourceDescriptor, 
ResourceContext)` ctor via reflection |
   | live `SubagentSetup` | Python | runtime class + `model_dump()` args | 
`PythonResourceProvider.get` |
   | YAML descriptor | either | the declared descriptor | as above, per 
language |
   
   #### Behavioral contracts
   
   - A sub-agent registered as a live setup is rebuilt on a remote task with 
its construction configuration intact.
   - A plan carrying a registered sub-agent resolves the same way whether 
compiled in Java or Python.
   - Constructing a `SubagentSetup` whose descriptor does not name its own 
concrete type fails at construction rather than later rebuilding the wrong 
class.
   
   #### Failure behavior
   
   - Descriptor `null` at construction → `NullPointerException`.
   - Descriptor `getClazz()` ≠ concrete type name → `IllegalArgumentException` 
naming both classes.
   - These replace the previous remote behavior: a crash-loop at TaskManager 
`open()` for a live-instance sub-agent, and a silently wrong-class rebuild for 
a mismatched descriptor.
   
   ### Tests
   
   | Contract | Tests |
   |---|---|
   | Live setup rebuilt with config after a plan round-trip | Java 
`AgentPlanSubagentSerializationTest.liveExternalSubagentIsRebuiltAfterPlanSerializationRoundTrip`;
 Python 
`test_agent_plan_subagent_resources.py::test_live_external_subagent_is_rebuilt_after_plan_json_round_trip`
 |
   | Registered sub-agent compiles to a descriptor-backed AGENT provider | 
Python `test_subagent_setup_compiles_into_agent_provider`, 
`test_agent_descriptor_compiles_into_agent_provider`; Java 
`AgentPlanSubagentResourceTest` |
   | Non-`SubagentSetup` AGENT resource rejected | Python 
`test_non_setup_agent_resource_is_rejected` |
   | Null descriptor rejected | 
`SubagentSetupTest.nullDescriptorIsRejectedAtConstruction` |
   | Descriptor clazz must match concrete type | 
`SubagentSetupTest.mismatchedDescriptorClazzIsRejectedAtConstruction` |
   
   The round-trip test is the coverage the bug report identified as missing. 
Not verified: an actual multi-TaskManager cluster transfer (the round-trip 
tests serialize and rebuild in-process, not across a live JM→TM boundary), and 
the model-authentication integration path, which this change does not touch.
   
   <details>
   <summary>Implementation invariants and supporting detail</summary>
   
   - `(ResourceDescriptor, ResourceContext)` is the base's only constructor; 
subclass convenience constructors build a descriptor and funnel through it, and 
the rebuild reflects over it.
   - `getDescriptor()` is non-null for any constructed setup, so `AgentPlan` 
can always store a descriptor-backed provider for a live setup.
   - Descriptor argument keys are named through `FIELD_` constants; 
`status_poll_interval_millis`, read by the async base and written by 
subclasses, is one shared constant.
   - Python derives `clazz` from the runtime class and arguments from 
`model_dump()`, so a live Python setup's descriptor matches what the rebuild 
expects.
   - `SerializableResource.validateSerializable()`'s write-only dead code is 
not sub-agent-specific and is left untouched; removing sub-agents from the 
live-object path makes it moot here.
   
   </details>
   
   ### API
   
   The base `SubagentSetup` now has a single `protected 
SubagentSetup(ResourceDescriptor, ResourceContext)` constructor and a public 
`getDescriptor()`; a custom subclass must expose that constructor and carry a 
descriptor naming its own type. A YAML-declared sub-agent is unaffected (it 
already traveled as a descriptor); what changes is programmatic live-instance 
registration, whose previous behavior was a remote crash-loop rather than a 
contract to rely on. Sub-agent registration is a recent API not yet covered by 
end-user documentation.
   
   ### Documentation
   
   - [ ] `doc-needed`
   - [x] `doc-not-needed`
   - [ ] `doc-included`
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   - [x] Yes
   - [ ] No
   
   Generated-by: Qoder 1.29.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