export interface RunnerSubagentStep { agent: string; task: string; importAsyncRoot?: { runId: string; asyncDir: string; resultPath: string; index: number; }; phase?: string; label?: string; outputName?: string; structured?: boolean; cwd?: string; model?: string; thinking?: string; modelCandidates?: string[]; tools?: string[]; extensions?: string[]; subagentOnlyExtensions?: string[]; mcpDirectTools?: string[]; completionGuard?: boolean; systemPrompt?: string | null; systemPromptMode?: "append" | "replace"; inheritProjectContext: boolean; inheritSkills: boolean; skills?: string[]; outputPath?: string; outputMode?: "inline" | "file-only"; sessionFile?: string; maxSubagentDepth?: number; structuredOutput?: { schema: import("../../shared/types.ts").JsonSchemaObject; schemaPath: string; outputPath: string; }; structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject; effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig; } export interface ParallelStepGroup { parallel: RunnerSubagentStep[]; concurrency?: number; failFast?: boolean; worktree?: boolean; } export interface DynamicRunnerGroup { expand: import("../../shared/settings.ts").DynamicExpandSpec; parallel: RunnerSubagentStep; collect: import("../../shared/settings.ts").DynamicCollectSpec; concurrency?: number; failFast?: boolean; phase?: string; label?: string; effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig; } export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup; export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup { return "parallel" in step && Array.isArray(step.parallel); } export function isDynamicRunnerGroup(step: RunnerStep): step is DynamicRunnerGroup { return "expand" in step && "collect" in step && "parallel" in step && !Array.isArray((step as { parallel?: unknown }).parallel); } export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] { const flat: RunnerSubagentStep[] = []; for (const step of steps) { if (isParallelGroup(step)) { for (const task of step.parallel) flat.push(task); } else if (isDynamicRunnerGroup(step)) { continue; } else { flat.push(step); } } return flat; } export async function mapConcurrent( items: T[], limit: number, fn: (item: T, i: number) => Promise, ): Promise { const safeLimit = Math.max(1, Math.floor(limit) || 1); const results: R[] = new Array(items.length); let next = 0; async function worker(_workerIndex: number): Promise { while (next < items.length) { const i = next++; results[i] = await fn(items[i], i); } } await Promise.all( Array.from({ length: Math.min(safeLimit, items.length) }, (_, wi) => worker(wi)), ); return results; } export interface ParallelTaskResult { agent: string; taskIndex?: number; output: string; exitCode: number | null; error?: string; model?: string; attemptedModels?: string[]; outputTargetPath?: string; outputTargetExists?: boolean; } export function aggregateParallelOutputs( results: ParallelTaskResult[], headerFormat: (index: number, agent: string) => string = (i, agent) => `=== Parallel Task ${i + 1} (${agent}) ===`, ): string { return results .map((r, i) => { const header = headerFormat(r.taskIndex ?? i, r.agent); const hasOutput = Boolean(r.output?.trim()); const status = r.exitCode === -1 ? "SKIPPED" : r.exitCode !== 0 && r.exitCode !== null ? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}` : r.error ? `WARNING: ${r.error}` : !hasOutput && r.outputTargetPath && r.outputTargetExists === false ? `EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})` : !hasOutput && !r.outputTargetPath ? "EMPTY OUTPUT (no textual response returned)" : ""; const body = status ? (hasOutput ? `${status}\n${r.output}` : status) : r.output; return `${header}\n${body}`; }) .join("\n\n"); } export const MAX_PARALLEL_CONCURRENCY = 4;