@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
JavaScript
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