UNPKG

@stackmemoryai/stackmemory

Version:

Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a

509 lines (508 loc) 16.3 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { v4 as uuidv4 } from "uuid"; import { logger } from "../../../core/monitoring/logger.js"; import { FrameManager } from "../../../core/context/index.js"; import { sessionManager } from "../../../core/session/index.js"; import { RalphStackMemoryBridge } from "../bridge/ralph-stackmemory-bridge.js"; class MultiLoopOrchestrator { frameManager; activeTasks = /* @__PURE__ */ new Map(); activeLoops = /* @__PURE__ */ new Map(); config; constructor(config) { this.config = { maxConcurrentLoops: 3, dependencyResolutionTimeout: 3e4, // 30 seconds enableAdaptivePlanning: true, sharedContextEnabled: true, fallbackStrategy: "sequential", ...config }; logger.info("Multi-loop orchestrator initialized", this.config); } async initialize() { try { await sessionManager.initialize(); const session = await sessionManager.getOrCreateSession({}); if (session.database) { this.frameManager = new FrameManager( session.database, session.projectId ); } logger.info("Orchestrator initialized successfully"); } catch (error) { logger.error("Failed to initialize orchestrator", error); throw error; } } /** * Break down complex task into manageable loops */ async orchestrateComplexTask(description, criteria, options) { logger.info("Orchestrating complex task", { task: description.substring(0, 100), criteriaCount: criteria.length, maxLoops: options?.maxLoops || this.config.maxConcurrentLoops }); const orchestrationId = uuidv4(); try { const breakdown = options?.customBreakdown || await this.analyzeAndBreakdownTask(description, criteria); const executionPlan = await this.createExecutionPlan(breakdown, options); const dependencyErrors = this.validateDependencies(executionPlan); if (dependencyErrors.length > 0) { throw new Error(`Dependency errors: ${dependencyErrors.join(", ")}`); } const orchestratedTask = { id: orchestrationId, description, breakdown, executionPlan, status: "planning", startTime: Date.now(), loops: /* @__PURE__ */ new Map(), sharedContext: {} }; this.activeTasks.set(orchestrationId, orchestratedTask); const result = await this.executeOrchestration(orchestratedTask); logger.info("Complex task orchestration completed", { orchestrationId, status: result.success ? "success" : "failure", loopsExecuted: result.completedLoops.length, duration: Date.now() - orchestratedTask.startTime }); return result; } catch (error) { logger.error("Orchestration failed", error); throw error; } finally { this.activeTasks.delete(orchestrationId); } } /** * Execute coordinated parallel loops */ async executeParallelLoops(tasks, coordination) { logger.info(`Executing ${tasks.length} parallel loops`); const execution = { id: uuidv4(), tasks, startTime: Date.now(), results: /* @__PURE__ */ new Map(), sharedState: coordination?.sharedState || {} }; const promises = tasks.map( (task) => this.executeParallelTask(task, execution) ); try { await Promise.allSettled(promises); execution.endTime = Date.now(); execution.status = Array.from(execution.results.values()).every( (r) => r.success ) ? "success" : "partial"; return execution; } catch (error) { logger.error("Parallel execution failed", error); execution.status = "failed"; execution.error = error.message; return execution; } } /** * Analyze and break down complex task */ async analyzeAndBreakdownTask(description, criteria) { const complexity = this.assessTaskComplexity(description); if (complexity.score < 5) { return [ { id: uuidv4(), title: description, description, criteria, priority: 1, estimatedIterations: 3, dependencies: [], type: "single" } ]; } const subtasks = []; if (this.needsSetup(description)) { subtasks.push({ id: uuidv4(), title: "Project Setup", description: "Set up project structure and dependencies", criteria: ["Project structure created", "Dependencies installed"], priority: 1, estimatedIterations: 2, dependencies: [], type: "setup" }); } const coreTask = this.extractCoreTask(description); if (coreTask) { subtasks.push({ id: uuidv4(), title: "Core Implementation", description: coreTask, criteria: criteria.filter( (c) => c.toLowerCase().includes("function") || c.toLowerCase().includes("implement") ), priority: 2, estimatedIterations: 5, dependencies: subtasks.length > 0 ? [subtasks[0].id] : [], type: "implementation" }); } if (this.needsTesting(criteria)) { subtasks.push({ id: uuidv4(), title: "Testing Implementation", description: "Create comprehensive tests", criteria: criteria.filter((c) => c.toLowerCase().includes("test")), priority: 3, estimatedIterations: 3, dependencies: subtasks.length > 0 ? [subtasks[subtasks.length - 1].id] : [], type: "testing" }); } if (this.needsDocumentation(criteria)) { subtasks.push({ id: uuidv4(), title: "Documentation", description: "Create documentation and examples", criteria: criteria.filter((c) => c.toLowerCase().includes("doc")), priority: 4, estimatedIterations: 2, dependencies: [], type: "documentation" }); } return subtasks.length > 0 ? subtasks : [ { id: uuidv4(), title: description, description, criteria, priority: 1, estimatedIterations: Math.min(8, Math.max(3, complexity.score)), dependencies: [], type: "single" } ]; } /** * Create execution plan from breakdown */ async createExecutionPlan(breakdown, options) { const plan = { phases: [], totalEstimatedTime: 0, parallelizable: !options?.forceSequential && breakdown.length > 1 }; if (options?.forceSequential || !this.canExecuteInParallel(breakdown)) { plan.phases = breakdown.map((task, index) => ({ id: `phase-${index + 1}`, tasks: [task], dependencies: index > 0 ? [`phase-${index}`] : [], parallelExecution: false })); } else { const phases = this.groupTasksByDependencies(breakdown); plan.phases = phases; } plan.totalEstimatedTime = plan.phases.reduce( (sum, phase) => sum + Math.max(...phase.tasks.map((t) => t.estimatedIterations)) * 3e4, // 30s per iteration 0 ); return plan; } /** * Execute the orchestration plan */ async executeOrchestration(task) { const result = { orchestrationId: task.id, success: false, completedLoops: [], failedLoops: [], totalDuration: 0, insights: [] }; try { task.status = "executing"; for (const phase of task.executionPlan.phases) { logger.info( `Executing phase ${phase.id} with ${phase.tasks.length} tasks` ); if (phase.parallelExecution && phase.tasks.length > 1) { const parallelResult = await this.executeParallelLoops(phase.tasks); for (const [_taskId, taskResult] of parallelResult.results) { if (taskResult.success) { result.completedLoops.push(taskResult.loopId); } else { result.failedLoops.push({ loopId: taskResult.loopId, error: taskResult.error || "Unknown error" }); } } } else { for (const phaseTask of phase.tasks) { const loopResult = await this.executeTaskLoop(phaseTask, task); if (loopResult.success) { result.completedLoops.push(loopResult.loopId); if (this.config.sharedContextEnabled) { await this.updateSharedContext(task, loopResult); } } else { result.failedLoops.push({ loopId: loopResult.loopId, error: loopResult.error || "Unknown error" }); if (this.config.fallbackStrategy === "abort") { throw new Error(`Task failed: ${loopResult.error}`); } } } } } task.status = "completed"; result.success = result.failedLoops.length === 0; result.totalDuration = Date.now() - task.startTime; result.insights = this.generateOrchestrationInsights(task, result); return result; } catch (error) { task.status = "failed"; result.success = false; result.error = error.message; return result; } } /** * Execute a single task as a Ralph loop */ async executeTaskLoop(taskBreakdown, orchestratedTask) { try { const bridge = new RalphStackMemoryBridge({ baseDir: `.ralph-${taskBreakdown.id}`, maxIterations: taskBreakdown.estimatedIterations * 2, // Allow extra iterations useStackMemory: true }); await bridge.initialize({ task: taskBreakdown.description, criteria: taskBreakdown.criteria.join("\n") }); this.activeLoops.set(taskBreakdown.id, bridge); orchestratedTask.loops.set(taskBreakdown.id, { bridge, status: "running", startTime: Date.now() }); await bridge.run(); const loopInfo = orchestratedTask.loops.get(taskBreakdown.id); if (loopInfo) { loopInfo.status = "completed"; loopInfo.endTime = Date.now(); } this.activeLoops.delete(taskBreakdown.id); return { success: true, loopId: taskBreakdown.id }; } catch (error) { logger.error(`Task loop failed: ${taskBreakdown.title}`, error); const loopInfo = orchestratedTask.loops.get(taskBreakdown.id); if (loopInfo) { loopInfo.status = "failed"; loopInfo.error = error.message; loopInfo.endTime = Date.now(); } this.activeLoops.delete(taskBreakdown.id); return { success: false, loopId: taskBreakdown.id, error: error.message }; } } /** * Execute a task in parallel context */ async executeParallelTask(task, execution) { try { const result = await this.executeTaskLoop(task, { id: execution.id, description: `Parallel task: ${task.title}`, breakdown: [task], executionPlan: { phases: [], totalEstimatedTime: 0, parallelizable: false }, status: "executing", startTime: execution.startTime, loops: /* @__PURE__ */ new Map(), sharedContext: execution.sharedState }); execution.results.set(task.id, result); } catch (error) { execution.results.set(task.id, { success: false, loopId: task.id, error: error.message }); } } /** * Update shared context between tasks */ async updateSharedContext(orchestratedTask, loopResult) { logger.debug("Updating shared context", { orchestrationId: orchestratedTask.id, loopId: loopResult.loopId }); } /** * Generate insights from orchestration */ generateOrchestrationInsights(task, result) { const insights = []; const avgLoopDuration = Array.from(task.loops.values()).filter((l) => l.endTime && l.startTime).map((l) => l.endTime - l.startTime).reduce((sum, duration) => sum + duration, 0) / task.loops.size; if (avgLoopDuration > 0) { insights.push( `Average loop duration: ${Math.round(avgLoopDuration / 1e3)}s` ); } const successRate = result.completedLoops.length / (result.completedLoops.length + result.failedLoops.length); insights.push(`Success rate: ${Math.round(successRate * 100)}%`); if (task.breakdown.length > 3) { insights.push( "Complex task benefited from breakdown into multiple loops" ); } return insights; } // Helper methods for task analysis assessTaskComplexity(description) { const factors = []; let score = 1; if (description.length > 200) { score += 2; factors.push("long description"); } if (description.includes("and")) { score += 1; factors.push("multiple requirements"); } if (description.toLowerCase().includes("test")) { score += 2; factors.push("testing required"); } if (description.toLowerCase().includes("document")) { score += 1; factors.push("documentation needed"); } if (description.toLowerCase().includes("refactor")) { score += 3; factors.push("refactoring complexity"); } return { score, factors }; } needsSetup(description) { const setupKeywords = [ "project", "initialize", "setup", "scaffold", "create structure" ]; return setupKeywords.some( (keyword) => description.toLowerCase().includes(keyword) ); } needsTesting(criteria) { return criteria.some((c) => c.toLowerCase().includes("test")); } needsDocumentation(criteria) { return criteria.some((c) => c.toLowerCase().includes("doc")); } extractCoreTask(description) { const sentences = description.split("."); return sentences.find( (s) => s.toLowerCase().includes("implement") || s.toLowerCase().includes("create") || s.toLowerCase().includes("add") ) || null; } canExecuteInParallel(breakdown) { return breakdown.some((task) => task.dependencies.length === 0); } groupTasksByDependencies(breakdown) { const phases = []; const processed = /* @__PURE__ */ new Set(); while (processed.size < breakdown.length) { const readyTasks = breakdown.filter( (task) => !processed.has(task.id) && task.dependencies.every((dep) => processed.has(dep)) ); if (readyTasks.length === 0) break; phases.push({ id: `phase-${phases.length + 1}`, tasks: readyTasks, dependencies: phases.length > 0 ? [`phase-${phases.length}`] : [], parallelExecution: readyTasks.length > 1 }); readyTasks.forEach((task) => processed.add(task.id)); } return phases; } validateDependencies(plan) { const errors = []; const allTaskIds = new Set( plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)) ); for (const phase of plan.phases) { for (const task of phase.tasks) { for (const dep of task.dependencies) { if (!allTaskIds.has(dep)) { errors.push(`Task ${task.id} depends on non-existent task ${dep}`); } } } } return errors; } /** * Monitor orchestration progress */ getOrchestrationStatus(orchestrationId) { return this.activeTasks.get(orchestrationId) || null; } /** * Stop orchestration */ async stopOrchestration(orchestrationId) { const task = this.activeTasks.get(orchestrationId); if (!task) return; for (const [loopId, loopInfo] of task.loops) { if (loopInfo.status === "running") { try { loopInfo.status = "stopped"; this.activeLoops.delete(loopId); } catch (error) { logger.error(`Failed to stop loop ${loopId}`, error); } } } task.status = "stopped"; this.activeTasks.delete(orchestrationId); logger.info("Orchestration stopped", { orchestrationId }); } } const multiLoopOrchestrator = new MultiLoopOrchestrator(); export { MultiLoopOrchestrator, multiLoopOrchestrator };