import { randomUUID } from "node:crypto"; import * as fs from "node:fs"; import * as path from "node:path"; import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { type AgentConfig, type AgentScope } from "../agents/agents.ts"; import { resolveExecutionAgentScope } from "../agents/agent-scope.ts"; import { handleManagementAction } from "../agents/agent-management.ts"; import { normalizeSkillInput } from "../agents/skills.ts"; import { buildDoctorReport } from "../extension/doctor.ts"; import { resolveModelCandidate } from "./shared/model-fallback.ts"; import { toModelInfo, type ModelInfo } from "../shared/model-info.ts"; import { createForkContextResolver } from "../shared/fork-context.ts"; import { resolveCurrentSessionId } from "../shared/session-identity.ts"; import { getStepAgents, isParallelStep, type ChainStep, type SequentialStep, } from "../shared/settings.ts"; import { readStatus, resolveChildCwd } from "../shared/utils.ts"; import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState, } from "../intercom/intercom-bridge.ts"; import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts"; import { buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation, type AsyncResumeTarget } from "./background/async-resume.ts"; import { inspectSubagentStatus } from "./background/run-status.ts"; import type { ResolvedOutputStoreConfig } from "./background/durable-store.ts"; import { executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable } from "./background/async-execution.ts"; import { resolveControlConfig } from "./shared/subagent-control.ts"; import { findWorktreeTaskCwdConflict, formatWorktreeTaskCwdConflict, } from "./shared/worktree.ts"; import { type ArtifactConfig, type ControlConfig, type Details, type ExtensionConfig, type MaxOutputConfig, type SubagentState, DEFAULT_ARTIFACT_CONFIG, CHAIN_RUNS_DIR, RESULTS_DIR, TEMP_ROOT_DIR, SUBAGENT_ACTIONS, checkSubagentDepth, resolveTopLevelParallelConcurrency, resolveTopLevelParallelMaxTasks, resolveChildMaxSubagentDepth, resolveCurrentMaxSubagentDepth, wrapForkTask, } from "../shared/types.ts"; const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2"; interface TaskParam { agent: string; task: string; cwd?: string; count?: number; output?: string | boolean; outputMode?: "inline" | "file-only"; reads?: string[] | boolean; progress?: boolean; model?: string; skill?: string | string[] | boolean; } export interface SubagentParamsLike { action?: string; id?: string; runId?: string; dir?: string; index?: number; agent?: string; task?: string; message?: string; chain?: ChainStep[]; tasks?: TaskParam[]; concurrency?: number; worktree?: boolean; context?: "fresh" | "fork"; async?: boolean; clarify?: boolean; share?: boolean; control?: ControlConfig; sessionDir?: string; cwd?: string; maxOutput?: MaxOutputConfig; artifacts?: boolean; includeProgress?: boolean; model?: string; skill?: string | string[] | boolean; output?: string | boolean; outputMode?: "inline" | "file-only"; agentScope?: unknown; chainDir?: string; } interface ExecutorDeps { pi: ExtensionAPI; state: SubagentState; config: ExtensionConfig; asyncDirRoot: string; outputStore: ResolvedOutputStoreConfig; tempArtifactsDir: string; getSubagentSessionRoot: (parentSessionFile: string | null) => string; expandTilde: (p: string) => string; discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[] }; } interface AsyncExecutionData { params: SubagentParamsLike; effectiveCwd: string; ctx: ExtensionContext; agents: AgentConfig[]; runId: string; shareEnabled: boolean; sessionRoot: string; sessionFileForIndex: (idx?: number) => string | undefined; artifactConfig: ArtifactConfig; artifactsDir: string; controlConfig: ReturnType; intercomBridge: IntercomBridgeState; } function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string { return requestedCwd ? path.resolve(runtimeCwd, requestedCwd) : runtimeCwd; } function getRequestedModeLabel(params: SubagentParamsLike): Details["mode"] { if ((params.chain?.length ?? 0) > 0) return "chain"; if ((params.tasks?.length ?? 0) > 0) return "parallel"; if (params.agent) return "single"; return "single"; } function toExecutionErrorResult(params: SubagentParamsLike, error: unknown): AgentToolResult
{ const message = error instanceof Error ? error.message : String(error); return withForkContext( { content: [{ type: "text", text: message }], isError: true, details: { mode: getRequestedModeLabel(params), results: [] }, }, params.context, ); } function withForkContext(result: AgentToolResult
, context: SubagentParamsLike["context"]): AgentToolResult
{ if (context !== "fork" || !result.details) return result; return { ...result, details: { ...result.details, context: "fork" } }; } function buildRequestedModeError(params: SubagentParamsLike, message: string): AgentToolResult
{ return withForkContext( { content: [{ type: "text", text: message }], isError: true, details: { mode: getRequestedModeLabel(params), results: [] }, }, params.context, ); } function expandTopLevelTaskCounts(tasks: TaskParam[]): { tasks?: TaskParam[]; error?: string } { const expanded: TaskParam[] = []; for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) { const task = tasks[taskIndex]!; const rawCount = task.count; if (rawCount !== undefined && (typeof rawCount !== "number" || !Number.isInteger(rawCount) || rawCount < 1)) { return { error: `tasks[${taskIndex}].count must be an integer >= 1` }; } const { count, ...concreteTask } = task; for (let repeat = 0; repeat < (rawCount ?? 1); repeat++) expanded.push({ ...concreteTask }); } return { tasks: expanded }; } function expandChainParallelCounts(chain: ChainStep[]): { chain?: ChainStep[]; error?: string } { const expandedChain: ChainStep[] = []; for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) { const step = chain[stepIndex]!; if (!isParallelStep(step)) { expandedChain.push(step); continue; } const expandedParallel = []; for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) { const task = step.parallel[taskIndex]!; const rawCount = task.count; if (rawCount !== undefined && (typeof rawCount !== "number" || !Number.isInteger(rawCount) || rawCount < 1)) { return { error: `chain[${stepIndex}].parallel[${taskIndex}].count must be an integer >= 1` }; } const { count, ...concreteTask } = task; for (let repeat = 0; repeat < (rawCount ?? 1); repeat++) expandedParallel.push({ ...concreteTask }); } expandedChain.push({ ...step, parallel: expandedParallel }); } return { chain: expandedChain }; } function normalizeRepeatedParallelCounts(params: SubagentParamsLike): { params?: SubagentParamsLike; error?: AgentToolResult
} { if (params.tasks) { const expandedTasks = expandTopLevelTaskCounts(params.tasks); if (expandedTasks.error) return { error: buildRequestedModeError(params, expandedTasks.error) }; return { params: { ...params, tasks: expandedTasks.tasks } }; } if (params.chain) { const expandedChain = expandChainParallelCounts(params.chain); if (expandedChain.error) return { error: buildRequestedModeError(params, expandedChain.error) }; return { params: { ...params, chain: expandedChain.chain } }; } return { params }; } function applyAgentDefaultContext(params: SubagentParamsLike, agents: AgentConfig[]): SubagentParamsLike { if (params.context !== undefined) return params; const byName = new Map(agents.map((agent) => [agent.name, agent])); const names: string[] = []; if (params.agent) names.push(params.agent); for (const task of params.tasks ?? []) names.push(task.agent); for (const step of params.chain ?? []) names.push(...getStepAgents(step)); return names.some((name) => byName.get(name)?.defaultContext === "fork") ? { ...params, context: "fork" } : params; } function validateExecutionInput( params: SubagentParamsLike, agents: AgentConfig[], hasChain: boolean, hasTasks: boolean, hasSingle: boolean, ): AgentToolResult
| null { if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) { return { content: [{ type: "text", text: `Provide exactly one mode. Agents: ${agents.map((a) => a.name).join(", ") || "none"}` }], isError: true, details: { mode: "single", results: [] }, }; } if (hasSingle && params.agent && !agents.find((agent) => agent.name === params.agent)) { return { content: [{ type: "text", text: `Unknown agent: ${params.agent}` }], isError: true, details: { mode: "single", results: [] } }; } if (hasTasks && params.tasks) { for (let i = 0; i < params.tasks.length; i++) { const task = params.tasks[i]!; if (!agents.find((agent) => agent.name === task.agent)) { return { content: [{ type: "text", text: `Unknown agent: ${task.agent} (task ${i + 1})` }], isError: true, details: { mode: "parallel", results: [] } }; } } } if (hasChain && params.chain) { const firstStep = params.chain[0] as ChainStep | undefined; if (!firstStep) return { content: [{ type: "text", text: "Chain must have at least one step" }], isError: true, details: { mode: "chain", results: [] } }; if (isParallelStep(firstStep)) { const missingTaskIndex = firstStep.parallel.findIndex((task) => !task.task); if (missingTaskIndex !== -1) { return { content: [{ type: "text", text: `First parallel step: task ${missingTaskIndex + 1} must have a task (no previous output to reference)` }], isError: true, details: { mode: "chain", results: [] } }; } } else if (!(firstStep as SequentialStep).task && !params.task) { return { content: [{ type: "text", text: "First step in chain must have a task" }], isError: true, details: { mode: "chain", results: [] } }; } for (let i = 0; i < params.chain.length; i++) { const step = params.chain[i]!; for (const agentName of getStepAgents(step)) { if (!agents.find((agent) => agent.name === agentName)) { return { content: [{ type: "text", text: `Unknown agent: ${agentName} (step ${i + 1})` }], isError: true, details: { mode: "chain", results: [] } }; } } if (isParallelStep(step) && step.parallel.length === 0) { return { content: [{ type: "text", text: `Parallel step ${i + 1} must have at least one task` }], isError: true, details: { mode: "chain", results: [] } }; } } } return null; } function collectChainSessionFiles(chain: ChainStep[], sessionFileForIndex: (idx?: number) => string | undefined): (string | undefined)[] { const sessionFiles: (string | undefined)[] = []; let flatIndex = 0; for (const step of chain) { if (isParallelStep(step)) { for (let i = 0; i < step.parallel.length; i++) sessionFiles.push(sessionFileForIndex(flatIndex++)); continue; } sessionFiles.push(sessionFileForIndex(flatIndex++)); } return sessionFiles; } function wrapChainTasksForFork(chain: ChainStep[], context: SubagentParamsLike["context"]): ChainStep[] { if (context !== "fork") return chain; return chain.map((step, stepIndex) => { if (isParallelStep(step)) { return { ...step, parallel: step.parallel.map((task) => ({ ...task, task: wrapForkTask(task.task ?? "{previous}") })) }; } const sequential = step as SequentialStep; return { ...sequential, task: wrapForkTask(sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}")) }; }); } function buildParallelWorktreeTaskCwdError(tasks: ReadonlyArray<{ agent: string; cwd?: string }>, sharedCwd: string): string | undefined { const conflict = findWorktreeTaskCwdConflict(tasks, sharedCwd); return conflict ? formatWorktreeTaskCwdConflict(conflict, sharedCwd) : undefined; } function buildChainWorktreeTaskCwdError(chain: ChainStep[], sharedCwd: string): string | undefined { for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) { const step = chain[stepIndex]!; if (!isParallelStep(step) || !step.worktree) continue; const stepCwd = resolveChildCwd(sharedCwd, step.cwd); const conflict = findWorktreeTaskCwdConflict(step.parallel, stepCwd); if (!conflict) continue; return `parallel chain step ${stepIndex + 1}: ${formatWorktreeTaskCwdConflict(conflict, stepCwd)}`; } return undefined; } function getAsyncInterruptTarget(state: SubagentState, runId: string | undefined, asyncDirRoot: string): { asyncId: string; asyncDir: string } | undefined { if (runId) { const direct = state.asyncJobs.get(runId); if (direct) return { asyncId: direct.asyncId, asyncDir: direct.asyncDir }; try { const location = resolveAsyncRunLocation({ id: runId }, asyncDirRoot, RESULTS_DIR); if (location.asyncDir) return { asyncId: location.resolvedId ?? path.basename(location.asyncDir), asyncDir: location.asyncDir }; } catch { // Fall back to live in-memory jobs below. } } let newest: { asyncId: string; asyncDir: string; updatedAt: number } | undefined; for (const job of state.asyncJobs.values()) { if (job.status !== "running") continue; if (!newest || (job.updatedAt ?? 0) > newest.updatedAt) newest = { asyncId: job.asyncId, asyncDir: job.asyncDir, updatedAt: job.updatedAt ?? 0 }; } return newest ? { asyncId: newest.asyncId, asyncDir: newest.asyncDir } : undefined; } function interruptAsyncRun(state: SubagentState, runId: string | undefined, asyncDirRoot: string): AgentToolResult
| null { const target = getAsyncInterruptTarget(state, runId, asyncDirRoot); if (!target) return null; const status = readStatus(target.asyncDir); if (!status || status.state !== "running" || typeof status.pid !== "number") { return { content: [{ type: "text", text: `No running async run with an interrupt-capable pid was found for '${runId ?? "current"}'.` }], isError: true, details: { mode: "management", results: [] }, }; } try { process.kill(status.pid, ASYNC_INTERRUPT_SIGNAL); const tracked = state.asyncJobs.get(target.asyncId); if (tracked) { tracked.activityState = undefined; tracked.updatedAt = Date.now(); } return { content: [{ type: "text", text: `Interrupt requested for async run ${target.asyncId}.` }], details: { mode: "management", results: [] } }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Failed to interrupt async run ${target.asyncId}: ${message}` }], isError: true, details: { mode: "management", results: [] } }; } } function formatRevivedAsyncStart(result: AgentToolResult
, revived: { runId?: string; asyncId?: string; asyncDir?: string }, source: AsyncResumeTarget): AgentToolResult
{ const revivedId = revived.asyncId ?? revived.runId ?? result.details?.asyncId ?? result.details?.runId ?? "unknown"; const lines = [ `Revived async child ${source.agent} from run ${source.runId}.`, `New run: ${revivedId}`, revived.asyncDir ? `Dir: ${revived.asyncDir}` : undefined, `Status if needed: subagent({ action: "status", id: "${revivedId}" })`, ].filter((line): line is string => Boolean(line)); return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details }; } async function resumeAsyncRun(input: { params: SubagentParamsLike; requestCwd: string; ctx: ExtensionContext; deps: ExecutorDeps }): Promise> { const followUp = (input.params.message ?? input.params.task ?? "").trim(); if (!followUp) return { content: [{ type: "text", text: "action='resume' requires message." }], isError: true, details: { mode: "management", results: [] } }; let target: AsyncResumeTarget; try { target = resolveAsyncResumeTarget(input.params, { asyncDirRoot: input.deps.asyncDirRoot }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } }; } if (target.kind === "live") { const delivered = await deliverSubagentIntercomMessageEvent( input.deps.pi.events, target.intercomTarget, `Follow-up for async run ${target.runId} (${target.agent}):\n\n${followUp}`, 500, { source: "async-resume", runId: target.runId, agent: target.agent, index: target.index }, ); if (delivered) { return { content: [{ type: "text", text: [`Delivered follow-up to live async child.`, `Run: ${target.runId}`, `Intercom target: ${target.intercomTarget}`].join("\n") }], details: { mode: "management", results: [] } }; } return { content: [{ type: "text", text: [`Async child appears live but its intercom target is not registered.`, `Run: ${target.runId}`, `Intercom target: ${target.intercomTarget}`, `Wait for completion, then retry action='resume'.`].join("\n") }], isError: true, details: { mode: "management", results: [] } }; } const { blocked, depth, maxDepth } = checkSubagentDepth(input.deps.config.maxSubagentDepth); if (blocked) { return { content: [{ type: "text", text: `Nested subagent resume blocked (depth=${depth}, max=${maxDepth}). Complete the follow-up directly instead.` }], isError: true, details: { mode: "management", results: [] } }; } const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null; input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager); const effectiveCwd = target.cwd ?? input.requestCwd; const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope); const discoveredAgents = input.deps.discoverAgents(effectiveCwd, scope).agents; const sessionName = resolveIntercomSessionTarget(input.deps.pi.getSessionName(), input.ctx.sessionManager.getSessionId()); const intercomBridge = resolveIntercomBridge({ config: input.deps.config.intercomBridge, context: input.params.context, orchestratorTarget: sessionName, cwd: effectiveCwd, }); const agents = intercomBridge.active ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge)) : discoveredAgents; const agentConfig = agents.find((agent) => agent.name === target.agent); if (!agentConfig) return { content: [{ type: "text", text: `Unknown agent for resume: ${target.agent}` }], isError: true, details: { mode: "management", results: [] } }; const runId = randomUUID().slice(0, 8); const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false }; const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo); const result = executeAsyncSingle(runId, { agent: target.agent, task: buildRevivedAsyncTask(target, followUp), agentConfig, ctx: { pi: input.deps.pi, cwd: input.requestCwd, currentSessionId: input.deps.state.currentSessionId!, currentModelProvider: input.ctx.model?.provider, parentSessionFile: parentSessionFile ?? undefined, }, cwd: effectiveCwd, maxOutput: input.params.maxOutput, artifactsDir: input.deps.tempArtifactsDir, artifactConfig, shareEnabled: input.params.share === true, sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile), asyncDirRoot: input.deps.asyncDirRoot, outputStore: input.deps.outputStore, sessionFile: target.sessionFile, maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth), worktreeSetupHook: input.deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs, controlConfig: resolveControlConfig(input.deps.config.control, input.params.control), controlIntercomTarget: intercomBridge.active ? intercomBridge.orchestratorTarget : undefined, childIntercomTarget: intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(runId, agent, index) : undefined, availableModels, }); return formatRevivedAsyncStart(result, { runId: result.details.runId, asyncId: result.details.asyncId, asyncDir: result.details.asyncDir }, target); } function runAsyncPath(data: AsyncExecutionData, deps: ExecutorDeps): AgentToolResult
{ const { params, effectiveCwd, agents, ctx, runId, shareEnabled, sessionRoot, sessionFileForIndex, artifactConfig, artifactsDir, controlConfig, intercomBridge } = data; const hasChain = (params.chain?.length ?? 0) > 0; const hasTasks = (params.tasks?.length ?? 0) > 0; const hasSingle = !hasChain && !hasTasks && Boolean(params.agent); if (hasChain && params.chain) { const error = buildChainWorktreeTaskCwdError(params.chain, effectiveCwd); if (error) return { content: [{ type: "text", text: error }], isError: true, details: { mode: "chain", results: [] } }; } if (hasTasks && params.tasks) { const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks); if (params.tasks.length > maxParallelTasks) return { content: [{ type: "text", text: `Max ${maxParallelTasks} tasks` }], isError: true, details: { mode: "parallel", results: [] } }; if (params.worktree) { const error = buildParallelWorktreeTaskCwdError(params.tasks, effectiveCwd); if (error) return { content: [{ type: "text", text: error }], isError: true, details: { mode: "parallel", results: [] } }; } } if (!isAsyncAvailable()) { return { content: [{ type: "text", text: "Async mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-agents package dependencies are installed." }], isError: true, details: { mode: getRequestedModeLabel(params), results: [] } }; } const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId!, currentModelProvider: ctx.model?.provider, parentSessionFile: ctx.sessionManager.getSessionFile() ?? undefined, }; const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo); const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth); const currentProvider = ctx.model?.provider; const controlIntercomTarget = intercomBridge.active ? intercomBridge.orchestratorTarget : undefined; const childIntercomTarget = intercomBridge.active ? (agent: string, index: number) => resolveSubagentIntercomTarget(runId, agent, index) : undefined; if (hasTasks && params.tasks) { const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent)); const parallelTasks = params.tasks.map((task, index) => { const skillOverride = normalizeSkillInput(task.skill); const output = task.output === true ? agentConfigs[index]?.output : task.output !== undefined ? task.output : undefined; return { agent: task.agent, task: params.context === "fork" ? wrapForkTask(task.task) : task.task, cwd: task.cwd, ...(task.model ? { model: resolveModelCandidate(task.model, availableModels, currentProvider) } : {}), ...(skillOverride !== undefined ? { skill: skillOverride } : {}), ...(output !== undefined ? { output } : {}), ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}), ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}), ...(task.progress !== undefined ? { progress: task.progress } : {}), }; }); return executeAsyncChain(runId, { chain: [{ parallel: parallelTasks, concurrency: resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency), worktree: params.worktree }], resultMode: "parallel", agents, ctx: asyncCtx, availableModels, cwd: effectiveCwd, maxOutput: params.maxOutput, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, shareEnabled, sessionRoot, asyncDirRoot: deps.asyncDirRoot, outputStore: deps.outputStore, chainSkills: [], sessionFilesByFlatIndex: params.tasks.map((_, index) => sessionFileForIndex(index)), maxSubagentDepth: currentMaxSubagentDepth, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, controlConfig, controlIntercomTarget, childIntercomTarget, }); } if (hasChain && params.chain) { const normalized = normalizeSkillInput(params.skill); const chainSkills = normalized === false ? [] : (normalized ?? []); const chain = wrapChainTasksForFork(params.chain, params.context); return executeAsyncChain(runId, { chain, task: params.task, agents, ctx: asyncCtx, availableModels, cwd: effectiveCwd, maxOutput: params.maxOutput, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, shareEnabled, sessionRoot, asyncDirRoot: deps.asyncDirRoot, outputStore: deps.outputStore, chainSkills, sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForIndex), maxSubagentDepth: currentMaxSubagentDepth, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, controlConfig, controlIntercomTarget, childIntercomTarget, }); } if (hasSingle) { const agentConfig = agents.find((agent) => agent.name === params.agent)!; const rawOutput = params.output !== undefined ? params.output : agentConfig.output; const output = rawOutput === true ? agentConfig.output : rawOutput as string | false | undefined; const normalizedSkills = normalizeSkillInput(params.skill); const skills = normalizedSkills === false ? [] : normalizedSkills; const modelOverride = resolveModelCandidate(params.model ?? agentConfig.model, availableModels, currentProvider); return executeAsyncSingle(runId, { agent: params.agent!, task: params.context === "fork" ? wrapForkTask(params.task ?? "") : (params.task ?? ""), agentConfig, ctx: asyncCtx, availableModels, cwd: effectiveCwd, maxOutput: params.maxOutput, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, shareEnabled, sessionRoot, asyncDirRoot: deps.asyncDirRoot, outputStore: deps.outputStore, sessionFile: sessionFileForIndex(0), skills, output, outputMode: params.outputMode ?? "inline", modelOverride, maxSubagentDepth: resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth), worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, controlConfig, controlIntercomTarget, childIntercomTarget, }); } return { content: [{ type: "text", text: "Invalid params" }], isError: true, details: { mode: "single", results: [] } }; } export function createSubagentExecutor(deps: ExecutorDeps): { execute: ( id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult
) => void) | undefined, ctx: ExtensionContext, ) => Promise>; } { const execute = async ( _id: string, params: SubagentParamsLike, _signal: AbortSignal, _onUpdate: ((r: AgentToolResult
) => void) | undefined, ctx: ExtensionContext, ): Promise> => { deps.state.baseCwd = ctx.cwd; const requestCwd = resolveRequestedCwd(ctx.cwd, params.cwd); const paramsWithResolvedCwd = params.cwd === undefined ? params : { ...params, cwd: requestCwd }; if (params.action) { if (params.action === "doctor") { let currentSessionFile: string | null = null; let currentSessionId = deps.state.currentSessionId; let sessionError: string | undefined; try { currentSessionFile = ctx.sessionManager.getSessionFile() ?? null; currentSessionId = ctx.sessionManager.getSessionId(); } catch (error) { sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error); } let orchestratorTarget: string | undefined; try { orchestratorTarget = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId()); } catch {} return { content: [{ type: "text", text: buildDoctorReport({ cwd: requestCwd, config: deps.config, state: deps.state, context: paramsWithResolvedCwd.context, requestedSessionDir: paramsWithResolvedCwd.sessionDir, currentSessionFile, currentSessionId, orchestratorTarget, sessionError, expandTilde: deps.expandTilde, paths: { tempRootDir: TEMP_ROOT_DIR, asyncDir: deps.asyncDirRoot, resultsDir: RESULTS_DIR, chainRunsDir: CHAIN_RUNS_DIR, }, }), }], details: { mode: "management", results: [] }, }; } if (params.action === "status") return inspectSubagentStatus(paramsWithResolvedCwd, { asyncDirRoot: deps.asyncDirRoot }); if (params.action === "resume") return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps }); if (params.action === "interrupt") { const asyncInterruptResult = interruptAsyncRun(deps.state, paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id, deps.asyncDirRoot); if (asyncInterruptResult) return asyncInterruptResult; return { content: [{ type: "text", text: "No interrupt-capable async run found in this session." }], isError: true, details: { mode: "management", results: [] } }; } if (!(SUBAGENT_ACTIONS as readonly string[]).includes(params.action)) { return { content: [{ type: "text", text: `Unknown action: ${params.action}. Valid: ${SUBAGENT_ACTIONS.join(", ")}` }], isError: true, details: { mode: "management", results: [] } }; } return handleManagementAction(params.action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd }); } if (paramsWithResolvedCwd.async === false) { return { content: [{ type: "text", text: "Foreground mode has been removed. All subagent execution runs in the background. Omit async or set async: true, then inspect progress with /agents or subagent({ action: \"status\", id: \"...\" })." }], isError: true, details: { mode: getRequestedModeLabel(paramsWithResolvedCwd), results: [] }, }; } if (paramsWithResolvedCwd.clarify === true) { return { content: [{ type: "text", text: "clarify:true requires the removed foreground TUI flow. Start an async run without clarify, then inspect progress with /agents." }], isError: true, details: { mode: getRequestedModeLabel(paramsWithResolvedCwd), results: [] }, }; } const { blocked, depth, maxDepth } = checkSubagentDepth(deps.config.maxSubagentDepth); if (blocked) { return { content: [{ type: "text", text: `Nested subagent call blocked (depth=${depth}, max=${maxDepth}). You are running at the maximum subagent nesting depth. Complete your current task directly without delegating to further subagents.` }], isError: true, details: { mode: "single", results: [] }, }; } const normalized = normalizeRepeatedParallelCounts(paramsWithResolvedCwd); if (normalized.error) return normalized.error; let effectiveParams = { ...normalized.params!, async: true, clarify: false }; const scope: AgentScope = resolveExecutionAgentScope(effectiveParams.agentScope); const effectiveCwd = effectiveParams.cwd ?? ctx.cwd; const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null; deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager); const discoveredAgents = deps.discoverAgents(effectiveCwd, scope).agents; effectiveParams = applyAgentDefaultContext(effectiveParams, discoveredAgents); const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId()); const intercomBridge = resolveIntercomBridge({ config: deps.config.intercomBridge, context: effectiveParams.context, orchestratorTarget: sessionName, cwd: effectiveCwd, }); const agents = intercomBridge.active ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge)) : discoveredAgents; const hasChain = (effectiveParams.chain?.length ?? 0) > 0; const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0; const hasSingle = !hasChain && !hasTasks && Boolean(effectiveParams.agent); const validationError = validateExecutionInput(effectiveParams, agents, hasChain, hasTasks, hasSingle); if (validationError) return validationError; let sessionFileForIndex: (idx?: number) => string | undefined = () => undefined; try { sessionFileForIndex = createForkContextResolver(ctx.sessionManager, effectiveParams.context).sessionFileForIndex; } catch (error) { return toExecutionErrorResult(effectiveParams, error); } const runId = randomUUID().slice(0, 8); let sessionRoot: string; if (effectiveParams.sessionDir) { sessionRoot = path.resolve(deps.expandTilde(effectiveParams.sessionDir)); } else { const baseSessionRoot = deps.config.defaultSessionDir ? path.resolve(deps.expandTilde(deps.config.defaultSessionDir)) : deps.getSubagentSessionRoot(parentSessionFile); sessionRoot = path.join(baseSessionRoot, runId); } try { fs.mkdirSync(sessionRoot, { recursive: true }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return toExecutionErrorResult(effectiveParams, new Error(`Failed to create session directory '${sessionRoot}': ${message}`)); } const sessionDirForIndex = (idx?: number) => path.join(sessionRoot, `run-${idx ?? 0}`); const childSessionFileForIndex = (idx?: number) => sessionFileForIndex(idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl"); const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: effectiveParams.artifacts !== false }; const execData: AsyncExecutionData = { params: effectiveParams, effectiveCwd, ctx, agents, runId, shareEnabled: effectiveParams.share === true, sessionRoot, sessionFileForIndex: childSessionFileForIndex, artifactConfig, artifactsDir: deps.tempArtifactsDir, controlConfig: resolveControlConfig(deps.config.control, effectiveParams.control), intercomBridge, }; try { return withForkContext(runAsyncPath(execData, deps), effectiveParams.context); } catch (error) { return toExecutionErrorResult(effectiveParams, error); } }; return { execute }; }