From f3130980b545012a8f95a5c202d626ecd7039c7b Mon Sep 17 00:00:00 2001 From: wjc <1183245856@qq.com> Date: Thu, 23 Jul 2026 18:23:15 +0800 Subject: [PATCH] fix(core): reject nested background requests --- docs/developers/tools/task.md | 2 +- packages/core/src/tools/agent/agent.test.ts | 30 ++++++++++++++------- packages/core/src/tools/agent/agent.ts | 18 ++++++++++--- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/docs/developers/tools/task.md b/docs/developers/tools/task.md index e5615387bda..b2a74c283b6 100644 --- a/docs/developers/tools/task.md +++ b/docs/developers/tools/task.md @@ -14,7 +14,7 @@ Use `agent` to launch a specialized subagent to handle complex, multi-step tasks - `prompt` (string, required): The detailed task prompt for the subagent to execute. Should contain comprehensive instructions for autonomous execution. - `subagent_type` (string, optional): The type of specialized agent to use for this task. Defaults to `general-purpose` if omitted. - `fork_turns` (string, optional): Only valid with `subagent_type="fork"`. Omit it or use `all` for the full parent conversation, or use a positive integer string such as `"3"` for the most recent three real user turns. Tool responses and pure system reminders do not count as turns. -- `run_in_background` (boolean, optional): Defaults to `true` for top-level one-shot agents. Set to `false` to wait for the result inline. Nested agents run in the foreground. Caller-owned `working_dir` launches default to foreground and reject explicit or configured background execution. +- `run_in_background` (boolean, optional): Defaults to `true` for top-level one-shot agents. Set to `false` to wait for the result inline. Nested agents run in the foreground unless `run_in_background` is explicitly `true`, which is rejected because nested agents cannot receive background completion notifications. Caller-owned `working_dir` launches default to foreground and reject explicit or configured background execution. - `isolation` (string, optional): Set to `"worktree"` to run the agent in an isolated git worktree. ## How to use `agent` with Qwen Code diff --git a/packages/core/src/tools/agent/agent.test.ts b/packages/core/src/tools/agent/agent.test.ts index 83c19636c8c..caf17360d57 100644 --- a/packages/core/src/tools/agent/agent.test.ts +++ b/packages/core/src/tools/agent/agent.test.ts @@ -479,6 +479,9 @@ describe('AgentTool', () => { expect(properties.properties.run_in_background.description).toContain( 'interactive fork', ); + expect(properties.properties.run_in_background.description).toContain( + 'Nested agents run in the foreground unless run_in_background is explicitly true', + ); }); it('declares fork_turns for fork agents without a none option', () => { @@ -1329,6 +1332,7 @@ describe('AgentTool', () => { description: 'Fork from a nested sub-agent', prompt: 'Do work', subagent_type: 'fork', + run_in_background: true, }); const result = await runWithAgentContext('sub-1', () => invocation.execute(new AbortController().signal), @@ -5058,12 +5062,10 @@ describe('AgentTool', () => { ); }); - it('downgrades a background request from a nested sub-agent to an awaited foreground run', async () => { + it('rejects an explicit background request from a nested sub-agent', async () => { // Background delegation is top-level-only in v1: a nested launcher - // cannot honor the background completion contract (send_message / - // task_stop are excluded from its toolset, and completion - // notifications go to the top-level session). The run must complete - // inline instead of orphaning the child's results. + // cannot honor the completion contract. Do not silently turn an + // explicit background request into an awaited foreground run. vi.mocked(config.getMaxSubagentDepth).mockReturnValue(5); const params: AgentParams = { description: 'Start monitor from a nested sub-agent', @@ -5080,11 +5082,19 @@ describe('AgentTool', () => { ); const llmText = partToString(result.llmContent); - expect(llmText).not.toContain('Background agent launched'); - expect(llmText).toContain('Monitor done'); - expect(mockRegistry.register).toHaveBeenCalledWith( - expect.objectContaining({ isBackgrounded: false }), - ); + expect(llmText).toContain('run_in_background: true'); + expect(llmText).toContain('not supported from within a sub-agent'); + expect(result.error?.message).toBe(llmText); + expect(result.returnDisplay).toMatchObject({ + type: 'task_execution', + status: 'failed', + subagentName: 'monitor', + }); + expect(mockSubagentManager.loadSubagent).not.toHaveBeenCalled(); + expect(mockSubagentManager.createAgentHeadless).not.toHaveBeenCalled(); + expect(mockAgent.execute).not.toHaveBeenCalled(); + expect(mockRegistry.register).not.toHaveBeenCalled(); + expect(mockRegistry.tryReserveBackgroundSlot).not.toHaveBeenCalled(); }); it('keeps an omitted background flag in the foreground for nested sub-agents', async () => { diff --git a/packages/core/src/tools/agent/agent.ts b/packages/core/src/tools/agent/agent.ts index 2b157e995a1..003a8ae4bb5 100644 --- a/packages/core/src/tools/agent/agent.ts +++ b/packages/core/src/tools/agent/agent.ts @@ -791,7 +791,7 @@ export class AgentTool extends BaseDeclarativeTool { type: 'boolean', default: true, description: - 'Defaults to true for top-level regular subagents. Set to false to run a regular agent in the foreground and return its result inline. Set to true for an interactive fork to receive its completion notification; headless forks always run in the background. Nested agents run in the foreground. Caller-owned working_dir launches default to foreground and cannot run in the background.', + 'Defaults to true for top-level regular subagents. Set to false to run a regular agent in the foreground and return its result inline. Set to true for an interactive fork to receive its completion notification; headless forks always run in the background. Nested agents run in the foreground unless run_in_background is explicitly true, which is rejected because they cannot receive background completion notifications. Caller-owned working_dir launches default to foreground and cannot run in the background.', }, ...(config.isAgentTeamEnabled() ? { @@ -911,7 +911,7 @@ Usage notes: - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent - If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement. - If the user asks for agents "in parallel", group independent launches in a single message with multiple Agent tool use content blocks. Do not parallelize overlapping code changes. -- Top-level regular subagents run in the background by default. Set \`run_in_background: false\` when the current turn must wait for the result before continuing. Nested agent launches run in the foreground and return to their direct parent, so the main agent cannot independently address them as background tasks. Caller-owned \`working_dir\` launches default to foreground and cannot run in the background. +- Top-level regular subagents run in the background by default. Set \`run_in_background: false\` when the current turn must wait for the result before continuing. Nested agent launches run in the foreground and return to their direct parent; an explicit \`run_in_background: true\` request is rejected because nested agents cannot receive background completion notifications. Caller-owned \`working_dir\` launches default to foreground and cannot run in the background. - You can optionally set \`isolation: "worktree"\` to run the agent in a temporary git worktree, giving it an isolated copy of the repository. The worktree is automatically cleaned up if the agent makes no changes; if changes are made, the worktree path and branch are returned in the result so you can review or merge them. ## When to fork @@ -2363,6 +2363,15 @@ class AgentToolInvocation extends BaseToolInvocation { 'Nested forks are not supported', ); } + if (this.params.run_in_background === true && !isTopLevelSession()) { + debugLogger.debug( + '[AgentTool] Explicit background request rejected because background agents do not nest.', + ); + return this.buildSpawnBlockedResult( + 'Error: run_in_background: true is not supported from within a sub-agent. Run this agent in the foreground by omitting run_in_background or setting it to false.', + 'Background execution is not supported from a nested sub-agent', + ); + } const isFork = isForkRequested; if (isFork) { debugLogger.debug( @@ -2447,8 +2456,9 @@ class AgentToolInvocation extends BaseToolInvocation { // BackgroundTaskRegistry's single session-level notification callback // would inject the child's completion into the top-level conversation // while the launcher (typically finished by then) never hears back. - // Downgrade to an awaited foreground run instead of orphaning the - // child's results. + // Implicit background requests downgrade to an awaited foreground run + // instead of orphaning the child's results. The runtime spawn guard + // above rejects an explicit run_in_background: true request. const backgroundRequested = isFork && !this.config.isInteractive() ? true