import { randomUUID } from "node:crypto"; import * as fs from "node:fs"; import * as path from "node:path"; import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { AgentConfig, 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 { beginForegroundChild, finishForegroundChild, updateForegroundChild } from "./foreground-control.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 { handleWatchdogToolAction, WATCHDOG_TOOL_ACTIONS } from "../../watchdog/tool-actions.ts"; import type { MainWatchdogRuntime } from "../../watchdog/runtime.ts"; import { buildModelCandidates, normalizeParentModel, resolveEffectiveSubagentModel, resolveModelCandidate, type ParentModel } 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 { validateExecutionAcceptance } from "../shared/acceptance.ts"; import { createForkContextResolver, forkedChildRequiresThinkingOff } 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 { resolveTurnBudgetConfig } from "../shared/turn-budget.ts"; import { formatSpawnBudget, getSpawnBudgetSnapshot, grantSpawnBudget, preflightSpawnBudget, preflightSpawnBudgetGrant, reserveSpawnBudget } from "../shared/spawn-budget.ts"; import { validateToolBudgetConfig } from "../shared/tool-budget.ts"; import { intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts"; import { isAgentContractV1 } from "../shared/agent-contract.ts"; import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts"; import { cleanupStructuredOutputRuntime, createStructuredOutputRuntime } from "../shared/structured-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 { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts"; import { summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts"; import { attachNestedChildrenToResultChildren, buildSubagentResultIntercomPayload, deliverSubagentResultIntercomEvent, formatSubagentResultReceipt, resolveSubagentResultStatus, stripDetailsOutputsForIntercomReceipt, } from "../../intercom/result-intercom.ts"; import { applySteeringRecoveryAgentConfig, buildRevivedAsyncTask, resolveAsyncResumeTarget, resolveAsyncRunLocation } from "../background/async-resume.ts"; import { deliverInterruptRequest, deliverStopRequest, requestAsyncSteer } from "../background/control-channel.ts"; import { waitForSteeringAction } from "../background/steering.ts"; import { steerAsyncRun } from "./async-steering-action.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 AsyncStatus, type AcceptanceInput, type AgentContract, type ArtifactConfig, type ArtifactPaths, type ControlConfig, type ControlEvent, type Details, type ExtensionConfig, type ForegroundRunControl, type IntercomEventBus, type JsonSchemaObject, 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, DEFAULT_FORK_PREAMBLE, RESULTS_DIR, SUBAGENT_ACTIONS, SUBAGENT_CONTROL_EVENT, SUBAGENT_CONTROL_INTERCOM_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT, checkSubagentDepth, resolveTopLevelParallelConcurrency, resolveTopLevelParallelMaxTasks, resolveChildMaxSubagentDepth, resolveCurrentMaxSubagentDepth, wrapForkTask, } from "../../shared/types.ts"; const MUTATING_MANAGEMENT_ACTIONS = new Set(["create", "update", "delete", "eject", "disable", "enable", "reset", "grant-spawn-budget", "watchdog.configure"]); 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; outputSchema?: JsonSchemaObject; acceptance?: AcceptanceInput; agentContract?: AgentContract; 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; steeringRecovery?: boolean; chain?: ChainStep[]; tasks?: TaskParam[]; concurrency?: number; worktree?: boolean; context?: "fresh" | "fork"; async?: boolean; foregroundOnly?: boolean; timeoutMs?: number; maxRuntimeMs?: number; turnBudget?: TurnBudgetConfig; /** Internal-only strict turn-boundary enforcement for versioned foreground delegation. */ enforceHardTurnLimit?: boolean; toolBudget?: ToolBudgetConfig; clarify?: boolean; share?: boolean; control?: ControlConfig; sessionDir?: string; cwd?: string; maxOutput?: MaxOutputConfig; artifacts?: boolean; includeProgress?: boolean; model?: string; thinking?: string | false; scope?: string; target?: string; skill?: string | string[] | boolean; output?: string | boolean; outputMode?: "inline" | "file-only"; outputSchema?: JsonSchemaObject; agentScope?: unknown; chainDir?: string; acceptance?: AcceptanceInput; agentContract?: AgentContract; schedule?: string; scheduleName?: string; additional?: number; } function rememberParentModel(state: { currentSessionId?: string | null; lastParentModel?: ParentModel }, sessionId: string | null, model: unknown): ParentModel | undefined { if (state.currentSessionId !== sessionId) state.lastParentModel = undefined; state.currentSessionId = sessionId; const parentModel = normalizeParentModel(model); if (!sessionId) return parentModel; if (parentModel) state.lastParentModel = parentModel; return parentModel ?? state.lastParentModel; } interface ExecutorDeps { pi: ExtensionAPI; state: SubagentState; config: ExtensionConfig; asyncByDefault: boolean; waitToolEnabled?: boolean; handleScheduledRunAction?: (params: SubagentParamsLike, ctx: ExtensionContext) => Promise>; watchdog?: MainWatchdogRuntime; 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; } type ForkSessionFileForTask = (agentName: string, idx?: number, modelOverride?: string) => string | undefined; type ForkThinkingOverrideForTask = (agentName: string, idx?: number, modelOverride?: string) => AgentConfig["thinking"] | undefined; 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: ForkSessionFileForTask; thinkingOverrideForTask: ForkThinkingOverrideForTask; artifactConfig: ArtifactConfig; artifactsDir: string; backgroundRequestedWhileClarifying: boolean; effectiveAsync: boolean; controlConfig: ResolvedControlConfig; intercomBridge: IntercomBridgeState; nestedRoute?: NestedRouteInfo; timeoutMs?: number; deadlineAt?: number; turnBudget?: ResolvedTurnBudget; toolBudget?: ResolvedToolBudget; allowZeroToolBudget?: boolean; configToolBudget?: ResolvedToolBudget; contextPolicy: AgentDefaultContextPolicy; modelScope?: ModelScopeConfig; parentModel?: ParentModel; parentSessionId: string | null; capabilityCeiling?: ResolvedSubagentCapabilityCeiling; } 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 spawnBudgetErrorResult(message: string, mode: "single" | "parallel" | "chain"): AgentToolResult
{ return { content: [{ type: "text", text: message }], isError: true, details: { mode, results: [] }, }; } function withSpawnBudgetStatus( result: AgentToolResult
, state: SubagentState, config: ExtensionConfig, sessionId: string | null, ): AgentToolResult
{ const spawnBudget = getSpawnBudgetSnapshot(state, config, sessionId); return { ...result, content: result.content.map((item, index) => index === 0 && item.type === "text" ? { ...item, text: `${formatSpawnBudget(spawnBudget)}\n${item.text}` } : item), details: { ...result.details, spawnBudget }, }; } function hasActiveSubagentChildren(state: SubagentState): boolean { if (state.subagentInProgress || state.foregroundControls.size > 0) return true; const isActive = (status: string) => status === "queued" || status === "running"; return [...state.asyncJobs.values(), ...(state.fleetJobs?.values() ?? [])].some((job) => isActive(job.status)); } 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 oldestTerminal = [...state.foregroundRuns.values()] .filter((run) => !run.children.some((child) => child.status === "detached")) .sort((left, right) => left.updatedAt - right.updatedAt)[0]; if (!oldestTerminal) break; state.foregroundRuns.delete(oldestTerminal.runId); } } function foregroundChildActivityFromProgress(progress: SingleResult["progress"] | undefined) { return { ...(progress?.activityState ? { activityState: progress.activityState } : {}), ...(progress?.lastActivityAt !== undefined ? { lastActivityAt: progress.lastActivityAt } : {}), ...(progress?.currentTool ? { currentTool: progress.currentTool } : {}), ...(progress?.currentToolStartedAt !== undefined ? { currentToolStartedAt: progress.currentToolStartedAt } : {}), ...(progress?.currentPath ? { currentPath: progress.currentPath } : {}), ...(progress?.turnCount !== undefined ? { turnCount: progress.turnCount } : {}), ...(progress?.tokens !== undefined ? { tokens: progress.tokens } : {}), ...(progress?.toolCount !== undefined ? { toolCount: progress.toolCount } : {}), }; } function rememberForegroundRun(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; 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, ...(input.sessionId ? { sessionId: input.sessionId } : {}), updatedAt, children: input.results.map((result, index) => { const child = { agent: result.agent, index, ...(result.context ? { context: result.context } : {}), status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached }), ...foregroundChildActivityFromProgress(result.progress), updatedAt, ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}), ...(result.error ? { error: result.error } : {}), ...(result.finalOutput ? { finalOutput: result.finalOutput } : {}), ...(result.outputMode ? { outputMode: result.outputMode } : {}), ...(result.savedOutputPath ? { savedOutputPath: result.savedOutputPath } : {}), ...(result.outputSaveError ? { outputSaveError: result.outputSaveError } : {}), ...(result.sessionFile ? { sessionFile: result.sessionFile } : {}), ...(result.model ? { model: result.model } : {}), ...(result.thinking ? { thinking: result.thinking } : {}), ...(result.artifactPaths ? { artifactPaths: result.artifactPaths } : {}), ...(result.transcriptPath ? { transcriptPath: result.transcriptPath } : {}), ...(result.transcriptError ? { transcriptError: result.transcriptError } : {}), ...(result.detachedReason ? { detachedReason: result.detachedReason } : {}), ...(result.acceptance ? { acceptance: result.acceptance } : {}), ...(result.launchContractDigest ? { launchContractDigest: result.launchContractDigest } : {}), ...(result.capabilityCeiling ? { capabilityCeiling: result.capabilityCeiling } : {}), ...(result.capabilityAudit ? { capabilityAudit: result.capabilityAudit } : {}), }; const recovered = previous?.children[index]; return child.status === "detached" && recovered && recovered.status !== "detached" ? recovered : child; }), }); trimRememberedForegroundRuns(state); } function applyControlEventToRememberedForegroundRun(state: SubagentState, event: ControlEvent): void { const run = state.foregroundRuns?.get(event.runId); if (!run) return; const index = event.index ?? (run.children.length === 1 ? run.children[0]?.index : undefined); if (index === undefined) return; const child = run.children[index]; if (!child || child.status !== "detached") return; const updatedAt = event.ts; run.updatedAt = updatedAt; run.children[index] = { ...child, activityState: event.to, updatedAt, ...(event.elapsedMs !== undefined ? { lastActivityAt: event.ts - event.elapsedMs } : {}), ...(event.currentTool ? { currentTool: event.currentTool } : {}), ...(event.currentToolDurationMs !== undefined ? { currentToolStartedAt: event.ts - event.currentToolDurationMs } : {}), ...(event.currentPath ? { currentPath: event.currentPath } : {}), ...(event.turns !== undefined ? { turnCount: event.turns } : {}), ...(event.tokens !== undefined ? { tokens: event.tokens } : {}), ...(event.toolCount !== undefined ? { toolCount: event.toolCount } : {}), }; } function updateRememberedForegroundChild(state: SubagentState, input: { runId: string; mode: "single" | "parallel" | "chain"; cwd: string; sessionId: string | null; index: number; result: SingleResult; events: IntercomEventBus }): 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, ...(input.sessionId ? { sessionId: input.sessionId } : {}), 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, ...(input.result.context ? { context: input.result.context } : {}), status: input.result.acceptance?.status === "rejected" ? "failed" : resolveSubagentResultStatus({ exitCode: input.result.exitCode, interrupted: input.result.interrupted, detached: false }), ...foregroundChildActivityFromProgress(input.result.progress), updatedAt, ...(input.result.exitCode !== undefined ? { exitCode: input.result.exitCode } : {}), ...(input.result.error ? { error: input.result.error } : {}), ...(input.result.finalOutput ? { finalOutput: input.result.finalOutput } : {}), outputMode: input.result.outputMode, savedOutputPath: input.result.savedOutputPath, outputSaveError: input.result.outputSaveError, ...(input.result.sessionFile ? { sessionFile: input.result.sessionFile } : {}), ...(input.result.model ? { model: input.result.model } : {}), ...(input.result.thinking ? { thinking: input.result.thinking } : {}), ...(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 } : {}), ...(input.result.acceptance ? { acceptance: input.result.acceptance } : {}), ...(input.result.launchContractDigest ? { launchContractDigest: input.result.launchContractDigest } : {}), ...(input.result.capabilityCeiling ? { capabilityCeiling: input.result.capabilityCeiling } : {}), ...(input.result.capabilityAudit ? { capabilityAudit: input.result.capabilityAudit } : {}), }; trimRememberedForegroundRuns(state); const output = getSingleResultOutput(input.result).trim(); const success = input.result.exitCode === 0 && input.result.acceptance?.status !== "rejected"; const summary = !success && input.result.error ? `${input.result.error}${output ? `\n\nOutput:\n${output}` : ""}` : output || input.result.error || "Detached child exited without final output."; // A detached callback may outlive its extension runtime. Stale sessions are // intentionally dropped rather than routed through a replacement runtime. if (!input.sessionId || input.sessionId !== state.currentSessionId) return; input.events.emit(SUBAGENT_FOREGROUND_COMPLETE_EVENT, { id: `${input.runId}:${input.index}`, runId: input.runId, source: "foreground", mode: input.mode, agent: input.result.agent, success, summary, exitCode: input.result.exitCode, state: success ? "complete" : "failed", timestamp: updatedAt, cwd: input.cwd, sessionFile: input.result.sessionFile, sessionId: input.sessionId, taskIndex: input.index, }); } function resolveForegroundResumeTarget(params: SubagentParamsLike, state: SubagentState): { runId: string; mode: "single" | "parallel" | "chain"; state: "complete"; agent: string; index: number; cwd: string; sessionFile: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling } | undefined { const requested = (params.id ?? params.runId)?.trim(); if (!requested || !state.foregroundRuns?.size || !state.currentSessionId) return undefined; const sessionRuns = [...state.foregroundRuns.values()].filter((run) => run.sessionId === state.currentSessionId); const direct = sessionRuns.find((run) => run.runId === requested); const matches = direct ? [direct] : sessionRuns.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.some((child) => child.status === "detached")) throw new Error(`Foreground run '${run.runId}' is detached for intercom coordination and cannot be revived safely while any child may still be live. Reply to the supervisor request first, then wait with subagent_wait({ id: "${run.runId}" }); use status to recover the result and do not launch a replacement while it remains detached.`); 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.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, cwd: run.cwd, sessionFile, ...(child.model ? { model: child.model } : {}), ...(child.thinking ? { thinking: child.thinking } : {}), ...(child.launchContractDigest ? { launchContractDigest: child.launchContractDigest } : {}), ...(child.capabilityCeiling ? { capabilityCeiling: child.capabilityCeiling } : {}), }; } 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; cwd?: string; sessionFile: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling; }; 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, sessionId: state.currentSessionId ?? undefined, }), }; } 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 }, options: { fallbackToNewest?: boolean } = {}, ): { 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 }; if (options.fallbackToNewest === false) return undefined; } 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 stopAsyncRun( 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, { fallbackToNewest: false }); if (!target) return null; const status = reconcileAsyncRun(target.asyncDir, { kill }).status; if (state.currentSessionId && status?.sessionId !== state.currentSessionId) { return { content: [{ type: "text", text: `Async run '${target.asyncId}' was not found in the active session.` }], isError: true, details: { mode: "management", results: [] }, }; } if (!status || (status.state !== "running" && status.state !== "queued")) { return { content: [{ type: "text", text: `No running or queued async run was found for '${runId ?? "current"}'.` }], isError: true, details: { mode: "management", results: [] }, }; } try { deliverStopRequest({ asyncDir: target.asyncDir, pid: typeof status.pid === "number" ? status.pid : undefined, kill, source: "stop-action" }); const tracked = state.asyncJobs.get(target.asyncId); if (tracked) { tracked.activityState = undefined; tracked.updatedAt = Date.now(); } return { content: [{ type: "text", text: `Stop 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 stop async run ${target.asyncId}: ${message}` }], isError: true, 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; parentModel?: ParentModel; }): 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 parentModel = input.parentModel; const asyncCtx = { pi: input.deps.pi, cwd: input.ctx.cwd, currentSessionId: resolveCurrentSessionId(input.ctx.sessionManager), parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: parentModel?.provider, currentModel: parentModel, modelScope: discoveredForAppend.modelScope, interactive: input.ctx.hasUI, }; 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), waitToolEnabled: input.deps.waitToolEnabled, contextForAgent: contextPolicy.contextForAgent, asyncDir: resolved.location.asyncDir, validateOutputBindings: false, capabilityCeiling: intersectSubagentCapabilityCeilings(status.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(asyncCtx.currentSessionId)), }); 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.`); if (run.state === "stopped") throw new Error(`Nested run '${run.id}' was stopped and cannot be resumed. Start a new run 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, cwd: asyncDir ? path.dirname(asyncDir) : undefined, sessionFile: validateNestedSessionFile(run, trustedSessionRoots), ...(run.capabilityCeiling ? { capabilityCeiling: run.capabilityCeiling } : {}), }; } 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: [] } }; } } async function directNestedAsyncSteer(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number; signal?: AbortSignal }): Promise | 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: [] } }; } const runningIndexes = steps .map((step, index) => step.status === "running" ? index : undefined) .filter((index): index is number => index !== undefined); const effectiveTargetIndex = input.index ?? (status.mode === "single" && runningIndexes.length === 0 && steps[0]?.status === "pending" ? 0 : undefined); const targetIndexes = effectiveTargetIndex !== undefined ? [effectiveTargetIndex] : runningIndexes; if (targetIndexes.length === 0) return { content: [{ type: "text", text: `Nested async run ${run.id} has no running child to steer.` }], isError: true, details: { mode: "management", results: [] } }; const requestId = randomUUID(); try { requestAsyncSteer(asyncDir, { message: input.message, ...(effectiveTargetIndex !== undefined ? { targetIndex: effectiveTargetIndex } : { targetIndexes }), source: "nested-steer", id: requestId, }); } catch (error) { return { content: [{ type: "text", text: `Failed to queue steering for nested async run ${run.id}: ${error instanceof Error ? error.message : String(error)}` }], isError: true, details: { mode: "management", results: [] } }; } const targets = targetIndexes.map((index) => ({ index, state: steps[index]?.status === "pending" ? "scheduled" as const : "pending" as const })); if (targets.every((target) => target.state === "scheduled")) { const scheduled = { requestId, state: "scheduled" as const, sourceRunId: run.id, targets }; return { content: [{ type: "text", text: `Steering scheduled for nested async run ${run.id} (request ${requestId}).` }], details: { mode: "management", results: [], steering: scheduled } }; } const waited = await waitForSteeringAction({ asyncDir, sourceRunId: run.id, requestId, timeoutMs: 3_000, signal: input.signal }); const result = waited ?? { requestId, state: "pending" as const, sourceRunId: run.id, targets }; const stateText = result.state === "delivered" ? "delivered" : result.state === "failed" ? "failed" : result.state === "partial" ? "partial" : "pending"; return { content: [{ type: "text", text: `Steering ${stateText} for nested async run ${run.id} (request ${requestId}).` }], ...(result.state === "failed" || result.state === "partial" ? { isError: true } : {}), details: { mode: "management", results: [], steering: result } }; } 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: [] } }; } async function steerNestedRun(input: { target: ResolvedSubagentRunId & { kind: "nested" }; message: string; index?: number; signal?: AbortSignal }): Promise> { 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 = await 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; parentModel?: ParentModel; absoluteDeadlineAt?: number; }): 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: [] }, }; } if (input.params.model !== undefined) { return { content: [{ type: "text", text: "action='resume' reuses the persisted child model and does not accept a model override." }], isError: true, details: { mode: "management", results: [] }, }; } const acceptanceErrors = validateExecutionAcceptance(input.params); if (acceptanceErrors.length > 0) { return { content: [{ type: "text", text: `Cannot resume: ${acceptanceErrors.join(" ")}` }], isError: true, details: { mode: "management", results: [] }, }; } input.deps.state.currentSessionId = resolveCurrentSessionId(input.ctx.sessionManager); 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) { return { content: [{ type: "text", text: [ `Async child '${target.runId}' index ${target.index} is still running. action='resume' only revives paused, completed, or failed children.`, `Send live input with subagent({ action: "steer", id: "${target.runId}", index: ${target.index}, message: "..." }).`, ].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 recoveryDescriptor = "recoveryDescriptor" in target ? target.recoveryDescriptor : undefined; const discoveredAgentConfig = agents.find((agent) => agent.name === target.agent); const agentConfig: AgentConfig | undefined = discoveredAgentConfig ?? (recoveryDescriptor ? { name: recoveryDescriptor.agent, description: "Persisted async recovery contract", systemPrompt: "", systemPromptMode: recoveryDescriptor.systemPromptMode, inheritProjectContext: recoveryDescriptor.inheritProjectContext, inheritSkills: recoveryDescriptor.inheritSkills, source: "project", filePath: recoveryDescriptor.agentFilePath ?? path.join(recoveryDescriptor.cwd, ".pi-subagents-recovery-agent"), } : undefined); 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, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir }; const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo); const contextPolicy = resolveExplicitContextPolicy(input.params); const workflowTask = (input.params.task ?? followUp) || undefined; const goal = resolveAsyncEventGoal(workflowTask, attachChain); const chain = wrapChainTasksForFork(attachChain, contextPolicy); const normalized = normalizeSkillInput(input.params.skill); const parentModel = input.parentModel; const result = executeAsyncChain(runId, { chain, task: workflowTask, goal, 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: parentModel?.provider, currentModel: parentModel, modelScope, interactive: input.ctx.hasUI, }, availableModels, cwd: effectiveCwd, maxOutput: input.params.maxOutput, artifactsDir: getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir), artifactConfig, shareEnabled: input.params.share === true, sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile), chainSkills: normalized === false ? [] : (normalized ?? []), agentContract: input.params.agentContract, dynamicFanoutMaxItems: input.deps.config.chain?.dynamicFanout?.maxItems, maxSubagentDepth: resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth), waitToolEnabled: input.deps.waitToolEnabled, 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, capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)), }); 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"), input.ctx.hasUI) }], details: result.details }; } const runId = randomUUID().slice(0, 8); const recoveryAgentConfig = recoveryDescriptor ? applySteeringRecoveryAgentConfig(agentConfig, recoveryDescriptor) : agentConfig; const artifactConfig: ArtifactConfig = recoveryDescriptor?.artifactConfig ?? { ...DEFAULT_ARTIFACT_CONFIG, enabled: input.params.artifacts !== false, dir: input.deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir }; const artifactsDir = recoveryDescriptor?.artifactsDir ?? getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir); const availableModels = input.ctx.modelRegistry.getAvailable().map(toModelInfo); const parentModel = input.parentModel; const result = executeAsyncSingle(runId, { agent: target.agent, task: buildRevivedAsyncTask(target, followUp), goal: followUp, agentConfig: recoveryAgentConfig, ctx: { pi: input.deps.pi, cwd: input.requestCwd, currentSessionId: input.deps.state.currentSessionId, parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: parentModel?.provider, currentModel: parentModel, modelScope, interactive: input.ctx.hasUI, }, cwd: effectiveCwd, maxOutput: input.params.maxOutput ?? recoveryDescriptor?.maxOutput, artifactsDir, artifactConfig, shareEnabled: recoveryDescriptor?.share ?? input.params.share === true, sessionRoot: input.deps.getSubagentSessionRoot(parentSessionFile), ...(recoveryDescriptor?.sessionDir ? { sessionDir: recoveryDescriptor.sessionDir } : {}), sessionFile: target.sessionFile, revivalLease: { sessionFile: target.sessionFile, runId, sourceRunId: target.runId, ...(input.deps.state.currentSessionId ? { parentSessionId: input.deps.state.currentSessionId } : {}), }, modelOverride: recoveryDescriptor?.model ?? target.model, thinkingOverride: recoveryDescriptor?.thinking ?? target.thinking, outputBaseDir: resolveSingleRunOutputBaseDir(input.deps, artifactsDir, runId), maxSubagentDepth: recoveryDescriptor?.maxSubagentDepth ?? resolveCurrentMaxSubagentDepth(input.deps.config.maxSubagentDepth), waitToolEnabled: input.deps.waitToolEnabled, worktreeSetupHook: input.deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: input.deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: input.deps.config.worktreeBaseDir, controlConfig: recoveryDescriptor?.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, output: typeof input.params.output === "string" ? input.params.output : recoveryDescriptor?.outputPath, outputMode: input.params.outputMode ?? recoveryDescriptor?.outputMode, ...(recoveryDescriptor?.agentContract ? { agentContract: recoveryDescriptor.agentContract } : {}), ...(recoveryDescriptor?.structuredOutputSchema ? { structuredOutputSchema: recoveryDescriptor.structuredOutputSchema } : {}), ...(recoveryDescriptor?.skills ? { skills: [...recoveryDescriptor.skills] } : {}), ...(recoveryDescriptor?.acceptance !== undefined && input.params.acceptance === undefined ? { acceptance: recoveryDescriptor.acceptance } : {}), ...(input.params.timeoutMs !== undefined ? { timeoutMs: input.params.timeoutMs } : {}), ...(input.absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt: input.absoluteDeadlineAt } : {}), ...(input.params.turnBudget !== undefined ? { turnBudget: input.params.turnBudget } : {}), ...(input.params.toolBudget !== undefined ? { toolBudget: input.params.toolBudget } : {}), capabilityCeiling: intersectSubagentCapabilityCeilings("capabilityCeiling" in target ? target.capabilityCeiling : undefined, recoveryDescriptor?.capabilityCeiling, resolveCurrentSubagentCapabilityCeiling(input.deps.state.currentSessionId)), }); 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"), input.ctx.hasUI) }], details: { ...result.details, ...(target.launchContractDigest ? { sourceLaunchContractDigest: target.launchContractDigest } : {}), }, }; } 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 && fs.existsSync(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) => { applyControlEventToRememberedForegroundRun(deps.state, 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[]; parallelHandoff?: Details["parallelHandoff"]; }): Promise | null> { if (!input.intercomBridge.active || !input.intercomBridge.resultDelivery || !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 } : {}), ...(input.parallelHandoff ? { parallelHandoff: input.parallelHandoff } : {}), }); 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 } : {}), ...(input.details.parallelHandoff ? { parallelHandoff: input.details.parallelHandoff } : {}), }); 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 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): ContextMode; contextSummary?: ContextSummary; 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): ContextMode => byName.get(agentName)?.defaultContext === "fork" ? "fork" : "fresh"; const requestedAgentNames = collectRequestedAgentNames(params); const contextSummary = summarizeContextModes(requestedAgentNames.map((name) => contextForAgent(name))); const usesFork = contextSummary === "fork" || contextSummary === "mixed"; return { params, contextForAgent, contextSummary, usesFork, }; } function resolveExplicitContextPolicy(params: SubagentParamsLike): AgentDefaultContextPolicy { const context = params.context === "fork" ? "fork" : "fresh"; return { params, contextForAgent: () => context, contextSummary: 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 summarizeResultContext(details: Details, fallback: ContextSummary | undefined): ContextSummary | undefined { return summarizeContextModes(details.results.map((result) => result.context)) ?? fallback; } function buildRequestedModeError(params: SubagentParamsLike, message: string): AgentToolResult
{ return withResolvedContext( { content: [{ type: "text", text: message }], isError: true, details: { mode: getRequestedModeLabel(params), results: [] }, }, params.context, ); } function applySingleAgentLaunchDefaults(params: SubagentParamsLike, agents: AgentConfig[]): SubagentParamsLike { if ((params.chain?.length ?? 0) > 0 || (params.tasks?.length ?? 0) > 0 || !params.agent) return params; const agent = agents.find((candidate) => candidate.name === params.agent); if (!agent) return params; return { ...params, ...(params.async === undefined && agent.defaultAsync !== undefined ? { async: agent.defaultAsync } : {}), ...(params.timeoutMs === undefined && params.maxRuntimeMs === undefined && agent.defaultTimeoutMs !== undefined ? { timeoutMs: agent.defaultTimeoutMs } : {}), ...(params.turnBudget === undefined && agent.defaultTurnBudget !== undefined ? { turnBudget: agent.defaultTurnBudget } : {}), ...(params.acceptance === undefined && agent.defaultAcceptance !== undefined ? { acceptance: agent.defaultAcceptance } : {}), }; } export const DEFAULT_FOREGROUND_TIMEOUT_MS = 30 * 60 * 1000; function resolveForegroundTimeout(params: SubagentParamsLike, defaultTimeoutMs?: number): { timeoutMs?: number; error?: string } { const rawTimeout = params.timeoutMs; const rawMaxRuntime = params.maxRuntimeMs; if (rawTimeout === undefined && rawMaxRuntime === undefined) { return defaultTimeoutMs === undefined ? {} : { timeoutMs: defaultTimeoutMs }; } 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 resolveToolBudget( raw: unknown, label = "toolBudget", options?: { minimumHard?: 0 | 1 }, ): { toolBudget?: ResolvedToolBudget; error?: string } { const resolved = validateToolBudgetConfig(raw, label, options); 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 withResolvedContext( result: AgentToolResult
, fallback: ContextSummary | undefined, ): AgentToolResult
{ if (!result.details) return result; const context = summarizeResultContext(result.details, fallback); if (!context) return result; return { ...result, details: { ...result.details, context, }, }; } function withForkThinkingNotes( result: AgentToolResult
, downgrades: Map, ): AgentToolResult
{ if (downgrades.size === 0) return result; const children = [...downgrades.entries()] .sort(([a], [b]) => a - b) .map(([index, agent]) => `${agent} (child ${index})`) .join(", "); const note = `Note: fork context forced thinking off for ${children}. The forked transcript contained signed Anthropic thinking blocks that were sanitized, and Anthropic children cannot resume such a transcript with thinking enabled. Use context: "fresh" when an Anthropic child needs thinking.`; return { ...result, content: [...result.content, { type: "text", text: note }] }; } function toExecutionErrorResult(params: SubagentParamsLike, error: unknown, contextSummary?: ContextSummary): AgentToolResult
{ const message = error instanceof Error ? error.message : String(error); return withResolvedContext( { content: [{ type: "text", text: message }], isError: true, details: { mode: getRequestedModeLabel(params), results: [] }, }, contextSummary, ); } function collectChainSessionFiles( chain: ChainStep[], sessionFileForTask: ForkSessionFileForTask, 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, task.model)); 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, step.parallel.model)); flatIndex++; } continue; } const sequential = step as SequentialStep; sessionFiles.push(sessionFileForTask(sequential.agent, flatIndex, sequential.model)); flatIndex++; } return sessionFiles; } function collectChainThinkingOverrides( chain: ChainStep[], thinkingOverrideForTask: ForkThinkingOverrideForTask, 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, task.model)); 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, step.parallel.model)); flatIndex++; } continue; } const sequential = step as SequentialStep; thinkingOverrides.push(thinkingOverrideForTask(sequential.agent, flatIndex, sequential.model)); flatIndex++; } return thinkingOverrides; } function firstChainAgent(chain: ChainStep[]): string | undefined { const first = chain[0]; if (!first) return undefined; if (isParallelStep(first)) return first.parallel[0]?.agent; if (isDynamicParallelStep(first)) return first.parallel.agent; return (first as SequentialStep).agent; } function firstRawChainTask(chain: ChainStep[]): string | undefined { const first = chain[0]; if (!first) return undefined; if (isParallelStep(first)) return first.parallel[0]?.task; if (isDynamicParallelStep(first)) return first.parallel.task; return (first as SequentialStep).task; } function resolveAsyncEventGoal(workflowTask: string | undefined, rawChain: ChainStep[], unwrapForkFallback = false): string { if (workflowTask?.trim()) return workflowTask; const fallback = firstRawChainTask(rawChain) || ""; if (!unwrapForkFallback) return fallback; const forkPrefix = `${DEFAULT_FORK_PREAMBLE}\n\nTask:\n`; return fallback.startsWith(forkPrefix) ? fallback.slice(forkPrefix.length) : fallback; } 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: ForkSessionFileForTask, dynamicFanoutMaxItems?: number, ): void { if (!contextPolicy.usesFork) return; if (params.agent) { if (shouldForkAgent(contextPolicy, params.agent)) sessionFileForTask(params.agent, 0, params.model); return; } if (params.tasks) { params.tasks.forEach((task, index) => { if (shouldForkAgent(contextPolicy, task.agent)) sessionFileForTask(task.agent, index, task.model); }); 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, task.model); 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, step.parallel.model); } flatIndex += maxItems; continue; } const sequential = step as SequentialStep; if (shouldForkAgent(contextPolicy, sequential.agent)) sessionFileForTask(sequential.agent, flatIndex, sequential.model); 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 parentModel = data.parentModel; const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId!, parentSessionId: ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: parentModel?.provider, currentModel: parentModel, modelScope: data.modelScope, interactive: ctx.hasUI, }; const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo); const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth); const currentProvider = parentModel?.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 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, ...(task.model !== undefined ? { model: task.model } : {}), ...(skillOverrides[index] !== undefined ? { skill: skillOverrides[index] } : {}), ...(task.output !== undefined && task.output !== true ? { 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.outputSchema !== undefined ? { outputSchema: task.outputSchema } : {}), ...(task.agentContract !== undefined ? { agentContract: task.agentContract } : {}), ...(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", goal: params.tasks[0]?.task ?? "", 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, task.model)), thinkingOverridesByFlatIndex: params.tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, task.model)), contextForAgent: contextPolicy.contextForAgent, maxSubagentDepth: currentMaxSubagentDepth, waitToolEnabled: deps.waitToolEnabled, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, agentContract: params.agentContract, controlIntercomTarget, childIntercomTarget, nestedRoute, timeoutMs: data.timeoutMs, turnBudget: data.turnBudget, toolBudget: data.toolBudget, configToolBudget: data.configToolBudget, capabilityCeiling: data.capabilityCeiling, globalConcurrencyLimit: deps.config.globalConcurrencyLimit, }); } if (hasChain && params.chain) { const normalized = normalizeSkillInput(params.skill); const chainSkills = normalized === false ? [] : (normalized ?? []); const rawChain = params.chain as ChainStep[]; const chain = wrapChainTasksForFork(rawChain, contextPolicy); return executeAsyncChain(id, { chain, task: params.task, goal: resolveAsyncEventGoal(params.task, rawChain), 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), contextForAgent: contextPolicy.contextForAgent, dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems, maxSubagentDepth: currentMaxSubagentDepth, waitToolEnabled: deps.waitToolEnabled, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, agentContract: params.agentContract, controlIntercomTarget, childIntercomTarget, nestedRoute, timeoutMs: data.timeoutMs, turnBudget: data.turnBudget, toolBudget: data.toolBudget, configToolBudget: data.configToolBudget, capabilityCeiling: data.capabilityCeiling, 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 = resolveEffectiveSubagentModel(params.model as string | undefined, a.model, parentModel, availableModels, currentProvider, { scope: data.modelScope }); return executeAsyncSingle(id, { agent: params.agent!, task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(params.task ?? "") : (params.task ?? ""), goal: 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, modelOverride), context: contextPolicy.contextForAgent(params.agent!), skills, output: effectiveOutput, outputMode: effectiveOutputMode, outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id), modelOverride, thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride), maxSubagentDepth, waitToolEnabled: deps.waitToolEnabled, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, controlIntercomTarget, childIntercomTarget: childIntercomTarget ? (agent, index) => childIntercomTarget(agent, index) : undefined, nestedRoute, agentContract: params.agentContract, structuredOutputSchema: params.outputSchema, acceptance: params.acceptance, timeoutMs: data.timeoutMs, turnBudget: data.turnBudget, toolBudget: data.toolBudget, configToolBudget: data.configToolBudget, capabilityCeiling: data.capabilityCeiling, }); } 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 chainCtx = normalizeParentModel(ctx.model) || !data.parentModel ? ctx : { ...ctx, model: data.parentModel }; const chainResult = await executeChain({ chain, task: params.task, agents, ctx: chainCtx, modelScope: data.modelScope, intercomEvents: deps.pi.events, signal, runId, cwd: effectiveCwd, shareEnabled, sessionDirForIndex, sessionFileForIndex, sessionFileForTask, thinkingOverrideForTask, contextForAgent: contextPolicy.contextForAgent, artifactsDir, artifactConfig, includeProgress: params.includeProgress, clarify: params.clarify, onUpdate, onControlEvent, controlConfig, agentContract: params.agentContract, 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, sessionId: data.parentSessionId, index, result, events: deps.pi.events }), toolBudget: data.toolBudget, configToolBudget: data.configToolBudget, globalConcurrencyLimit: deps.config.globalConcurrencyLimit, capabilityCeiling: data.capabilityCeiling, }); 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 parentModel = data.parentModel; const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId!, parentSessionId: ctx.sessionManager.getSessionId() ?? undefined, currentModelProvider: parentModel?.provider, currentModel: parentModel, modelScope: data.modelScope, interactive: ctx.hasUI, }; const rawAsyncChain = chainResult.requestedAsync.chain; const asyncChain = wrapChainTasksForFork(rawAsyncChain, contextPolicy); const firstAgent = firstChainAgent(rawAsyncChain); return executeAsyncChain(id, { chain: asyncChain, task: params.task, goal: resolveAsyncEventGoal(params.task, rawAsyncChain, firstAgent ? shouldForkAgent(contextPolicy, firstAgent) : false), 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), contextForAgent: contextPolicy.contextForAgent, dynamicFanoutMaxItems: deps.config.chain?.dynamicFanout?.maxItems, maxSubagentDepth: currentMaxSubagentDepth, waitToolEnabled: deps.waitToolEnabled, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, agentContract: params.agentContract, 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, capabilityCeiling: data.capabilityCeiling, globalConcurrencyLimit: deps.config.globalConcurrencyLimit, }); } const rawChainDetails = chainResult.details ? { ...chainResult.details, runId, timeoutMs: data.timeoutMs } : 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, sessionId: data.parentSessionId, 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[]; taskDescriptions: string[]; agents: AgentConfig[]; ctx: ExtensionContext; state: SubagentState; intercomEvents: IntercomEventBus; parentSessionId: string | null; capabilityCeiling?: ResolvedSubagentCapabilityCeiling; signal: AbortSignal; runId: string; sessionDirForIndex: (idx?: number) => string | undefined; sessionFileForIndex: (idx?: number) => string | undefined; sessionFileForTask: ForkSessionFileForTask; thinkingOverrideForTask: ForkThinkingOverrideForTask; shareEnabled: boolean; artifactConfig: ArtifactConfig; artifactsDir: string; outputBaseDir: string; maxOutput?: MaxOutputConfig; paramsCwd: string; progressDir: string; maxSubagentDepths: number[]; waitToolEnabled?: boolean; availableModels: ModelInfo[]; modelScope?: ModelScopeConfig; parentModel?: ParentModel; modelOverrides: (string | undefined)[]; behaviors: Array>; firstProgressIndex: number; controlConfig: ResolvedControlConfig; contextPolicy: AgentDefaultContextPolicy; 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)[]; agentContract?: AgentContract; } 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 finalizeParallelWorktreeHandoff(input: { worktreeSetup: WorktreeSetup; artifactsDir: string; runId: string; cwd: string; tasks: TaskParam[]; results: SingleResult[]; }): { suffix: string; reference?: NonNullable } { const diffsDir = path.join(input.artifactsDir, "worktree-diffs", input.runId); const diffs = diffWorktrees(input.worktreeSetup, input.tasks.map((task) => task.agent), diffsDir); const cleanup = cleanupWorktrees(input.worktreeSetup); const diffSummary = formatWorktreeDiffSummary(diffs); try { const reference = writeParallelHandoffGroup({ manifestPath: parallelHandoffPath(input.artifactsDir, input.runId), runId: input.runId, mode: "parallel", source: "foreground", cwd: input.cwd, stepIndex: 0, flatStartIndex: 0, setup: input.worktreeSetup, diffs, cleanup, results: input.results.map((result) => ({ agent: result.agent, status: resolveSubagentResultStatus({ exitCode: result.exitCode, interrupted: result.interrupted, detached: result.detached, state: result.stopped ? "stopped" : undefined, }), summary: resultSummaryForIntercom(result), ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}), ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}), ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}), ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}), })), }); return { suffix: [diffSummary, formatParallelHandoffReference(reference)].filter(Boolean).join("\n\n"), reference, }; } catch (error) { return { suffix: [diffSummary, formatParallelHandoffError(error)].filter(Boolean).join("\n\n") }; } } 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. for (let i = 0; i < input.tasks.length; i++) { input.sessionFileForTask(input.tasks[i]!.agent, i, input.modelOverrides[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 agentConfig = input.agents.find((agent) => agent.name === task.agent); const taskText = injectSingleOutputInstruction( `${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`, outputPath, agentConfig, ); const interruptController = new AbortController(); if (input.foregroundControl) { beginForegroundChild(input.foregroundControl, { index, agent: task.agent, description: input.taskDescriptions[index], interrupt: () => { if (interruptController.signal.aborted) return false; interruptController.abort(); return true; }, }); } const structuredRuntime = task.outputSchema ? createStructuredOutputRuntime(task.outputSchema, path.join(input.artifactsDir, "structured-output", input.runId)) : undefined; return runSync(input.ctx.cwd, input.agents, task.agent, taskText, { parentSessionId: input.ctx.sessionManager.getSessionId() ?? undefined, context: input.contextPolicy.contextForAgent(task.agent), 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, input.modelOverrides[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], waitToolEnabled: input.waitToolEnabled, capabilityCeiling: input.capabilityCeiling, controlConfig: input.controlConfig, onControlEvent: input.onControlEvent, onDetachedExit: (result) => updateRememberedForegroundChild(input.state, { runId: input.runId, mode: "parallel", cwd: taskCwd, sessionId: input.parentSessionId, index, result, events: input.intercomEvents }), intercomSessionName: input.childIntercomTarget?.(task.agent, index), orchestratorIntercomTarget: input.orchestratorIntercomTarget, nestedRoute: input.foregroundControl?.nestedRoute, modelOverride: input.modelOverrides[index], thinkingOverride: input.thinkingOverrideForTask(task.agent, index, input.modelOverrides[index]), availableModels: input.availableModels, preferredModelProvider: input.parentModel?.provider, modelScope: input.modelScope, skills: effectiveSkills === false ? [] : effectiveSkills, structuredOutput: structuredRuntime, agentContract: task.agentContract ?? input.agentContract, 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) { updateForegroundChild(input.foregroundControl, index, stepProgress[0]); } 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) finishForegroundChild(input.foregroundControl, index); }); }, 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 parentModel = data.parentModel; const currentProvider = parentModel?.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 && task.output !== true ? { output: 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 !== undefined ? { model: task.model } : {}), })); const modelOverrides: (string | undefined)[] = tasks.map((_, i) => resolveEffectiveSubagentModel(behaviorOverrides[i]?.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, { scope: data.modelScope }), ); 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 !== undefined) { modelOverrides[i] = resolveEffectiveSubagentModel(override.model, agentConfigs[i]?.model, parentModel, availableModels, currentProvider, { scope: data.modelScope }); 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: parentModel?.provider, currentModel: parentModel, modelScope: data.modelScope, interactive: ctx.hasUI, }; 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, ...(behaviorOverrides[i]?.model !== undefined ? { model: behaviorOverrides[i]!.model } : {}), ...(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.outputSchema !== undefined ? { outputSchema: t.outputSchema } : {}), ...(t.acceptance !== undefined ? { acceptance: t.acceptance } : {}), ...(t.agentContract !== undefined ? { agentContract: t.agentContract } : {}), }; }); return executeAsyncChain(id, { chain: [{ parallel: parallelTasks, concurrency: parallelConcurrency, worktree: params.worktree }], resultMode: "parallel", goal: taskTexts[0] ?? "", 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, modelOverrides[index])), thinkingOverridesByFlatIndex: tasks.map((task, index) => thinkingOverrideForTask(task.agent, index, modelOverrides[index])), contextForAgent: contextPolicy.contextForAgent, maxSubagentDepth: currentMaxSubagentDepth, waitToolEnabled: deps.waitToolEnabled, worktreeSetupHook: deps.config.worktreeSetupHook, worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs, worktreeBaseDir: deps.config.worktreeBaseDir, controlConfig, agentContract: params.agentContract, 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 behaviors = tasks.map((task, index) => { let behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agentConfigs[index]!, behaviorOverrides[index]!), taskTexts[index]); if (behaviorOverrides[index]?.output === undefined && typeof behavior.output === "string" && !path.isAbsolute(behavior.output)) { behavior = { ...behavior, output: path.join("parallel-0", `${index}-${task.agent}`, behavior.output) }; } return behavior; }); 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; let worktreeFinalized = false; 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); const taskDescriptions = taskTexts.map((taskText) => taskText.trim()); 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, taskDescriptions, agents, ctx, state: deps.state, intercomEvents: deps.pi.events, parentSessionId: data.parentSessionId, capabilityCeiling: data.capabilityCeiling, signal, runId, sessionDirForIndex, sessionFileForIndex, sessionFileForTask, thinkingOverrideForTask, shareEnabled, artifactConfig, artifactsDir, outputBaseDir, maxOutput: params.maxOutput, paramsCwd: effectiveCwd, progressDir: parallelProgressDir, availableModels, modelScope: data.modelScope, parentModel, modelOverrides, behaviors, firstProgressIndex: parallelProgressPrecreated ? -1 : firstProgressIndex, controlConfig, contextPolicy, 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, waitToolEnabled: deps.waitToolEnabled, liveResults, liveProgress, onUpdate, worktreeSetup, timeoutMs: data.timeoutMs, deadlineAt, turnBudget: data.turnBudget, toolBudgets, agentContract: params.agentContract, }); 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); } let handoff: ReturnType | undefined; if (worktreeSetup) { worktreeFinalized = true; handoff = finalizeParallelWorktreeHandoff({ worktreeSetup, artifactsDir, runId, cwd: effectiveCwd, tasks, results }); } const interrupted = results.find((result) => result.interrupted); const details = compactForegroundDetails({ mode: "parallel", runId, timeoutMs: data.timeoutMs, results, progress: params.includeProgress ? allProgress : undefined, artifacts: allArtifactPaths.length ? { dir: artifactsDir, files: allArtifactPaths } : undefined, totalChildUsage: sumResultsUsage(results), totalCost: sumResultsCost(results), ...(handoff?.reference ? { parallelHandoff: handoff.reference } : {}), }); rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, sessionId: data.parentSessionId, 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, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.` }], 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 = handoff?.suffix ?? ""; 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 && !worktreeFinalized) 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), data.contextPolicy.contextSummary); const parentModel = data.parentModel; const currentProvider = parentModel?.provider; const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo); let task = params.task ?? ""; let modelOverride: string | undefined = resolveEffectiveSubagentModel( params.model as string | undefined, agentConfig.model, parentModel, availableModels, currentProvider, { scope: data.modelScope }, ); 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 !== undefined) modelOverride = resolveEffectiveSubagentModel(override.model, agentConfig.model, parentModel, availableModels, currentProvider, { scope: data.modelScope }); 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: parentModel?.provider, currentModel: parentModel, modelScope: data.modelScope, interactive: ctx.hasUI, }; return executeAsyncSingle(id, { agent: params.agent!, task: shouldForkAgent(contextPolicy, params.agent!) ? wrapForkTask(task) : task, goal: task, agentConfig, ctx: asyncCtx, availableModels, cwd: effectiveCwd, maxOutput: params.maxOutput, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, shareEnabled, sessionRoot, sessionFile: sessionFileForTask(params.agent!, 0, modelOverride), context: contextPolicy.contextForAgent(params.agent!), skills: skillOverride === false ? [] : skillOverride, output: effectiveOutput, outputMode: effectiveOutputMode, outputBaseDir: resolveSingleRunOutputBaseDir(deps, artifactsDir, id), modelOverride, thinkingOverride: thinkingOverrideForTask(params.agent!, 0, modelOverride), maxSubagentDepth, waitToolEnabled: deps.waitToolEnabled, 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, agentContract: params.agentContract, structuredOutputSchema: params.outputSchema, acceptance: params.acceptance, timeoutMs: data.timeoutMs, turnBudget: data.turnBudget, toolBudget: effectiveToolBudget.toolBudget, allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.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: [] } }; } const structuredRuntime = params.outputSchema ? createStructuredOutputRuntime(params.outputSchema, artifactConfig.enabled ? path.join(artifactsDir, "structured-output", runId) : undefined) : undefined; task = injectSingleOutputInstruction(task, outputPath, agentConfig); let effectiveSkills: string[] | undefined; if (skillOverride === false) { effectiveSkills = []; } else { effectiveSkills = skillOverride; } const interruptController = new AbortController(); const foregroundControl = deps.state.foregroundControls.get(runId); if (foregroundControl) { beginForegroundChild(foregroundControl, { index: 0, agent: params.agent!, description: foregroundControl.description, interrupt: () => { if (interruptController.signal.aborted) return false; interruptController.abort(); return true; }, }); } const forwardSingleUpdate = onUpdate ? (update: AgentToolResult
) => { if (foregroundControl) updateForegroundChild(foregroundControl, 0, update.details?.progress?.[0]); onUpdate(update); } : undefined; const deadlineAt = data.deadlineAt ?? (data.timeoutMs !== undefined ? Date.now() + data.timeoutMs : undefined); let r: Awaited>; try { r = await runSync(ctx.cwd, agents, params.agent!, task, { parentSessionId: ctx.sessionManager.getSessionId() ?? undefined, context: data.contextPolicy.contextForAgent(params.agent!), 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, modelOverride), share: shareEnabled, artifactsDir: artifactConfig.enabled ? artifactsDir : undefined, artifactConfig, maxOutput: params.maxOutput, outputPath, outputMode: effectiveOutputMode, maxSubagentDepth, waitToolEnabled: deps.waitToolEnabled, 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, modelOverride), availableModels, preferredModelProvider: currentProvider, modelScope: data.modelScope, skills: effectiveSkills, structuredOutput: structuredRuntime, agentContract: params.agentContract, acceptance: params.acceptance, acceptanceContext: { mode: "single" }, onDetachedExit: (result) => updateRememberedForegroundChild(deps.state, { runId, mode: "single", cwd: effectiveCwd, sessionId: data.parentSessionId, index: 0, result, events: deps.pi.events }), timeoutMs: data.timeoutMs, deadlineAt, turnBudget: data.turnBudget, enforceHardTurnLimit: params.enforceHardTurnLimit, toolBudget: effectiveToolBudget.toolBudget, capabilityCeiling: data.capabilityCeiling, allowZeroToolBudget: data.allowZeroToolBudget && effectiveToolBudget.toolBudget === data.toolBudget, }); } finally { if (!artifactConfig.enabled) cleanupStructuredOutputRuntime(structuredRuntime); } if (foregroundControl) finishForegroundChild(foregroundControl, 0); 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, timeoutMs: data.timeoutMs, 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, sessionId: data.parentSessionId, 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, then wait with subagent_wait({ id: "${runId}" }). Use subagent({ action: "status", id: "${runId}" }) to recover the result; do not resume or launch a replacement while it remains detached.` }], 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>; /** * Correlated extension-to-extension delegation owns its request IDs and * cancellation controllers, so independent requests may execute concurrently. * The ordinary model-facing tool keeps the one-foreground-call-per-turn guard. */ executeDelegated: ( id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult
) => void) | undefined, ctx: ExtensionContext, ) => Promise>; } { const delegatedThinkingOverrides = new WeakMap(); const delegatedZeroToolBudgets = new WeakSet(); const execute = async ( _id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult
) => void) | undefined, ctx: ExtensionContext, ): Promise> => { const delegatedThinkingOverride = delegatedThinkingOverrides.get(params); const allowZeroToolBudget = delegatedZeroToolBudgets.has(params); 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; let requestParentModel: ParentModel | undefined; try { requestParentModel = rememberParentModel(deps.state, resolveCurrentSessionId(ctx.sessionManager), ctx.model); } catch (error) { if (action?.toLowerCase() !== "doctor") throw error; requestParentModel = normalizeParentModel(ctx.model); } if (action) { if ((WATCHDOG_TOOL_ACTIONS as readonly string[]).includes(action)) { 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 handleWatchdogToolAction(action, paramsWithResolvedCwd, ctx, deps.watchdog); } if (action === "grant-spawn-budget") { if (deps.allowMutatingManagementActions === false || !ctx.hasUI) { return { content: [{ type: "text", text: "Action 'grant-spawn-budget' is available only from the root interactive parent session." }], isError: true, details: { mode: "management", results: [] }, }; } deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager); if (!deps.state.currentSessionId) { return { content: [{ type: "text", text: "Action 'grant-spawn-budget' requires an active parent session id." }], isError: true, details: { mode: "management", results: [] }, }; } if (hasActiveSubagentChildren(deps.state)) { const spawnBudget = getSpawnBudgetSnapshot(deps.state, deps.config, deps.state.currentSessionId); return { content: [{ type: "text", text: "Spawn budget grants are rejected while current-session children are queued or running. Wait for them to settle, then retry the explicit grant." }], isError: true, details: { mode: "management", results: [], spawnBudget }, }; } const sessionId = deps.state.currentSessionId; const additional = paramsWithResolvedCwd.additional ?? Number.NaN; const preview = preflightSpawnBudgetGrant(deps.state, deps.config, sessionId, additional); if (preview.error) { return { content: [{ type: "text", text: preview.error }], isError: true, details: { mode: "management", results: [], spawnBudget: preview.snapshot }, }; } const confirmed = await ctx.ui.confirm( "Grant subagent spawn budget?", `Add ${additional} launches to this logical session?\n\n${formatSpawnBudget(preview.snapshot)}\n\nUsage is not reset. Compaction keeps the same budget; a new parent session starts a fresh one.`, ); if (!confirmed) { return { content: [{ type: "text", text: "Spawn budget grant canceled; no capacity was added." }], details: { mode: "management", results: [], spawnBudget: preview.snapshot }, }; } const currentBudget = getSpawnBudgetSnapshot(deps.state, deps.config, deps.state.currentSessionId); if ( resolveCurrentSessionId(ctx.sessionManager) !== sessionId || hasActiveSubagentChildren(deps.state) || currentBudget.used !== preview.snapshot.used || currentBudget.granted !== preview.snapshot.granted ) { return { content: [{ type: "text", text: "Spawn budget grant was not applied because the session, budget, or active-child state changed while confirmation was open." }], isError: true, details: { mode: "management", results: [], spawnBudget: currentBudget }, }; } const granted = grantSpawnBudget(deps.state, deps.config, sessionId, additional); return { content: [{ type: "text", text: granted.error ?? `Spawn budget grant applied: +${additional}. ${formatSpawnBudget(granted.snapshot)}` }], ...(granted.error ? { isError: true } : {}), details: { mode: "management", results: [], spawnBudget: granted.snapshot }, }; } 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); } const spawnBudget = getSpawnBudgetSnapshot(deps.state, deps.config, currentSessionId); 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: [], spawnBudget }, }; } if (action === "status") { deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager); const withBudget = (result: AgentToolResult
) => withSpawnBudgetStatus( result, deps.state, deps.config, deps.state.currentSessionId, ); const targetRunId = paramsWithResolvedCwd.id ?? paramsWithResolvedCwd.runId; const nestedScope = nestedResolutionScopeForExecutor(deps); const sessionRoots = trustedSessionRootsForStatus(ctx, deps); if (paramsWithResolvedCwd.view === "fleet") { return withBudget(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 withBudget({ 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 withBudget(foregroundStatusResult(foreground)); } } } catch (error) { const message = error instanceof Error ? error.message : String(error); return withBudget({ content: [{ type: "text", text: message }], isError: true, details: { mode: "management", results: [] } }); } } else { const foreground = getForegroundControl(deps.state, undefined); if (foreground && paramsWithResolvedCwd.view !== "transcript") return withBudget(foregroundStatusResult(foreground)); if (foreground && paramsWithResolvedCwd.view === "transcript") { return withBudget({ 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 withBudget(inspectSubagentStatus(paramsWithResolvedCwd, { state: deps.state, nested: nestedScope, sessionRoots })); } if (action === "resume") { return resumeAsyncRun({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }); } if (action === "steer") { deps.state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager); 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, signal, ...(paramsWithResolvedCwd.steeringRecovery === false ? {} : { recover: ({ absoluteDeadlineAt, ...limits }) => resumeAsyncRun({ params: { ...limits, action: "resume", id: runId, message }, requestCwd, ctx, deps, parentModel: requestParentModel, absoluteDeadlineAt }), } ), }); } 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, signal }); 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, signal, ...(paramsWithResolvedCwd.steeringRecovery === false ? {} : { recover: ({ absoluteDeadlineAt, ...limits }) => resumeAsyncRun({ params: { ...limits, action: "resume", id: resolved!.id, message }, requestCwd, ctx, deps, parentModel: requestParentModel, absoluteDeadlineAt, }), } ), }); } if (action === "append-step") { return appendStepToAsyncChain({ params: paramsWithResolvedCwd, requestCwd, ctx, deps, parentModel: requestParentModel }); } 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 === "stop") { const targetRunId = paramsWithResolvedCwd.runId ?? paramsWithResolvedCwd.id; let resolved: ResolvedSubagentRunId | undefined; if (paramsWithResolvedCwd.dir) { try { const location = resolveAsyncRunLocation(paramsWithResolvedCwd, ASYNC_DIR, RESULTS_DIR); return stopAsyncRun(deps.state, location.resolvedId ?? targetRunId ?? path.basename(location.asyncDir ?? paramsWithResolvedCwd.dir), 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='stop' requires id or dir." }], isError: true, details: { mode: "management", results: [] } }; 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 { content: [{ type: "text", text: "action='stop' supports current-session top-level async runs only." }], isError: true, details: { mode: "management", results: [] } }; if (resolved?.kind === "foreground") return { content: [{ type: "text", text: "action='stop' supports async runs only. Use action='interrupt' for foreground runs." }], isError: true, details: { mode: "management", results: [] } }; const stopResult = stopAsyncRun( deps.state, resolved?.kind === "async" ? resolved.id : targetRunId, deps.kill, resolved?.kind === "async" ? resolved.location : undefined, ); if (stopResult) return stopResult; return { content: [{ type: "text", text: "No stoppable async run found in this session." }], isError: true, details: { mode: "management", results: [] }, }; } 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 runToolBudget = resolveToolBudget( effectiveParams.toolBudget, "toolBudget", allowZeroToolBudget ? { minimumHard: 0 } : undefined, ); 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; const discovered = deps.discoverAgents(effectiveCwd, scope); const discoveredAgents = discovered.agents; const modelScope = discovered.modelScope; effectiveParams = applySingleAgentLaunchDefaults(effectiveParams, discoveredAgents); const turnBudget = resolveTurnBudgetConfig(effectiveParams.turnBudget ?? deps.config.turnBudget); if (turnBudget.error) return buildRequestedModeError(effectiveParams, turnBudget.error); 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 ?? (contextPolicy.usesFork ? "fork" : undefined), 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; const foregroundMode: "single" | "parallel" | "chain" = hasChain ? "chain" : hasTasks ? "parallel" : "single"; const requestedSpawns = countRequestedSubagentSpawns(effectiveParams, deps.config); const spawnPreflight = preflightSpawnBudget( deps.state, deps.config, deps.state.currentSessionId, requestedSpawns, ); if (spawnPreflight.error) return spawnBudgetErrorResult(spawnPreflight.error, foregroundMode); let forkSessionFileForIndex: (idx?: number) => string | undefined = () => undefined; let forkThinkingOverrideForIndex: (idx?: number) => AgentConfig["thinking"] | undefined = () => undefined; let prepareForkThinking = (_agentName: string, _index: number, _modelOverride?: string): void => {}; const forkThinkingRequirements = new Map(); const forkThinkingDowngrades = new Map(); try { const forkAvailableModels = contextPolicy.usesFork ? ctx.modelRegistry.getAvailable().map(toModelInfo) : []; const parentModel = requestParentModel; prepareForkThinking = (agentName, index, modelOverride) => { const agentConfig = agents.find((agent) => agent.name === agentName); const primaryModel = resolveEffectiveSubagentModel( modelOverride, agentConfig?.model, parentModel, forkAvailableModels, parentModel?.provider, ); const candidates = buildModelCandidates( primaryModel, agentConfig?.fallbackModels, forkAvailableModels, parentModel?.provider, ); forkThinkingRequirements.set( index, candidates.length === 0 || candidates.some((candidate) => forkedChildRequiresThinkingOff(candidate, forkAvailableModels, parentModel?.provider)), ); }; const forkContextResolver = createForkContextResolver(ctx.sessionManager, contextPolicy.usesFork ? "fork" : undefined, { forceThinkingOffForIndex: (index) => forkThinkingRequirements.get(index) ?? true, }); forkSessionFileForIndex = forkContextResolver.sessionFileForIndex; forkThinkingOverrideForIndex = forkContextResolver.thinkingOverrideForIndex; } catch (error) { return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary); } const requestedAsync = effectiveParams.async ?? deps.asyncByDefault; const backgroundRequestedWhileClarifying = (hasChain || hasTasks) && requestedAsync && effectiveParams.clarify === true; const effectiveAsync = requestedAsync && effectiveParams.clarify !== true; const foregroundTimeout = resolveForegroundTimeout( effectiveParams, effectiveAsync ? undefined : DEFAULT_FOREGROUND_TIMEOUT_MS, ); if (foregroundTimeout.error) return buildRequestedModeError(effectiveParams, foregroundTimeout.error); const controlConfig = resolveControlConfig(deps.config.control, effectiveParams.control); const artifactConfig: ArtifactConfig = { ...DEFAULT_ARTIFACT_CONFIG, enabled: effectiveParams.artifacts !== false, dir: deps.config.artifactDir ?? DEFAULT_ARTIFACT_CONFIG.dir, }; const artifactsDir = getArtifactsDir(parentSessionFile, effectiveCwd, artifactConfig.dir); 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}`), contextPolicy.contextSummary, ); } const sessionDirForIndex = (idx?: number) => path.join(sessionRoot, `run-${idx ?? 0}`); const forkSessionFileForTask: ForkSessionFileForTask = (agentName, idx = 0, modelOverride) => { if (!shouldForkAgent(contextPolicy, agentName)) return undefined; prepareForkThinking(agentName, idx, modelOverride); return forkSessionFileForIndex(idx); }; const forkThinkingOverrideForTask: ForkThinkingOverrideForTask = (agentName, idx = 0, modelOverride) => { if (!shouldForkAgent(contextPolicy, agentName)) return delegatedThinkingOverride; prepareForkThinking(agentName, idx, modelOverride); const override = forkThinkingOverrideForIndex(idx); if (override === "off") forkThinkingDowngrades.set(idx, agentName); return override ?? delegatedThinkingOverride; }; const childSessionFileForTask: ForkSessionFileForTask = (agentName, idx, modelOverride) => forkSessionFileForTask(agentName, idx, modelOverride) ?? path.join(sessionDirForIndex(idx), "session.jsonl"); const childSessionFileForIndex = (idx?: number) => path.join(sessionDirForIndex(idx), "session.jsonl"); try { if (!(effectiveParams.clarify === true && ctx.hasUI)) { preflightForkSessionsForStaticTasks(effectiveParams, contextPolicy, forkSessionFileForTask, deps.config.chain?.dynamicFanout?.maxItems); } } catch (error) { return toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary); } const chainBindingsError = validateExecutionChainBindings(effectiveParams, deps.config.chain?.dynamicFanout?.maxItems); if (chainBindingsError) return withResolvedContext(chainBindingsError, contextPolicy.contextSummary); const onUpdateWithContext = onUpdate ? (r: AgentToolResult
) => onUpdate(withResolvedContext({ ...r, details: { ...r.details, runId }, }, contextPolicy.contextSummary)) : undefined; const reservation = reserveSpawnBudget( deps.state, deps.config, deps.state.currentSessionId, requestedSpawns, ); if (reservation.error) return spawnBudgetErrorResult(reservation.error, foregroundMode); 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, allowZeroToolBudget, configToolBudget: configToolBudget.toolBudget, contextPolicy, modelScope, parentModel: requestParentModel, parentSessionId: deps.state.currentSessionId, capabilityCeiling: resolveCurrentSubagentCapabilityCeiling(deps.state.currentSessionId ?? undefined), }; const foregroundDescription = effectiveParams.task?.trim() || effectiveParams.tasks?.[0]?.task?.trim() || (effectiveParams.chain ? firstRawChainTask(effectiveParams.chain)?.trim() : undefined); const foregroundControl: ForegroundRunControl | undefined = effectiveAsync ? undefined : { runId, mode: foregroundMode, startedAt: Date.now(), updatedAt: Date.now(), cwd: effectiveCwd, currentAgent: undefined, currentIndex: undefined, description: foregroundDescription, currentActivityState: undefined, activeChildren: new Map(), 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" : details?.results.some((child) => child.interrupted || child.detached) ? "paused" : result?.isError || details?.results.some((child) => child.exitCode !== 0) ? "failed" : "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.PI_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 || child.detached ? "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 withResolvedContext(withForkThinkingNotes(asyncResult, forkThinkingDowngrades), contextPolicy.contextSummary); if (foregroundControl) { writeNestedForegroundEvent("subagent.nested.started"); nestedForegroundStarted = true; } if (hasChain && effectiveParams.chain) { const result = await runChainPath(execData, deps); writeNestedForegroundEvent("subagent.nested.completed", result); return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary); } if (hasTasks && effectiveParams.tasks) { const result = await runParallelPath(execData, deps); writeNestedForegroundEvent("subagent.nested.completed", result); return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary); } if (hasSingle) { const result = await runSinglePath(execData, deps); writeNestedForegroundEvent("subagent.nested.completed", result); return withResolvedContext(withForkThinkingNotes(result, forkThinkingDowngrades), contextPolicy.contextSummary); } } catch (error) { const errorResult = withForkThinkingNotes(toExecutionErrorResult(effectiveParams, error, contextPolicy.contextSummary), forkThinkingDowngrades); 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 withResolvedContext({ content: [{ type: "text", text: "Invalid params" }], isError: true, details: { mode: "single" as const, results: [] }, }, contextPolicy.contextSummary); }; 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); const { depth } = checkSubagentDepth(deps.config.maxSubagentDepth); const dispatchParams = applyForceTopLevelAsyncOverride(requestParams, depth, deps.config.forceTopLevelAsync === true); const runsForeground = dispatchParams.clarify === true || (dispatchParams.async ?? deps.asyncByDefault) !== true; if (!runsForeground) 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; } }; const executeDelegated = async ( id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((r: AgentToolResult
) => void) | undefined, ctx: ExtensionContext, ): Promise> => { const delegatedParams = { ...params }; const privateParams = delegatedParams as SubagentParamsLike & { delegatedThinkingOverride?: AgentConfig["thinking"]; delegatedAllowZeroToolBudget?: true; }; const thinkingOverride = privateParams.delegatedThinkingOverride; const allowZeroToolBudget = privateParams.delegatedAllowZeroToolBudget === true; delete privateParams.delegatedThinkingOverride; delete privateParams.delegatedAllowZeroToolBudget; if (thinkingOverride !== undefined) delegatedThinkingOverrides.set(delegatedParams, thinkingOverride); if (allowZeroToolBudget) delegatedZeroToolBudgets.add(delegatedParams); return execute(id, delegatedParams, signal, onUpdate, ctx); }; return { execute: executeWithSingleDispatchGuard, executeDelegated }; }