[api][plan][python] Expose sub-agents to chat models as callable tools - #1114
Open
yunfengzhou-hub wants to merge 3 commits into
Open
[api][plan][python] Expose sub-agents to chat models as callable tools#1114yunfengzhou-hub wants to merge 3 commits into
yunfengzhou-hub wants to merge 3 commits into
Conversation
A sub-agent setup now carries the routing information a caller needs to expose it as a model callable: a description and a JSON Schema for its arguments, defaulting to a single required prompt field and rejecting a blank schema at construction. Both sides also define the reserved subagent_ callable-name prefix, under which a sub-agent is exposed to a chat model and which tools are forbidden to register under. Co-Authored-By: Qoder <noreply@qoder.com> AI-Contributed/Feature: 87/87 AI-Contributed/UT: 0/160 Generated-by: Qoder 1.29.0 (Qwen3.8-Max)
…lables A chat model setup takes the sub-agents it may delegate to under the subagents argument and derives one callable per sub-agent, named under the reserved subagent_ prefix, with a description marking it as a sub-agent; no separate listing message is injected. Tool registration rejects the reserved prefix at plan-construction time, so a tool and a sub-agent may share a resource name and each callable name resolves to exactly one namespace. AI-Contributed/Feature: 0/343 AI-Contributed/UT: 0/667 Generated-by: Qoder 1.29.0 (Qwen3.8-Max)
…re reason exposed Tool-call dispatch resolves a callable name under the reserved subagent_ prefix in the AGENT namespace and any other name in the TOOL namespace, so the two kinds never collide. A sub-agent call hands the model arguments to the setup unchanged, normalizes its result into the tool-response content, and reports a failed delegation back to the model with the reason, so the model can correct the call instead of repeating it blindly. AI-Contributed/Feature: 0/583 AI-Contributed/UT: 0/903 Generated-by: Qoder 1.29.0 (Qwen3.8-Max)
yunfengzhou-hub
force-pushed
the
subagent_tool_call_v2
branch
from
September 10, 2026 04:03
3641d5a to
680cf2d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 thesubagentsargument after the tools and, for each name resolving to aSubagentSetupwith a usable input schema, adds a metadata-onlySubagentToolnamedsubagent_<name>. The model builds a function call against that schema.ToolCallActionreads the prefix off the callable name, resolves theAGENTresource once, and passes the model's arguments tosubmit(...).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.SubagentToolis metadata-only (call()throws); dispatch resolves theAGENTresource 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
subagentsentry resolves toSubagentSetup, explicit schemaSubagentSetup, input type rendering as an objectSubagentSetup, type rendering as no object / noneopen()failsAt dispatch a
subagent_name resolves in theAGENTnamespace and a plain name inTOOL; a success returns normalized JSON content, and any failure returns an error carrying the reason.Behavioral contracts
subagent_<name>, after the tool callables, carrying its description and schema.Failure behavior
Construction and plan-building raise; call-time failures are absorbed into the tool response.
IllegalArgumentExceptionat construction.IllegalArgumentExceptionnaming the remedy.subagentsentry resolving to a bridge handle, or a repeated callable name →open()fails (checkState), not silently dropped.SubagentSetupresource, an exception fromsubmit/await, a failed result, or an inexpressible result are each caught and returned asToolResponse.errorwith 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 mirroredtest_chat_model_subagents,test_subagent,test_agent_plan,test_tool_result_utils,test_tool_call_action_subagent. Every contract is pinned on both sides: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.Implementation invariants (not caller-observable)
SubagentTool.getToolType()isFUNCTIONandcall()throwsUnsupportedOperationException; the callable is never invoked throughTool.call().getInputType()/getResultType()/getResourceType()are@JsonIgnore(behavior, not state); Python pins the same viatest_the_declared_types_stay_out_of_the_plan_jsonandtest_the_metadata_serializes_under_the_cross_language_keys.FAIL_ON_UNKNOWN_PROPERTIESoff, mirroring pydantic's ignore-extra, then re-checked for JSON compatibility because a declared type can still render an inexpressible field.resolveSubagentresolves and type-checks theAGENTresource once and carries the setup down, so a plain tool call never attempts anAGENTresolution.API
Additive, aligned across Java / Python / YAML, building on the AGENT resource type and
SubagentSetup:SubagentSetupgainsgetDescription(),getInputSchema(),getInputType(),getResultType()and theCALLABLE_NAME_PREFIXconstant.subagentsargument (Java/Python constructor + YAMLsubagents:): theAGENTresources it may delegate to.ToolResultUtils(plan) normalizes a result;SubagentToolandInputSchemasare 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 newsubagentsargument 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 failsopen().Documentation
doc-neededdoc-not-neededdoc-includeddoc-needed but deferred, matching the AGENT-resource change: document the
subagentschat-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?
Generated-by: Qoder 1.29.0 (Qwen3.8-Max)