yunfengzhou-hub opened a new pull request, #1114: URL: https://github.com/apache/flink-agents/pull/1114
Linked issue: #1112 ### Purpose of change A chat model can now delegate to a sub-agent by issuing a tool call. The setup declares each sub-agent it names as one callable under the reserved `subagent_` prefix; at execution a prefixed call is routed to that sub-agent and its result is handed back to the model. Which sub-agent to call, and with what arguments, becomes the model's decision — previously a sub-agent could only be invoked from action code. A sub-agent the caller never describes is simply not offered, and a delegation that fails tells the model why, so it can correct the call rather than repeat it blindly. #### Runtime flow `open()` walks the `subagents` argument after the tools and, for each name resolving to a `SubagentSetup` with a usable input schema, adds a metadata-only `SubagentTool` named `subagent_<name>`. The model builds a function call against that schema. `ToolCallAction` reads the prefix off the callable name, resolves the `AGENT` resource once, and passes the model's arguments to `submit(...).await()` unchanged; the result is normalized to JSON-generic form and rendered as the tool-message content. Python mirrors this in its setup and tool-call action. #### Key decisions The reserved prefix, not a distinct tool type, separates the namespaces: tool registration rejects `subagent_` at plan-construction, so a prefixed name can only address a sub-agent, and a tool and a sub-agent may share a resource name. No listing message is injected — every sub-agent description ends with a marker, so the model learns a callable is a delegation from its description alone. `SubagentTool` is metadata-only (`call()` throws); dispatch resolves the `AGENT` resource at execution, keeping the sub-agent's own durable execution as the single source of the result and avoiding nested durable cursors, so sub-agent calls stay synchronous even when tool calls run as a parallel durable batch. An input schema is derived from the declared input type through the same Jackson generator the ReAct output schema uses, adding no dependency. ### Behavioral Semantics #### Interaction decisions | `subagents` entry resolves to | Input schema | Offered to the model | | --- | --- | --- | | `SubagentSetup`, explicit schema | that schema | yes | | `SubagentSetup`, input type rendering as an object | derived schema | yes | | `SubagentSetup`, type rendering as no object / none | none | no — dropped with a warning; job continues | | bridge handle (owned by the other language) | carries none | no — `open()` fails | | a tool registered under the prefix | — | impossible — rejected at plan-construction | At dispatch a `subagent_` name resolves in the `AGENT` namespace and a plain name in `TOOL`; a success returns normalized JSON content, and any failure returns an error carrying the reason. #### Behavioral contracts 1. Each named sub-agent with a usable input schema is declared as exactly one callable `subagent_<name>`, after the tool callables, carrying its description and schema. 2. A sub-agent with neither a schema nor a shape-bearing input type is not offered, and does not stop the others. 3. Every sub-agent description ends with the marker; an undescribed one gets a generic delegation description, and no separate listing message is injected. 4. The schema is the explicit one if declared, else derived from the input type; explicit wins. 5. A tool and a sub-agent may share a resource name; a callable name resolves to exactly one namespace. 6. A tool name must not carry the prefix — rejected at plan-construction. 7. A prefixed call hands the model's arguments over unchanged; injection stays tool-only. 8. A success is reported as JSON-generic content; a declared result type narrows it (extras ignored), an undeclared one takes it as it arrived. 9. A result JSON cannot express is refused with the path where it was found, and reported as a failed delegation — not dropped, stringified, or made a job failure. 10. A failed delegation reaches the model with the reason. 11. Re-opening rebuilds the callables from scratch and does not duplicate them. 12. Java and Python decide alike on declaration, dispatch, and result handling. #### Failure behavior Construction and plan-building raise; call-time failures are absorbed into the tool response. - Blank explicit input schema → `IllegalArgumentException` at construction. - An input type that cannot be rendered, including a self-referential one → `IllegalArgumentException` naming the remedy. - A `subagents` entry resolving to a bridge handle, or a repeated callable name → `open()` fails (`checkState`), not silently dropped. - A tool under the reserved prefix → raises at plan-construction. - At call time, an absent/non-`SubagentSetup` resource, an exception from `submit`/`await`, a failed result, or an inexpressible result are each caught and returned as `ToolResponse.error` with the reason; the job continues. ### Tests The new suites run offline, with no live model or external agent. Java: `BaseChatModelSetupSubagentTest`, `SubagentSetupTest`, `AgentPlanSubagentResourceTest`, `ToolResultUtilsTest`, `ToolCallActionSubagentTest`; Python: the mirrored `test_chat_model_subagents`, `test_subagent`, `test_agent_plan`, `test_tool_result_utils`, `test_tool_call_action_subagent`. Every contract is pinned on both sides: | Contract | Java | Python | | --- | --- | --- | | 1 declared as one prefixed callable, after tools | ✅ | ✅ | | 2 no shape → not offered, others unaffected | ✅ | ✅ | | 3 description marker; no listing message | ✅ | ✅ | | 4 explicit schema wins, else derived | ✅ | ✅ | | 5 tool and sub-agent share a name | ✅ | ✅ | | 6 tool under the prefix rejected | ✅ | ✅ | | 7 arguments handed over unchanged | ✅ | ✅ | | 8 result type narrows; undeclared as-is | ✅ | ✅ | | 9 non-JSON result refused with its path | ✅ | ✅ | | 10 failed delegation carries the reason | ✅ | ✅ | | 11 re-open does not duplicate | ✅ | ✅ | | 12 Java and Python decide alike | ✅ | ✅ | Highest risk is result normalization (9): non-string map keys, non-finite numbers, a POJO inside a JSON tree, and arrays walked by index, each asserted on the path in the message. Not verified: no test drives a live model or a real external sub-agent, so these pin what the framework declares, dispatches, and reports, never that a provider accepts the derived schema. A sub-agent owned by the other language is checked only up to the rejection at `open()`; cross-language delegation end-to-end is out of scope. Parallel-mode routing of a sub-agent beside tools is covered, not its concurrency timing. <details> <summary>Implementation invariants (not caller-observable)</summary> - `SubagentTool.getToolType()` is `FUNCTION` and `call()` throws `UnsupportedOperationException`; the callable is never invoked through `Tool.call()`. - `getInputType()`/`getResultType()`/`getResourceType()` are `@JsonIgnore` (behavior, not state); Python pins the same via `test_the_declared_types_stay_out_of_the_plan_json` and `test_the_metadata_serializes_under_the_cross_language_keys`. - A declared result type is read with `FAIL_ON_UNKNOWN_PROPERTIES` off, mirroring pydantic's ignore-extra, then re-checked for JSON compatibility because a declared type can still render an inexpressible field. - `resolveSubagent` resolves and type-checks the `AGENT` resource once and carries the setup down, so a plain tool call never attempts an `AGENT` resolution. - A derived input schema is carried by the plan JSON; an explicit one is kept verbatim. </details> ### API Additive, aligned across Java / Python / YAML, building on the AGENT resource type and `SubagentSetup`: - `SubagentSetup` gains `getDescription()`, `getInputSchema()`, `getInputType()`, `getResultType()` and the `CALLABLE_NAME_PREFIX` constant. - The chat-model setup takes a new `subagents` argument (Java/Python constructor + YAML `subagents:`): the `AGENT` resources it may delegate to. - `ToolResultUtils` (plan) normalizes a result; `SubagentTool` and `InputSchemas` are package-private, not public API. One thing changes for a caller who does nothing differently: a tool named `subagent_*` is now rejected at plan-construction. Otherwise nothing changes unless the new `subagents` argument is set, and a plain tool call routes as before. A sub-agent owned by the other language cannot yet be exposed as a callable — declaring one fails `open()`. ### Documentation - [x] `doc-needed` - [ ] `doc-not-needed` - [ ] `doc-included` doc-needed but deferred, matching the AGENT-resource change: document the `subagents` chat-model argument and the sub-agent-as-tool flow once the internal sub-agent lands and the API stabilizes. ### 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]
