You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: Sub-agents inherit the parent's client-declared frontend tools; calling one silently loses the sub-task #3308
main @ 857e3e6 (not released yet; 2.0.3 is not affected)
Description
Since frontend tools are passed per call via RuntimeContext#getToolRequestConfig() (#3283), a sub-agent spawned with agent_spawn also gets the parent's client-declared frontend tools: DefaultAgentManager#invokeAgent builds the child context with RuntimeContext.builder(parentRc), and RuntimeContext.Builder#from copies toolRequestConfig.
The sub-agent has no client to execute those tools. When it calls one, the call suspends with [Awaiting external execution] inside the sub-agent, the sub-agent ends with an empty reply, and the parent's agent_spawn result is status: ok with an empty reply. The call never reaches the AG-UI client (no pending call in the parent run), so the sub-task is silently lost.
On 2.0.3 the sub-agent does not see client-declared tools (calling one yields Tool not found), so this is a behaviour change on main.
Reproduction (verified on 2.0.4-SNAPSHOT built from main, AG-UI server, scripted model)
The client declares a frontend tool choose_x in RunAgentInput.tools.
The parent model calls agent_spawn (general-purpose) with a task; the sub-agent's model calls choose_x.
Events show subagent.tool_call choose_x, then a sub-agent tool result [Awaiting external execution] with state RUNNING, then AGENT_END; the parent's agent_spawn result is status: ok, reply: (empty); the parent run finishes normally with nothing pending for the client.
Expected
Either sub-agents do not inherit the parent's per-call frontend tools (as in 2.0.3 — a sub-agent cannot interact with the client), or a frontend call made inside a sub-agent is surfaced to the client through the parent run. In neither case should the sub-task be reported as ok with an empty reply.
Verified against main@857e3e6cf by static code analysis (no runtime reproduction: this check host has no JDK/Maven toolchain). Every claim in the report holds; the chain is traced below.
Root cause — the parent's per-call tool difference is copied into the child context and nothing strips it:
agentscope-harness/.../subagent/DefaultAgentManager.java:181-187 (sync) and :236-242 (stream) build the child context as RuntimeContext.builder(parentRc).sessionId(..).userId(..).build().
agentscope-core/.../agent/RuntimeContext.java:276-278 delegates builder(source) to Builder#from (:442-465), which copies toolRequestConfig unconditionally at :453. The field javadoc (:28-34, :69-73) describes it as "per-call metadata for an agent run"; the deliberate runId copy shows the inheritance of toolRequestConfig is incidental rather than an intended contract. No code in agentscope-harness/src/main references toolRequestConfig at all, so there is no stripping anywhere on the spawn path.
The child ReActAgent seeds its per-call scope from that context (ReActAgent.java:762-763) and composes the model tool surface with it (:2452-2455); Toolkit.getToolSchemas(groups, config) (Toolkit.java:410-436) appends the external/schema-only entries at :425-434. The sub-agent is therefore offered tools it has no executor for.
Why it fails silently instead of loudly — three independent effects compound:
ToolExecutor.java:247-249 short-circuits an external tool to ToolResultBlock suspension (ToolResultBlock.java:184-194, text [Awaiting external execution]) instead of Tool not found (ToolExecutor.java:211-213), so the child never sees an error.
The child turn terminates with GenerateReason.TOOL_SUSPENDED (ReActAgent.java:2986-2996, :3042-3045, buildSuspendedMsg at :3530-3541). That message contains only ToolUseBlock + ToolResultBlock, so Msg.getTextContent() (Msg.java:544-550, top-level TextBlocks only) returns "" — the suspension text is in memory but discarded by the caller.
AgentSpawnTool.java:960-965 (textOf at :1120-1122) formats that empty text as status: ok\nreply:\n. On the client side, sub-agent events are routed through SubagentEventConverter (AgentEventConverterRegistry.java:100-106), bypassing AgentLifecycleEventConverter's interrupt collection (:95-125), and that collector also skips suspended results whose name is in the run's frontend tool set (skip at :120-122). No interrupt is raised, so there is nothing to resume.
Additional severity the report does not mention — the inherited config also hides the child's own tools. With ToolMergeMode.EXTERNAL_ONLY (ToolMergeMode.java:34, ToolRequestConfig.hidesBackend) the child's entire backend toolkit is dropped (Toolkit.java:415-424), and with MERGE_EXTERNAL_PRIORITY a child tool whose name collides with a frontend tool is shadowed (ToolRequestConfig.resolveTool:89-98). A sub-agent can thus lose legitimate server-side tools it was expected to use.
Blast radius — every DefaultAgentManager-driven spawn: agent_spawn and agent_send (sync :800/:825, stream :849/:855, async :474/:651/:1651), plus the SubagentsMiddleware / DynamicSubagentsMiddleware paths. agent_send has the identical defect (:688-698 + :1651). The async paths store the empty string as the task result, so task_output / wait_async_results report an empty success too. Not affected: callers that pass no parent context (e.g. DefaultAgentManager.java:175-177).
Severity — high. Silent loss of an entire sub-task under a success-looking status, with no error, no client interrupt and no resume path.
Behaviour change vs 2.0.3 — consistent with the report, but only partially verifiable from this checkout: it is shallow and its tags stop at v2.0.2, which has no toolRequestConfig field at all (RuntimeContext.java: 0 occurrences) and injected frontend tools by mutating the top-level agent's shared toolkit (v2.0.2 AguiAgentAdapter#injectFrontendTools + ToolInjection.close() in doFinally) — a child built with its own toolkit never saw them, hence the old Tool not found. The v2.0.3 tag itself could not be inspected here.
Fix direction (harness layer, smallest blast radius) — stop propagating the parent's per-call tool difference when the child context is built in DefaultAgentManager (explicitly clear toolRequestConfig, or add a dedicated child-context factory), so a sub-agent only ever sees tools it can execute and a client-only tool call fails loudly with Tool not found again. Copying runId / attributes / state should stay as-is. Clearing it inside RuntimeContext.Builder#from instead would be a global semantic change to every derived context and is not recommended.
Complementary hardening (worth doing regardless of the above) — agent_spawn / agent_send should inspect the child reply for GenerateReason.TOOL_SUSPENDED and return an error/suspended status naming the offending tools, instead of ok with an empty reply. Silently-empty success is the part that makes this hard to diagnose.
On the report's second option (surface the child's frontend call to the client through the parent run): not straightforwardly viable today. The sub-agent run is a separate memory/run boundary nested inside a parent tool call that has already completed, and there is no resume path keyed to the sub-agent session id. If the maintainers want that behaviour, it is a design change rather than a fix.
Tests — agentscope-harness/src/test contains zero references to ToolRequestConfig or external-tool execution, so child-context inheritance is entirely uncovered. Existing coverage is parent-path only (ToolRequestConfigTest, ReActAgentToolkitConcurrencyTest:189-211, AguiAgentAdapterTest:107-160/:300-345). A fix should assert that a spawned child's tool surface excludes client-declared frontend tools, and that agent_spawn never reports status: ok with an empty reply when the child ended suspended.
Reply /approve to generate a PR for the harness-layer containment + loud-failure hardening, /revise <notes> to adjust the proposal, or /reject to stop.
Hi, I'd like to work on this issue. I'll first reproduce it on main, then investigate preventing client-declared frontend tools from being inherited by spawned sub-agents, with regression tests for both synchronous and streaming paths. I'll also check the case where a suspended child run is reported as a successful empty result. If someone is already working on this or there's a preferred approach, please let me know. Thanks!
Proposal window closed — no competing PR will be filed from this proposal window
The 72 h feedback window opened by the evaluation above (2026-09-26T12:14:27Z) reached its bound on 2026-09-29T12:14Z with no /approve, /revise or /reject directive from a harness owner.
The thread answered the question in another way: @wyi184246-creator wrote on 2026-09-29T02:53:14Z that they were taking this issue, and the work is now up as PR #3357. As of this read #3357 is OPEN, not a draft, review decision APPROVED, head dcf3f51dd4, +533/-8 across 8 files, last updated 2026-09-29T07:44:46Z, and it declares Fixes #3308.
So the implementation path for this request is already owned by a community PR awaiting maintainer merge. Filing a second PR against the same issue is exactly the collision this window policy exists to prevent, so this proposal is stood down and no automated PR will be generated from it.
Nothing here disputes the evaluation: a sub-agent inheriting the parent's client-declared frontend tools, so that the sub-task is silently lost, is a real defect, and #3357 addresses the inheritance boundary the evaluation pointed at. Merge, close, or request changes on #3357 — that is the productive thread now. If #3357 is closed unmerged and this remains wanted, reply here and the question will be re-evaluated against whatever main looks like at that point.
Automated feedback-window maintenance by github-manager-bot
Activity
oss-maintainer commentedon Sep 26, 2026
Issue Evaluation
Category:
bug| Status: ConfirmedVerified against
main@857e3e6cfby static code analysis (no runtime reproduction: this check host has no JDK/Maven toolchain). Every claim in the report holds; the chain is traced below.Root cause — the parent's per-call tool difference is copied into the child context and nothing strips it:
agentscope-harness/.../subagent/DefaultAgentManager.java:181-187(sync) and:236-242(stream) build the child context asRuntimeContext.builder(parentRc).sessionId(..).userId(..).build().agentscope-core/.../agent/RuntimeContext.java:276-278delegatesbuilder(source)toBuilder#from(:442-465), which copiestoolRequestConfigunconditionally at:453. The field javadoc (:28-34,:69-73) describes it as "per-call metadata for an agent run"; the deliberaterunIdcopy shows the inheritance oftoolRequestConfigis incidental rather than an intended contract. No code inagentscope-harness/src/mainreferencestoolRequestConfigat all, so there is no stripping anywhere on the spawn path.ReActAgentseeds its per-call scope from that context (ReActAgent.java:762-763) and composes the model tool surface with it (:2452-2455);Toolkit.getToolSchemas(groups, config)(Toolkit.java:410-436) appends the external/schema-only entries at:425-434. The sub-agent is therefore offered tools it has no executor for.Why it fails silently instead of loudly — three independent effects compound:
ToolExecutor.java:247-249short-circuits an external tool toToolResultBlocksuspension (ToolResultBlock.java:184-194, text[Awaiting external execution]) instead ofTool not found(ToolExecutor.java:211-213), so the child never sees an error.GenerateReason.TOOL_SUSPENDED(ReActAgent.java:2986-2996,:3042-3045,buildSuspendedMsgat:3530-3541). That message contains onlyToolUseBlock+ToolResultBlock, soMsg.getTextContent()(Msg.java:544-550, top-levelTextBlocks only) returns""— the suspension text is in memory but discarded by the caller.AgentSpawnTool.java:960-965(textOfat:1120-1122) formats that empty text asstatus: ok\nreply:\n. On the client side, sub-agent events are routed throughSubagentEventConverter(AgentEventConverterRegistry.java:100-106), bypassingAgentLifecycleEventConverter's interrupt collection (:95-125), and that collector also skips suspended results whose name is in the run's frontend tool set (skip at:120-122). No interrupt is raised, so there is nothing to resume.Additional severity the report does not mention — the inherited config also hides the child's own tools. With
ToolMergeMode.EXTERNAL_ONLY(ToolMergeMode.java:34,ToolRequestConfig.hidesBackend) the child's entire backend toolkit is dropped (Toolkit.java:415-424), and withMERGE_EXTERNAL_PRIORITYa child tool whose name collides with a frontend tool is shadowed (ToolRequestConfig.resolveTool:89-98). A sub-agent can thus lose legitimate server-side tools it was expected to use.Blast radius — every
DefaultAgentManager-driven spawn:agent_spawnandagent_send(sync:800/:825, stream:849/:855, async:474/:651/:1651), plus theSubagentsMiddleware/DynamicSubagentsMiddlewarepaths.agent_sendhas the identical defect (:688-698+:1651). The async paths store the empty string as the task result, sotask_output/wait_async_resultsreport an empty success too. Not affected: callers that pass no parent context (e.g.DefaultAgentManager.java:175-177).Severity — high. Silent loss of an entire sub-task under a success-looking status, with no error, no client interrupt and no resume path.
Behaviour change vs 2.0.3 — consistent with the report, but only partially verifiable from this checkout: it is shallow and its tags stop at
v2.0.2, which has notoolRequestConfigfield at all (RuntimeContext.java: 0 occurrences) and injected frontend tools by mutating the top-level agent's shared toolkit (v2.0.2 AguiAgentAdapter#injectFrontendTools+ToolInjection.close()indoFinally) — a child built with its own toolkit never saw them, hence the oldTool not found. Thev2.0.3tag itself could not be inspected here.Fix direction (harness layer, smallest blast radius) — stop propagating the parent's per-call tool difference when the child context is built in
DefaultAgentManager(explicitly cleartoolRequestConfig, or add a dedicated child-context factory), so a sub-agent only ever sees tools it can execute and a client-only tool call fails loudly withTool not foundagain. CopyingrunId/ attributes / state should stay as-is. Clearing it insideRuntimeContext.Builder#frominstead would be a global semantic change to every derived context and is not recommended.Complementary hardening (worth doing regardless of the above) —
agent_spawn/agent_sendshould inspect the child reply forGenerateReason.TOOL_SUSPENDEDand return an error/suspended status naming the offending tools, instead ofokwith an empty reply. Silently-empty success is the part that makes this hard to diagnose.On the report's second option (surface the child's frontend call to the client through the parent run): not straightforwardly viable today. The sub-agent run is a separate memory/run boundary nested inside a parent tool call that has already completed, and there is no resume path keyed to the sub-agent session id. If the maintainers want that behaviour, it is a design change rather than a fix.
Tests —
agentscope-harness/src/testcontains zero references toToolRequestConfigor external-tool execution, so child-context inheritance is entirely uncovered. Existing coverage is parent-path only (ToolRequestConfigTest,ReActAgentToolkitConcurrencyTest:189-211,AguiAgentAdapterTest:107-160/:300-345). A fix should assert that a spawned child's tool surface excludes client-declared frontend tools, and thatagent_spawnnever reportsstatus: okwith an empty reply when the child ended suspended.Reply
/approveto generate a PR for the harness-layer containment + loud-failure hardening,/revise <notes>to adjust the proposal, or/rejectto stop.Automated evaluation by github-manager-bot
wyi184246-creator commentedon Sep 29, 2026
Hi, I'd like to work on this issue. I'll first reproduce it on main, then investigate preventing client-declared frontend tools from being inherited by spawned sub-agents, with regression tests for both synchronous and streaming paths. I'll also check the case where a suspended child run is reported as a successful empty result. If someone is already working on this or there's a preferred approach, please let me know. Thanks!
oss-maintainer commentedon Sep 29, 2026
Proposal window closed — no competing PR will be filed from this proposal window
The 72 h feedback window opened by the evaluation above (
2026-09-26T12:14:27Z) reached its bound on2026-09-29T12:14Zwith no/approve,/reviseor/rejectdirective from a harness owner.The thread answered the question in another way: @wyi184246-creator wrote on 2026-09-29T02:53:14Z that they were taking this issue, and the work is now up as PR #3357. As of this read #3357 is OPEN, not a draft, review decision APPROVED, head
dcf3f51dd4,+533/-8across 8 files, last updated 2026-09-29T07:44:46Z, and it declaresFixes #3308.So the implementation path for this request is already owned by a community PR awaiting maintainer merge. Filing a second PR against the same issue is exactly the collision this window policy exists to prevent, so this proposal is stood down and no automated PR will be generated from it.
Nothing here disputes the evaluation: a sub-agent inheriting the parent's client-declared frontend tools, so that the sub-task is silently lost, is a real defect, and #3357 addresses the inheritance boundary the evaluation pointed at. Merge, close, or request changes on #3357 — that is the productive thread now. If #3357 is closed unmerged and this remains wanted, reply here and the question will be re-evaluated against whatever
mainlooks like at that point.Automated feedback-window maintenance by github-manager-bot