crewai-ts
Version:
TypeScript port of crewAI for agent-based workflows
555 lines (554 loc) • 18.5 kB
JavaScript
/**
* SegmentedMemory implementation
* Provides tiered memory storage with automatic promotion/demotion
* between hot, warm, and cold segments based on access patterns
*/
import { v4 as uuidv4 } from 'uuid';
/**
* SegmentedMemory class
* Implements tiered memory storage with automatic promotion/demotion based on access patterns
*/
export class SegmentedMemory {
// Tiered storage
hotCache = new Map();
warmCache = new Map();
coldStorage = new Map();
// Configuration
hotCacheSize;
warmCacheSize;
hotDemotionTimeMs;
warmDemotionTimeMs;
trackStats;
enablePrefetching;
prefetchCount;
// Tier management interval
tierManagementIntervalId;
// Relation tracking for prefetching
itemRelations = new Map(); // Maps items to related items
// Statistics for monitoring performance
stats = {
tiers: {
hot: { size: 0, hits: 0 },
warm: { size: 0, hits: 0 },
cold: { size: 0, hits: 0 }
},
promotions: {
coldToWarm: 0,
warmToHot: 0
},
demotions: {
hotToWarm: 0,
warmToCold: 0
},
prefetches: 0,
totalItems: 0
};
constructor(options = {}) {
this.hotCacheSize = options.hotCacheSize ?? 100;
this.warmCacheSize = options.warmCacheSize ?? 1000;
this.hotDemotionTimeMs = options.hotDemotionTimeMs ?? 5 * 60 * 1000; // 5 minutes
this.warmDemotionTimeMs = options.warmDemotionTimeMs ?? 60 * 60 * 1000; // 1 hour
this.trackStats = options.trackStats ?? true;
this.enablePrefetching = options.enablePrefetching ?? false;
this.prefetchCount = options.prefetchCount ?? 5;
// Set up automatic tier management if interval is provided
const tierManagementIntervalMs = options.tierManagementIntervalMs ?? 60 * 1000; // 1 minute
this.tierManagementIntervalId = setInterval(() => {
this.manageTiers();
}, tierManagementIntervalMs);
// Initialize stats
if (this.trackStats) {
this.updateStats();
}
}
/**
* Add a memory item
* New items are always inserted into hot cache first
*/
async add(content, metadata) {
// Create a new memory item
const item = {
id: uuidv4(),
content,
metadata,
createdAt: Date.now(),
lastAccessedAt: Date.now()
};
// Check if we need to make room in hot cache
this.ensureHotCacheCapacity();
// Add the item to hot cache
this.hotCache.set(item.id, item);
// Update statistics
if (this.trackStats) {
this.stats.totalItems++;
this.stats.tiers.hot.size = this.hotCache.size;
this.updateStats();
}
return item;
}
/**
* Search for memory items across all tiers
*/
async search(params) {
const { query, metadata, limit = 10, offset = 0, minRelevance = 0 } = params;
// Collect items from all tiers
const allItems = [];
// Helper function to filter items
const filterItem = (item) => {
// Filter by metadata if provided
if (metadata) {
for (const [key, value] of Object.entries(metadata)) {
if (item.metadata?.[key] !== value) {
return false;
}
}
}
// If no query is provided, include all items that match metadata
if (!query) {
return true;
}
// Simple text matching for now
return item.content.toLowerCase().includes(query.toLowerCase());
};
// Check hot cache first (most relevant items)
for (const item of this.hotCache.values()) {
if (filterItem(item)) {
allItems.push(item);
}
}
// Then check warm cache
for (const item of this.warmCache.values()) {
if (filterItem(item)) {
allItems.push(item);
}
}
// Finally check cold storage
for (const item of this.coldStorage.values()) {
if (filterItem(item)) {
allItems.push(item);
}
}
// Calculate relevance scores
const scoredItems = allItems.map(item => {
let score = 0;
if (query) {
// Simple text matching score
const content = item.content.toLowerCase();
const queryLower = query.toLowerCase();
// Count occurrences
let count = 0;
let pos = content.indexOf(queryLower);
while (pos !== -1) {
count++;
pos = content.indexOf(queryLower, pos + 1);
}
// Base score on occurrence count and position
score = count * 0.1;
// Boost score if query appears at the beginning
if (content.startsWith(queryLower)) {
score += 0.2;
}
// Boost score based on recency
const recencyBoost = 1 - Math.min(1, (Date.now() - item.createdAt) / (30 * 24 * 60 * 60 * 1000));
score += recencyBoost * 0.3;
// Tier-based boosting
if (this.hotCache.has(item.id)) {
score += 0.3; // Hot items get highest boost
}
else if (this.warmCache.has(item.id)) {
score += 0.1; // Warm items get medium boost
}
}
else {
// If no query, score based on recency
score = 1 - Math.min(1, (Date.now() - item.createdAt) / (30 * 24 * 60 * 60 * 1000));
}
return { item, score };
});
// Filter by minimum relevance
const filteredItems = scoredItems
.filter(({ score }) => score >= minRelevance)
.sort((a, b) => b.score - a.score);
// Apply pagination
const paginatedItems = filteredItems
.slice(offset, offset + limit)
.map(({ item }) => {
// Update access time and promote item (important for tiered memory management)
this.accessItem(item.id);
return item;
});
return {
items: paginatedItems,
total: filteredItems.length
};
}
/**
* Get a memory item by ID
* Updates access time and promotes item if found
*/
async get(id) {
return this.accessItem(id);
}
/**
* Update a memory item
*/
async update(id, updates) {
// Find the item
const item = this.findItem(id);
if (!item) {
return null;
}
// Update item properties
const updatedItem = {
...item,
...updates,
updatedAt: Date.now(),
lastAccessedAt: Date.now()
};
// Update in appropriate tier
if (this.hotCache.has(id)) {
this.hotCache.set(id, updatedItem);
}
else if (this.warmCache.has(id)) {
// Promote to hot when updated
this.ensureHotCacheCapacity();
this.hotCache.set(id, updatedItem);
this.warmCache.delete(id);
if (this.trackStats) {
this.stats.promotions.warmToHot++;
}
}
else if (this.coldStorage.has(id)) {
// Promote to warm when updated
this.ensureWarmCacheCapacity();
this.warmCache.set(id, updatedItem);
this.coldStorage.delete(id);
if (this.trackStats) {
this.stats.promotions.coldToWarm++;
}
}
// Update access stats
if (this.trackStats) {
this.updateStats();
}
return updatedItem;
}
/**
* Remove a memory item
*/
async remove(id) {
let removed = false;
// Remove from all tiers
if (this.hotCache.delete(id)) {
removed = true;
}
if (this.warmCache.delete(id)) {
removed = true;
}
if (this.coldStorage.delete(id)) {
removed = true;
}
// Remove from relation tracking
this.itemRelations.delete(id);
// Also remove references to this item from other items' relations
for (const [relatedId, relations] of this.itemRelations.entries()) {
if (relations.has(id)) {
relations.delete(id);
}
}
// Update statistics
if (removed && this.trackStats) {
this.stats.totalItems--;
this.updateStats();
}
return removed;
}
/**
* Clear all memory items
*/
async clear() {
this.hotCache.clear();
this.warmCache.clear();
this.coldStorage.clear();
this.itemRelations.clear();
// Reset statistics
if (this.trackStats) {
this.stats = {
tiers: {
hot: { size: 0, hits: 0 },
warm: { size: 0, hits: 0 },
cold: { size: 0, hits: 0 }
},
promotions: {
coldToWarm: 0,
warmToHot: 0
},
demotions: {
hotToWarm: 0,
warmToCold: 0
},
prefetches: 0,
totalItems: 0
};
}
}
/**
* Reset the memory
*/
async reset() {
await this.clear();
}
/**
* Clean up resources when disposing
*/
dispose() {
if (this.tierManagementIntervalId) {
clearInterval(this.tierManagementIntervalId);
this.tierManagementIntervalId = undefined;
}
}
/**
* Get the total number of items across all tiers
*/
get size() {
return this.hotCache.size + this.warmCache.size + this.coldStorage.size;
}
/**
* Get memory performance statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Get memory tier distribution
*/
getTierDistribution() {
return {
hot: this.hotCache.size,
warm: this.warmCache.size,
cold: this.coldStorage.size
};
}
/**
* Add a relation between two items
* Used for prefetching related items
*/
addRelation(sourceId, targetId) {
// Check if both items exist
const sourceItem = this.findItem(sourceId);
const targetItem = this.findItem(targetId);
if (!sourceItem || !targetItem) {
return false;
}
// Initialize relation set if it doesn't exist
if (!this.itemRelations.has(sourceId)) {
this.itemRelations.set(sourceId, new Set());
}
// Add relation
this.itemRelations.get(sourceId).add(targetId);
return true;
}
/**
* Manage tiers by promoting and demoting items based on access patterns
*/
manageTiers() {
const now = Date.now();
// Demote items from hot to warm based on last access time
const hotDemotionCutoff = now - this.hotDemotionTimeMs;
const itemsToDemoteToWarm = [];
for (const [id, item] of this.hotCache.entries()) {
if ((item.lastAccessedAt ?? item.createdAt) < hotDemotionCutoff) {
itemsToDemoteToWarm.push(item);
this.hotCache.delete(id);
}
}
// Demote items from warm to cold based on last access time
const warmDemotionCutoff = now - this.warmDemotionTimeMs;
const itemsToDemoteToCold = [];
for (const [id, item] of this.warmCache.entries()) {
if ((item.lastAccessedAt ?? item.createdAt) < warmDemotionCutoff) {
itemsToDemoteToCold.push(item);
this.warmCache.delete(id);
}
}
// Move demoted items to their new tiers
for (const item of itemsToDemoteToWarm) {
this.warmCache.set(item.id, item);
if (this.trackStats) {
this.stats.demotions.hotToWarm++;
}
}
for (const item of itemsToDemoteToCold) {
this.coldStorage.set(item.id, item);
if (this.trackStats) {
this.stats.demotions.warmToCold++;
}
}
// Update statistics
if (this.trackStats) {
this.updateStats();
}
}
/**
* Find an item across all tiers
*/
findItem(id) {
// Check each tier
return this.hotCache.get(id) || this.warmCache.get(id) || this.coldStorage.get(id) || null;
}
/**
* Access an item, update its access time, and promote it to appropriate tier
*/
accessItem(id) {
let item;
let tier = null;
// Check which tier contains the item
if (this.hotCache.has(id)) {
item = this.hotCache.get(id);
tier = 'hot';
if (this.trackStats) {
this.stats.tiers.hot.hits++;
}
}
else if (this.warmCache.has(id)) {
item = this.warmCache.get(id);
tier = 'warm';
if (this.trackStats) {
this.stats.tiers.warm.hits++;
}
// Promote from warm to hot
this.warmCache.delete(id);
this.ensureHotCacheCapacity();
this.hotCache.set(id, item);
if (this.trackStats) {
this.stats.promotions.warmToHot++;
}
}
else if (this.coldStorage.has(id)) {
item = this.coldStorage.get(id);
tier = 'cold';
if (this.trackStats) {
this.stats.tiers.cold.hits++;
}
// Promote from cold to warm
this.coldStorage.delete(id);
this.ensureWarmCacheCapacity();
this.warmCache.set(id, item);
if (this.trackStats) {
this.stats.promotions.coldToWarm++;
}
}
if (!item) {
return null;
}
// Update access time
item.lastAccessedAt = Date.now();
// Prefetch related items if enabled
if (this.enablePrefetching && tier !== 'hot') {
this.prefetchRelatedItems(id);
}
// Update statistics
if (this.trackStats) {
this.updateStats();
}
return item;
}
/**
* Prefetch related items
*/
prefetchRelatedItems(id) {
const relations = this.itemRelations.get(id);
if (!relations || relations.size === 0) {
return;
}
// Get the most important related items
const relatedIds = Array.from(relations).slice(0, this.prefetchCount);
for (const relatedId of relatedIds) {
const item = this.findItem(relatedId);
if (item) {
// Promote related items by one tier
if (this.coldStorage.has(relatedId)) {
this.coldStorage.delete(relatedId);
this.ensureWarmCacheCapacity();
this.warmCache.set(relatedId, item);
if (this.trackStats) {
this.stats.prefetches++;
this.stats.promotions.coldToWarm++;
}
}
else if (this.warmCache.has(relatedId)) {
this.warmCache.delete(relatedId);
this.ensureHotCacheCapacity();
this.hotCache.set(relatedId, item);
if (this.trackStats) {
this.stats.prefetches++;
this.stats.promotions.warmToHot++;
}
}
}
}
}
/**
* Ensure hot cache has capacity for a new item
*/
ensureHotCacheCapacity() {
if (this.hotCache.size >= this.hotCacheSize) {
// Find the least recently accessed item in hot cache
let oldestId = null;
let oldestTime = Infinity;
for (const [id, item] of this.hotCache.entries()) {
const accessTime = item.lastAccessedAt ?? item.createdAt;
if (accessTime < oldestTime) {
oldestTime = accessTime;
oldestId = id;
}
}
if (oldestId) {
// Demote to warm cache
const item = this.hotCache.get(oldestId);
this.hotCache.delete(oldestId);
this.ensureWarmCacheCapacity();
this.warmCache.set(oldestId, item);
if (this.trackStats) {
this.stats.demotions.hotToWarm++;
}
}
}
}
/**
* Ensure warm cache has capacity for a new item
*/
ensureWarmCacheCapacity() {
if (this.warmCache.size >= this.warmCacheSize) {
// Find the least recently accessed item in warm cache
let oldestId = null;
let oldestTime = Infinity;
for (const [id, item] of this.warmCache.entries()) {
const accessTime = item.lastAccessedAt ?? item.createdAt;
if (accessTime < oldestTime) {
oldestTime = accessTime;
oldestId = id;
}
}
if (oldestId) {
// Demote to cold storage
const item = this.warmCache.get(oldestId);
this.warmCache.delete(oldestId);
this.coldStorage.set(oldestId, item);
if (this.trackStats) {
this.stats.demotions.warmToCold++;
}
}
}
}
/**
* Update memory statistics
*/
updateStats() {
if (!this.trackStats)
return;
this.stats.tiers.hot.size = this.hotCache.size;
this.stats.tiers.warm.size = this.warmCache.size;
this.stats.tiers.cold.size = this.coldStorage.size;
}
}