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]

Reply via email to