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]