Repository navigation
feat(core): allow sub-agents to spawn nested sub-agents up to a configurable depth #6189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
b8e8046
2c50077
819739a
07c002b
5477164
ed1177b
4b0c5ac
6598ab8
699cb26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1360,6 +1360,16 @@ const SETTINGS_SCHEMA = { | |||||
| 'Cumulative tool-call budget for a run (counts every executed tool, success or failure; structured_output under --json-schema is exempt). -1 means unlimited; 0 means "no tool calls allowed" (first call aborts). Capped at 1,000,000 to catch typos. Overridable via --max-tool-calls.', | ||||||
| showInDialog: false, | ||||||
| }, | ||||||
| maxSubagentDepth: { | ||||||
| type: 'number', | ||||||
| label: 'Max Sub-agent Nesting Depth', | ||||||
| category: 'Model', | ||||||
| requiresRestart: false, | ||||||
| default: 5, | ||||||
| description: | ||||||
| 'Maximum number of nested sub-agent levels. 1 disables nesting (sub-agents cannot spawn sub-agents); the default 5 allows a sub-agent to spawn sub-agents up to five levels deep. Values below 1 are clamped to 1. Teammates, forks, and the workflow tool are excluded from nesting.', | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This schema description is already stale relative to the runtime and
Suggested change
— gpt-5 via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 5477164 — took the suggested wording for settingsSchema.ts and regenerated the VS Code schema from it (scripts/generate-settings-schema.ts), so both now match settings.md and the runtime behavior. |
||||||
| showInDialog: false, | ||||||
| }, | ||||||
| chatCompression: { | ||||||
| type: 'object', | ||||||
| label: 'Chat Compression', | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1028,7 +1028,11 @@ export class BackgroundAgentResumeService { | |
| } | ||
| }; | ||
|
|
||
| const framedRunBody = () => runWithAgentContext(meta.agentId, runBody); | ||
| // Restore the persisted launch depth so a resumed nested agent keeps | ||
| // its original nesting level (and spawn eligibility) instead of | ||
| // recomputing to depth 0 from this top-level resume frame. | ||
| const framedRunBody = () => | ||
| runWithAgentContext(meta.agentId, runBody, meta.depth); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So a malformed or hand-edited sidecar with This is exactly the threat model the same resume path already defends against for the sibling field: Suggest mirroring the normalize pattern before pinning: const restoredDepth = Number.isFinite(meta.depth)
? Math.max(0, Math.floor(meta.depth!))
: undefined; // fall back to auto-increment
const framedRunBody = () =>
runWithAgentContext(meta.agentId, runBody, restoredDepth);
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 4b0c5ac — |
||
| void (target.isFork ? runInForkContext(framedRunBody) : framedRunBody()); | ||
| return entry; | ||
| } catch (error) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,12 +46,16 @@ const storage = new AsyncLocalStorage<AgentContext>(); | |
| export function runWithAgentContext<T>( | ||
| agentId: string, | ||
| fn: () => Promise<T>, | ||
| depthOverride?: number, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Stale comment at line 38: the — qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 2c50077 — the docstring now says the depth is auto-incremented by default and that resume paths (background resume, AgentInteractive, deferred approvals) pin the original launch depth via |
||
| ): Promise<T> { | ||
| const current = storage.getStore() ?? {}; | ||
| // Auto-increment depth: top-level = 0, nested = parent+1. No caller has | ||
| // to know about it; telemetry reads it back via getCurrentAgentDepth | ||
| // (#3731 Phase 3 subagent spans). | ||
| const depth = (current.depth ?? -1) + 1; | ||
| // (#3731 Phase 3 subagent spans). depthOverride restores the original | ||
| // launch depth on background/foreground resume — otherwise the frame | ||
| // recomputes from a top-level (depth 0) parent and a resumed nested agent | ||
| // would regain spawn capacity it should not have. | ||
| const depth = depthOverride ?? (current.depth ?? -1) + 1; | ||
| return storage.run({ ...current, agentId, depth }, fn); | ||
| } | ||
|
|
||
|
|
@@ -89,3 +93,41 @@ export function getRuntimeContentGenerator(): | |
| | undefined { | ||
| return storage.getStore()?.runtimeView; | ||
| } | ||
|
|
||
| /** | ||
| * True when there is no active agent frame — i.e. we are in the top-level | ||
| * user session, not inside a sub-agent. The canonical "top-level only" | ||
| * predicate for gating capabilities (teammate spawning, forking) that must | ||
| * not be reachable from a nested sub-agent. | ||
| */ | ||
| export function isTopLevelSession(): boolean { | ||
| return getCurrentAgentId() === null; | ||
| } | ||
|
|
||
| /** | ||
| * The 0-based depth a child spawned by the current invoker would have: | ||
| * 0 when spawning from the top-level session (no agent frame), parent | ||
| * depth + 1 inside a sub-agent frame. Single source of the launch-depth | ||
| * formula — used for subagent telemetry spans, persisted in AgentMeta so | ||
| * background/foreground resume can restore the original nesting level (via | ||
| * the runWithAgentContext depthOverride), and underlying | ||
| * {@link canSpawnNestedAgent}. | ||
| */ | ||
| export function childLaunchDepth(): number { | ||
| return getCurrentAgentId() !== null ? getCurrentAgentDepth() + 1 : 0; | ||
| } | ||
|
|
||
| /** | ||
| * Whether the current invoker may spawn a nested sub-agent given the | ||
| * configured maximum nesting depth (1-based levels; a top-level sub-agent is | ||
| * level 1). Single source of truth for the depth relationship, shared by | ||
| * AgentCore.prepareTools() (schema gating) and AgentTool.execute() (runtime | ||
| * guard) so the two cannot drift apart. | ||
| * | ||
| * The would-be child sits at level `childLaunchDepth() + 1` (levels are | ||
| * 1-based, depths 0-based), which must not exceed `maxDepth`. See | ||
| * knowledge/qwen-code/design/nested-subagents.md. | ||
| */ | ||
| export function canSpawnNestedAgent(maxDepth: number): boolean { | ||
| return childLaunchDepth() + 1 <= maxDepth; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] No
--max-subagent-depthCLI flag, unlike sibling settings--max-session-turns(resolved viaresolveMaxSessionTurns(argv, settings)) and--max-tool-calls(resolved viaresolveMaxToolCalls(argv, settings)). Users cannot override nesting depth for a single run from the command line. Consider adding a--max-subagent-depthflag with a resolver for consistency.— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 2c50077 —
--max-subagent-depthmirrors the sibling budget flags: flag takes precedence overmodel.maxSubagentDepth, validates loudly as an integer in 1–100 (yargs NaN from a typo fails at startup instead of silently falling back), and the setting is now documented in settings.md alongside the flag.