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 "@selesai/code"; import { type AgentConfig, type AgentScope } from "../../agents/agents.ts"; import { getArtifactsDir, getProjectChainRunsDir } from "../../shared/artifacts.ts"; import { ChainClarifyComponent, type ChainClarifyResult } from "./chain-clarify.ts"; import { toModelInfo, type ModelInfo } from "../../shared/model-info.ts"; import { executeChain } from "./chain-execution.ts"; import { resolveExecutionAgentScope } from "../../agents/agent-scope.ts"; import { handleManagementAction } from "../../agents/agent-management.ts"; import { buildDoctorReport } from "../../extension/doctor.ts"; import { clearPendingForegroundControlNotices } from "../../extension/control-notices.ts"; import { runSync } from "./execution.ts"; import { resolveModelCandidate, resolveSubagentModelOverride } from "../shared/model-fallback.ts"; import type { ModelScopeConfig } from "../shared/model-scope.ts"; import { aggregateParallelOutputs } from "../shared/parallel-utils.ts"; import { recordRun } from "../shared/run-history.ts"; import { buildChainInstructions, writeInitialProgressFile, getStepAgents, isParallelStep, isDynamicParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, taskDisallowsFileUpdates, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides, } from "../../shared/settings.ts"; import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skills.ts"; import { buildAsyncRunnerSteps, executeAsyncChain, executeAsyncSingle, formatAsyncStartedMessage, isAsyncAvailable } from "../background/async-execution.ts"; import type { ScheduledRunAction } from "../background/scheduled-runs.ts"; import { enqueueChainAppendRequest, readPendingChainAppendRequests, runnerStepOutputNames } from "../background/chain-append.ts"; import { ChainOutputValidationError, validateChainOutputBindingsWithContext } from "../shared/chain-outputs.ts"; import { validateAcceptanceInput } from "../shared/acceptance.ts"; import { createForkContextResolver } from "../../shared/fork-context.ts"; import { resolveCurrentSessionId } from "../../shared/session-identity.ts"; import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts"; import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts"; import { DEFAULT_TURN_BUDGET_GRACE_TURNS } from "../shared/turn-budget.ts"; import { validateToolBudgetConfig } from "../shared/tool-budget.ts"; import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts"; import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts"; import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts"; import { attachNestedChildrenToResultChildren, buildSubagentResultIntercomPayload, deliverSubagentIntercomMessageEvent, deliverSubagentResultIntercomEvent, formatSubagentResultReceipt, resolveSubagentResultStatus, stripDetailsOutputsForIntercomReceipt, } from "../../intercom/result-intercom.ts"; import { buildRevivedAsyncTask, interruptLiveAsyncResumeTarget, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts"; import { deliverInterruptRequest, requestAsyncSteer } from "../background/control-channel.ts"; import { reconcileAsyncRun } from "../background/stale-run-reconciler.ts"; import { resolveAsyncRootResultPath } from "../background/chain-root-attachment.ts"; import { attachRootChildrenToSteps, createNestedRoute, readNestedControlResults, resolveInheritedNestedRouteFromEnv, resolveNestedAsyncDir, resolveNestedParentAddressFromEnv, updateForegroundNestedProjection, writeNestedControlRequest, writeNestedEvent, type NestedRunResolutionScope } from "../shared/nested-events.ts"; import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/run-id-resolver.ts"; import { formatNestedRunStatusLines } from "../shared/nested-render.ts"; import { inspectSubagentStatus } from "../background/run-status.ts"; import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts"; import { cleanupWorktrees, createWorktrees, diffWorktrees, findWorktreeTaskCwdConflict, formatWorktreeDiffSummary, formatWorktreeTaskCwdConflict, type WorktreeSetup, } from "../shared/worktree.ts"; import { type AgentProgress, type AcceptanceInput, type ArtifactConfig, type ArtifactPaths, type ControlConfig, type ControlEvent, type Details, type ExtensionConfig, type IntercomEventBus, type MaxOutputConfig, type NestedRouteInfo, type NestedRunSummary, type ResolvedControlConfig, type ResolvedTurnBudget, type ResolvedToolBudget, type SingleResult, type ToolBudgetConfig, type TurnBudgetConfig, type SubagentRunMode, type SubagentState, ASYNC_DIR, DEFAULT_ARTIFACT_CONFIG, RESULTS_DIR, SUBAGENT_ACTIONS, SUBAGENT_CONTROL_EVENT, SUBAGENT_CONTROL_INTERCOM_EVENT, checkSubagentDepth, resolveMaxSubagentSpawnsPerSession, resolveTopLevelParallelConcurrency, resolveTopLevelParallelMaxTasks, resolveChildMaxSubagentDepth, resolveCurrentMaxSubagentDepth, wrapForkTask, } from "../../shared/types.ts"; const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset"]); 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; acceptance?: AcceptanceInput; toolBudget?: ToolBudgetConfig; } export interface SubagentParamsLike { action?: string; id?: string; runId?: string; dir?: string; index?: number; view?: "fleet" | "transcript"; lines?: number; agent?: string; task?: string; message?: string; chain?: ChainStep[]; tasks?: TaskParam[]; concurrency?: number; worktree?: boolean; context?: "fresh" | "fork"; async?: boolean; timeoutMs?: number; maxRuntimeMs?: number; turnBudget?: TurnBudgetConfig; toolBudget?: ToolBudgetConfig; 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; acceptance?: AcceptanceInput; schedule?: string; scheduleName?: string; } interface ExecutorDeps { pi: ExtensionAPI; state: SubagentState; config: ExtensionConfig; asyncByDefault: boolean; handleScheduledRunAction?: (params: SubagentParamsLike, ctx: ExtensionContext) => Promise>; tempArtifactsDir: string; getSubagentSessionRoot: (parentSessionFile: string | null) => string; expandTilde: (p: string) => string; discoverAgents: (cwd: string, scope: AgentScope) => { agents: AgentConfig[]; modelScope?: ModelScopeConfig }; allowMutatingManagementActions?: boolean; kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean; } interface ExecutionContextData { params: SubagentParamsLike; effectiveCwd: string; ctx: ExtensionContext; signal: AbortSignal; onUpdate?: (r: AgentToolResult
) => void; agents: AgentConfig[]; runId: string; shareEnabled: boolean; sessionRoot: string; sessionDirForIndex: (idx?: number) => string; sessionFileForIndex: (idx?: number) => string | undefined; sessionFileForTask: (agentName: string, idx?: number) => string | undefined; thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined; artifactConfig: ArtifactConfig; artifactsDir: string; backgroundRequestedWhileClarifying: boolean; effectiveAsync: boolean; controlConfig: ResolvedControlConfig; intercomBridge: IntercomBridgeState; nestedRoute?: NestedRouteInfo; timeoutMs?: number; deadlineAt?: number; turnBudget?: ResolvedTurnBudget; toolBudget?: ResolvedToolBudget; configToolBudget?: ResolvedToolBudget; contextPolicy: AgentDefaultContextPolicy; modelScope?: ModelScopeConfig; } function resolveRequestedCwd(runtimeCwd: string, requestedCwd: string | undefined): string { return requestedCwd ? path.resolve(runtimeCwd, requestedCwd) : runtimeCwd; } function getForegroundControl(state: SubagentState, runId: string | undefined) { if (runId) return state.foregroundControls.get(runId); if (state.lastForegroundControlId) { const latest = state.foregroundControls.get(state.lastForegroundControlId); if (latest) return latest; } let newest: (SubagentState["foregroundControls"] extends Map ? T : never) | undefined; for (const control of state.foregroundControls.values()) { if (!newest || control.updatedAt > newest.updatedAt) newest = control; } return newest; } function formatForegroundActivity(control: SubagentState["foregroundControls"] extends Map ? T : never): string | undefined { const facts: string[] = []; if (control.currentTool && control.currentToolStartedAt) facts.push(`tool ${control.currentTool} for ${Math.floor(Math.max(0, Date.now() - control.currentToolStartedAt) / 1000)}s`); else if (control.currentTool) facts.push(`tool ${control.currentTool}`); if (control.currentPath) facts.push(`path ${control.currentPath}`); if (control.turnCount !== undefined) facts.push(`${control.turnCount} turns`); if (control.tokens !== undefined) facts.push(`${control.tokens} tokens`); if (control.toolCount !== undefined) facts.push(`${control.toolCount} tools`); if (!control.lastActivityAt) { if (control.currentActivityState === "needs_attention") return ["needs attention", ...facts].join(" | "); if (control.currentActivityState === "active_long_running") return ["active but long-running", ...facts].join(" | "); return facts.length ? facts.join(" | ") : undefined; } const seconds = Math.floor(Math.max(0, Date.now() - control.lastActivityAt) / 1000); if (control.currentActivityState === "needs_attention") return [`no activity for ${seconds}s`, ...facts].join(" | "); if (control.currentActivityState === "active_long_running") return [`active but long-running; last activity ${seconds}s ago`, ...facts].join(" | "); return [`active ${seconds}s ago`, ...facts].join(" | "); } function nestedResolutionScopeForExecutor(deps: ExecutorDeps): NestedRunResolutionScope | undefined { if (deps.allowMutatingManagementActions !== false) return undefined; const route = resolveInheritedNestedRouteFromEnv(); const address = route ? resolveNestedParentAddressFromEnv() : undefined; return { routes: route ? [route] : [], ...(address ? { descendantOf: { parentRunId: address.parentRunId, ...(address.parentStepIndex !== undefined ? { parentStepIndex: address.parentStepIndex } : {}) } } : {}), }; } function trustedSessionRootsForStatus(ctx: ExtensionContext, deps: ExecutorDeps): string[] { const roots = deps.config.defaultSessionDir ? [path.resolve(deps.expandTilde(deps.config.defaultSessionDir))] : []; const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null; if (parentSessionFile) roots.push(deps.getSubagentSessionRoot(parentSessionFile)); return [...new Set(roots)]; } function reserveSubagentSpawns(input: { state: SubagentState; config: ExtensionConfig; sessionId: string | null; requested: number; mode: "single" | "parallel" | "chain" }): AgentToolResult
| undefined { if (input.requested <= 0) return undefined; if (input.state.subagentSpawns?.sessionId !== input.sessionId) { input.state.subagentSpawns = { sessionId: input.sessionId, count: 0 }; } const maxSpawns = resolveMaxSubagentSpawnsPerSession(input.config.maxSubagentSpawnsPerSession); const used = input.state.subagentSpawns.count; if (used + input.requested > maxSpawns) { return { content: [{ type: "text", text: `Subagent spawn limit reached for this session (${used}/${maxSpawns} used, ${input.requested} requested). Complete the work directly or start a new session.` }], isError: true, details: { mode: input.mode, results: [] }, }; } input.state.subagentSpawns.count = used + input.requested; return undefined; } function countRequestedSubagentSpawns(params: SubagentParamsLike, config: ExtensionConfig): number { if (params.tasks) return params.tasks.length; if (params.chain) { return params.chain.reduce((total, step) => { if (isDynamicParallelStep(step)) return total + (step.expand.maxItems ?? config.chain?.dynamicFanout?.maxItems ?? 0); return total + getStepAgents(step).length; }, 0); } return params.agent ? 1 : 0; } function foregroundStatusResult(control: SubagentState["foregroundControls"] extends Map ? T : never): AgentToolResult
{ let nestedWarning: string | undefined; try { updateForegroundNestedProjection(control); } catch (error) { nestedWarning = `Nested status unavailable: ${error instanceof Error ? error.message : String(error)}`; } const activity = formatForegroundActivity(control); const lines = [ `Run: ${control.runId}`, "State: running", `Mode: ${control.mode}`, control.currentAgent ? `Current: ${control.currentAgent}${control.currentIndex !== undefined ? ` step ${control.currentIndex + 1}` : ""}` : undefined, activity ? `Activity: ${activity}` : undefined, ].filter((line): line is string => Boolean(line)); lines.push(...formatNestedRunStatusLines(control.nestedChildren, { indent: "", commandHints: true, maxLines: 20 })); if (nestedWarning) lines.push(`Warning: ${nestedWarning}`); return { content: [{ type: "text", text: lines.join("\n") }], details: { mode: "management", results: [] } }; } function trimRememberedForegroundRuns(state: SubagentState): void { if (!state.foregroundRuns) return; while (state.foregroundRuns.size > 50) { const oldest = [...state.foregroundRuns.values()].sort((left, right) => left.updatedAt - right.updatedAt)[0]; if (!oldest) break; state.foregroundRuns.delete(oldest.runId); } } function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; results: SingleResult[] }): void { state.foregroundRuns ??= new Map(); const previous = state.foregroundRuns.get(input.runId); const updatedAt = Date.now(); state.foregroundRuns.set(input.runId, { runId: input.runId, mode: input.mode, cwd: input.cwd, updatedAt, children: input.results.map((result, index) => { const child = { agent: result.agent, index, status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }), updatedAt, ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}), ...(result.finalOutput ? { finalOutput: result.finalOutput } : {}), ...(result.sessionFile ? { sessionFile: result.sessionFile } : {}), ...(result.artifactPaths ? { artifactPaths: result.artifactPaths } : {}), ...(result.transcriptPath ? { transcriptPath: result.transcriptPath } : {}), ...(result.transcriptError ? { transcriptError: result.transcriptError } : {}), ...(result.detachedReason ? { detachedReason: result.detachedReason } : {}), }; const recovered = previous?.children[index]; return child.status === "detached" && recovered && recovered.status !== "detached" ? recovered : child; }), }); trimRememberedForegroundRuns(state); } function updateRememberedForegroundChild(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; index: number; result: SingleResult }): void { state.foregroundRuns ??= new Map(); const updatedAt = Date.now(); let run = state.foregroundRuns.get(input.runId); if (!run) { run = { runId: input.runId, mode: input.mode, cwd: input.cwd, updatedAt, children: [] }; state.foregroundRuns.set(input.runId, run); } run.updatedAt = updatedAt; const child = run.children[input.index] ?? { agent: input.result.agent, index: input.index, status: "detached" as const }; run.children[input.index] = { ...child, agent: input.result.agent, index: input.index, status: resolveSubagentResultStatus({ exitCode: input.result.exitCode, interrupted: input.result.interrupted, detached: false }), updatedAt, ...(input.result.exitCode !== undefined ? { exitCode: input.result.exitCode } : {}), ...(input.result.finalOutput ? { finalOutput: input.result.finalOutput } : {}), ...(input.result.sessionFile ? { sessionFile: input.result.sessionFile } : {}), ...(input.result.artifactPaths ? { artifactPaths: input.result.artifactPaths } : {}), ...(input.result.transcriptPath ? { transcriptPath: input.result.transcriptPath } : {}), ...(input.result.transcriptError ? { transcriptError: input.result.transcriptError } : {}), ...(input.result.detachedReason ? { detachedReason: input.result.detachedReason } : {}), }; trimRememberedForegroundRuns(state); } function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; intercomTarget: string; cwd: string; sessionFile: string } | undefined { const requested = (params.id ?? params.runId)?.trim(); if (!requested || !state.foregroundRuns?.size) return undefined; const direct = state.foregroundRuns.get(requested); const matches = direct ? [direct] : [...state.foregroundRuns.values()].filter((run) => run.runId.startsWith(requested)); if (matches.length === 0) return undefined; if (matches.length > 1) throw new Error(`Ambiguous foreground run id prefix '${requested}' matched: ${matches.map((run) => run.runId).join(", ")}. Provide a longer id.`); const run = matches[0]!; if (run.children.length > 1 && params.index === undefined) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Provide index to choose one.`); const index = params.index ?? 0; if (!Number.isInteger(index)) throw new Error(`Foreground run '${run.runId}' index must be an integer.`); if (index < 0 || index >= run.children.length) throw new Error(`Foreground run '${run.runId}' has ${run.children.length} children. Index ${index} is out of range.`); const child = run.children[index]!; if (child.status === "detached") throw new Error(`Foreground run '${run.runId}' child ${index} is detached for intercom coordination and cannot be revived safely from the remembered foreground state. Reply to the supervisor request first; after the child exits, start a fresh follow-up if needed.`); if (!child.sessionFile) throw new Error(`Foreground run '${run.runId}' child ${index} does not have a persisted session file to resume from.`); if (path.extname(child.sessionFile) !== ".jsonl") throw new Error(`Foreground run '${run.runId}' child ${index} session file must be a .jsonl file: ${child.sessionFile}`); const sessionFile = path.resolve(child.sessionFile); if (!fs.existsSync(sessionFile)) throw new Error(`Foreground run '${run.runId}' child ${index} session file does not exist: ${child.sessionFile}`); return { runId: run.runId, mode: run.mode, state: "complete", agent: child.agent, index, intercomTarget: resolveSubagentIntercomTarget(run.runId, child.agent, index), cwd: run.cwd, sessionFile }; } type AsyncResumeSourceTarget = ReturnType & { source: "async" }; type ForegroundResumeSourceTarget = NonNullable> & { kind: "revive"; source: "foreground" }; type NestedResumeSourceTarget = { kind: "revive"; source: "nested"; runId: string; state: "complete" | "failed" | "paused"; agent: string; index: number; intercomTarget: string; cwd?: string; sessionFile: string; }; type ResumeSourceTarget = AsyncResumeSourceTarget | ForegroundResumeSourceTarget | NestedResumeSourceTarget; function isAsyncRunNotFound(error: unknown): boolean { return error instanceof Error && error.message.startsWith("Async run not found."); } function isResumeAmbiguity(error: unknown): boolean { return error instanceof Error && /Ambiguous .*run id prefix/.test(error.message); } function resumeTargetExact(target: { runId: string } | undefined, requested: string): boolean { return target?.runId === requested; } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function isExactResumeError(error: unknown, source: "async" | "foreground", requested: string): boolean { if (!(error instanceof Error) || !requested) return false; return new RegExp(`\\b${source} run '${escapeRegExp(requested)}'`, "i").test(error.message); } function resolveResumeTarget(params: SubagentParamsLike, state: SubagentState, options: { asyncRequireSessionFile?: boolean } = {}): ResumeSourceTarget { const requested = (params.id ?? params.runId)?.trim() ?? ""; let foregroundTarget: ForegroundResumeSourceTarget | undefined; let foregroundError: unknown; let asyncTarget: AsyncResumeSourceTarget | undefined; let asyncError: unknown; try { const target = resolveForegroundResumeTarget(params, state); if (target) foregroundTarget = { kind: "revive", source: "foreground", ...target }; } catch (error) { foregroundError = error; } try { asyncTarget = { source: "async", ...resolveAsyncResumeTarget(params, {}, { requireSessionFile: options.asyncRequireSessionFile }) }; } catch (error) { asyncError = error; } if (foregroundTarget && asyncTarget) { const foregroundExact = resumeTargetExact(foregroundTarget, requested); const asyncExact = resumeTargetExact(asyncTarget, requested); if (foregroundExact && !asyncExact) return foregroundTarget; if (asyncExact && !foregroundExact) return asyncTarget; throw new Error(`Resume id '${requested}' is ambiguous between foreground run '${foregroundTarget.runId}' and async run '${asyncTarget.runId}'. Provide a full run id.`); } if (foregroundTarget) { if (isExactResumeError(asyncError, "async", requested)) throw asyncError; if (isResumeAmbiguity(asyncError) && !resumeTargetExact(foregroundTarget, requested)) throw asyncError; return foregroundTarget; } if (asyncTarget) { if (isExactResumeError(foregroundError, "foreground", requested)) throw foregroundError; if (isResumeAmbiguity(foregroundError) && !resumeTargetExact(asyncTarget, requested)) throw foregroundError; return asyncTarget; } if (foregroundError && !isAsyncRunNotFound(asyncError)) throw foregroundError; if (foregroundError) throw foregroundError; if (asyncError) throw asyncError; throw new Error("Run not found. Provide id or runId."); } function getAsyncInterruptTarget( state: SubagentState, runId: string | undefined, location?: { asyncDir: string | null; resolvedId?: string }, ): { asyncId: string; asyncDir: string } | undefined { if (location?.asyncDir) { return { asyncId: location.resolvedId ?? runId ?? path.basename(location.asyncDir), asyncDir: location.asyncDir, }; } if (runId) { const direct = state.asyncJobs.get(runId); if (direct) return { asyncId: direct.asyncId, asyncDir: direct.asyncDir }; } 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 emitControlNotification(input: { pi: ExtensionAPI; controlConfig: ResolvedControlConfig; intercomBridge: IntercomBridgeState; event: ControlEvent; }): void { if (!shouldNotifyControlEvent(input.controlConfig, input.event)) return; const childIntercomTarget = input.intercomBridge.active ? resolveSubagentIntercomTarget(input.event.runId, input.event.agent, input.event.index) : undefined; const payload = { event: input.event, source: "foreground" as const, childIntercomTarget, noticeText: formatControlNoticeMessage(input.event, childIntercomTarget), }; if (input.controlConfig.notifyChannels.includes("event")) { input.pi.events.emit(SUBAGENT_CONTROL_EVENT, payload); } if (input.event.type !== "active_long_running" && input.controlConfig.notifyChannels.includes("intercom") && input.intercomBridge.active && input.intercomBridge.orchestratorTarget) { input.pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, { ...payload, to: input.intercomBridge.orchestratorTarget, message: formatControlIntercomMessage(input.event, childIntercomTarget), }); } } function interruptAsyncRun( state: SubagentState, runId: string | undefined, kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean, location?: { asyncDir: string | null; resolvedId?: string }, ): AgentToolResult
| null { const target = getAsyncInterruptTarget(state, runId, location); if (!target) return null; const status = reconcileAsyncRun(target.asyncDir, { kill }).status; 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 { deliverInterruptRequest({ asyncDir: target.asyncDir, pid: status.pid, kill, source: "interrupt-action" }); 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 steerAsyncRun(input: { state: SubagentState; runId: string; message: string; index?: number; kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean; location: { asyncDir: string | null; resolvedId?: string }; }): AgentToolResult
{ if (!input.location.asyncDir) { return { content: [{ type: "text", text: `Async run '${input.runId}' has no live run directory to steer.` }], isError: true, details: { mode: "management", results: [] }, }; } const status = reconcileAsyncRun(input.location.asyncDir, { kill: input.kill }).status; if (!status || (status.state !== "running" && status.state !== "queued")) { return { content: [{ type: "text", text: `Async run '${input.runId}' is not running or queued and cannot be steered.` }], isError: true, details: { mode: "management", results: [] }, }; } const steps = status.steps ?? []; if (input.index !== undefined) { if (input.index < 0 || input.index >= steps.length) { return { content: [{ type: "text", text: `Async run '${status.runId}' has ${steps.length} children. Index ${input.index} is out of range.` }], isError: true, details: { mode: "management", results: [] }, }; } const targetStep = steps[input.index]; if (targetStep && targetStep.status !== "running" && targetStep.status !== "pending") { return { content: [{ type: "text", text: `Async run '${status.runId}' child ${input.index} is ${targetStep.status} and cannot be steered.` }], isError: true, details: { mode: "management", results: [] }, }; } } else { const running = steps.filter((step) => step.status === "running"); if (running.length === 0 && steps.length > 1) { return { content: [{ type: "text", text: `Async run '${status.runId}' has no running child yet. Provide index to steer a queued child.` }], isError: true, details: { mode: "management", results: [] }, }; } } requestAsyncSteer(input.location.asyncDir, { message: input.message, targetIndex: input.index, source: "steer-action" }); const tracked = input.state.asyncJobs.get(status.runId); if (tracked) tracked.updatedAt = Date.now(); const childText = input.index !== undefined ? ` child ${input.index}` : " running child"; return { content: [{ type: "text", text: `Steering queued for async run ${status.runId}${childText}. Delivery requires a live Pi child session that supports mid-run steering.` }], details: { mode: "management", results: [] }, }; } function duplicateNames(names: string[]): string[] { const seen = new Set(); const duplicates = new Set(); for (const name of names) { if (seen.has(name)) duplicates.add(name); else seen.add(name); } return [...duplicates]; } function appendStepToAsyncChain(input: { params: SubagentParamsLike; requestCwd: string; ctx: ExtensionContext; deps: ExecutorDeps; }): AgentToolResult
{ const targetRunId = input.params.id ?? input.params.runId; if (!targetRunId) { return { content: [{ type: "text", text: "action='append-step' requires id." }], isError: true, details: { mode: "management", results: [] }, }; } if (!input.params.chain || input.params.chain.length !== 1) { return { content: [{ type: "text", text: "action='append-step' requires chain with exactly one step." }], isError: true, details: { mode: "management", results: [] }, }; } const acceptanceErrors = validateExecutionAcceptance(input.params); if (acceptanceErrors.length > 0) { return { content: [{ type: "text", text: `Cannot append step: ${acceptanceErrors.join(" ")}` }], isError: true, details: { mode: "management", results: [] }, }; } let resolved: ResolvedSubagentRunId | undefined; try { resolved = resolveSubagentRunId(targetRunId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } }; } if (!resolved) { return { content: [{ type: "text", text: `No async chain run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] }, }; } if (resolved.kind !== "async" || !resolved.location.asyncDir) { return { content: [{ type: "text", text: `Run '${resolved.id}' is not an append-capable async chain run.` }], isError: true, details: { mode: "management", results: [] }, }; } const status = readStatus(resolved.location.asyncDir); if (!status) { return { content: [{ type: "text", text: `No async run status found for '${resolved.id}'.` }], isError: true, details: { mode: "management", results: [] }, }; } if (status.mode !== "chain") { return { content: [{ type: "text", text: `Run '${resolved.id}' is ${status.mode}; only active chain runs accept appended steps.` }], isError: true, details: { mode: "management", results: [] }, }; } if (status.state !== "running") { return { content: [{ type: "text", text: `Run '${resolved.id}' is ${status.state}; only running chain runs accept appended steps.` }], isError: true, details: { mode: "management", results: [] }, }; } const stillInProgress = (status.steps ?? []).some((step) => step.status === "running" || step.status === "pending") || (status.pendingAppends ?? 0) > 0; if (!stillInProgress) { return { content: [{ type: "text", text: `Run '${resolved.id}' has no running or pending chain steps left; append-step must target an in-progress chain.` }], isError: true, details: { mode: "management", results: [] }, }; } const pendingAppendRequests = readPendingChainAppendRequests(resolved.location.asyncDir); const reservedOutputNames = new Set([ ...Object.keys(status.outputs ?? {}), ...(status.steps ?? []).map((step) => step.outputName).filter((name): name is string => Boolean(name)), ...pendingAppendRequests.flatMap((request) => runnerStepOutputNames(request.steps)), ]); try { validateChainOutputBindingsWithContext(input.params.chain, { maxItems: input.deps.config.chain?.dynamicFanout?.maxItems }, { priorOutputNames: reservedOutputNames, startStepIndex: status.chainStepCount ?? status.steps?.length ?? 0, }); } catch (error) { if (!(error instanceof ChainOutputValidationError)) throw error; return { content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': ${error.message}` }], isError: true, details: { mode: "management", results: [] }, }; } const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope); const discoveredForAppend = input.deps.discoverAgents(input.requestCwd, scope); const agents = discoveredForAppend.agents; const contextPolicy = resolveExplicitContextPolicy(input.params); const chainSkillInput = normalizeSkillInput(input.params.skill); const chainSkills = chainSkillInput === false ? [] : (chainSkillInput ?? []); const asyncCtx = { pi: input.deps.pi, cwd: input.ctx.cwd, currentSessionId: resolveCurrentSessionId(input.ctx.sessionManager), parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: input.ctx.model?.provider, currentModel: input.ctx.model, modelScope: discoveredForAppend.modelScope, }; const built = buildAsyncRunnerSteps(resolved.id, { chain: wrapChainTasksForFork(input.params.chain, contextPolicy), task: input.params.task, resultMode: "chain", agents, ctx: asyncCtx, availableModels: input.ctx.modelRegistry.getAvailable().map(toModelInfo), cwd: status.cwd ?? input.requestCwd, chainSkills, dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems, maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth), asyncDir: resolved.location.asyncDir, validateOutputBindings: false, }); if ("error" in built) { return { content: [{ type: "text", text: built.error }], isError: true, details: { mode: "management", results: [] }, }; } const appendedOutputNames = runnerStepOutputNames(built.steps); const duplicateAppendedOutputs = duplicateNames(appendedOutputNames); if (duplicateAppendedOutputs.length > 0) { return { content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': duplicate output name in appended step: ${duplicateAppendedOutputs.join(", ")}.` }], isError: true, details: { mode: "management", results: [] }, }; } const pendingOutputNames = new Set(pendingAppendRequests.flatMap((request) => runnerStepOutputNames(request.steps))); const pendingDuplicateOutputs = appendedOutputNames.filter((name) => pendingOutputNames.has(name)); if (pendingDuplicateOutputs.length > 0) { return { content: [{ type: "text", text: `Cannot append step to run '${resolved.id}': output name already belongs to a pending append: ${pendingDuplicateOutputs.join(", ")}.` }], isError: true, details: { mode: "management", results: [] }, }; } try { const result = enqueueChainAppendRequest({ asyncDir: resolved.location.asyncDir, runId: resolved.id, steps: built.steps, }); const stepText = built.steps.length === 1 ? "step" : "steps"; return { content: [{ type: "text", text: `Append queued for chain run ${resolved.id}: ${built.steps.length} ${stepText}. It becomes eligible after the chain's already-queued steps finish. Pending appends: ${result.pendingCount}.`, }], details: { mode: "management", results: [], asyncId: resolved.id, asyncDir: resolved.location.asyncDir }, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Failed to append step to chain run ${resolved.id}: ${message}` }], isError: true, details: { mode: "management", results: [] }, }; } } function nestedRunSessionFile(run: NestedRunSummary): string | undefined { return run.sessionFile ?? (run.steps?.length === 1 ? run.steps[0]?.sessionFile : undefined); } function nestedRunAgent(run: NestedRunSummary): string | undefined { return run.agent ?? run.agents?.[0] ?? (run.steps?.length === 1 ? run.steps[0]?.agent : undefined); } function pathWithin(base: string, candidate: string): boolean { const resolvedBase = path.resolve(base); const resolvedCandidate = path.resolve(candidate); return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`); } function validateNestedSessionFile(run: NestedRunSummary, trustedSessionRoots: string[]): string { const sessionFile = nestedRunSessionFile(run); if (!sessionFile) throw new Error(`Nested run '${run.id}' does not have a persisted session file to resume from.`); if (path.extname(sessionFile) !== ".jsonl") throw new Error(`Nested run '${run.id}' session file must be a .jsonl file: ${sessionFile}`); const resolved = path.resolve(sessionFile); if (!path.isAbsolute(sessionFile)) throw new Error(`Nested run '${run.id}' session file must be absolute: ${sessionFile}`); if (!fs.existsSync(resolved)) throw new Error(`Nested run '${run.id}' session file does not exist: ${sessionFile}`); const stat = fs.lstatSync(resolved); if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Nested run '${run.id}' session file is not a regular file: ${sessionFile}`); const realSessionFile = fs.realpathSync(resolved); const trustedRoots = trustedSessionRoots .filter((root) => fs.existsSync(root)) .map((root) => fs.realpathSync(root)); if (!trustedRoots.some((root) => pathWithin(root, realSessionFile))) { throw new Error(`Nested run '${run.id}' session file is outside trusted nested session roots: ${sessionFile}`); } if (!realSessionFile.split(path.sep).includes(run.id)) { throw new Error(`Nested run '${run.id}' session file is not under that nested run's session directory: ${sessionFile}`); } return realSessionFile; } function resolveNestedResumeTarget(match: ResolvedSubagentRunId & { kind: "nested" }, trustedSessionRoots: string[]): NestedResumeSourceTarget { const run = match.match.run; if (run.state === "running" || run.state === "queued") throw new Error(`Nested run '${run.id}' is live; route the follow-up to the owner process instead.`); const agent = nestedRunAgent(run); if (!agent) throw new Error(`Could not determine child agent for nested run '${run.id}'.`); const state = run.state === "complete" || run.state === "failed" || run.state === "paused" ? run.state : "failed"; const asyncDir = resolveNestedAsyncDir(match.match.rootRunId, run); return { kind: "revive", source: "nested", runId: run.id, state, agent, index: 0, intercomTarget: resolveSubagentIntercomTarget(run.id, agent, 0), cwd: asyncDir ? path.dirname(asyncDir) : undefined, sessionFile: validateNestedSessionFile(run, trustedSessionRoots), }; } async function waitForNestedControlResult(target: ResolvedSubagentRunId & { kind: "nested" }, requestId: string, timeoutMs = 1_000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const result = readNestedControlResults(target.match.route).find((candidate) => candidate.requestId === requestId && candidate.targetRunId === target.match.run.id); if (result) return result; await new Promise((resolve) => setTimeout(resolve, 50)); } return undefined; } async function sendNestedControlRequest(target: ResolvedSubagentRunId & { kind: "nested" }, action: "interrupt" | "resume", message?: string) { const requestId = randomUUID(); writeNestedControlRequest(target.match.route, { ts: Date.now(), requestId, targetRunId: target.match.run.id, action, ...(message ? { message } : {}), }); return waitForNestedControlResult(target, requestId); } function directNestedAsyncInterrupt(target: ResolvedSubagentRunId & { kind: "nested" }): AgentToolResult
| undefined { const run = target.match.run; const asyncDir = resolveNestedAsyncDir(target.match.rootRunId, run); if (!asyncDir) return undefined; const status = reconcileAsyncRun(asyncDir, { resultsDir: path.join(RESULTS_DIR, "nested", target.match.rootRunId) }).status; const pid = typeof status?.pid === "number" && status.pid > 0 ? status.pid : run.pid; if (!status || status.state !== "running" || typeof pid !== "number" || pid <= 0) return undefined; try { deliverInterruptRequest({ asyncDir, pid, source: "nested-interrupt" }); return { content: [{ type: "text", text: `Interrupt requested for nested async run ${run.id}.` }], details: { mode: "management", results: [] } }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Failed to interrupt nested async run ${run.id}: ${message}` }], isError: true, details: { mode: "management", results: [] } }; } } function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number }): AgentToolResult
| undefined { const run = input.target.match.run; const asyncDir = resolveNestedAsyncDir(input.target.match.rootRunId, run); if (!asyncDir) return undefined; const status = reconcileAsyncRun(asyncDir, { resultsDir: path.join(RESULTS_DIR, "nested", input.target.match.rootRunId) }).status; if (!status || (status.state !== "running" && status.state !== "queued")) return undefined; const steps = status.steps ?? []; if (input.index !== undefined) { if (input.index < 0 || input.index >= steps.length) return { content: [{ type: "text", text: `Nested async run ${run.id} has ${steps.length} children. Index ${input.index} is out of range.` }], isError: true, details: { mode: "management", results: [] } }; const step = steps[input.index]; if (step && step.status !== "running" && step.status !== "pending") return { content: [{ type: "text", text: `Nested async run ${run.id} child ${input.index} is ${step.status} and cannot be steered.` }], isError: true, details: { mode: "management", results: [] } }; } requestAsyncSteer(asyncDir, { message: input.message, targetIndex: input.index, source: "nested-steer" }); return { content: [{ type: "text", text: `Steering queued for nested async run ${run.id}. Delivery requires a live Pi child session that supports mid-run steering.` }], details: { mode: "management", results: [] } }; } async function interruptNestedRun(target: ResolvedSubagentRunId & { kind: "nested" }): Promise> { const run = target.match.run; if (run.state === "complete") return { content: [{ type: "text", text: `Nested run ${run.id} is already complete and cannot be interrupted.` }], isError: true, details: { mode: "management", results: [] } }; if (run.state === "failed") return { content: [{ type: "text", text: `Nested run ${run.id} has failed and cannot be interrupted.` }], isError: true, details: { mode: "management", results: [] } }; if (run.state === "paused") return { content: [{ type: "text", text: `Nested run ${run.id} is already paused.` }], isError: true, details: { mode: "management", results: [] } }; const result = await sendNestedControlRequest(target, "interrupt"); if (result) return { content: [{ type: "text", text: result.message }], isError: result.ok ? undefined : true, details: { mode: "management", results: [] } }; const direct = directNestedAsyncInterrupt(target); if (direct) return direct; return { content: [{ type: "text", text: `Nested run ${run.id} owner is not reachable and no safe direct async interrupt fallback is available.` }], isError: true, details: { mode: "management", results: [] } }; } async function resumeLiveNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string }): Promise> { const run = input.target.match.run; const result = await sendNestedControlRequest(input.target, "resume", input.message); if (result) return { content: [{ type: "text", text: result.message }], isError: result.ok ? undefined : true, details: { mode: "management", results: [] } }; return { content: [{ type: "text", text: `Nested run ${run.id} appears live but its owner route is not reachable. Wait for completion, then retry action='resume'.` }], isError: true, details: { mode: "management", results: [] } }; } function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number }): AgentToolResult
{ const run = input.target.match.run; if (run.state !== "running" && run.state !== "queued") return { content: [{ type: "text", text: `Nested run ${run.id} is ${run.state} and cannot be steered.` }], isError: true, details: { mode: "management", results: [] } }; const direct = directNestedAsyncSteer(input); if (direct) return direct; return { content: [{ type: "text", text: `Nested run ${run.id} is not a live async Pi child session with a steering inbox. action='steer' cannot target foreground nested runs.` }], isError: true, details: { mode: "management", results: [] } }; } async function resumeAsyncRun(input: { params: SubagentParamsLike; requestCwd: string; ctx: ExtensionContext; deps: ExecutorDeps; }): Promise> { const followUp = (input.params.message ?? input.params.task ?? "").trim(); const attachChain = (input.params.chain?.length ?? 0) > 0 ? input.params.chain as ChainStep[] : undefined; if (!followUp && !attachChain) { return { content: [{ type: "text", text: "action='resume' requires message." }], isError: true, details: { mode: "management", results: [] }, }; } let target: ResumeSourceTarget; const parentSessionFile = input.ctx.sessionManager.getSessionFile() ?? null; try { const requestedId = input.params.id ?? input.params.runId; let resolved: ResolvedSubagentRunId | undefined; try { resolved = requestedId ? resolveSubagentRunId(requestedId, { state: input.deps.state, nested: nestedResolutionScopeForExecutor(input.deps) }) : undefined; } catch (error) { const message = error instanceof Error ? error.message : ""; const asyncMatches = message.match(/async:/g)?.length ?? 0; if (!isResumeAmbiguity(error) || !message.includes("foreground:") || asyncMatches !== 1) throw error; } if (resolved?.kind === "nested") { if (attachChain) { return { content: [{ type: "text", text: "Attaching a running subagent as a chain root is currently available for top-level async runs only." }], isError: true, details: { mode: "management", results: [] }, }; } if (resolved.match.run.state === "running" || resolved.match.run.state === "queued") { return resumeLiveNestedRun({ target: resolved, message: followUp }); } const trustedSessionRoots = [ ...(input.deps.config.defaultSessionDir ? [path.resolve(input.deps.expandTilde(input.deps.config.defaultSessionDir))] : []), ...(parentSessionFile ? [input.deps.getSubagentSessionRoot(parentSessionFile)] : []), ]; target = resolveNestedResumeTarget(resolved, trustedSessionRoots); } else { target = resolveResumeTarget(input.params, input.deps.state, { asyncRequireSessionFile: !attachChain }); } } 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" && !attachChain) { const interrupt = interruptLiveAsyncResumeTarget({ target, state: input.deps.state, kill: input.deps.kill, resultsDir: RESULTS_DIR, }); if (!interrupt.ok) { return { content: [{ type: "text", text: interrupt.message }], isError: true, details: { mode: "management", results: [] }, }; } 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: [`Interrupted live async child, then delivered follow-up.`, `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: [] }, }; } input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager); const effectiveCwd = target.cwd ?? input.requestCwd; const scope: AgentScope = resolveExecutionAgentScope(input.params.agentScope); const discovered = input.deps.discoverAgents(effectiveCwd, scope); const discoveredAgents = discovered.agents; const modelScope = discovered.modelScope; 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, }); 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: [] }, }; } if (attachChain) { if (target.source !== "async") { return { content: [{ type: "text", text: "Attaching a running subagent as a chain root is currently available for async runs only." }], isError: true, details: { mode: "management", 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-subagents package dependencies are installed." }], isError: true, details: { mode: "chain", 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 contextPolicy = resolveExplicitContextPolicy(input.params); const chain = wrapChainTasksForFork(attachChain, contextPolicy); const normalized = normalizeSkillInput(input.params.skill); const result = executeAsyncChain(runId, { chain, task: (input.params.task ?? followUp) || undefined, attachRoot: { runId: target.runId, asyncDir: target.asyncDir ?? path.join(ASYNC_DIR, target.runId), resultPath: resolveAsyncRootResultPath(RESULTS_DIR, target.runId), index: target.index, agent: target.agent, label: `Attached ${target.runId}`, }, agents, ctx: { pi: input.deps.pi, cwd: input.requestCwd, currentSessionId: input.deps.state.currentSessionId, parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: input.ctx.model?.provider, currentModel: input.ctx.model, modelScope, }, availableModels, cwd: effectiveCwd, maxOutput: input.params.maxOutput, artifactsDir: getArtifactsDir(parentSessionFile, effectiveCwd), artifactConfig, shareEnabled: input.params.share === true, sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile), chainSkills: normalized === false ? [] : (normalized ?? []), dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems, maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth), worktreeSetupHook: input.deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: input.deps.config.worktreeBaseDir, 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, globalConcurrencyLimit: input.deps.config.globalConcurrencyLimit, }); if (result.isError) return result; const attachedId = result.details.asyncId ?? runId; const lines = [ `Attached async subagent ${target.runId} as the first step of a new chain.`, `Chain run: ${attachedId}`, `Root: ${target.agent} (step ${target.index + 1})`, result.details.asyncDir ? `Async dir: ${result.details.asyncDir}` : undefined, `Status if needed: subagent({ action: "status", id: "${attachedId}" })`, ].filter((line): line is string => Boolean(line)); return { content: [{ type: "text", text: formatAsyncStartedMessage(lines.join("\n")) }], details: result.details }; } const runId = randomUUID().slice(0, 8); const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false }; const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd); 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, parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: input.ctx.model?.provider, currentModel: input.ctx.model, modelScope, }, cwd: effectiveCwd, maxOutput: input.params.maxOutput, artifactsDir, artifactConfig, shareEnabled: input.params.share === true, sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile), sessionFile: target.sessionFile, outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId), maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth), worktreeSetupHook: input.deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: input.deps.config.worktreeBaseDir, 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, }); if (result.isError) return result; const revivedId = result.details.asyncId ?? runId; const revivedTarget = intercomBridge.active ? resolveSubagentIntercomTarget(revivedId, target.agent, 0) : undefined; const sourceLabel = target.source; const lines = [ `Revived ${sourceLabel} subagent from ${target.runId}.`, `Revived run: ${revivedId}`, `Agent: ${target.agent}`, `Session: ${target.sessionFile}`, result.details.asyncDir ? `Async dir: ${result.details.asyncDir}` : undefined, revivedTarget ? `Intercom target: ${revivedTarget} (if registered)` : 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 }; } function resultSummaryForIntercom(result: SingleResult): string { const output = getSingleResultOutput(result); if (result.exitCode !== 0 && result.error) { return output ? `${result.error}\n\nOutput:\n${output}` : result.error; } return output || result.error || "(no output)"; } function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string): string { const error = result.error || "Failed"; const output = displayOutput.trim(); const lines = [error]; if (output && output !== error.trim()) { lines.push("", "Output:", output); } if (result.artifactPaths?.outputPath) { lines.push("", `Output artifact: ${result.artifactPaths.outputPath}`); } return lines.join("\n"); } function createForegroundControlNotifier(data: Pick, deps: Pick): (event: ControlEvent) => void { return (event) => emitControlNotification({ pi: deps.pi, controlConfig: data.controlConfig, intercomBridge: data.intercomBridge, event, }); } async function emitForegroundResultIntercom(input: { pi: ExtensionAPI; intercomBridge: IntercomBridgeState; runId: string; mode: SubagentRunMode; results: SingleResult[]; chainSteps?: number; nestedChildren?: NestedRunSummary[]; }): Promise | null> { if (!input.intercomBridge.active || !input.intercomBridge.orchestratorTarget) return null; const children = input.results.flatMap((result, index) => result.detached ? [] : [{ agent: result.agent, status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached, }), summary: resultSummaryForIntercom(result), index, artifactPath: result.artifactPaths?.outputPath, sessionPath: result.sessionFile, intercomTarget: resolveSubagentIntercomTarget(input.runId, result.agent, index), }]); if (children.length === 0) return null; const payload = buildSubagentResultIntercomPayload({ to: input.intercomBridge.orchestratorTarget, runId: input.runId, mode: input.mode, source: "foreground", children: attachNestedChildrenToResultChildren(input.runId, children, input.nestedChildren), ...(typeof input.chainSteps === "number" ? { chainSteps: input.chainSteps } : {}), }); const delivered = await deliverSubagentResultIntercomEvent(input.pi.events, payload); if (!delivered) return null; return payload; } async function maybeBuildForegroundIntercomReceipt(input: { pi: ExtensionAPI; intercomBridge: IntercomBridgeState; runId: string; mode: SubagentRunMode; details: Details; nestedChildren?: NestedRunSummary[]; }): Promise<{ text: string; details: Details } | null> { const payload = await emitForegroundResultIntercom({ pi: input.pi, intercomBridge: input.intercomBridge, runId: input.runId, mode: input.mode, results: input.details.results, ...(typeof input.details.totalSteps === "number" ? { chainSteps: input.details.totalSteps } : {}), ...(input.nestedChildren?.length ? { nestedChildren: input.nestedChildren } : {}), }); if (!payload) return null; return { text: formatSubagentResultReceipt({ mode: input.mode, runId: input.runId, payload }), details: stripDetailsOutputsForIntercomReceipt(input.details), }; } function validateExecutionInput( params: SubagentParamsLike, agents: AgentConfig[], hasChain: boolean, hasTasks: boolean, hasSingle: boolean, allowClarifyTaskPrompt: 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" as const, results: [] }, }; } const acceptanceErrors = validateExecutionAcceptance(params); if (acceptanceErrors.length > 0) { return { content: [{ type: "text", text: acceptanceErrors.join(" ") }], isError: true, details: { mode: getRequestedModeLabel(params), 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" as const, 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" as const, results: [] }, }; } } } if (hasChain && params.chain) { if (params.chain.length === 0) { return { content: [{ type: "text", text: "Chain must have at least one step" }], isError: true, details: { mode: "chain" as const, results: [] }, }; } const firstStep = params.chain[0] as ChainStep; if (isParallelStep(firstStep)) { const missingTaskIndex = firstStep.parallel.findIndex((t) => !t.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" as const, results: [] }, }; } } else if (isDynamicParallelStep(firstStep)) { return { content: [{ type: "text", text: "First step in chain cannot be dynamic fanout; expand.from requires a prior structured named output" }], isError: true, details: { mode: "chain" as const, results: [] }, }; } else if (!(firstStep as SequentialStep).task && !params.task && !allowClarifyTaskPrompt) { return { content: [{ type: "text", text: "First step in chain must have a task" }], isError: true, details: { mode: "chain" as const, results: [] }, }; } for (let i = 0; i < params.chain.length; i++) { const step = params.chain[i] as ChainStep; const stepAgents = getStepAgents(step); for (const agentName of stepAgents) { if (!agents.find((a) => a.name === agentName)) { return { content: [{ type: "text", text: `Unknown agent: ${agentName} (step ${i + 1})` }], isError: true, details: { mode: "chain" as const, 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" as const, results: [] }, }; } } } return null; } function validateExecutionChainBindings(params: SubagentParamsLike, dynamicFanoutMaxItems?: number): AgentToolResult
| null { if ((params.chain?.length ?? 0) === 0) return null; try { validateChainOutputBindingsWithContext(params.chain as ChainStep[], { maxItems: dynamicFanoutMaxItems }); } catch (error) { if (error instanceof ChainOutputValidationError) { return { content: [{ type: "text", text: error.message }], isError: true, details: { mode: "chain" as const, results: [] }, }; } throw error; } return null; } function validateExecutionAcceptance(params: SubagentParamsLike): string[] { const errors: string[] = []; errors.push(...validateAcceptanceInput(params.acceptance, "acceptance")); for (const [index, task] of (params.tasks ?? []).entries()) { errors.push(...validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`)); } for (const [stepIndex, step] of (params.chain ?? []).entries()) { errors.push(...validateAcceptanceInput((step as { acceptance?: unknown }).acceptance, `chain[${stepIndex}].acceptance`)); if (isParallelStep(step)) { for (const [taskIndex, task] of step.parallel.entries()) { errors.push(...validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`)); } } else if (isDynamicParallelStep(step)) { errors.push(...validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`)); } } return errors; } 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"; } interface AgentDefaultContextPolicy { params: SubagentParamsLike; contextForAgent(agentName: string): "fresh" | "fork"; usesFork: boolean; } function resolveAgentDefaultContextPolicy(params: SubagentParamsLike, agents: AgentConfig[]): AgentDefaultContextPolicy { if (params.context !== undefined) { return resolveExplicitContextPolicy(params); } const byName = new Map(agents.map((agent) => [agent.name, agent])); const contextForAgent = (agentName: string): "fresh" | "fork" => byName.get(agentName)?.defaultContext === "fork" ? "fork" : "fresh"; const usesFork = collectRequestedAgentNames(params).some((name) => contextForAgent(name) === "fork"); return { params: usesFork ? { ...params, context: "fork" } : params, contextForAgent, usesFork, }; } function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultContextPolicy { const context = params.context === "fork" ? "fork" : "fresh"; return { params, contextForAgent: () => context, usesFork: context === "fork", }; } function collectRequestedAgentNames(params: SubagentParamsLike): string[] { 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; } function shouldForkAgent(contextPolicy: AgentDefaultContextPolicy, agentName: string): boolean { return contextPolicy.contextForAgent(agentName) === "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 resolveForegroundTimeout(params: SubagentParamsLike): { timeoutMs?: number; error?: string } { const rawTimeout = params.timeoutMs; const rawMaxRuntime = params.maxRuntimeMs; if (rawTimeout === undefined && rawMaxRuntime === undefined) return {}; for (const [name, value] of [["timeoutMs", rawTimeout], ["maxRuntimeMs", rawMaxRuntime]] as const) { if (value === undefined) continue; if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) { return { error: `${name} must be a positive integer.` }; } } if (rawTimeout !== undefined && rawMaxRuntime !== undefined && rawTimeout !== rawMaxRuntime) { return { error: "timeoutMs and maxRuntimeMs are aliases; provide only one value or use the same value for both." }; } return { timeoutMs: rawTimeout ?? rawMaxRuntime }; } function resolveTurnBudget(params: SubagentParamsLike, config: ExtensionConfig): { turnBudget?: ResolvedTurnBudget; error?: string } { const raw = params.turnBudget ?? config.turnBudget; if (raw === undefined) return {}; if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { error: "turnBudget must be an object with maxTurns and optional graceTurns." }; if (typeof raw.maxTurns !== "number" || !Number.isInteger(raw.maxTurns) || raw.maxTurns < 1) { return { error: "turnBudget.maxTurns must be an integer >= 1." }; } const graceTurns = raw.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS; if (typeof graceTurns !== "number" || !Number.isInteger(graceTurns) || graceTurns < 0) { return { error: "turnBudget.graceTurns must be an integer >= 0." }; } return { turnBudget: { maxTurns: raw.maxTurns, graceTurns } }; } function resolveToolBudget(raw: unknown, label = "toolBudget"): { toolBudget?: ResolvedToolBudget; error?: string } { const resolved = validateToolBudgetConfig(raw, label); return { toolBudget: resolved.budget, error: resolved.error }; } function resolveEffectiveToolBudget(input: { stepBudget?: ToolBudgetConfig; runBudget?: ResolvedToolBudget; agentBudget?: ToolBudgetConfig; configBudget?: ToolBudgetConfig }): { toolBudget?: ResolvedToolBudget; error?: string } { if (input.stepBudget !== undefined) return resolveToolBudget(input.stepBudget, "toolBudget"); if (input.runBudget !== undefined) return { toolBudget: input.runBudget }; if (input.agentBudget !== undefined) return resolveToolBudget(input.agentBudget, "agent.toolBudget"); return resolveToolBudget(input.configBudget, "config.toolBudget"); } 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 as TaskParam & { count?: unknown }).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 as typeof task & { count?: unknown }).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 withForkContext( result: AgentToolResult
, context: SubagentParamsLike["context"], ): AgentToolResult
{ if (context !== "fork" || !result.details) return result; return { ...result, details: { ...result.details, context: "fork", }, }; } 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 collectChainSessionFiles( chain: ChainStep[], sessionFileForTask: (agentName: string, idx?: number) => string | undefined, dynamicFanoutMaxItems?: number, ): (string | undefined)[] { const sessionFiles: (string | undefined)[] = []; let flatIndex = 0; for (const step of chain) { if (isParallelStep(step)) { for (const task of step.parallel) { sessionFiles.push(sessionFileForTask(task.agent, flatIndex)); flatIndex++; } continue; } if (isDynamicParallelStep(step)) { const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0; for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) { sessionFiles.push(sessionFileForTask(step.parallel.agent, flatIndex)); flatIndex++; } continue; } sessionFiles.push(sessionFileForTask((step as SequentialStep).agent, flatIndex)); flatIndex++; } return sessionFiles; } function collectChainThinkingOverrides( chain: ChainStep[], thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined, dynamicFanoutMaxItems?: number, ): (AgentConfig["thinking"] | undefined)[] { const thinkingOverrides: (AgentConfig["thinking"] | undefined)[] = []; let flatIndex = 0; for (const step of chain) { if (isParallelStep(step)) { for (const task of step.parallel) { thinkingOverrides.push(thinkingOverrideForTask(task.agent, flatIndex)); flatIndex++; } continue; } if (isDynamicParallelStep(step)) { const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0; for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) { thinkingOverrides.push(thinkingOverrideForTask(step.parallel.agent, flatIndex)); flatIndex++; } continue; } thinkingOverrides.push(thinkingOverrideForTask((step as SequentialStep).agent, flatIndex)); flatIndex++; } return thinkingOverrides; } function wrapChainTasksForFork(chain: ChainStep[], contextPolicy: AgentDefaultContextPolicy): ChainStep[] { return chain.map((step, stepIndex) => { if (isParallelStep(step)) { return { ...step, parallel: step.parallel.map((task) => ({ ...task, task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task ?? "{previous}") : task.task, })), }; } if (isDynamicParallelStep(step)) { return { ...step, parallel: { ...step.parallel, task: shouldForkAgent(contextPolicy, step.parallel.agent) ? wrapForkTask(step.parallel.task ?? "{previous}") : step.parallel.task, }, }; } const sequential = step as SequentialStep; return { ...sequential, task: shouldForkAgent(contextPolicy, sequential.agent) ? wrapForkTask(sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}")) : sequential.task, }; }); } function preflightForkSessionsForStaticTasks( params: SubagentParamsLike, contextPolicy: AgentDefaultContextPolicy, sessionFileForTask: (agentName: string, idx?: number) => string | undefined, dynamicFanoutMaxItems?: number, ): void { if (!contextPolicy.usesFork) return; if (params.agent) { if (shouldForkAgent(contextPolicy, params.agent)) sessionFileForTask(params.agent, 0); return; } if (params.tasks) { params.tasks.forEach((task, index) => { if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, index); }); return; } if (!params.chain?.length) return; let flatIndex = 0; for (const step of params.chain) { if (isParallelStep(step)) { for (const task of step.parallel) { if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, flatIndex); flatIndex++; } continue; } if (isDynamicParallelStep(step)) { const maxItems = step.expand.maxItems ?? dynamicFanoutMaxItems ?? 0; if (shouldForkAgent(contextPolicy, step.parallel.agent)) { for (let itemIndex = 0; itemIndex < maxItems; itemIndex++) sessionFileForTask(step.parallel.agent, flatIndex + itemIndex); } flatIndex += maxItems; continue; } const sequential = step as SequentialStep; if (shouldForkAgent(contextPolicy, sequential.agent)) sessionFileForTask(sequential.agent, flatIndex); flatIndex++; } } function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentToolResult
| null { const { params, effectiveCwd, agents, ctx, shareEnabled, sessionRoot, sessionFileForIndex, sessionFileForTask, thinkingOverrideForTask, artifactConfig, artifactsDir, effectiveAsync, controlConfig, intercomBridge, nestedRoute, contextPolicy, } = data; const hasChain = (params.chain?.length ?? 0) > 0; const hasTasks = (params.tasks?.length ?? 0) > 0; const hasSingle = !hasChain && !hasTasks && Boolean(params.agent); if (!effectiveAsync) return null; if (hasChain && params.chain) { const chainWorktreeTaskCwdError = buildChainWorktreeTaskCwdError(params.chain as ChainStep[], effectiveCwd); if (chainWorktreeTaskCwdError) { return { content: [{ type: "text", text: chainWorktreeTaskCwdError }], isError: true, details: { mode: "chain" as const, results: [] }, }; } } if (hasTasks && params.tasks) { const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks); if (params.tasks.length > maxParallelTasks) { return buildParallelModeError(`Max ${maxParallelTasks} tasks`); } if (params.worktree) { const worktreeTaskCwdError = buildParallelWorktreeTaskCwdError(params.tasks, effectiveCwd); if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError); } } if (!isAsyncAvailable()) { return { content: [{ type: "text", text: "Async mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }], isError: true, details: { mode: "single" as const, results: [] }, }; } const id = randomUUID(); const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId!, parentSessionId: ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: ctx.model?.provider, currentModel: ctx.model, modelScope: data.modelScope, }; 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(id, agent, index) : undefined; if (hasTasks && params.tasks) { const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent)); const modelOverrides = params.tasks.map((task, index) => resolveSubagentModelOverride(task.model ?? agentConfigs[index]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: task.model ? "explicit" : "inherited" }), ); const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill)); const parallelTasks = params.tasks.map((task, index) => ({ agent: task.agent, task: shouldForkAgent(contextPolicy, task.agent) ? wrapForkTask(task.task) : task.task, cwd: task.cwd, ...(modelOverrides[index] ? { model: modelOverrides[index] } : {}), ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}), ...(task.output === true ? (agentConfigs[index]?.output ? { output: agentConfigs[index]!.output } : {}) : task.output !== undefined ? { output: task.output } : {}), ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}), ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}), ...(task.progress !== undefined ? { progress: task.progress } : {}), ...(task.toolBudget !== undefined ? { toolBudget: task.toolBudget } : {}), ...(task.acceptance !== undefined ? { acceptance: task.acceptance } : {}), })); return executeAsyncChain(id, { 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, chainSkills: [], sessionFilesByFlatIndex: params.tasks.map((task, index) => sessionFileForTask(task.agent, index)), thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index)), maxSubagentDepth: currentMaxSubagentDepth, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, controlIntercomTarget, childIntercomTarget, nestedRoute, timeoutMs: data.timeoutMs, turnBudget: data.turnBudget, toolBudget: data.toolBudget, configToolBudget: data.configToolBudget, globalConcurrencyLimit: deps.config.globalConcurrencyLimit, }); } if (hasChain && params.chain) { const normalized = normalizeSkillInput(params.skill); const chainSkills = normalized === false ? [] : (normalized ?? []); const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy); return executeAsyncChain(id, { chain, task: params.task, agents, ctx: asyncCtx, availableModels, cwd: effectiveCwd, maxOutput: params.maxOutput, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, shareEnabled, sessionRoot, chainSkills, sessionFilesByFlatIndex: collectChainSessionFiles(chain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems), thinkingOverridesByFlatIndex: collectChainThinkingOverrides(chain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems), dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems, maxSubagentDepth: currentMaxSubagentDepth, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, controlIntercomTarget, childIntercomTarget, nestedRoute, timeoutMs: data.timeoutMs, turnBudget: data.turnBudget, toolBudget: data.toolBudget, configToolBudget: data.configToolBudget, globalConcurrencyLimit: deps.config.globalConcurrencyLimit, }); } if (hasSingle) { const a = agents.find((x) => x.name === params.agent); if (!a) { return { content: [{ type: "text", text: `Unknown agent: ${params.agent}` }], isError: true, details: { mode: "single" as const, results: [] }, }; } const rawOutput = params.output !== undefined ? params.output : a.output; const effectiveOutput = normalizeSingleOutputOverride(rawOutput, a.output); const effectiveOutputMode = params.outputMode ?? "inline"; const normalizedSkills = normalizeSkillInput(params.skill); const skills = normalizedSkills === false ? [] : normalizedSkills; const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth); const modelOverride = resolveSubagentModelOverride((params.model as string | undefined) ?? a.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: (params.model as string | undefined) ? "explicit" : "inherited" }); return executeAsyncSingle(id, { agent: params.agent!, task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""), agentConfig: a, ctx: asyncCtx, availableModels, cwd: effectiveCwd, maxOutput: params.maxOutput, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, shareEnabled, sessionRoot, sessionFile: sessionFileForTask(params.agent!, 0), skills, output: effectiveOutput, outputMode: effectiveOutputMode, outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id), modelOverride, thinkingOverride: thinkingOverrideForTask(params.agent!, 0), maxSubagentDepth, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, controlIntercomTarget, childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined, nestedRoute, acceptance: params.acceptance, timeoutMs: data.timeoutMs, turnBudget: data.turnBudget, toolBudget: data.toolBudget, configToolBudget: data.configToolBudget, }); } return null; } async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Promise> { const { params, effectiveCwd, agents, ctx, signal, runId, shareEnabled, sessionDirForIndex, sessionFileForIndex, sessionFileForTask, thinkingOverrideForTask, artifactsDir, artifactConfig, onUpdate, sessionRoot, controlConfig, contextPolicy, } = data; const onControlEvent = createForegroundControlNotifier(data, deps); const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined; const foregroundControl = deps.state.foregroundControls.get(runId); const normalized = normalizeSkillInput(params.skill); const chainSkills = normalized === false ? [] : (normalized ?? []); const chain = wrapChainTasksForFork(params.chain as ChainStep[], contextPolicy); const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth); const chainResult = await executeChain({ chain, task: params.task, agents, ctx, modelScope: data.modelScope, intercomEvents: deps.pi.events, signal, runId, cwd: effectiveCwd, shareEnabled, sessionDirForIndex, sessionFileForIndex, sessionFileForTask, thinkingOverrideForTask, artifactsDir, artifactConfig, includeProgress: params.includeProgress, clarify: params.clarify, onUpdate, onControlEvent, controlConfig, childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined, orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined, foregroundControl, nestedRoute: foregroundControl?.nestedRoute, chainSkills, chainDir: params.chainDir ?? getProjectChainRunsDir(effectiveCwd), dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems, maxSubagentDepth: currentMaxSubagentDepth, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, timeoutMs: data.timeoutMs, deadlineAt: data.deadlineAt, turnBudget: data.turnBudget, onDetachedExit: (index, result) => updateRememberedForegroundChild(deps.state, { runId, mode: "chain", cwd: effectiveCwd, index, result }), toolBudget: data.toolBudget, configToolBudget: data.configToolBudget, globalConcurrencyLimit: deps.config.globalConcurrencyLimit, }); if (chainResult.requestedAsync) { if (!isAsyncAvailable()) { return { content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }], isError: true, details: { mode: "chain" as const, results: [] }, }; } const id = randomUUID(); const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId!, parentSessionId: ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: ctx.model?.provider, currentModel: ctx.model, modelScope: data.modelScope, }; const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, contextPolicy); return executeAsyncChain(id, { chain: asyncChain, task: params.task, agents, ctx: asyncCtx, availableModels: ctx.modelRegistry.getAvailable().map(toModelInfo), cwd: effectiveCwd, maxOutput: params.maxOutput, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, shareEnabled, sessionRoot, chainSkills: chainResult.requestedAsync.chainSkills, sessionFilesByFlatIndex: collectChainSessionFiles(asyncChain, sessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems), thinkingOverridesByFlatIndex: collectChainThinkingOverrides(asyncChain, thinkingOverrideForTask, deps.config.chain?.dynamicFanout?.maxItems), dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems, maxSubagentDepth: currentMaxSubagentDepth, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined, childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined, nestedRoute: data.nestedRoute, timeoutMs: data.timeoutMs, turnBudget: data.turnBudget, toolBudget: data.toolBudget, configToolBudget: data.configToolBudget, globalConcurrencyLimit: deps.config.globalConcurrencyLimit, }); } const rawChainDetails = chainResult.details ? { ...chainResult.details, runId } : undefined; if (foregroundControl && rawChainDetails) { updateForegroundNestedProjection(foregroundControl); attachRootChildrenToSteps(runId, rawChainDetails.results, foregroundControl.nestedChildren); rawChainDetails.totalCost = sumResultsCost(rawChainDetails.results); } const chainDetails = rawChainDetails ? compactForegroundDetails(rawChainDetails) : undefined; if (chainDetails) rememberForegroundRun(deps.state, { runId, mode: "chain", cwd: effectiveCwd, results: chainDetails.results }); const intercomReceipt = chainDetails && !chainDetails.results.some((result) => result.interrupted || result.detached) ? await maybeBuildForegroundIntercomReceipt({ pi: deps.pi, intercomBridge: data.intercomBridge, runId, mode: "chain", details: chainDetails, ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}), }) : null; if (intercomReceipt) { return { ...chainResult, content: [{ type: "text", text: intercomReceipt.text }], details: intercomReceipt.details, }; } return chainDetails ? { ...chainResult, details: chainDetails } : chainResult; } interface ForegroundParallelRunInput { tasks: TaskParam[]; taskTexts: string[]; agents: AgentConfig[]; ctx: ExtensionContext; state: SubagentState; intercomEvents: IntercomEventBus; signal: AbortSignal; runId: string; sessionDirForIndex: (idx?: number) => string | undefined; sessionFileForIndex: (idx?: number) => string | undefined; sessionFileForTask: (agentName: string, idx?: number) => string | undefined; thinkingOverrideForTask: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined; shareEnabled: boolean; artifactConfig: ArtifactConfig; artifactsDir: string; outputBaseDir: string; maxOutput?: MaxOutputConfig; paramsCwd: string; progressDir: string; maxSubagentDepths: number[]; availableModels: ModelInfo[]; modelScope?: ModelScopeConfig; modelOverrides: (string | undefined)[]; behaviors: Array>; firstProgressIndex: number; controlConfig: ResolvedControlConfig; onControlEvent?: (event: ControlEvent) => void; childIntercomTarget?: (agent: string, index: number) => string | undefined; orchestratorIntercomTarget?: string; foregroundControl?: SubagentState["foregroundControls"] extends Map ? T : never; concurrencyLimit: number; globalSemaphore?: Semaphore; liveResults: (SingleResult | undefined)[]; liveProgress: (AgentProgress | undefined)[]; onUpdate?: (r: AgentToolResult
) => void; worktreeSetup?: WorktreeSetup; timeoutMs?: number; deadlineAt?: number; turnBudget?: ResolvedTurnBudget; toolBudgets: (ResolvedToolBudget | undefined)[]; } function buildParallelModeError(message: string): AgentToolResult
{ return { content: [{ type: "text", text: message }], isError: true, details: { mode: "parallel" as const, results: [] }, }; } function createParallelWorktreeSetup( enabled: boolean | undefined, cwd: string, runId: string, tasks: TaskParam[], setupHook: ExtensionConfig["worktreeSetupHook"], setupHookTimeoutMs: ExtensionConfig["worktreeSetupHookTimeoutMs"], baseDir: ExtensionConfig["worktreeBaseDir"], ): { setup?: WorktreeSetup; errorResult?: AgentToolResult
} { if (!enabled) return {}; try { return { setup: createWorktrees(cwd, runId, tasks.length, { agents: tasks.map((task) => task.agent), setupHook: setupHook ? { hookPath: setupHook, timeoutMs: setupHookTimeoutMs } : undefined, baseDir, }), }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { errorResult: buildParallelModeError(message) }; } } function buildParallelWorktreeTaskCwdError( tasks: ReadonlyArray<{ agent: string; cwd?: string }>, sharedCwd: string, ): string | undefined { const conflict = findWorktreeTaskCwdConflict(tasks, sharedCwd); if (!conflict) return undefined; return formatWorktreeTaskCwdConflict(conflict, sharedCwd); } function resolveSingleRunOutputBaseDir(deps: ExecutorDeps, artifactsDir: string, runId: string): string { return deps.config.singleRunOutputBaseDir ? path.resolve(deps.expandTilde(deps.config.singleRunOutputBaseDir)) : path.join(artifactsDir, "outputs", runId); } 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; const detail = formatWorktreeTaskCwdConflict(conflict, stepCwd); return `parallel chain step ${stepIndex + 1}: ${detail}`; } return undefined; } function resolveParallelTaskCwd( task: TaskParam, paramsCwd: string, worktreeSetup: WorktreeSetup | undefined, index: number, ): string { if (worktreeSetup) return worktreeSetup.worktrees[index]!.agentCwd; return resolveChildCwd(paramsCwd, task.cwd); } function buildParallelWorktreeSuffix( worktreeSetup: WorktreeSetup | undefined, artifactsDir: string, tasks: TaskParam[], ): string { if (!worktreeSetup) return ""; const diffsDir = path.join(artifactsDir, "worktree-diffs"); const diffs = diffWorktrees(worktreeSetup, tasks.map((task) => task.agent), diffsDir); return formatWorktreeDiffSummary(diffs); } function findDuplicateParallelOutputPath(input: { tasks: TaskParam[]; behaviors: ResolvedStepBehavior[]; paramsCwd: string; ctxCwd: string; outputBaseDir: string; worktreeSetup?: WorktreeSetup; }): string | undefined { const seen = new Map(); for (let index = 0; index < input.tasks.length; index++) { const behavior = input.behaviors[index]; if (!behavior?.output) continue; const task = input.tasks[index]!; const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index); const outputPath = resolveSingleOutputPath(behavior.output, input.ctxCwd, taskCwd, input.outputBaseDir); if (!outputPath) continue; const previous = seen.get(outputPath); if (previous) { return `Parallel tasks ${previous.index + 1} (${previous.agent}) and ${index + 1} (${task.agent}) resolve output to the same path: ${outputPath}. Use distinct output paths.`; } seen.set(outputPath, { index, agent: task.agent }); } return undefined; } async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Promise { // Pre-warm fork session files sequentially before concurrent dispatch to avoid // races where multiple workers simultaneously try to branch the same parent session. // sessionFileForIndex caches results, so these calls return instantly inside mapConcurrent. for (let i = 0; i < input.tasks.length; i++) { input.sessionFileForIndex(i); } return mapConcurrent(input.tasks, input.concurrencyLimit, async (task, index) => { const behavior = input.behaviors[index]; const effectiveSkills = behavior?.skills; const taskCwd = resolveParallelTaskCwd(task, input.paramsCwd, input.worktreeSetup, index); const readInstructions = behavior ? buildChainInstructions({ ...behavior, output: false, progress: false }, taskCwd, false) : { prefix: "", suffix: "" }; const progressInstructions = behavior ? buildChainInstructions({ ...behavior, output: false, reads: false }, input.progressDir, index === input.firstProgressIndex) : { prefix: "", suffix: "" }; const outputPath = resolveSingleOutputPath(behavior?.output, input.ctx.cwd, taskCwd, input.outputBaseDir); const taskText = injectSingleOutputInstruction( `${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`, outputPath, ); const interruptController = new AbortController(); if (input.foregroundControl) { input.foregroundControl.currentAgent = task.agent; input.foregroundControl.currentIndex = index; input.foregroundControl.currentActivityState = undefined; input.foregroundControl.updatedAt = Date.now(); input.foregroundControl.interrupt = () => { if (interruptController.signal.aborted) return false; interruptController.abort(); input.foregroundControl!.currentActivityState = undefined; input.foregroundControl!.updatedAt = Date.now(); return true; }; } const agentConfig = input.agents.find((agent) => agent.name === task.agent); return runSync(input.ctx.cwd, input.agents, task.agent, taskText, { parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined, cwd: taskCwd, signal: input.signal, interruptSignal: interruptController.signal, allowIntercomDetach: agentConfig?.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true, intercomEvents: input.intercomEvents, runId: input.runId, index, sessionDir: input.sessionDirForIndex(index), sessionFile: input.sessionFileForTask(task.agent, index), share: input.shareEnabled, artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined, artifactConfig: input.artifactConfig, maxOutput: input.maxOutput, outputPath, outputMode: behavior?.outputMode, maxSubagentDepth: input.maxSubagentDepths[index], controlConfig: input.controlConfig, onControlEvent: input.onControlEvent, onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, index, result }), intercomSessionName: input.childIntercomTarget?.(task.agent, index), orchestratorIntercomTarget: input.orchestratorIntercomTarget, nestedRoute: input.foregroundControl?.nestedRoute, modelOverride: input.modelOverrides[index], thinkingOverride: input.thinkingOverrideForTask(task.agent, index), availableModels: input.availableModels, preferredModelProvider: input.ctx.model?.provider, modelScope: input.modelScope, skills: effectiveSkills === false ? [] : effectiveSkills, acceptance: task.acceptance, acceptanceContext: { mode: "parallel" }, timeoutMs: input.timeoutMs, deadlineAt: input.deadlineAt, turnBudget: input.turnBudget, toolBudget: input.toolBudgets[index], onUpdate: input.onUpdate ? (progressUpdate) => { const stepResults = progressUpdate.details?.results || []; const stepProgress = progressUpdate.details?.progress || []; if (input.foregroundControl && stepProgress.length > 0) { const current = stepProgress[0]; input.foregroundControl.currentAgent = task.agent; input.foregroundControl.currentIndex = index; input.foregroundControl.currentActivityState = current?.activityState; input.foregroundControl.lastActivityAt = current?.lastActivityAt; input.foregroundControl.currentTool = current?.currentTool; input.foregroundControl.currentToolStartedAt = current?.currentToolStartedAt; input.foregroundControl.currentPath = current?.currentPath; input.foregroundControl.turnCount = current?.turnCount; input.foregroundControl.tokens = current?.tokens; input.foregroundControl.toolCount = current?.toolCount; input.foregroundControl.updatedAt = Date.now(); } if (stepResults.length > 0) input.liveResults[index] = stepResults[0]; if (stepProgress.length > 0) input.liveProgress[index] = stepProgress[0]; const mergedResults = input.liveResults.filter((result): result is SingleResult => result !== undefined); const mergedProgress = input.liveProgress.filter((progress): progress is AgentProgress => progress !== undefined); input.onUpdate?.({ content: progressUpdate.content, details: { mode: "parallel", results: mergedResults, progress: mergedProgress, controlEvents: progressUpdate.details?.controlEvents, totalSteps: input.tasks.length, }, }); } : undefined, }).finally(() => { if (input.foregroundControl?.currentIndex === index) { input.foregroundControl.interrupt = undefined; input.foregroundControl.updatedAt = Date.now(); } }); }, input.globalSemaphore); } async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps): Promise> { const { params, effectiveCwd, agents, ctx, signal, runId, sessionDirForIndex, sessionFileForIndex, sessionFileForTask, thinkingOverrideForTask, shareEnabled, artifactConfig, artifactsDir, backgroundRequestedWhileClarifying, onUpdate, sessionRoot, controlConfig, contextPolicy, } = data; const onControlEvent = createForegroundControlNotifier(data, deps); const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget : undefined; const allProgress: AgentProgress[] = []; const allArtifactPaths: ArtifactPaths[] = []; const tasks = params.tasks!; const maxParallelTasks = resolveTopLevelParallelMaxTasks(deps.config.parallel?.maxTasks); const parallelConcurrency = resolveTopLevelParallelConcurrency(params.concurrency, deps.config.parallel?.concurrency); if (tasks.length > maxParallelTasks) return { content: [{ type: "text", text: `Max ${maxParallelTasks} tasks` }], isError: true, details: { mode: "parallel" as const, results: [] }, }; const agentConfigs: AgentConfig[] = []; for (const t of tasks) { const config = agents.find((a) => a.name === t.agent); if (!config) { return { content: [{ type: "text", text: `Unknown agent: ${t.agent}` }], isError: true, details: { mode: "parallel" as const, results: [] }, }; } agentConfigs.push(config); } const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth); const maxSubagentDepths = agentConfigs.map((config) => resolveChildMaxSubagentDepth(currentMaxSubagentDepth, config.maxSubagentDepth), ); const toolBudgets: (ResolvedToolBudget | undefined)[] = []; for (let index = 0; index < tasks.length; index++) { const resolved = resolveEffectiveToolBudget({ stepBudget: tasks[index]?.toolBudget, runBudget: data.toolBudget, agentBudget: agentConfigs[index]?.toolBudget, configBudget: data.configToolBudget }); if (resolved.error) return buildParallelModeError(resolved.error); toolBudgets.push(resolved.toolBudget); } if (params.worktree) { const worktreeTaskCwdError = buildParallelWorktreeTaskCwdError(tasks, effectiveCwd); if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError); } const currentProvider = ctx.model?.provider; const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo); let taskTexts = tasks.map((t) => t.task); const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) => normalizeSkillInput(t.skill), ); const behaviorOverrides: StepOverrides[] = tasks.map((task, index) => ({ ...(task.output !== undefined ? { output: task.output === true ? agentConfigs[index]?.output ?? false : task.output } : {}), ...(task.outputMode !== undefined ? { outputMode: task.outputMode } : {}), ...(task.reads !== undefined && task.reads !== true ? { reads: task.reads } : {}), ...(task.progress !== undefined ? { progress: task.progress } : {}), ...(skillOverrides[index] !== undefined ? { skills: skillOverrides[index] } : {}), ...(task.model ? { model: task.model } : {}), })); const modelOverrides: (string | undefined)[] = tasks.map((_, i) => resolveSubagentModelOverride(behaviorOverrides[i]?.model ?? agentConfigs[i]?.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: behaviorOverrides[i]?.model ? "explicit" : "inherited" }), ); if (params.clarify === true && ctx.hasUI) { const behaviors = agentConfigs.map((c, i) => resolveStepBehavior(c, behaviorOverrides[i]!), ); const availableSkills = discoverAvailableSkills(effectiveCwd); const result = await ctx.ui.custom( (tui, theme, _kb, done) => new ChainClarifyComponent( tui, theme, agentConfigs, taskTexts, "", undefined, behaviors, availableModels, currentProvider, availableSkills, done, "parallel", ), { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } }, ); if (!result || !result.confirmed) { return { content: [{ type: "text", text: "Cancelled" }], details: { mode: "parallel", results: [] } }; } taskTexts = result.templates; for (let i = 0; i < result.behaviorOverrides.length; i++) { const override = result.behaviorOverrides[i]; if (override?.model) { modelOverrides[i] = resolveSubagentModelOverride(override.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: "explicit" }); behaviorOverrides[i]!.model = override.model; } if (override?.output !== undefined) behaviorOverrides[i]!.output = override.output; if (override?.reads !== undefined) behaviorOverrides[i]!.reads = override.reads; if (override?.progress !== undefined) behaviorOverrides[i]!.progress = override.progress; if (override?.skills !== undefined) { skillOverrides[i] = override.skills; behaviorOverrides[i]!.skills = override.skills; } } if (result.runInBackground) { if (!isAsyncAvailable()) { return { content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }], isError: true, details: { mode: "parallel" as const, results: [] }, }; } const id = randomUUID(); const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId!, parentSessionId: ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: ctx.model?.provider, currentModel: ctx.model, modelScope: data.modelScope, }; const parallelTasks = tasks.map((t, i) => { const taskText = shouldForkAgent(contextPolicy, t.agent) ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!; const progress = taskDisallowsFileUpdates(taskText) ? false : behaviorOverrides[i]?.progress; return { agent: t.agent, task: taskText, cwd: t.cwd, ...(modelOverrides[i] ? { model: modelOverrides[i] } : {}), ...(skillOverrides[i] !== undefined ? { skill: skillOverrides[i] } : {}), ...(behaviorOverrides[i]?.output !== undefined ? { output: behaviorOverrides[i]!.output } : {}), ...(behaviorOverrides[i]?.outputMode !== undefined ? { outputMode: behaviorOverrides[i]!.outputMode } : {}), ...(behaviorOverrides[i]?.reads !== undefined ? { reads: behaviorOverrides[i]!.reads } : {}), ...(progress !== undefined ? { progress } : {}), ...(t.toolBudget !== undefined ? { toolBudget: t.toolBudget } : {}), ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}), }; }); return executeAsyncChain(id, { chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }], resultMode: "parallel", agents, ctx: asyncCtx, availableModels, cwd: effectiveCwd, maxOutput: params.maxOutput, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, shareEnabled, sessionRoot, chainSkills: [], sessionFilesByFlatIndex: tasks.map((task, index) => sessionFileForTask(task.agent, index)), thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index)), maxSubagentDepth: currentMaxSubagentDepth, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined, childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined, timeoutMs: data.timeoutMs, turnBudget: data.turnBudget, globalConcurrencyLimit: deps.config.globalConcurrencyLimit, }); } } const behaviors = agentConfigs.map((config, index) => suppressProgressForReadOnlyTask(resolveStepBehavior(config, behaviorOverrides[index]!), taskTexts[index])); const firstProgressIndex = behaviors.findIndex((behavior) => behavior.progress); const liveResults: (SingleResult | undefined)[] = new Array(tasks.length).fill(undefined); const liveProgress: (AgentProgress | undefined)[] = new Array(tasks.length).fill(undefined); const foregroundControl = deps.state.foregroundControls.get(runId); const { setup: worktreeSetup, errorResult } = createParallelWorktreeSetup( params.worktree, effectiveCwd, runId, tasks, deps.config.worktreeSetupHook, deps.config.worktreeSetupHookTimeoutMs, deps.config.worktreeBaseDir, ); if (errorResult) return errorResult; try { const outputBaseDir = path.join(artifactsDir, "outputs", runId); const duplicateOutputError = findDuplicateParallelOutputPath({ tasks, behaviors, paramsCwd: effectiveCwd, ctxCwd: ctx.cwd, outputBaseDir, worktreeSetup, }); if (duplicateOutputError) return buildParallelModeError(duplicateOutputError); for (let index = 0; index < tasks.length; index++) { const taskCwd = resolveParallelTaskCwd(tasks[index]!, effectiveCwd, worktreeSetup, index); const outputPath = resolveSingleOutputPath(behaviors[index]?.output, ctx.cwd, taskCwd, outputBaseDir); const validationError = validateFileOnlyOutputMode(behaviors[index]?.outputMode, outputPath, `Parallel task ${index + 1} (${tasks[index]!.agent})`); if (validationError) return buildParallelModeError(validationError); } const parallelProgressPrecreated = firstProgressIndex !== -1; const parallelProgressDir = path.join(artifactsDir, "progress", runId); if (parallelProgressPrecreated) writeInitialProgressFile(parallelProgressDir); for (let i = 0; i < taskTexts.length; i++) { if (shouldForkAgent(contextPolicy, tasks[i]!.agent)) taskTexts[i] = wrapForkTask(taskTexts[i]!); } const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined); const results = await runForegroundParallelTasks({ tasks, taskTexts, agents, ctx, state: deps.state, intercomEvents: deps.pi.events, signal, runId, sessionDirForIndex, sessionFileForIndex, sessionFileForTask, thinkingOverrideForTask, shareEnabled, artifactConfig, artifactsDir, outputBaseDir, maxOutput: params.maxOutput, paramsCwd: effectiveCwd, progressDir: parallelProgressDir, availableModels, modelScope: data.modelScope, modelOverrides, behaviors, firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex, controlConfig, onControlEvent, childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(runId, agent, index) : undefined, orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined, foregroundControl, concurrencyLimit: parallelConcurrency, globalSemaphore: new Semaphore(deps.config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT), maxSubagentDepths, liveResults, liveProgress, onUpdate, worktreeSetup, timeoutMs: data.timeoutMs, deadlineAt, turnBudget: data.turnBudget, toolBudgets, }); for (let i = 0; i < results.length; i++) { const run = results[i]!; recordRun(run.agent, taskTexts[i]!, run.exitCode, run.progressSummary?.durationMs ?? 0); } for (const result of results) { if (result.progress) allProgress.push(result.progress); if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths); } if (foregroundControl) { updateForegroundNestedProjection(foregroundControl); attachRootChildrenToSteps(runId, results, foregroundControl.nestedChildren); } const interrupted = results.find((result) => result.interrupted); const details = compactForegroundDetails({ mode: "parallel", runId, results, progress: params.includeProgress ? allProgress : undefined, artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined, totalChildUsage: sumResultsUsage(results), totalCost: sumResultsCost(results), }); rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results }); if (interrupted) { return { content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }], details, }; } const detachedIndex = results.findIndex((result) => result.detached); const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined; if (detached) { return { content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }], details, }; } if (foregroundControl) updateForegroundNestedProjection(foregroundControl); const intercomReceipt = await maybeBuildForegroundIntercomReceipt({ pi: deps.pi, intercomBridge: data.intercomBridge, runId, mode: "parallel", details, ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}), }); if (intercomReceipt) { return { content: [{ type: "text", text: intercomReceipt.text }], details: intercomReceipt.details, }; } const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, tasks); const ok = results.filter((result) => result.exitCode === 0).length; const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : ""; const aggregatedOutput = aggregateParallelOutputs( results.map((result) => ({ agent: result.agent, output: result.truncation?.text || getSingleResultOutput(result), exitCode: result.exitCode, error: result.error, timedOut: result.timedOut, })), (i, agent) => `=== Task ${i + 1}: ${agent} ===`, ); const summary = `${ok}/${results.length} succeeded${downgradeNote}`; const fullContent = worktreeSuffix ? `${summary}\n\n${aggregatedOutput}\n\n${worktreeSuffix}` : `${summary}\n\n${aggregatedOutput}`; return { content: [{ type: "text", text: fullContent }], details, }; } finally { if (worktreeSetup) cleanupWorktrees(worktreeSetup); } } async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Promise> { const { params, effectiveCwd, agents, ctx, signal, runId, sessionDirForIndex, sessionFileForTask, thinkingOverrideForTask, shareEnabled, artifactConfig, artifactsDir, onUpdate, sessionRoot, controlConfig, contextPolicy, } = data; const onControlEvent = createForegroundControlNotifier(data, deps); const childIntercomTarget = data.intercomBridge.active ? resolveSubagentIntercomTarget(runId, params.agent!, 0) : undefined; const allProgress: AgentProgress[] = []; const allArtifactPaths: ArtifactPaths[] = []; const agentConfig = agents.find((a) => a.name === params.agent); if (!agentConfig) { return { content: [{ type: "text", text: `Unknown agent: ${params.agent}` }], isError: true, details: { mode: "single", results: [] }, }; } const effectiveToolBudget = resolveEffectiveToolBudget({ runBudget: data.toolBudget, agentBudget: agentConfig.toolBudget, configBudget: data.configToolBudget }); if (effectiveToolBudget.error) return toExecutionErrorResult(params, new Error(effectiveToolBudget.error)); const currentProvider = ctx.model?.provider; const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo); let task = params.task ?? ""; let modelOverride: string | undefined = resolveSubagentModelOverride( (params.model as string | undefined) ?? agentConfig.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: (params.model as string | undefined) ? "explicit" : "inherited" }, ); let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill); const rawOutput = params.output !== undefined ? params.output : agentConfig.output; let effectiveOutput = normalizeSingleOutputOverride(rawOutput, agentConfig.output); const effectiveOutputMode = params.outputMode ?? "inline"; const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth); const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, agentConfig.maxSubagentDepth); if (params.clarify === true && ctx.hasUI) { const behavior = resolveStepBehavior(agentConfig, { output: effectiveOutput, skills: skillOverride }); const availableSkills = discoverAvailableSkills(effectiveCwd); const result = await ctx.ui.custom( (tui, theme, _kb, done) => new ChainClarifyComponent( tui, theme, [agentConfig], [task], task, undefined, [behavior], availableModels, currentProvider, availableSkills, done, "single", ), { overlay: true, overlayOptions: { anchor: "center", width: 84, maxHeight: "80%" } }, ); if (!result || !result.confirmed) { return { content: [{ type: "text", text: "Cancelled" }], details: { mode: "single", results: [] } }; } task = result.templates[0]!; const override = result.behaviorOverrides[0]; if (override?.model) modelOverride = resolveSubagentModelOverride(override.model, ctx.model, availableModels, currentProvider, { scope: data.modelScope, source: "explicit" }); if (override?.output !== undefined) effectiveOutput = normalizeSingleOutputOverride(override.output, agentConfig.output); if (override?.skills !== undefined) skillOverride = override.skills; if (result.runInBackground) { if (!isAsyncAvailable()) { return { content: [{ type: "text", text: "Background mode requires upstream jiti for TypeScript execution but it could not be found. Ensure the pi-subagents package dependencies are installed." }], isError: true, details: { mode: "single" as const, results: [] }, }; } const id = randomUUID(); const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId!, parentSessionId: ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: ctx.model?.provider, currentModel: ctx.model, modelScope: data.modelScope, }; return executeAsyncSingle(id, { agent: params.agent!, task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task, agentConfig, ctx: asyncCtx, availableModels, cwd: effectiveCwd, maxOutput: params.maxOutput, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, shareEnabled, sessionRoot, sessionFile: sessionFileForTask(params.agent!, 0), skills: skillOverride === false ? [] : skillOverride, output: effectiveOutput, outputMode: effectiveOutputMode, outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id), modelOverride, thinkingOverride: thinkingOverrideForTask(params.agent!, 0), maxSubagentDepth, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, controlIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined, childIntercomTarget: data.intercomBridge.active ? (agent, index) => resolveSubagentIntercomTarget(id, agent, index) : undefined, timeoutMs: data.timeoutMs, turnBudget: data.turnBudget, toolBudget: effectiveToolBudget.toolBudget, }); } } if (shouldForkAgent(contextPolicy, params.agent!)) { task = wrapForkTask(task); } const cleanTask = task; const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, effectiveCwd, resolveSingleRunOutputBaseDir(deps, artifactsDir, runId)); const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Single run (${params.agent})`); if (validationError) { return { content: [{ type: "text", text: validationError }], isError: true, details: { mode: "single", results: [] } }; } task = injectSingleOutputInstruction(task, outputPath); let effectiveSkills: string[] | undefined; if (skillOverride === false) { effectiveSkills = []; } else { effectiveSkills = skillOverride; } const interruptController = new AbortController(); const foregroundControl = deps.state.foregroundControls.get(runId); if (foregroundControl) { foregroundControl.currentAgent = params.agent; foregroundControl.currentIndex = 0; foregroundControl.currentActivityState = undefined; foregroundControl.updatedAt = Date.now(); foregroundControl.interrupt = () => { if (interruptController.signal.aborted) return false; interruptController.abort(); foregroundControl.currentActivityState = undefined; foregroundControl.updatedAt = Date.now(); return true; }; } const forwardSingleUpdate = onUpdate ? (update: AgentToolResult
) => { if (foregroundControl) { const firstProgress = update.details?.progress?.[0]; foregroundControl.currentAgent = params.agent; foregroundControl.currentIndex = firstProgress?.index ?? 0; foregroundControl.currentActivityState = firstProgress?.activityState; foregroundControl.lastActivityAt = firstProgress?.lastActivityAt; foregroundControl.currentTool = firstProgress?.currentTool; foregroundControl.currentToolStartedAt = firstProgress?.currentToolStartedAt; foregroundControl.currentPath = firstProgress?.currentPath; foregroundControl.turnCount = firstProgress?.turnCount; foregroundControl.tokens = firstProgress?.tokens; foregroundControl.toolCount = firstProgress?.toolCount; foregroundControl.updatedAt = Date.now(); } onUpdate(update); } : undefined; const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined); const r = await runSync(ctx.cwd, agents, params.agent!, task, { parentSessionId: ctx.sessionManager.getSessionId() ?? undefined, cwd: effectiveCwd, signal, interruptSignal: interruptController.signal, allowIntercomDetach: agentConfig.systemPrompt?.includes(INTERCOM_BRIDGE_MARKER) === true, intercomEvents: deps.pi.events, runId, sessionDir: sessionDirForIndex(0), sessionFile: sessionFileForTask(params.agent!, 0), share: shareEnabled, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, maxOutput: params.maxOutput, outputPath, outputMode: effectiveOutputMode, maxSubagentDepth, onUpdate: forwardSingleUpdate, controlConfig, onControlEvent, intercomSessionName: childIntercomTarget, orchestratorIntercomTarget: data.intercomBridge.active ? data.intercomBridge.orchestratorTarget : undefined, nestedRoute: foregroundControl?.nestedRoute, index: 0, modelOverride, thinkingOverride: thinkingOverrideForTask(params.agent!, 0), availableModels, preferredModelProvider: currentProvider, modelScope: data.modelScope, skills: effectiveSkills, acceptance: params.acceptance, acceptanceContext: { mode: "single" }, onDetachedExit: (result) => updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, index: 0, result }), timeoutMs: data.timeoutMs, deadlineAt, turnBudget: data.turnBudget, toolBudget: effectiveToolBudget.toolBudget, }); if (foregroundControl?.currentIndex === 0) { foregroundControl.interrupt = undefined; foregroundControl.currentActivityState = r.progress?.activityState; foregroundControl.lastActivityAt = r.progress?.lastActivityAt; foregroundControl.currentTool = r.progress?.currentTool; foregroundControl.currentToolStartedAt = r.progress?.currentToolStartedAt; foregroundControl.currentPath = r.progress?.currentPath; foregroundControl.turnCount = r.progress?.turnCount; foregroundControl.tokens = r.progress?.tokens; foregroundControl.toolCount = r.progress?.toolCount; foregroundControl.updatedAt = Date.now(); } recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0); if (r.progress) allProgress.push(r.progress); if (r.artifactPaths) allArtifactPaths.push(r.artifactPaths); const fullOutput = getSingleResultOutput(r); const finalizedOutput = finalizeSingleOutput({ fullOutput, truncatedOutput: r.truncation?.text, outputPath, outputMode: r.outputMode, exitCode: r.exitCode, savedPath: r.savedOutputPath, outputReference: r.outputReference, saveError: r.outputSaveError, }); if (foregroundControl) { updateForegroundNestedProjection(foregroundControl); attachRootChildrenToSteps(runId, [r], foregroundControl.nestedChildren); } const details = compactForegroundDetails({ mode: "single", runId, results: [r], ...(data.turnBudget ? { turnBudget: data.turnBudget } : {}), ...(effectiveToolBudget.toolBudget ? { toolBudget: effectiveToolBudget.toolBudget } : {}), progress: params.includeProgress ? allProgress : undefined, artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined, truncation: r.truncation, totalChildUsage: sumResultsUsage([r]), totalCost: sumResultsCost([r]), }); rememberForegroundRun(deps.state, { runId, mode: "single", cwd: effectiveCwd, results: details.results }); if (!r.detached && !r.interrupted) { if (foregroundControl) updateForegroundNestedProjection(foregroundControl); const intercomReceipt = await maybeBuildForegroundIntercomReceipt({ pi: deps.pi, intercomBridge: data.intercomBridge, runId, mode: "single", details, ...(foregroundControl?.nestedChildren?.length ? { nestedChildren: foregroundControl.nestedChildren } : {}), }); if (intercomReceipt) { return { content: [{ type: "text", text: intercomReceipt.text }], details: intercomReceipt.details, ...(r.exitCode !== 0 ? { isError: true } : {}), }; } } if (r.detached) { return { content: [{ type: "text", text: `Detached for intercom coordination: ${params.agent}. Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }], details, }; } if (r.interrupted) { return { content: [{ type: "text", text: `Run paused after interrupt (${params.agent}). Waiting for explicit next action.` }], details, }; } if (r.exitCode !== 0) return { content: [{ type: "text", text: formatFailedSingleRunOutput(r, finalizedOutput.displayOutput) }], details, isError: true, }; return { content: [{ type: "text", text: finalizedOutput.displayOutput || "(no output)" }], details, }; } function inferExecutionMode(params: SubagentParamsLike): SubagentRunMode { if ((params.chain?.length ?? 0) > 0) return "chain"; if ((params.tasks?.length ?? 0) > 0) return "parallel"; return "single"; } function duplicateSubagentCallResult(params: SubagentParamsLike): AgentToolResult
{ return { content: [{ type: "text", text: "Rejected: a subagent call is already in progress. Issue exactly ONE subagent call per turn.", }], isError: true, details: { mode: inferExecutionMode(params), results: [] }, }; } function omitExecutionModeActionAlias(params: SubagentParamsLike): SubagentParamsLike { const action = params.action?.toLowerCase(); if (action === "single" && (params.agent !== undefined || params.task !== undefined)) { const rest = { ...params }; delete rest.action; return rest; } if ((action === "parallel" || action === "tasks") && (params.tasks?.length ?? 0) > 0) { const rest = { ...params }; delete rest.action; return rest; } return params; } 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; deps.state.foregroundRuns ??= new Map(); deps.state.foregroundControls ??= new Map(); deps.state.lastForegroundControlId ??= null; const requestParams = omitExecutionModeActionAlias(params); const requestCwd = resolveRequestedCwd(ctx.cwd, requestParams.cwd); const paramsWithResolvedCwd = requestParams.cwd === undefined ? requestParams : { ...requestParams, cwd: requestCwd }; const action = paramsWithResolvedCwd.action; if (action) { if (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 (error) { if (!sessionError) sessionError = error instanceof Error ? `${error.name}: ${error.message}` : String(error); } 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, }), }], details: { mode: "management", results: [] }, }; } if (action === "status") { const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId; const nestedScope = nestedResolutionScopeForExecutor(deps); const sessionRoots = trustedSessionRootsForStatus(ctx, deps); if (paramsWithResolvedCwd.view === "fleet") { return inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }); } if (targetRunId) { try { const resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedScope }); if (resolved?.kind === "foreground") { const foreground = getForegroundControl(deps.state, resolved.id); if (foreground) { if (paramsWithResolvedCwd.view === "transcript") { return { content: [{ type: "text", text: "Live foreground transcript is already visible in the expanded running subagent result. Persisted session transcript becomes inspectable after the foreground run completes when sessions are enabled." }], details: { mode: "management", results: [] }, }; } return foregroundStatusResult(foreground); } } } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } }; } } else { const foreground = getForegroundControl(deps.state, undefined); if (foreground && paramsWithResolvedCwd.view !== "transcript") return foregroundStatusResult(foreground); if (foreground && paramsWithResolvedCwd.view === "transcript") { return { content: [{ type: "text", text: "Live foreground transcript is already visible in the expanded running subagent result. Pass an async run id to inspect a background transcript." }], details: { mode: "management", results: [] }, }; } } return inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots }); } if (action === "resume") { return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps }); } if (action === "steer") { const message = (paramsWithResolvedCwd.message ?? paramsWithResolvedCwd.task ?? "").trim(); if (!message) return { content: [{ type: "text", text: "action='steer' requires message." }], isError: true, details: { mode: "management", results: [] } }; const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id; if (paramsWithResolvedCwd.dir) { try { const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR); const runId = location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir); return steerAsyncRun({ state: deps.state, runId, message, index: paramsWithResolvedCwd.index, kill: deps.kill, location }); } catch (error) { const text = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } }; } } if (!targetRunId) return { content: [{ type: "text", text: "action='steer' requires id or dir." }], isError: true, details: { mode: "management", results: [] } }; let resolved: ResolvedSubagentRunId | undefined; try { resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }); } catch (error) { const text = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text }], isError: true, details: { mode: "management", results: [] } }; } if (resolved?.kind === "nested") return steerNestedRun({ target: resolved, message, index: paramsWithResolvedCwd.index }); if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='steer' currently supports live async Pi child sessions only; use action='interrupt' or action='resume' for foreground runs." }], isError: true, details: { mode: "management", results: [] } }; if (resolved?.kind !== "async") return { content: [{ type: "text", text: `No async run found for '${targetRunId}'.` }], isError: true, details: { mode: "management", results: [] } }; return steerAsyncRun({ state: deps.state, runId: resolved.id, message, index: paramsWithResolvedCwd.index, kill: deps.kill, location: resolved.location }); } if (action === "append-step") { return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps }); } if (action === "schedule" || action === "schedule-list" || action === "schedule-status" || action === "schedule-cancel") { if (!deps.handleScheduledRunAction) { return { content: [{ type: "text", text: `Action '${action}' is not available in this subagent context.` }], isError: true, details: { mode: "management", results: [] }, }; } return deps.handleScheduledRunAction(paramsWithResolvedCwd, ctx); } if (action === "interrupt") { const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id; let resolved: ResolvedSubagentRunId | undefined; if (targetRunId) { try { resolved = resolveSubagentRunId(targetRunId, { state: deps.state, nested: nestedResolutionScopeForExecutor(deps) }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } }; } } if (resolved?.kind === "nested") return interruptNestedRun(resolved); const foreground = getForegroundControl(deps.state, resolved?.kind === "foreground" ? resolved.id : targetRunId); if (foreground?.interrupt) { const interrupted = foreground.interrupt(); if (interrupted) { foreground.updatedAt = Date.now(); foreground.currentActivityState = undefined; return { content: [{ type: "text", text: `Interrupt requested for foreground run ${foreground.runId}.` }], details: { mode: "management", results: [] }, }; } return { content: [{ type: "text", text: `Foreground run ${foreground.runId} has no active child step to interrupt.` }], isError: true, details: { mode: "management", results: [] }, }; } const asyncInterruptResult = interruptAsyncRun( deps.state, resolved?.kind === "async" ? resolved.id : targetRunId, deps.kill, resolved?.kind === "async" ? resolved.location : undefined, ); if (asyncInterruptResult) return asyncInterruptResult; return { content: [{ type: "text", text: "No interrupt-capable run found in this session." }], isError: true, details: { mode: "management", results: [] }, }; } if (!(SUBAGENT_ACTIONS as readonly string[]).includes(action)) { return { content: [{ type: "text", text: `Unknown action: ${action}. Valid: ${SUBAGENT_ACTIONS.join(", ")}` }], isError: true, details: { mode: "management" as const, results: [] }, }; } if (deps.allowMutatingManagementActions === false && MUTATING_MANAGEMENT_ACTIONS.has(action)) { return { content: [{ type: "text", text: `Action '${action}' is not available from child-safe subagent fanout mode.` }], isError: true, details: { mode: "management" as const, results: [] }, }; } return handleManagementAction(action, paramsWithResolvedCwd, { ...ctx, cwd: requestCwd, config: deps.config, }); } 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" as const, results: [] }, }; } const normalized = normalizeRepeatedParallelCounts(paramsWithResolvedCwd); if (normalized.error) return normalized.error; const normalizedParams = normalized.params!; let effectiveParams = applyForceTopLevelAsyncOverride( normalizedParams, depth, deps.config.forceTopLevelAsync === true, ); const foregroundTimeout = resolveForegroundTimeout(effectiveParams); if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error); const turnBudget = resolveTurnBudget(effectiveParams, deps.config); if (turnBudget.error) return buildRequestedModeError(effectiveParams, turnBudget.error); const runToolBudget = resolveToolBudget(effectiveParams.toolBudget, "toolBudget"); if (runToolBudget.error) return buildRequestedModeError(effectiveParams, runToolBudget.error); const configToolBudget = resolveToolBudget(deps.config.toolBudget, "config.toolBudget"); if (configToolBudget.error) return buildRequestedModeError(effectiveParams, configToolBudget.error); 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 discovered = deps.discoverAgents(effectiveCwd, scope); const discoveredAgents = discovered.agents; const modelScope = discovered.modelScope; const contextPolicy = resolveAgentDefaultContextPolicy(effectiveParams, discoveredAgents); effectiveParams = contextPolicy.params; const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId()); const intercomBridge = resolveIntercomBridge({ config: deps.config.intercomBridge, context: effectiveParams.context, orchestratorTarget: sessionName, }); const agents = intercomBridge.active ? discoveredAgents.map((agent) => applyIntercomBridgeToAgent(agent, intercomBridge)) : discoveredAgents; const runId = randomUUID().slice(0, 8); const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv(); const nestedParentAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined; const nestedRoute = inheritedNestedRoute ?? createNestedRoute(runId); const shareEnabled = effectiveParams.share === true; const hasChain = (effectiveParams.chain?.length ?? 0) > 0; const hasTasks = (effectiveParams.tasks?.length ?? 0) > 0; const hasSingle = !hasChain && !hasTasks && Boolean(effectiveParams.agent); const allowClarifyTaskPrompt = hasChain && effectiveParams.clarify === true && ctx.hasUI && !(effectiveParams.chain?.some(isParallelStep) ?? false); const validationError = validateExecutionInput( effectiveParams, agents, hasChain, hasTasks, hasSingle, allowClarifyTaskPrompt, ); if (validationError) return validationError; let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined; let forkThinkingOverrideForIndex: (idx?: number) => AgentConfig["thinking"] | undefined = () => undefined; try { const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined); forkSessionFileForIndex = forkContextResolver.sessionFileForIndex; forkThinkingOverrideForIndex = forkContextResolver.thinkingOverrideForIndex; } catch (error) { return toExecutionErrorResult(effectiveParams, error); } const requestedAsync = effectiveParams.async ?? deps.asyncByDefault; const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true; const effectiveAsync = requestedAsync && effectiveParams.clarify !== true; const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control); const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: effectiveParams.artifacts !== false, }; const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd); 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 forkSessionFileForTask = (agentName: string, idx?: number) => shouldForkAgent(contextPolicy, agentName) ? forkSessionFileForIndex(idx) : undefined; const forkThinkingOverrideForTask = (agentName: string, idx?: number) => shouldForkAgent(contextPolicy, agentName) ? forkThinkingOverrideForIndex(idx) : undefined; const childSessionFileForTask = (agentName: string, idx?: number) => forkSessionFileForTask(agentName, idx) ?? path.join(sessionDirForIndex(idx), "session.jsonl"); const childSessionFileForIndex = (idx?: number) => path.join(sessionDirForIndex(idx), "session.jsonl"); try { preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems); } catch (error) { return toExecutionErrorResult(effectiveParams, error); } const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems); if (chainBindingsError) return chainBindingsError; const onUpdateWithContext = onUpdate ? (r: AgentToolResult
) => onUpdate(withForkContext(r, effectiveParams.context)) : undefined; const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single"; const spawnLimitError = reserveSubagentSpawns({ state: deps.state, config: deps.config, sessionId: deps.state.currentSessionId, requested: countRequestedSubagentSpawns(effectiveParams, deps.config), mode: foregroundMode, }); if (spawnLimitError) return spawnLimitError; const execData: ExecutionContextData = { params: effectiveParams, effectiveCwd, ctx, signal, onUpdate: onUpdateWithContext, agents, runId, shareEnabled, sessionRoot, sessionDirForIndex, sessionFileForIndex: childSessionFileForIndex, sessionFileForTask: childSessionFileForTask, thinkingOverrideForTask: forkThinkingOverrideForTask, artifactConfig, artifactsDir, backgroundRequestedWhileClarifying, effectiveAsync, controlConfig, intercomBridge, nestedRoute, timeoutMs: foregroundTimeout.timeoutMs, turnBudget: turnBudget.turnBudget, toolBudget: runToolBudget.toolBudget, configToolBudget: configToolBudget.toolBudget, contextPolicy, modelScope, }; const foregroundControl = effectiveAsync ? undefined : { runId, mode: foregroundMode, startedAt: Date.now(), updatedAt: Date.now(), currentAgent: undefined, currentIndex: undefined, currentActivityState: undefined, nestedRoute, interrupt: undefined, }; if (foregroundControl) { deps.state.foregroundControls.set(runId, foregroundControl); deps.state.lastForegroundControlId = runId; } const writeNestedForegroundEvent = (type: "subagent.nested.started" | "subagent.nested.completed", result?: AgentToolResult
): void => { if (!inheritedNestedRoute || !nestedParentAddress) return; const now = Date.now(); const details = result?.details; const state = type === "subagent.nested.started" ? "running" : result?.isError || details?.results.some((child) => child.exitCode !== 0) ? "failed" : details?.results.some((child) => child.interrupted) ? "paused" : "complete"; const errorText = result?.isError ? result.content.find((item) => item.type === "text")?.text : undefined; const agentsForSummary = hasTasks && effectiveParams.tasks ? effectiveParams.tasks.map((task) => task.agent) : hasChain && effectiveParams.chain ? effectiveParams.chain.flatMap((step) => isParallelStep(step) ? step.parallel.map((task) => task.agent) : [(step as SequentialStep).agent]) : effectiveParams.agent ? [effectiveParams.agent] : []; const leafIntercomTarget = intercomBridge.active && agentsForSummary[0] ? resolveSubagentIntercomTarget(runId, agentsForSummary[0], 0) : undefined; try { writeNestedEvent(inheritedNestedRoute, { type, ts: now, parentRunId: nestedParentAddress.parentRunId, parentStepIndex: nestedParentAddress.parentStepIndex, child: { id: runId, parentRunId: nestedParentAddress.parentRunId, parentStepIndex: nestedParentAddress.parentStepIndex, depth: nestedParentAddress.depth, path: nestedParentAddress.path, ownerIntercomTarget: process.env.SELESAI_SUBAGENT_INTERCOM_SESSION_NAME, leafIntercomTarget, intercomTarget: leafIntercomTarget, ownerState: state === "running" ? "live" : "gone", mode: foregroundMode, state, agent: agentsForSummary[0], agents: agentsForSummary, startedAt: foregroundControl?.startedAt ?? now, ...(state !== "running" ? { endedAt: now } : {}), lastUpdate: now, ...(details?.totalCost ? { totalCost: details.totalCost } : {}), ...(errorText ? { error: errorText } : {}), ...(details?.results.length ? { steps: details.results.map((child) => ({ agent: child.agent, status: child.interrupted ? "paused" : child.exitCode === 0 ? "complete" : "failed", ...(child.sessionFile ? { sessionFile: child.sessionFile } : {}), ...(child.error ? { error: child.error } : {}), })) } : {}), }, }); } catch (error) { console.error("Failed to emit nested foreground status event:", error); } }; let nestedForegroundStarted = false; try { const asyncResult = runAsyncPath(execData, deps); if (asyncResult) return withForkContext(asyncResult, effectiveParams.context); if (foregroundControl) { writeNestedForegroundEvent("subagent.nested.started"); nestedForegroundStarted = true; } if (hasChain && effectiveParams.chain) { const result = await runChainPath(execData, deps); writeNestedForegroundEvent("subagent.nested.completed", result); return withForkContext(result, effectiveParams.context); } if (hasTasks && effectiveParams.tasks) { const result = await runParallelPath(execData, deps); writeNestedForegroundEvent("subagent.nested.completed", result); return withForkContext(result, effectiveParams.context); } if (hasSingle) { const result = await runSinglePath(execData, deps); writeNestedForegroundEvent("subagent.nested.completed", result); return withForkContext(result, effectiveParams.context); } } catch (error) { const errorResult = toExecutionErrorResult(effectiveParams, error); if (nestedForegroundStarted) writeNestedForegroundEvent("subagent.nested.completed", errorResult); return errorResult; } finally { if (foregroundControl) { clearPendingForegroundControlNotices(deps.state, runId); deps.state.foregroundControls.delete(runId); if (deps.state.lastForegroundControlId === runId) { deps.state.lastForegroundControlId = null; } } } return withForkContext({ content: [{ type: "text", text: "Invalid params" }], isError: true, details: { mode: "single" as const, results: [] }, }, effectiveParams.context); }; const executeWithSingleDispatchGuard = async ( id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult
) => void) | undefined, ctx: ExtensionContext, ): Promise> => { const requestParams = omitExecutionModeActionAlias(params); if (requestParams.action) return execute(id, requestParams, signal, onUpdate, ctx); if (deps.state.subagentInProgress === true) return duplicateSubagentCallResult(requestParams); deps.state.subagentInProgress = true; try { return await execute(id, requestParams, signal, onUpdate, ctx); } finally { deps.state.subagentInProgress = false; } }; return { execute: executeWithSingleDispatchGuard }; }