UNPKG

@claude-vector/claude-tools

Version:

Claude integration tools for AI-powered development assistance

641 lines (523 loc) 16.3 kB
/** * Context Manager for Claude Vector Search * Manages working memory for current development task * * Responsibilities: * - Store and organize information relevant to current task * - Optimize content to fit within token limits * - Maintain information freshness for long sessions * - Does NOT perform searches (delegated to VectorSearchEngine) */ import { EmbeddingGenerator } from '@claude-vector/core'; /** * Simple LRU Cache implementation */ class LRUCache { constructor(maxSize = 1000) { this.maxSize = maxSize; this.cache = new Map(); } get(key) { if (!this.cache.has(key)) return null; // Move to end (most recently used) const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); return value; } set(key, value) { // Remove if exists if (this.cache.has(key)) { this.cache.delete(key); } // Add to end this.cache.set(key, value); // Remove oldest if over limit if (this.cache.size > this.maxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } } has(key) { return this.cache.has(key); } delete(key) { return this.cache.delete(key); } clear() { this.cache.clear(); } get size() { return this.cache.size; } } export class ContextManager { constructor(config = {}) { // Token management this.maxTokens = config.maxTokens || 150000; this.reservedTokens = Math.floor(this.maxTokens * 0.2); // Reserve 20% for safety this.usedTokens = 0; // Context storage this.contextItems = []; this.itemCache = new LRUCache(1000); // Task context this.currentTask = null; this.taskType = null; // Priorities this.priorities = { critical: 10, high: 7, medium: 5, low: 3, optional: 1 }; // Session management this.sessionConfig = { maxItems: 5000, maintenanceInterval: 300000, // 5 minutes retentionPolicy: { hot: 1800000, // 30 minutes warm: 7200000, // 2 hours cold: 14400000 // 4 hours (max) } }; // Smart features configuration this.smartFeatures = { enableMerging: config.enableMerging !== false, mergeThreshold: 0.85, enableDynamicPriority: config.enableDynamicPriority !== false, decayRate: 0.9 }; // Statistics this.stats = { addedItems: 0, removedItems: 0, mergedItems: 0, maintenanceRuns: 0 }; // Initialize embedding generator if available if (process.env.OPENAI_API_KEY && this.smartFeatures.enableMerging) { this.embeddingGenerator = new EmbeddingGenerator(); this.embeddingCache = new Map(); } // Start maintenance timer this.startMaintenanceTimer(); } /** * Set current task context */ setTask(taskType, description) { this.taskType = taskType; this.currentTask = description; // Add task as critical context item this.addItem({ type: 'task', content: `Current Task: ${description}`, metadata: { taskType } }, 'critical'); } /** * Add item to context */ async addItem(item, priority = 'medium') { // Validate input if (!item || !item.content) { throw new Error('Item must have content'); } // Check capacity if (this.contextItems.length >= this.sessionConfig.maxItems) { await this.performMaintenance(); } // Prepare item const contextItem = { id: item.id || this.generateId(), type: item.type || 'general', content: item.content, metadata: item.metadata || {}, tokens: this.estimateTokens(item.content), priority: this.getPriorityValue(priority), timestamp: Date.now(), lastAccessed: Date.now(), accessCount: 0, source: item.source || 'unknown' }; // Check for duplicates if merging is enabled and API key is available if (this.smartFeatures.enableMerging && this.embeddingGenerator && process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY !== 'test-api-key') { try { const duplicate = await this.findDuplicate(contextItem); if (duplicate) { return this.handleDuplicate(duplicate, contextItem); } } catch (error) { // Fallback to simple duplicate detection without embeddings console.log('Embedding-based duplicate detection failed, using simple detection'); } } // Add to context this.contextItems.push(contextItem); this.itemCache.set(contextItem.id, contextItem); this.stats.addedItems++; // Optimize to fit token limit this.optimize(); // Ensure tokens are recalculated after optimization this.recalculateTokens(); return this.getStats(); } /** * Remove item from context */ removeItem(itemId) { const index = this.contextItems.findIndex(item => item.id === itemId); if (index === -1) return false; this.contextItems.splice(index, 1); this.itemCache.delete(itemId); this.stats.removedItems++; this.recalculateTokens(); return true; } /** * Get item by ID */ getItem(itemId) { // Check cache first const cached = this.itemCache.get(itemId); if (cached) { cached.lastAccessed = Date.now(); cached.accessCount++; return cached; } // Find in items const item = this.contextItems.find(i => i.id === itemId); if (item) { item.lastAccessed = Date.now(); item.accessCount++; this.itemCache.set(itemId, item); } return item; } /** * Clear all context */ clear() { this.contextItems = []; this.itemCache.clear(); this.usedTokens = 0; this.embeddingCache?.clear(); } /** * Optimize context to fit within token limits */ optimize() { const availableTokens = this.maxTokens - this.reservedTokens; // Calculate scores for all items const scoredItems = this.contextItems.map(item => ({ item, score: this.calculateItemScore(item) })); // Sort by score (highest first) scoredItems.sort((a, b) => b.score - a.score); // Select items that fit let totalTokens = 0; const optimized = []; for (const { item } of scoredItems) { if (totalTokens + item.tokens <= availableTokens) { optimized.push(item); totalTokens += item.tokens; } } this.contextItems = optimized; this.usedTokens = totalTokens; } /** * Calculate item score for optimization */ calculateItemScore(item) { let score = item.priority; // Apply dynamic priority adjustments if (this.smartFeatures.enableDynamicPriority) { // Recency boost const age = Date.now() - item.timestamp; const recencyScore = Math.exp(-age / 3600000); // Decay over hours score *= (1 + recencyScore * 0.5); // Access frequency boost if (item.accessCount > 0) { score *= (1 + Math.log(item.accessCount + 1) * 0.2); } // Task relevance boost if (this.taskType && item.metadata?.relevantToTask === this.taskType) { score *= 1.5; } } // Efficiency: priority per token return score / Math.max(item.tokens, 1); } /** * Perform maintenance for long sessions */ async performMaintenance() { const startTime = Date.now(); const now = Date.now(); // Calculate retention value for each item const evaluatedItems = this.contextItems.map(item => ({ item, retentionValue: this.calculateRetentionValue(item, now) })); // Sort by retention value (lowest first) evaluatedItems.sort((a, b) => a.retentionValue - b.retentionValue); // Remove items with low retention value const targetCount = Math.floor(this.sessionConfig.maxItems * 0.8); const removeCount = Math.max(0, this.contextItems.length - targetCount); for (let i = 0; i < removeCount; i++) { const { item } = evaluatedItems[i]; // Don't remove critical items from current session if (item.priority >= this.priorities.critical && now - item.timestamp < this.sessionConfig.retentionPolicy.hot) { continue; } this.removeItem(item.id); } // Update stats this.stats.maintenanceRuns++; const duration = Date.now() - startTime; return { removed: removeCount, duration, remaining: this.contextItems.length }; } /** * Calculate retention value for an item */ calculateRetentionValue(item, now) { const age = now - item.timestamp; const timeSinceAccess = now - item.lastAccessed; // Base value from priority let value = item.priority; // Data temperature classification let temperatureMultiplier = 1.0; if (timeSinceAccess < this.sessionConfig.retentionPolicy.hot) { temperatureMultiplier = 3.0; // Hot data } else if (timeSinceAccess < this.sessionConfig.retentionPolicy.warm) { temperatureMultiplier = 1.5; // Warm data } else { temperatureMultiplier = 0.5; // Cold data } value *= temperatureMultiplier; // Access frequency bonus if (item.accessCount > 0) { value *= (1 + Math.log(item.accessCount + 1)); } // Age penalty if (age > this.sessionConfig.retentionPolicy.cold) { value *= 0.1; // Very old items } // Task relevance if (this.taskType && item.metadata?.relevantToTask === this.taskType) { value *= 2.0; } return value; } /** * Get formatted context for AI */ getFormattedContext() { if (this.contextItems.length === 0) { return 'No context available.'; } let formatted = ''; // Add current task if set if (this.currentTask) { formatted += `# Current Task\n${this.currentTask}\n`; if (this.taskType) { formatted += `Task Type: ${this.taskType}\n`; } formatted += '\n'; } // Group items by type const grouped = this.groupItemsByType(); // Format each group for (const [type, items] of Object.entries(grouped)) { formatted += `## ${this.formatTypeName(type)}\n\n`; for (const item of items) { formatted += this.formatItem(item) + '\n\n'; } } // Add statistics const stats = this.getStats(); formatted += `\n---\n`; formatted += `Context: ${stats.totalItems} items, ${stats.usedTokens}/${stats.availableTokens} tokens (${stats.utilizationRate}%)\n`; return formatted; } /** * Get statistics */ getStats() { const availableTokens = this.maxTokens - this.reservedTokens; return { totalItems: this.contextItems.length, usedTokens: this.usedTokens, availableTokens, remainingTokens: availableTokens - this.usedTokens, utilizationRate: Math.round((this.usedTokens / availableTokens) * 100), stats: this.stats, sessionInfo: { maxItems: this.sessionConfig.maxItems, currentLoad: Math.round((this.contextItems.length / this.sessionConfig.maxItems) * 100) } }; } /** * Find duplicate items */ async findDuplicate(newItem) { if (!this.embeddingGenerator) return null; const newEmbedding = await this.getEmbedding(newItem.content); for (const existingItem of this.contextItems) { // Quick check: same file if (newItem.metadata?.file && existingItem.metadata?.file === newItem.metadata.file) { const similarity = await this.calculateSimilarity(newEmbedding, existingItem); if (similarity > 0.95) { return existingItem; } } // General similarity check const similarity = await this.calculateSimilarity(newEmbedding, existingItem); if (similarity > this.smartFeatures.mergeThreshold) { return existingItem; } } return null; } /** * Handle duplicate item */ handleDuplicate(existingItem, newItem) { // Update existing item existingItem.timestamp = Date.now(); existingItem.lastAccessed = Date.now(); existingItem.accessCount++; // Merge metadata existingItem.metadata = { ...existingItem.metadata, ...newItem.metadata, mergedAt: Date.now(), mergeCount: (existingItem.metadata.mergeCount || 0) + 1 }; // Update priority to highest existingItem.priority = Math.max(existingItem.priority, newItem.priority); this.stats.mergedItems++; return this.getStats(); } // Helper methods estimateTokens(text) { if (!text) return 0; const japaneseRegex = /[\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf\u3400-\u4dbf]/g; const japaneseChars = (text.match(japaneseRegex) || []).length; const otherChars = text.length - japaneseChars; return Math.ceil(japaneseChars / 2 + otherChars / 4); } getPriorityValue(priority) { return typeof priority === 'number' ? priority : this.priorities[priority] || this.priorities.medium; } generateId() { return `ctx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } recalculateTokens() { this.usedTokens = this.contextItems.reduce((sum, item) => sum + item.tokens, 0); } groupItemsByType() { const grouped = {}; for (const item of this.contextItems) { if (!grouped[item.type]) { grouped[item.type] = []; } grouped[item.type].push(item); } return grouped; } formatTypeName(type) { const typeNames = { task: 'Current Task', error: 'Error Context', code: 'Code References', example: 'Examples', general: 'General Information', context: 'Related Context' }; return typeNames[type] || type.charAt(0).toUpperCase() + type.slice(1); } formatItem(item) { let formatted = ''; // Add metadata if relevant if (item.metadata?.file) { formatted += `**File**: ${item.metadata.file}\n`; } if (item.metadata?.line) { formatted += `**Line**: ${item.metadata.line}\n`; } if (formatted) { formatted += '\n'; } // Add content formatted += item.content; // Add truncation notice if needed if (item.metadata?.truncated) { formatted += '\n*[Content truncated]*'; } return formatted; } async getEmbedding(content) { const cacheKey = content.substring(0, 100); if (this.embeddingCache.has(cacheKey)) { return this.embeddingCache.get(cacheKey); } const embedding = await this.embeddingGenerator.generateEmbedding(content); this.embeddingCache.set(cacheKey, embedding); // Limit cache size if (this.embeddingCache.size > 1000) { const firstKey = this.embeddingCache.keys().next().value; this.embeddingCache.delete(firstKey); } return embedding; } async calculateSimilarity(embedding1, existingItem) { const embedding2 = await this.getEmbedding(existingItem.content); return this.cosineSimilarity(embedding1, embedding2); } cosineSimilarity(a, b) { if (!a || !b || a.length !== b.length) return 0; let dotProduct = 0; let normA = 0; let normB = 0; for (let i = 0; i < a.length; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } normA = Math.sqrt(normA); normB = Math.sqrt(normB); if (normA === 0 || normB === 0) return 0; return dotProduct / (normA * normB); } startMaintenanceTimer() { this.maintenanceTimer = setInterval(() => { if (this.contextItems.length > this.sessionConfig.maxItems * 0.9) { this.performMaintenance(); } }, this.sessionConfig.maintenanceInterval); } stopMaintenanceTimer() { if (this.maintenanceTimer) { clearInterval(this.maintenanceTimer); this.maintenanceTimer = null; } } cleanup() { this.stopMaintenanceTimer(); this.clear(); } } export default ContextManager;