/** * Pipeline controller for swarm execution. * * Orchestrates execution waves within each iteration: * - Agents in the same wave execute in parallel * - Waves execute sequentially (wave N+1 starts after wave N completes) * - For pipeline mode, iterations repeat the full DAG execution */ import type { AgentSource, AuthStorage, ModelRegistry, Settings, SingleResult } from "@oh-my-pi/pi-coding-agent"; import { executeSwarmAgent } from "./executor"; import type { SwarmDefinition } from "./schema"; import type { StateTracker } from "./state"; // ============================================================================ // Types // ============================================================================ export interface PipelineOptions { workspace: string; signal?: AbortSignal; onProgress?: (state: PipelineProgress) => void; authStorage?: AuthStorage; modelRegistry?: ModelRegistry; settings?: Settings; } export interface PipelineProgress { iteration: number; targetCount: number; currentWave: number; totalWaves: number; agents: Record; } export interface PipelineResult { status: "completed" | "failed" | "aborted"; iterations: number; agentResults: Map; errors: string[]; } // ============================================================================ // Controller // ============================================================================ export class PipelineController { #def: SwarmDefinition; #waves: string[][]; #stateTracker: StateTracker; constructor(def: SwarmDefinition, waves: string[][], stateTracker: StateTracker) { this.#def = def; this.#waves = waves; this.#stateTracker = stateTracker; } async run(options: PipelineOptions): Promise { const { workspace, signal, onProgress, authStorage, modelRegistry, settings } = options; const allResults = new Map(); const errors: string[] = []; for (const name of this.#def.agents.keys()) { allResults.set(name, []); } const targetCount = this.#def.targetCount; await this.#stateTracker.appendOrchestratorLog( `Pipeline '${this.#def.name}' starting: mode=${this.#def.mode} iterations=${targetCount} waves=${this.#waves.length} agents=${this.#def.agents.size}`, ); try { for (let iteration = 0; iteration < targetCount; iteration++) { if (signal?.aborted) { await this.#stateTracker.updatePipeline({ status: "aborted" }); return { status: "aborted", iterations: iteration, agentResults: allResults, errors }; } await this.#stateTracker.updatePipeline({ iteration }); await this.#stateTracker.appendOrchestratorLog(`--- Iteration ${iteration + 1}/${targetCount} ---`); const emitProgress = (currentWave: number) => { onProgress?.({ iteration, targetCount, currentWave, totalWaves: this.#waves.length, agents: this.#buildProgressSnapshot(), }); }; const iterationResults = await this.#runIteration(iteration, { workspace, signal, emitProgress, authStorage, modelRegistry, settings, }); for (const [agentName, result] of iterationResults) { allResults.get(agentName)!.push(result); if (result.exitCode !== 0) { errors.push( `${agentName} (iteration ${iteration + 1}): ${result.error || `exit code ${result.exitCode}`}`, ); } } } const status = errors.length > 0 ? ("failed" as const) : ("completed" as const); await this.#stateTracker.updatePipeline({ status, completedAt: Date.now() }); await this.#stateTracker.appendOrchestratorLog(`Pipeline ${status} (${errors.length} errors)`); return { status, iterations: targetCount, agentResults: allResults, errors }; } catch (err) { const error = err instanceof Error ? err.message : String(err); await this.#stateTracker.updatePipeline({ status: "failed", completedAt: Date.now() }); await this.#stateTracker.appendOrchestratorLog(`Pipeline fatal error: ${error}`); errors.push(error); return { status: "failed", iterations: 0, agentResults: allResults, errors }; } } async #runIteration( iteration: number, options: { workspace: string; signal?: AbortSignal; emitProgress: (currentWave: number) => void; authStorage?: AuthStorage; modelRegistry?: ModelRegistry; settings?: Settings; }, ): Promise> { const results = new Map(); let agentIndex = 0; for (let waveIdx = 0; waveIdx < this.#waves.length; waveIdx++) { const wave = this.#waves[waveIdx]; if (options.signal?.aborted) break; await this.#stateTracker.appendOrchestratorLog( `Wave ${waveIdx + 1}/${this.#waves.length}: [${wave.join(", ")}]`, ); // Mark agents in this wave as waiting for (const agentName of wave) { await this.#stateTracker.updateAgent(agentName, { status: "waiting", iteration, wave: waveIdx, }); } options.emitProgress(waveIdx); // Execute all agents in wave in parallel, catching per-agent errors const waveResults = await Promise.all( wave.map(async agentName => { const agent = this.#def.agents.get(agentName)!; const currentIndex = agentIndex++; try { const result = await executeSwarmAgent(agent, currentIndex, { workspace: options.workspace, swarmName: this.#def.name, iteration, modelOverride: agent.model ?? this.#def.model, signal: options.signal, onProgress: (_name, _progress) => { options.emitProgress(waveIdx); }, authStorage: options.authStorage, modelRegistry: options.modelRegistry, settings: options.settings, stateTracker: this.#stateTracker, }); return { agentName, result }; } catch (err) { const error = err instanceof Error ? err.message : String(err); const failResult: SingleResult = { index: currentIndex, id: `swarm-${this.#def.name}-${agentName}-${iteration}`, agent: agentName, agentSource: "project" as AgentSource, task: agent.task, exitCode: 1, output: "", stderr: error, truncated: false, durationMs: 0, tokens: 0, error, }; return { agentName, result: failResult }; } }), ); for (const { agentName, result } of waveResults) { results.set(agentName, result); } options.emitProgress(waveIdx); } return results; } #buildProgressSnapshot(): Record { const snapshot: Record = {}; for (const [name, agent] of Object.entries(this.#stateTracker.state.agents)) { snapshot[name] = { status: agent.status, iteration: agent.iteration }; } return snapshot; } }