Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions packages/core/src/tools/agent/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,9 @@ describe('AgentTool', () => {
expect(tool.description).toContain(
'foreground regular agent returns its result inline',
);
expect(tool.description).toContain(
'explicitly setting `run_in_background: true` from a nested agent is rejected',
);
});

it('explains how to continue reusable background agents', async () => {
Expand Down Expand Up @@ -479,6 +482,9 @@ describe('AgentTool', () => {
expect(properties.properties.run_in_background.description).toContain(
'interactive fork',
);
expect(properties.properties.run_in_background.description).toContain(
'explicit true is rejected',
);
});

it('declares fork_turns for fork agents without a none option', () => {
Expand Down Expand Up @@ -895,6 +901,16 @@ describe('AgentTool', () => {
).toMatch(/fork/i);
});

it('rejects a non-boolean run_in_background value', () => {
expect(
agentTool.validateToolParams({
...validParams,
// @ts-expect-error: raw model parameters are untrusted
run_in_background: 'true',
}),
).toMatch(/run_in_background.*boolean/i);
});

it('rejects working_dir combined with run_in_background', () => {
expect(
agentTool.validateToolParams({
Expand Down Expand Up @@ -5058,12 +5074,7 @@ describe('AgentTool', () => {
);
});

it('downgrades a background request from a nested sub-agent to an awaited foreground run', 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.
it('rejects an explicit background request from a nested sub-agent', async () => {
vi.mocked(config.getMaxSubagentDepth).mockReturnValue(5);
const params: AgentParams = {
description: 'Start monitor from a nested sub-agent',
Expand All @@ -5080,11 +5091,15 @@ 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).toMatch(/run_in_background.*nested sub-agent/i);
expect(result.error?.message).toBe(llmText);
expect(result.returnDisplay).toMatchObject({
type: 'task_execution',
status: 'failed',
terminateReason: 'Nested background agents are not supported',
});
expect(mockSubagentManager.createAgentHeadless).not.toHaveBeenCalled();
expect(mockRegistry.register).not.toHaveBeenCalled();
});

it('keeps an omitted background flag in the foreground for nested sub-agents', async () => {
Expand Down
21 changes: 17 additions & 4 deletions packages/core/src/tools/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@ export class AgentTool extends BaseDeclarativeTool<AgentParams, ToolResult> {
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; an explicit true is rejected. Caller-owned working_dir launches default to foreground and cannot run in the background.',
},
...(config.isAgentTeamEnabled()
? {
Expand Down Expand Up @@ -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; explicitly setting \`run_in_background: true\` from a nested agent is rejected. 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

Expand Down Expand Up @@ -1007,6 +1007,13 @@ assistant: Uses the ${ToolNames.AGENT} tool to launch the test-runner agent
return 'Parameter "prompt" must be a non-empty string.';
}

if (
params.run_in_background !== undefined &&
typeof params.run_in_background !== 'boolean'
) {
return 'Parameter "run_in_background" must be a boolean when set.';
}

if (params.subagent_type !== undefined) {
if (
typeof params.subagent_type !== 'string' ||
Expand Down Expand Up @@ -2363,6 +2370,12 @@ class AgentToolInvocation extends BaseToolInvocation<AgentParams, ToolResult> {
'Nested forks are not supported',
);
}
if (this.params.run_in_background === true && !isTopLevelSession()) {
return this.buildSpawnBlockedResult(
'Error: run_in_background: true is not supported from within a nested sub-agent. Set run_in_background: false, or omit it, to run the child inline.',
'Nested background agents are not supported',
);
}
const isFork = isForkRequested;
if (isFork) {
debugLogger.debug(
Expand Down Expand Up @@ -2447,8 +2460,8 @@ class AgentToolInvocation extends BaseToolInvocation<AgentParams, ToolResult> {
// 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.
// Explicit background requests are rejected above. Implicit defaults
// downgrade to an awaited foreground run instead of orphaning results.
const backgroundRequested =
isFork && !this.config.isInteractive()
? true
Expand Down
Loading