// Package memory provides working memory management for current session state. package memory import ( "sync" "time" ) // WorkingMemory manages the current session's active memory state. // It holds recent entries and provides fast access to current context. type WorkingMemory struct { mu sync.RWMutex // SessionID identifies the current session SessionID string // UserID identifies the user UserID string // TurnCount is the number of turns in this session TurnCount int // RecentEntries holds the most recent memory entries RecentEntries []*MemoryEntry // Summary is a condensed summary of older entries Summary string // SizeBytes tracks the current memory size SizeBytes int64 // MaxSizeBytes is the maximum allowed size MaxSizeBytes int64 // CreatedAt is when this working memory was created CreatedAt time.Time // LastAccessedAt is the last access time LastAccessedAt time.Time // MaxEntries limits the number of entries MaxEntries int } // NewWorkingMemory creates a new working memory instance func NewWorkingMemory(sessionID, userID string, maxSizeBytes int64) *WorkingMemory { return &WorkingMemory{ SessionID: sessionID, UserID: userID, TurnCount: 0, RecentEntries: make([]*MemoryEntry, 0, 100), MaxSizeBytes: maxSizeBytes, CreatedAt: time.Now(), LastAccessedAt: time.Now(), MaxEntries: 100, } } // Add adds a new entry to working memory, pruning if necessary func (wm *WorkingMemory) Add(entry *MemoryEntry) { wm.mu.Lock() defer wm.mu.Unlock() wm.RecentEntries = append(wm.RecentEntries, entry) wm.TurnCount++ wm.SizeBytes += int64(len(entry.Content)) wm.LastAccessedAt = time.Now() // Prune if exceeding size limit for wm.SizeBytes > wm.MaxSizeBytes && len(wm.RecentEntries) > 1 { removed := wm.RecentEntries[0] wm.RecentEntries = wm.RecentEntries[1:] wm.SizeBytes -= int64(len(removed.Content)) } // Prune if exceeding entry limit for len(wm.RecentEntries) > wm.MaxEntries { removed := wm.RecentEntries[0] wm.RecentEntries = wm.RecentEntries[1:] wm.SizeBytes -= int64(len(removed.Content)) } } // AddBatch adds multiple entries efficiently func (wm *WorkingMemory) AddBatch(entries []*MemoryEntry) { wm.mu.Lock() defer wm.mu.Unlock() for _, entry := range entries { wm.RecentEntries = append(wm.RecentEntries, entry) wm.TurnCount++ wm.SizeBytes += int64(len(entry.Content)) } wm.LastAccessedAt = time.Now() // Prune after batch add wm.pruneUnlocked() } // pruneUnlocked handles pruning without acquiring locks (caller must hold lock) func (wm *WorkingMemory) pruneUnlocked() { for wm.SizeBytes > wm.MaxSizeBytes && len(wm.RecentEntries) > 1 { removed := wm.RecentEntries[0] wm.RecentEntries = wm.RecentEntries[1:] wm.SizeBytes -= int64(len(removed.Content)) } for len(wm.RecentEntries) > wm.MaxEntries { removed := wm.RecentEntries[0] wm.RecentEntries = wm.RecentEntries[1:] wm.SizeBytes -= int64(len(removed.Content)) } } // GetRecent returns the N most recent entries func (wm *WorkingMemory) GetRecent(n int) []*MemoryEntry { wm.mu.RLock() defer wm.mu.RUnlock() wm.LastAccessedAt = time.Now() if n >= len(wm.RecentEntries) { result := make([]*MemoryEntry, len(wm.RecentEntries)) copy(result, wm.RecentEntries) return result } start := len(wm.RecentEntries) - n result := make([]*MemoryEntry, n) copy(result, wm.RecentEntries[start:]) return result } // GetAll returns all entries in working memory func (wm *WorkingMemory) GetAll() []*MemoryEntry { wm.mu.RLock() defer wm.mu.RUnlock() result := make([]*MemoryEntry, len(wm.RecentEntries)) copy(result, wm.RecentEntries) return result } // GetByType returns entries filtered by memory type func (wm *WorkingMemory) GetByType(memType MemoryType) []*MemoryEntry { wm.mu.RLock() defer wm.mu.RUnlock() var result []*MemoryEntry for _, entry := range wm.RecentEntries { if entry.Type == memType { result = append(result, entry) } } return result } // GetBySource returns entries filtered by source func (wm *WorkingMemory) GetBySource(source string) []*MemoryEntry { wm.mu.RLock() defer wm.mu.RUnlock() var result []*MemoryEntry for _, entry := range wm.RecentEntries { if entry.Source == source { result = append(result, entry) } } return result } // Clear removes all entries from working memory func (wm *WorkingMemory) Clear() { wm.mu.Lock() defer wm.mu.Unlock() wm.RecentEntries = make([]*MemoryEntry, 0, 100) wm.SizeBytes = 0 wm.Summary = "" } // SetSummary updates the summary of older entries func (wm *WorkingMemory) SetSummary(summary string) { wm.mu.Lock() defer wm.mu.Unlock() wm.Summary = summary } // GetSummary returns the current summary func (wm *WorkingMemory) GetSummary() string { wm.mu.RLock() defer wm.mu.RUnlock() return wm.Summary } // Size returns the current size in bytes func (wm *WorkingMemory) Size() int64 { wm.mu.RLock() defer wm.mu.RUnlock() return wm.SizeBytes } // Count returns the number of entries func (wm *WorkingMemory) Count() int { wm.mu.RLock() defer wm.mu.RUnlock() return len(wm.RecentEntries) } // IsStale returns true if the working memory hasn't been accessed recently func (wm *WorkingMemory) IsStale(threshold time.Duration) bool { wm.mu.RLock() defer wm.mu.RUnlock() return time.Since(wm.LastAccessedAt) > threshold } // NeedsConsolidation returns true if memory should be consolidated func (wm *WorkingMemory) NeedsConsolidation(cfg ConsolidationConfig) bool { wm.mu.RLock() defer wm.mu.RUnlock() // Check turn threshold if wm.TurnCount >= cfg.TurnThreshold { return true } // Check time threshold if time.Since(wm.CreatedAt) >= cfg.TimeThreshold { return true } return false } // Stats returns working memory statistics func (wm *WorkingMemory) Stats() WorkingMemoryStats { wm.mu.RLock() defer wm.mu.RUnlock() return WorkingMemoryStats{ SessionID: wm.SessionID, UserID: wm.UserID, TurnCount: wm.TurnCount, EntryCount: len(wm.RecentEntries), SizeBytes: wm.SizeBytes, MaxSizeBytes: wm.MaxSizeBytes, HasSummary: wm.Summary != "", CreatedAt: wm.CreatedAt, LastAccessedAt: wm.LastAccessedAt, AgeSeconds: time.Since(wm.CreatedAt).Seconds(), IdleSeconds: time.Since(wm.LastAccessedAt).Seconds(), UtilizationPct: float64(wm.SizeBytes) / float64(wm.MaxSizeBytes) * 100, } } // WorkingMemoryStats holds statistics about working memory type WorkingMemoryStats struct { SessionID string `json:"session_id"` UserID string `json:"user_id"` TurnCount int `json:"turn_count"` EntryCount int `json:"entry_count"` SizeBytes int64 `json:"size_bytes"` MaxSizeBytes int64 `json:"max_size_bytes"` HasSummary bool `json:"has_summary"` CreatedAt time.Time `json:"created_at"` LastAccessedAt time.Time `json:"last_accessed_at"` AgeSeconds float64 `json:"age_seconds"` IdleSeconds float64 `json:"idle_seconds"` UtilizationPct float64 `json:"utilization_pct"` } // WorkingMemoryStore manages multiple working memory instances type WorkingMemoryStore struct { mu sync.RWMutex memories map[string]*WorkingMemory // keyed by sessionID config *MemoryConfig } // NewWorkingMemoryStore creates a new store for working memories func NewWorkingMemoryStore(config *MemoryConfig) *WorkingMemoryStore { return &WorkingMemoryStore{ memories: make(map[string]*WorkingMemory), config: config, } } // Get retrieves or creates a working memory for a session func (s *WorkingMemoryStore) Get(sessionID, userID string) *WorkingMemory { s.mu.Lock() defer s.mu.Unlock() if wm, exists := s.memories[sessionID]; exists { return wm } wm := NewWorkingMemory(sessionID, userID, s.config.MaxWorkingMemorySize) s.memories[sessionID] = wm return wm } // Remove removes a working memory instance func (s *WorkingMemoryStore) Remove(sessionID string) { s.mu.Lock() defer s.mu.Unlock() delete(s.memories, sessionID) } // GetStale returns session IDs of stale working memories func (s *WorkingMemoryStore) GetStale(threshold time.Duration) []string { s.mu.RLock() defer s.mu.RUnlock() var stale []string for sessionID, wm := range s.memories { if wm.IsStale(threshold) { stale = append(stale, sessionID) } } return stale } // GetNeedingConsolidation returns session IDs that need consolidation func (s *WorkingMemoryStore) GetNeedingConsolidation() []string { s.mu.RLock() defer s.mu.RUnlock() var needConsolidation []string for sessionID, wm := range s.memories { if wm.NeedsConsolidation(s.config.Consolidation) { needConsolidation = append(needConsolidation, sessionID) } } return needConsolidation } // Count returns the number of active working memories func (s *WorkingMemoryStore) Count() int { s.mu.RLock() defer s.mu.RUnlock() return len(s.memories) } // CleanupStale removes stale working memories func (s *WorkingMemoryStore) CleanupStale(threshold time.Duration) int { stale := s.GetStale(threshold) s.mu.Lock() defer s.mu.Unlock() for _, sessionID := range stale { delete(s.memories, sessionID) } return len(stale) } // AllStats returns stats for all working memories func (s *WorkingMemoryStore) AllStats() []WorkingMemoryStats { s.mu.RLock() defer s.mu.RUnlock() stats := make([]WorkingMemoryStats, 0, len(s.memories)) for _, wm := range s.memories { stats = append(stats, wm.Stats()) } return stats }