Skip to content

[api][plan][python] Expose sub-agents to chat models as callable tools - #1114

Open
yunfengzhou-hub wants to merge 3 commits into
apache:mainfrom
yunfengzhou-hub:subagent_tool_call_v2
Open

[api][plan][python] Expose sub-agents to chat models as callable tools#1114
yunfengzhou-hub wants to merge 3 commits into
apache:mainfrom
yunfengzhou-hub:subagent_tool_call_v2

Conversation

@yunfengzhou-hub

Copy link
Copy Markdown
Contributor

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.

Implementation invariants (not caller-observable)
  • 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.

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

  • 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?

  • Yes
  • No

Generated-by: Qoder 1.29.0 (Qwen3.8-Max)

@github-actions github-actions Bot added doc-needed Your PR changes impact docs. fixVersion/0.4.0 priority/major Default priority of the PR or issue. labels Sep 10, 2026
pltbkd and others added 3 commits September 10, 2026 11:56
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-needed Your PR changes impact docs. fixVersion/0.4.0 priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants