@claude-vector/claude-tools
Version:
Claude integration tools for AI-powered development assistance
641 lines (523 loc) • 16.3 kB
JavaScript
/**
* 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;