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

413 lines (412 loc) 12.8 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 { spawn } from "child_process"; import { logger } from "../../core/monitoring/logger.js"; class ClaudeCodeTaskCoordinator { activeTasks = /* @__PURE__ */ new Map(); completedTasks = []; metrics; constructor() { this.metrics = { totalTasks: 0, completedTasks: 0, failedTasks: 0, averageExecutionTime: 0, totalCost: 0, successRate: 0, agentUtilization: {} }; } /** * Execute task with Claude Code agent */ async executeTask(agentName, agentConfig, prompt, options = {}) { const taskId = uuidv4(); const { maxRetries = 2, timeout = 3e5, priority = "medium" } = options; const task = { id: taskId, agentName, agentType: agentConfig.type, prompt, startTime: Date.now(), status: "pending", retryCount: 0, estimatedCost: this.estimateTaskCost(prompt, agentConfig) }; this.activeTasks.set(taskId, task); this.metrics.totalTasks++; logger.info("Starting Claude Code task execution", { taskId, agentName, agentType: agentConfig.type, promptLength: prompt.length, estimatedCost: task.estimatedCost, priority }); try { let lastError = null; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { task.retryCount = attempt; task.status = "running"; const result = await this.executeWithTimeout( () => this.invokeClaudeCodeAgent(agentName, prompt, agentConfig), timeout ); task.status = "completed"; task.result = result; task.endTime = Date.now(); task.actualTokens = this.estimateTokenUsage(prompt, result); this.completeTask(task); return result; } catch (error) { lastError = error; task.status = "failed"; logger.warn(`Claude Code task attempt ${attempt + 1} failed`, { taskId, agentName, error: lastError.message, attempt: attempt + 1, maxRetries: maxRetries + 1 }); if (attempt === maxRetries) { break; } const backoffMs = Math.min(1e3 * Math.pow(2, attempt), 1e4); await new Promise((resolve) => setTimeout(resolve, backoffMs)); } } task.error = lastError?.message || "Unknown error"; task.endTime = Date.now(); this.failTask(task, lastError); throw lastError; } finally { this.activeTasks.delete(taskId); } } /** * Execute multiple tasks in parallel with coordination */ async executeParallelTasks(tasks) { logger.info("Executing parallel Claude Code tasks", { taskCount: tasks.length, agents: tasks.map((t) => t.agentName) }); const priorityGroups = { high: tasks.filter((t) => t.priority === "high"), medium: tasks.filter((t) => t.priority === "medium"), low: tasks.filter((t) => t.priority === "low") }; const results = []; const failures = []; for (const priorityLevel of ["high", "medium", "low"]) { const priorityTasks = priorityGroups[priorityLevel]; if (priorityTasks.length === 0) continue; logger.info(`Executing ${priorityLevel} priority tasks`, { count: priorityTasks.length }); const promises = priorityTasks.map(async (task) => { try { const result = await this.executeTask( task.agentName, task.agentConfig, task.prompt, { priority: task.priority } ); return { success: true, result }; } catch (error) { return { success: false, error }; } }); const outcomes = await Promise.allSettled(promises); for (const outcome of outcomes) { if (outcome.status === "fulfilled") { if (outcome.value.success) { results.push(outcome.value.result); } else { failures.push(outcome.value.error); } } else { failures.push(new Error(outcome.reason)); } } } logger.info("Parallel task execution completed", { totalTasks: tasks.length, successful: results.length, failed: failures.length, successRate: (results.length / tasks.length * 100).toFixed(1) }); return { results, failures }; } /** * Get coordination metrics and health status */ getCoordinationMetrics() { const recentTasks = this.completedTasks.slice(-10); const recentErrorRate = recentTasks.length > 0 ? recentTasks.filter((t) => t.status === "failed").length / recentTasks.length : 0; const performanceTrend = recentErrorRate < 0.1 ? "improving" : recentErrorRate < 0.3 ? "stable" : "degrading"; const recentErrors = this.completedTasks.slice(-5).filter((t) => t.status === "failed").map((t) => t.error || "Unknown error"); return { ...this.metrics, activeTasks: this.activeTasks.size, recentErrors, performanceTrend }; } /** * Clean up resources and reset metrics */ async cleanup() { logger.info("Cleaning up Claude Code Task Coordinator", { activeTasks: this.activeTasks.size, completedTasks: this.completedTasks.length }); if (this.activeTasks.size > 0) { let timer; const timeoutPromise = new Promise((resolve) => { timer = setTimeout(resolve, 3e4); }); const completionPromise = this.waitForTaskCompletion(); try { await Promise.race([completionPromise, timeoutPromise]); } finally { clearTimeout(timer); } if (this.activeTasks.size > 0) { logger.warn("Force terminating active tasks", { remainingTasks: this.activeTasks.size }); } } this.activeTasks.clear(); this.completedTasks = []; this.resetMetrics(); } /** * Execute with timeout wrapper */ async executeWithTimeout(fn, timeoutMs) { let timer; const timeoutPromise = new Promise((_, reject) => { timer = setTimeout(() => { reject(new Error(`Task execution timeout after ${timeoutMs}ms`)); }, timeoutMs); }); try { return await Promise.race([fn(), timeoutPromise]); } finally { clearTimeout(timer); } } /** * Invoke Claude Code agent (integration point) */ async invokeClaudeCodeAgent(agentName, prompt, agentConfig) { logger.debug("Invoking Claude Code agent", { agentName, agentType: agentConfig.type, promptTokens: this.estimateTokenUsage(prompt, "") }); return this.spawnClaudeCode(agentName, prompt, agentConfig); } /** * Spawn Claude Code CLI as a subprocess. * Uses `claude --print` for non-interactive execution with stdout capture. * Workspace cwd is inherited from the coordinator's process. */ spawnClaudeCode(agentName, prompt, agentConfig) { return new Promise((resolve, reject) => { const args = ["--print"]; if (agentConfig.type === "oracle") { args.push("--model", "opus"); } if (agentConfig.capabilities.includes("code_implementation")) { args.push("--allowedTools", "Edit,Write,Bash,Read,Glob,Grep"); } else { args.push("--allowedTools", "Read,Glob,Grep,Bash"); } args.push(prompt); logger.info("Spawning claude CLI", { agentName, agentType: agentConfig.type, cwd: process.cwd(), promptLength: prompt.length }); const proc = spawn("claude", args, { cwd: process.cwd(), env: { ...process.env }, stdio: ["pipe", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; proc.stdout.on("data", (data) => { stdout += data.toString(); }); proc.stderr.on("data", (data) => { stderr += data.toString(); }); proc.on("error", (err) => { logger.error("Failed to spawn claude CLI", { agentName, error: err.message }); reject(new Error(`Failed to spawn claude: ${err.message}`)); }); proc.on("close", (code) => { if (code === 0) { logger.debug("Claude CLI completed", { agentName, outputLength: stdout.length }); resolve(stdout.trim()); } else { const errMsg = stderr.slice(0, 500) || `exit code ${code}`; logger.warn("Claude CLI failed", { agentName, exitCode: code, stderr: errMsg }); reject(new Error(`Claude CLI exited with code ${code}: ${errMsg}`)); } }); }); } /** * Complete a successful task */ completeTask(task) { this.completedTasks.push({ ...task }); this.metrics.completedTasks++; this.updateExecutionMetrics(task); this.updateAgentUtilization(task.agentName); this.updateSuccessRate(); logger.info("Claude Code task completed", { taskId: task.id, agentName: task.agentName, executionTime: task.endTime - task.startTime, retries: task.retryCount, cost: this.calculateActualCost(task) }); } /** * Handle a failed task */ failTask(task, error) { this.completedTasks.push({ ...task }); this.metrics.failedTasks++; this.updateExecutionMetrics(task); this.updateSuccessRate(); logger.error("Claude Code task failed", { taskId: task.id, agentName: task.agentName, error: error.message, retries: task.retryCount, executionTime: task.endTime - task.startTime }); } /** * Update execution time metrics */ updateExecutionMetrics(task) { if (!task.endTime) return; const executionTime = task.endTime - task.startTime; const totalTasks = this.metrics.completedTasks + this.metrics.failedTasks; if (totalTasks === 1) { this.metrics.averageExecutionTime = executionTime; } else { this.metrics.averageExecutionTime = (this.metrics.averageExecutionTime * (totalTasks - 1) + executionTime) / totalTasks; } this.metrics.totalCost += this.calculateActualCost(task); } /** * Update agent utilization metrics */ updateAgentUtilization(agentName) { this.metrics.agentUtilization[agentName] = (this.metrics.agentUtilization[agentName] || 0) + 1; } /** * Update success rate */ updateSuccessRate() { const total = this.metrics.completedTasks + this.metrics.failedTasks; this.metrics.successRate = total > 0 ? this.metrics.completedTasks / total : 0; } /** * Estimate task cost based on prompt and agent */ estimateTaskCost(prompt, agentConfig) { const estimatedTokens = this.estimateTokenUsage(prompt, ""); const baseCost = agentConfig.type === "oracle" ? 0.015 : 25e-5; return estimatedTokens / 1e3 * baseCost * agentConfig.costMultiplier; } /** * Estimate token usage */ estimateTokenUsage(prompt, response) { return Math.ceil((prompt.length + response.length) / 4); } /** * Calculate actual task cost */ calculateActualCost(task) { if (!task.actualTokens) return task.estimatedCost; const baseCost = task.agentType === "oracle" ? 0.015 : 25e-5; return task.actualTokens / 1e3 * baseCost; } /** * Wait for all active tasks to complete */ async waitForTaskCompletion() { while (this.activeTasks.size > 0) { await new Promise((resolve) => setTimeout(resolve, 1e3)); } } /** * Reset metrics */ resetMetrics() { this.metrics = { totalTasks: 0, completedTasks: 0, failedTasks: 0, averageExecutionTime: 0, totalCost: 0, successRate: 0, agentUtilization: {} }; } /** * Get active task status */ getActiveTaskStatus() { return Array.from(this.activeTasks.values()).map((task) => ({ taskId: task.id, agentName: task.agentName, status: task.status, runtime: Date.now() - task.startTime })); } /** * Cancel active task */ async cancelTask(taskId) { const task = this.activeTasks.get(taskId); if (!task) return false; task.status = "failed"; task.error = "Task cancelled by user"; task.endTime = Date.now(); this.failTask(task, new Error("Task cancelled")); this.activeTasks.delete(taskId); logger.info("Task cancelled", { taskId, agentName: task.agentName }); return true; } } var task_coordinator_default = ClaudeCodeTaskCoordinator; export { ClaudeCodeTaskCoordinator, task_coordinator_default as default };