UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

528 lines (527 loc) 16.7 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../../core/monitoring/logger.js"; import { TaskError, ErrorCode } from "../../core/errors/index.js"; var AgentType = /* @__PURE__ */ ((AgentType2) => { AgentType2["FORMATTER"] = "formatter"; AgentType2["SECURITY"] = "security"; AgentType2["TESTING"] = "testing"; AgentType2["PERFORMANCE"] = "performance"; AgentType2["DOCUMENTATION"] = "documentation"; AgentType2["REFACTORING"] = "refactoring"; return AgentType2; })(AgentType || {}); class AgentTaskManager { taskStore; frameManager; activeSessions = /* @__PURE__ */ new Map(); sessionTimeouts = /* @__PURE__ */ new Map(); agentRegistry = /* @__PURE__ */ new Map(); // Spotify strategy constants MAX_TURNS_PER_SESSION = 10; MAX_SESSION_RETRIES = 3; SESSION_TIMEOUT_MS = 30 * 60 * 1e3; // 30 minutes CONTEXT_WINDOW_SIZE = 5; // Last 5 significant events constructor(taskStore, frameManager) { this.taskStore = taskStore; this.frameManager = frameManager; } /** * Start a new agent task session with Spotify's 10-turn limit */ async startTaskSession(taskId, frameId) { const task = this.taskStore.getTask(taskId); if (!task) { throw new TaskError( `Task ${taskId} not found`, ErrorCode.TASK_NOT_FOUND, { taskId } ); } if (this.needsBreakdown(task)) { const breakdown = await this.breakdownTask(task); return this.startMultiTaskSession(breakdown, frameId); } const sessionId = this.generateSessionId(taskId); const session = { id: sessionId, frameId, taskId, turnCount: 0, maxTurns: this.MAX_TURNS_PER_SESSION, status: "active", startedAt: /* @__PURE__ */ new Date(), verificationResults: [], contextWindow: [], feedbackLoop: [] }; this.activeSessions.set(sessionId, session); this.startSessionTimeout(sessionId); this.taskStore.updateTaskStatus( taskId, "in_progress", "Agent session started" ); logger.info("Started agent task session", { sessionId, taskId, taskTitle: task.title, maxTurns: this.MAX_TURNS_PER_SESSION }); return session; } /** * Execute a turn in the session with verification */ async executeTurn(sessionId, action, context) { const session = this.activeSessions.get(sessionId); if (!session || session.status !== "active") { throw new TaskError( "Invalid or inactive session", ErrorCode.TASK_INVALID_STATE, { sessionId } ); } session.turnCount++; if (session.turnCount >= session.maxTurns) { return this.handleTurnLimitReached(session); } const verificationResults = await this.runVerificationLoop( action, context, session ); this.updateContextWindow(session, action, verificationResults); const feedback = this.generateFeedback(verificationResults); const success = verificationResults.every( (r) => r.passed || r.severity !== "error" ); session.feedbackLoop.push({ turn: session.turnCount, action, result: feedback, verificationPassed: success, contextAdjustment: this.suggestContextAdjustment(verificationResults) || void 0 }); const shouldContinue = success && session.turnCount < session.maxTurns; if (!shouldContinue && success) { await this.completeSession(session); } return { success, feedback, shouldContinue, verificationResults }; } /** * Run Spotify-style verification loop */ async runVerificationLoop(action, context, session) { const results = []; const verifiers = this.getApplicableVerifiers(context); for (const verifier of verifiers) { const result = await this.runVerifier(verifier, action, context); results.push(result); session.verificationResults.push(result); if (!result.passed && result.severity === "error") { logger.warn("Verification failed, stopping execution", { verifier: result.verifierId, message: result.message }); break; } } return results; } /** * Get verifiers applicable to current context */ getApplicableVerifiers(context) { const verifiers = []; if (context["codeChange"]) { verifiers.push("formatter", "linter", "type-checker"); } if (context["testsPresent"]) { verifiers.push("test-runner"); } if (context["hasDocumentation"]) { verifiers.push("doc-validator"); } if (context["performanceCritical"]) { verifiers.push("performance-analyzer"); } verifiers.push("semantic-validator"); return verifiers; } /** * Run a single verifier */ async runVerifier(verifierId, action, context) { const mockResults = { formatter: () => ({ verifierId: "formatter", passed: Math.random() > 0.1, message: "Code formatting check", severity: "warning", timestamp: /* @__PURE__ */ new Date(), autoFix: "prettier --write" }), linter: () => ({ verifierId: "linter", passed: Math.random() > 0.2, message: "Linting check", severity: "error", timestamp: /* @__PURE__ */ new Date() }), "test-runner": () => ({ verifierId: "test-runner", passed: Math.random() > 0.3, message: "Test execution", severity: "error", timestamp: /* @__PURE__ */ new Date() }), "semantic-validator": () => ({ verifierId: "semantic-validator", passed: Math.random() > 0.25, // ~75% pass rate like Spotify message: "Semantic validation against original requirements", severity: "error", timestamp: /* @__PURE__ */ new Date() }) }; const verifierFn = mockResults[verifierId] || (() => ({ verifierId, passed: true, message: "Unknown verifier", severity: "info", timestamp: /* @__PURE__ */ new Date() })); return verifierFn(); } /** * Check if task needs breakdown (Spotify strategy for complex tasks) */ needsBreakdown(task) { const indicators = { hasMultipleComponents: (task.description?.match(/\band\b/gi)?.length || 0) > 2, longDescription: (task.description?.length || 0) > 500, highComplexityTags: task.tags.some( (tag) => ["refactor", "migration", "architecture", "redesign"].includes( tag.toLowerCase() ) ), hasManydependencies: task.depends_on.length > 3 }; const complexityScore = Object.values(indicators).filter(Boolean).length; return complexityScore >= 2; } /** * Break down complex task into subtasks */ async breakdownTask(task) { const subtasks = [ { title: `Analyze requirements for ${task.title}`, description: "Understand and document requirements", acceptanceCriteria: [ "Requirements documented", "Constraints identified" ], estimatedTurns: 2, verifiers: ["semantic-validator"] }, { title: `Implement core functionality for ${task.title}`, description: "Build the main implementation", acceptanceCriteria: ["Core logic implemented", "Tests passing"], estimatedTurns: 5, verifiers: ["linter", "test-runner"] }, { title: `Verify and refine ${task.title}`, description: "Final verification and improvements", acceptanceCriteria: ["All tests passing", "Documentation complete"], estimatedTurns: 3, verifiers: ["formatter", "linter", "test-runner", "semantic-validator"] } ]; return { parentTaskId: task.id, subtasks, dependencies: /* @__PURE__ */ new Map([ [subtasks[1].title, [subtasks[0].title]], [subtasks[2].title, [subtasks[1].title]] ]), estimatedTurns: subtasks.reduce((sum, st) => sum + st.estimatedTurns, 0) }; } /** * Start multi-task session for complex tasks */ async startMultiTaskSession(breakdown, frameId) { const subtaskIds = []; for (const subtask of breakdown.subtasks) { const subtaskId = this.taskStore.createTask({ title: subtask.title, description: subtask.description, frameId, parentId: breakdown.parentTaskId, tags: ["agent-subtask", ...subtask.verifiers], estimatedEffort: subtask.estimatedTurns * 5 // Rough conversion to minutes }); subtaskIds.push(subtaskId); } const titleToId = new Map( breakdown.subtasks.map((st, i) => [st.title, subtaskIds[i]]) ); for (const [title, deps] of breakdown.dependencies) { const taskId = titleToId.get(title); if (taskId) { for (const dep of deps) { const depId = titleToId.get(dep); if (depId) { this.taskStore.addDependency(taskId, depId); } } } } return this.startTaskSession(subtaskIds[0], frameId); } /** * Update context window with significant events */ updateContextWindow(session, action, verificationResults) { const significantEvent = { turn: session.turnCount, action: action.substring(0, 100), verificationSummary: verificationResults.map((r) => ({ verifier: r.verifierId, passed: r.passed })), timestamp: (/* @__PURE__ */ new Date()).toISOString() }; session.contextWindow.push(JSON.stringify(significantEvent)); if (session.contextWindow.length > this.CONTEXT_WINDOW_SIZE) { session.contextWindow = session.contextWindow.slice( -this.CONTEXT_WINDOW_SIZE ); } } /** * Generate feedback from verification results */ generateFeedback(results) { const failed = results.filter((r) => !r.passed); const warnings = results.filter( (r) => !r.passed && r.severity === "warning" ); const errors = results.filter((r) => !r.passed && r.severity === "error"); if (errors.length > 0) { const errorMessages = errors.map((e) => `- ${e.message}`).join("\n"); return `Verification failed with ${errors.length} error(s): ${errorMessages}`; } if (warnings.length > 0) { const warningMessages = warnings.map((w) => `- ${w.message}`).join("\n"); return `Verification passed with ${warnings.length} warning(s): ${warningMessages}`; } return "All verifications passed successfully"; } /** * Suggest context adjustment based on verification results */ suggestContextAdjustment(results) { const failed = results.filter((r) => !r.passed && r.severity === "error"); if (failed.length === 0) { return void 0; } const suggestions = []; if (failed.some((r) => r.verifierId === "test-runner")) { suggestions.push("Focus on fixing failing tests"); } if (failed.some((r) => r.verifierId === "linter")) { suggestions.push("Address linting errors before proceeding"); } if (failed.some((r) => r.verifierId === "semantic-validator")) { suggestions.push("Review original requirements and adjust approach"); } return suggestions.length > 0 ? suggestions.join("; ") : void 0; } /** * Handle turn limit reached */ async handleTurnLimitReached(session) { logger.warn("Session reached turn limit", { sessionId: session.id, taskId: session.taskId, turnCount: session.turnCount }); const task = this.taskStore.getTask(session.taskId); const isComplete = this.assessTaskCompletion(session); if (isComplete) { await this.completeSession(session); return { success: true, feedback: "Task completed successfully within turn limit", shouldContinue: false, verificationResults: [] }; } session.status = "timeout"; this.taskStore.updateTaskStatus( session.taskId, "blocked", "Session timeout - manual review needed" ); return { success: false, feedback: `Session reached ${this.MAX_TURNS_PER_SESSION} turn limit. Task requires manual review or retry.`, shouldContinue: false, verificationResults: [] }; } /** * Assess if task is complete enough */ assessTaskCompletion(session) { const recentResults = session.verificationResults.slice(-5); const recentPassRate = recentResults.filter((r) => r.passed).length / recentResults.length; const semanticPassed = recentResults.some( (r) => r.verifierId === "semantic-validator" && r.passed ); return recentPassRate >= 0.8 && semanticPassed; } /** * Complete a session */ async completeSession(session) { session.status = "completed"; session.completedAt = /* @__PURE__ */ new Date(); this.taskStore.updateTaskStatus( session.taskId, "completed", "Agent session completed" ); const timeout = this.sessionTimeouts.get(session.id); if (timeout) { clearTimeout(timeout); this.sessionTimeouts.delete(session.id); } const summary = this.generateSessionSummary(session); this.frameManager.addEvent("observation", { type: "session_summary", frameId: session.frameId, summary }); logger.info("Session completed", { sessionId: session.id, taskId: session.taskId, turnCount: session.turnCount, duration: session.completedAt.getTime() - session.startedAt.getTime() }); } /** * Generate session summary for frame digest */ generateSessionSummary(session) { const verificationStats = { total: session.verificationResults.length, passed: session.verificationResults.filter((r) => r.passed).length, failed: session.verificationResults.filter((r) => !r.passed).length }; return { sessionId: session.id, taskId: session.taskId, status: session.status, turnCount: session.turnCount, duration: session.completedAt ? session.completedAt.getTime() - session.startedAt.getTime() : 0, verificationStats, feedbackLoop: session.feedbackLoop.slice(-3), // Last 3 feedback entries contextWindow: session.contextWindow.slice(-2) // Last 2 context entries }; } /** * Start timeout for session */ startSessionTimeout(sessionId) { const timeout = setTimeout(() => { const session = this.activeSessions.get(sessionId); if (session && session.status === "active") { session.status = "timeout"; this.taskStore.updateTaskStatus( session.taskId, "blocked", "Session timeout - no activity" ); logger.warn("Session timed out due to inactivity", { sessionId }); } }, this.SESSION_TIMEOUT_MS); this.sessionTimeouts.set(sessionId, timeout); } /** * Generate unique session ID */ generateSessionId(taskId) { return `session-${taskId}-${Date.now()}`; } /** * Get active sessions summary */ getActiveSessions() { return Array.from(this.activeSessions.values()).map((session) => ({ sessionId: session.id, taskId: session.taskId, turnCount: session.turnCount, status: session.status, startedAt: session.startedAt })); } /** * Retry a failed session (Spotify's 3-retry strategy) */ async retrySession(sessionId) { const session = this.activeSessions.get(sessionId); if (!session || session.status === "active") { return null; } const retryCount = Array.from(this.activeSessions.values()).filter( (s) => s.taskId === session.taskId && s.status === "failed" ).length; if (retryCount >= this.MAX_SESSION_RETRIES) { logger.warn("Max retries reached for task", { taskId: session.taskId, retries: retryCount }); return null; } const newSession = await this.startTaskSession( session.taskId, session.frameId ); newSession.contextWindow = session.contextWindow.slice(-3); newSession.feedbackLoop = [ { turn: 0, action: "Session retry with learned context", result: `Retrying after ${retryCount} previous attempts`, verificationPassed: true, contextAdjustment: session.feedbackLoop.filter((f) => f.contextAdjustment).map((f) => f.contextAdjustment).join("; ") } ]; return newSession; } } export { AgentTaskManager, AgentType }; //# sourceMappingURL=agent-task-manager.js.map