diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 20e7bc78e..1282f292a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,9 +1,11 @@ # Changelog ## [Unreleased] - ### Added +- Added `task.maxRecursionDepth` setting to control how many levels deep subagents can spawn their own subagents (0=none, 1=one level, 2=two levels, -1=unlimited) +- Added support for nested task artifact naming with parent task prefixes (e.g., "0-Auth.1-Subtask") to organize hierarchical task outputs +- Added `taskDepth` and `parentTaskPrefix` options to `CreateAgentSessionOptions` for tracking subagent recursion depth and organizing nested artifacts - Added Grafana Pyroscope continuous profiling integration for CPU and heap profiling - Added `pyroscope.enabled`, `pyroscope.serverAddress`, `pyroscope.appName`, `pyroscope.basicAuthUser`, `pyroscope.basicAuthPassword`, `pyroscope.tenantID`, and `pyroscope.flushIntervalMs` settings for profiling configuration - Added support for Pyroscope configuration via environment variables (`PYROSCOPE_URL`, `PYROSCOPE_APP_NAME`, `PYROSCOPE_BASIC_AUTH_USER`, `PYROSCOPE_BASIC_AUTH_PASSWORD`, `PYROSCOPE_TENANT_ID`, `PYROSCOPE_FLUSH_INTERVAL_MS`) @@ -13,6 +15,9 @@ ### Changed +- Changed task tool spawns configuration from "explore" to "*" to allow subagents to spawn any agent type +- Changed system prompt to enable parallel delegation guidance for all agents (removed coordinator-only restriction) +- Changed task tool to automatically disable itself when maximum recursion depth is reached, preventing infinite nesting - Changed task concurrency from hardcoded constant to configurable setting via `task.maxConcurrency` - Changed concurrency limit calculation to support unlimited concurrency when set to 0 @@ -22,6 +27,7 @@ ### Fixed +- Fixed task ID display formatting to show hierarchical structure for nested tasks (e.g., "0.1 Auth>Subtask" instead of "0-Auth.1-Subtask") - Improved frontmatter parsing error messages to include source context for better debugging ## [10.6.1] - 2026-02-04 diff --git a/packages/coding-agent/src/config/settings-schema.ts b/packages/coding-agent/src/config/settings-schema.ts index d1ae1905b..79d176b43 100644 --- a/packages/coding-agent/src/config/settings-schema.ts +++ b/packages/coding-agent/src/config/settings-schema.ts @@ -410,6 +410,17 @@ export const SETTINGS_SCHEMA = { submenu: true, }, }, + "task.maxRecursionDepth": { + type: "number", + default: 2, + ui: { + tab: "tools", + label: "Task max recursion depth", + description: + "How many levels deep subagents can spawn their own subagents (0=none, 1=one level, 2=two levels, -1=unlimited)", + submenu: true, + }, + }, // ───────────────────────────────────────────────────────────────────────── // Startup settings diff --git a/packages/coding-agent/src/prompts/system/system-prompt.md b/packages/coding-agent/src/prompts/system/system-prompt.md index 4f6657a59..9d085e98f 100644 --- a/packages/coding-agent/src/prompts/system/system-prompt.md +++ b/packages/coding-agent/src/prompts/system/system-prompt.md @@ -281,7 +281,6 @@ Suppress: {{appendSystemPrompt}} {{/if}} -{{#if isCoordinator}} {{#has tools "task"}} When work forks, you fork. @@ -290,7 +289,7 @@ Notice sequential habit: - Comfort: one thing at a time - Illusion: order = correctness - Assumption: finish A before starting B -**Triggers requiring Task tool:** + **Triggers requiring Task tool:** - Editing 4+ files with no dependencies between edits - Investigating 2+ independent subsystems or questions - Work decomposing into pieces not needing each other's results @@ -303,7 +302,6 @@ If cannot articulate why B depends on A: parallel. Don't carry whole problem in one skull. Split load; bring facts back; then cut code. {{/has}} -{{/if}} This is not practice. diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 72269ad54..cee786723 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -183,6 +183,10 @@ export interface CreateAgentSessionOptions { outputSchema?: unknown; /** Whether to include the submit_result tool by default */ requireSubmitResultTool?: boolean; + /** Task recursion depth (for subagent sessions). Default: 0 */ + taskDepth?: number; + /** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */ + parentTaskPrefix?: string; /** Session manager. Default: SessionManager.create(cwd) */ sessionManager?: SessionManager; @@ -728,6 +732,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} eventBus, outputSchema: options.outputSchema, requireSubmitResultTool: options.requireSubmitResultTool, + taskDepth: options.taskDepth ?? 0, getSessionFile: () => sessionManager.getSessionFile() ?? null, getSessionId: () => sessionManager.getSessionId?.() ?? null, getSessionSpawns: () => options.spawns ?? "*", @@ -772,7 +777,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} ); toolSession.internalRouter = internalRouter; toolSession.getArtifactsDir = getArtifactsDir; - toolSession.agentOutputManager = new AgentOutputManager(getArtifactsDir); + toolSession.agentOutputManager = new AgentOutputManager( + getArtifactsDir, + options.parentTaskPrefix ? { parentPrefix: options.parentTaskPrefix } : undefined, + ); debugStartup("sdk:createTools:start"); // Create and wrap tools with meta notice formatting @@ -996,7 +1004,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} toolNames, rules: rulebookRules, skillsSettings: settingsInstance.getGroup("skills") as SkillsSettings, - isCoordinator: options.hasUI, }); if (options.systemPrompt === undefined) { @@ -1013,7 +1020,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} rules: rulebookRules, skillsSettings: settingsInstance.getGroup("skills") as SkillsSettings, customPrompt: options.systemPrompt, - isCoordinator: options.hasUI, }); } return options.systemPrompt(defaultPrompt); diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index d718320c6..a1f51b664 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -444,8 +444,6 @@ export interface BuildSystemPromptOptions { preloadedSkills?: Skill[]; /** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */ rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>; - /** Whether this is the main coordinator agent (not a subagent). Enables parallel delegation emphasis. */ - isCoordinator?: boolean; } /** Build the system prompt with tools, guidelines, and context */ @@ -465,7 +463,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): skills: providedSkills, preloadedSkills: providedPreloadedSkills, rules, - isCoordinator, } = options; const resolvedCwd = cwd ?? process.cwd(); const resolvedCustomPrompt = await resolvePromptInput(customPrompt, "system prompt"); @@ -533,7 +530,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): rules: rules ?? [], dateTime, cwd: resolvedCwd, - isCoordinator: isCoordinator ?? false, }); } @@ -550,6 +546,5 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): dateTime, cwd: resolvedCwd, appendSystemPrompt: resolvedAppendPrompt ?? "", - isCoordinator: isCoordinator ?? false, }); } diff --git a/packages/coding-agent/src/task/agents.ts b/packages/coding-agent/src/task/agents.ts index 12f3baf04..6cf8d02ad 100644 --- a/packages/coding-agent/src/task/agents.ts +++ b/packages/coding-agent/src/task/agents.ts @@ -46,7 +46,7 @@ const EMBEDDED_AGENT_DEFS: EmbeddedAgentDef[] = [ frontmatter: { name: "task", description: "General-purpose subagent with full capabilities for delegated multi-step tasks", - spawns: "explore", + spawns: "*", model: "default", }, template: taskMd, diff --git a/packages/coding-agent/src/task/executor.ts b/packages/coding-agent/src/task/executor.ts index 2930f7e0e..e80bdaabd 100644 --- a/packages/coding-agent/src/task/executor.ts +++ b/packages/coding-agent/src/task/executor.ts @@ -157,6 +157,8 @@ export interface ExecutorOptions { modelOverride?: string | string[]; thinkingLevel?: ThinkingLevel; outputSchema?: unknown; + /** Parent task recursion depth (0 = top-level, 1 = first child, etc.) */ + taskDepth?: number; enableLsp?: boolean; signal?: AbortSignal; onProgress?: (progress: AgentProgress) => void; @@ -428,17 +430,25 @@ export async function runSubprocess(options: ExecutorOptions): Promise= 0 && childDepth >= maxRecursionDepth; + // Add tools if specified let toolNames: string[] | undefined; if (agent.tools && agent.tools.length > 0) { toolNames = agent.tools; // Auto-include task tool if spawns defined but task not in tools - if (agent.spawns !== undefined && !toolNames.includes("task")) { + if (agent.spawns !== undefined && !toolNames.includes("task") && !atMaxDepth) { toolNames = [...toolNames, "task"]; } } - const settings = options.settings ?? Settings.isolated(); + if (atMaxDepth && toolNames?.includes("task")) { + toolNames = toolNames.filter(name => name !== "task"); + } const pythonToolMode = settings.get("python.toolMode") ?? "both"; if (toolNames?.includes("exec")) { const expanded = toolNames.filter(name => name !== "exec"); @@ -454,7 +464,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise string | null; + readonly #parentPrefix: string | undefined; - constructor(getArtifactsDir: () => string | null) { + constructor(getArtifactsDir: () => string | null, options?: { parentPrefix?: string }) { this.#getArtifactsDir = getArtifactsDir; + this.#parentPrefix = options?.parentPrefix; } /** @@ -41,12 +50,15 @@ export class AgentOutputManager { return; // Directory doesn't exist yet } + const pattern = this.#parentPrefix + ? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)-.*\\.md$`) + : /^(\d+)-.*\.md$/; + let maxId = -1; for (const file of files) { - // Agent outputs are named: {index}-{id}.md (e.g., "0-AuthProvider.md") - const match = file.match(/^(\d+)-.*\.md$/); + const match = file.match(pattern); if (match) { - const id = parseInt(match[1], 10); + const id = Number.parseInt(match[1], 10); if (id > maxId) maxId = id; } } @@ -61,7 +73,8 @@ export class AgentOutputManager { */ async allocate(id: string): Promise { await this.#ensureInitialized(); - return `${this.#nextId++}-${id}`; + const prefix = this.#parentPrefix ? `${this.#parentPrefix}.` : ""; + return `${prefix}${this.#nextId++}-${id}`; } /** @@ -72,7 +85,8 @@ export class AgentOutputManager { */ async allocateBatch(ids: string[]): Promise { await this.#ensureInitialized(); - return ids.map(id => `${this.#nextId++}-${id}`); + const prefix = this.#parentPrefix ? `${this.#parentPrefix}.` : ""; + return ids.map(id => `${prefix}${this.#nextId++}-${id}`); } /** diff --git a/packages/coding-agent/src/task/render.ts b/packages/coding-agent/src/task/render.ts index 69f76a5d9..0d4293f6b 100644 --- a/packages/coding-agent/src/task/render.ts +++ b/packages/coding-agent/src/task/render.ts @@ -77,6 +77,18 @@ function formatJsonScalar(value: unknown, _theme: Theme): string { return ""; } +function formatTaskId(id: string): string { + const segments = id.split("."); + if (segments.length < 2) return id; + + const parsed = segments.map(segment => segment.match(/^(\d+)-(.+)$/)); + if (parsed.some(match => !match)) return id; + + const indices = parsed.map(match => match![1]).join("."); + const labels = parsed.map(match => match![2]).join(">"); + return `${indices} ${labels}`; +} + const MISSING_SUBMIT_RESULT_WARNING_PREFIX = "SYSTEM WARNING: Subagent exited without calling submit_result tool"; function extractMissingSubmitResultWarning(output: string): { warning?: string; rest: string } { @@ -518,7 +530,8 @@ function renderAgentProgress( // Main status line: id: description [status] · stats · ⟨agent⟩ const description = progress.description?.trim(); - const titlePart = description ? `${theme.bold(progress.id)}: ${description}` : progress.id; + const displayId = formatTaskId(progress.id); + const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId; let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`; // Only show badge for non-running states (spinner already indicates running) @@ -748,7 +761,8 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool // Main status line: id: description [status] · stats · ⟨agent⟩ const description = result.description?.trim(); - const titlePart = description ? `${theme.bold(result.id)}: ${description}` : result.id; + const displayId = formatTaskId(result.id); + const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId; let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)} ${formatBadge( statusText, iconColor, diff --git a/packages/coding-agent/src/tools/index.ts b/packages/coding-agent/src/tools/index.ts index 110615510..a9aa27692 100644 --- a/packages/coding-agent/src/tools/index.ts +++ b/packages/coding-agent/src/tools/index.ts @@ -127,6 +127,8 @@ export interface ToolSession { outputSchema?: unknown; /** Whether to include the submit_result tool by default */ requireSubmitResultTool?: boolean; + /** Task recursion depth (0 = top-level, 1 = first child, etc.) */ + taskDepth?: number; /** Get session file */ getSessionFile: () => string | null; /** Get session ID */ @@ -287,6 +289,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P if (name === "lsp") return session.settings.get("lsp.enabled"); if (name === "calc") return session.settings.get("calc.enabled"); if (name === "browser") return session.settings.get("browser.enabled"); + if (name === "task") { + const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2; + const currentDepth = session.taskDepth ?? 0; + return maxDepth < 0 || currentDepth < maxDepth; + } return true; }; if (includeSubmitResult && requestedTools && !requestedTools.includes("submit_result")) {