// Package memory provides context window management with smart truncation. package memory import ( "fmt" "strings" "time" ) const ( // MaxContextTokens is the maximum context window size MaxContextTokens = 8192 // SummaryThreshold is when to start summarizing older entries SummaryThreshold = 4000 // DefaultMaxTokens for context building DefaultMaxTokens = 4096 // TokensPerChar is an approximation (1 token ≈ 4 chars for English) TokensPerChar = 0.25 ) // ContextWindow manages the construction of context for LLM prompts type ContextWindow struct { // Entries included in the context Entries []*MemoryEntry // FormattedContext is the final context string FormattedContext string // TotalTokens estimated in the context TotalTokens int // MaxTokens allowed MaxTokens int // IncludedTypes tracks which memory types are represented IncludedTypes map[MemoryType]int // TruncatedCount is how many entries were truncated TruncatedCount int // HasSummary indicates if older entries were summarized HasSummary bool // SummaryText is the summary of older entries SummaryText string } // ContextWindowBuilder builds optimized context windows type ContextWindowBuilder struct { maxTokens int includeMetadata bool includeSources bool includeTimestamp bool formatStyle ContextFormatStyle priorityOrder []MemoryType } // ContextFormatStyle defines how to format entries type ContextFormatStyle int const ( // FormatPlain uses simple text formatting FormatPlain ContextFormatStyle = iota // FormatMarkdown uses markdown formatting FormatMarkdown // FormatXML uses XML-like tags FormatXML // FormatJSON uses JSON formatting FormatJSON ) // NewContextWindowBuilder creates a new builder with defaults func NewContextWindowBuilder() *ContextWindowBuilder { return &ContextWindowBuilder{ maxTokens: DefaultMaxTokens, includeMetadata: false, includeSources: true, includeTimestamp: true, formatStyle: FormatMarkdown, priorityOrder: []MemoryType{ EpisodicMemory, SemanticMemory, ProceduralMemory, TemporalMemory, }, } } // WithMaxTokens sets the maximum token limit func (b *ContextWindowBuilder) WithMaxTokens(max int) *ContextWindowBuilder { b.maxTokens = max return b } // WithMetadata includes entry metadata func (b *ContextWindowBuilder) WithMetadata(include bool) *ContextWindowBuilder { b.includeMetadata = include return b } // WithSources includes source information func (b *ContextWindowBuilder) WithSources(include bool) *ContextWindowBuilder { b.includeSources = include return b } // WithTimestamps includes timestamps func (b *ContextWindowBuilder) WithTimestamps(include bool) *ContextWindowBuilder { b.includeTimestamp = include return b } // WithFormatStyle sets the formatting style func (b *ContextWindowBuilder) WithFormatStyle(style ContextFormatStyle) *ContextWindowBuilder { b.formatStyle = style return b } // WithPriorityOrder sets the memory type priority for inclusion func (b *ContextWindowBuilder) WithPriorityOrder(order []MemoryType) *ContextWindowBuilder { b.priorityOrder = order return b } // Build constructs the context window from entries func (b *ContextWindowBuilder) Build(entries []*MemoryEntry) *ContextWindow { window := &ContextWindow{ MaxTokens: b.maxTokens, IncludedTypes: make(map[MemoryType]int), } if len(entries) == 0 { window.FormattedContext = "" return window } // Sort entries by priority and recency sortedEntries := b.sortByPriority(entries) // Build context with token budget var contextParts []string usedTokens := 0 for _, entry := range sortedEntries { formatted := b.formatEntry(entry) entryTokens := estimateTokens(formatted) if usedTokens+entryTokens > b.maxTokens { window.TruncatedCount++ continue } contextParts = append(contextParts, formatted) window.Entries = append(window.Entries, entry) window.IncludedTypes[entry.Type]++ usedTokens += entryTokens } window.FormattedContext = strings.Join(contextParts, "\n\n") window.TotalTokens = usedTokens return window } // BuildWithSummary builds context with summarization of older entries func (b *ContextWindowBuilder) BuildWithSummary( entries []*MemoryEntry, summarizer func(entries []*MemoryEntry) string, ) *ContextWindow { window := &ContextWindow{ MaxTokens: b.maxTokens, IncludedTypes: make(map[MemoryType]int), } if len(entries) == 0 { window.FormattedContext = "" return window } // Reserve tokens for summary summaryBudget := b.maxTokens / 4 contentBudget := b.maxTokens - summaryBudget // Sort entries by timestamp (newest first) sortedEntries := make([]*MemoryEntry, len(entries)) copy(sortedEntries, entries) // Sort by timestamp descending for i := 0; i < len(sortedEntries)-1; i++ { for j := i + 1; j < len(sortedEntries); j++ { if sortedEntries[j].Timestamp.After(sortedEntries[i].Timestamp) { sortedEntries[i], sortedEntries[j] = sortedEntries[j], sortedEntries[i] } } } // Add recent entries until budget exhausted var recentParts []string var olderEntries []*MemoryEntry usedTokens := 0 for i, entry := range sortedEntries { formatted := b.formatEntry(entry) entryTokens := estimateTokens(formatted) if usedTokens+entryTokens > contentBudget { // Remaining entries go to summary olderEntries = sortedEntries[i:] break } recentParts = append(recentParts, formatted) window.Entries = append(window.Entries, entry) window.IncludedTypes[entry.Type]++ usedTokens += entryTokens } // Generate summary for older entries if len(olderEntries) > 0 && summarizer != nil { summary := summarizer(olderEntries) window.HasSummary = true window.SummaryText = summary window.TruncatedCount = len(olderEntries) // Format summary summaryFormatted := b.formatSummary(summary) usedTokens += estimateTokens(summaryFormatted) // Prepend summary to context window.FormattedContext = summaryFormatted + "\n\n---\n\n" + strings.Join(recentParts, "\n\n") } else { window.FormattedContext = strings.Join(recentParts, "\n\n") } window.TotalTokens = usedTokens return window } // sortByPriority sorts entries by memory type priority and recency func (b *ContextWindowBuilder) sortByPriority(entries []*MemoryEntry) []*MemoryEntry { // Create priority map priorityMap := make(map[MemoryType]int) for i, mt := range b.priorityOrder { priorityMap[mt] = i } sorted := make([]*MemoryEntry, len(entries)) copy(sorted, entries) // Sort by priority, then by timestamp (descending) for i := 0; i < len(sorted)-1; i++ { for j := i + 1; j < len(sorted); j++ { iPriority := priorityMap[sorted[i].Type] jPriority := priorityMap[sorted[j].Type] swap := false if iPriority > jPriority { swap = true } else if iPriority == jPriority { if sorted[j].Timestamp.After(sorted[i].Timestamp) { swap = true } } if swap { sorted[i], sorted[j] = sorted[j], sorted[i] } } } return sorted } // formatEntry formats a single entry based on style func (b *ContextWindowBuilder) formatEntry(entry *MemoryEntry) string { switch b.formatStyle { case FormatMarkdown: return b.formatEntryMarkdown(entry) case FormatXML: return b.formatEntryXML(entry) case FormatJSON: return b.formatEntryJSON(entry) default: return b.formatEntryPlain(entry) } } func (b *ContextWindowBuilder) formatEntryPlain(entry *MemoryEntry) string { var parts []string if b.includeTimestamp { parts = append(parts, fmt.Sprintf("[%s]", entry.Timestamp.Format(time.RFC3339))) } if b.includeSources { parts = append(parts, fmt.Sprintf("(%s/%s)", entry.Type.String(), entry.Source)) } parts = append(parts, entry.Content) return strings.Join(parts, " ") } func (b *ContextWindowBuilder) formatEntryMarkdown(entry *MemoryEntry) string { var sb strings.Builder // Header with type and source sb.WriteString(fmt.Sprintf("### %s Memory", strings.Title(entry.Type.String()))) if b.includeSources && entry.Source != "" { sb.WriteString(fmt.Sprintf(" (%s)", entry.Source)) } sb.WriteString("\n") if b.includeTimestamp { sb.WriteString(fmt.Sprintf("*%s*\n\n", entry.Timestamp.Format("Jan 2, 2006 3:04 PM"))) } sb.WriteString(entry.Content) if b.includeMetadata && len(entry.Tags) > 0 { sb.WriteString(fmt.Sprintf("\n\n**Tags**: %s", strings.Join(entry.Tags, ", "))) } return sb.String() } func (b *ContextWindowBuilder) formatEntryXML(entry *MemoryEntry) string { var sb strings.Builder sb.WriteString(fmt.Sprintf("\n") sb.WriteString(entry.Content) sb.WriteString("\n") return sb.String() } func (b *ContextWindowBuilder) formatEntryJSON(entry *MemoryEntry) string { // Simple JSON-like format without full JSON marshaling var parts []string parts = append(parts, fmt.Sprintf(`"type": "%s"`, entry.Type.String())) if b.includeSources { parts = append(parts, fmt.Sprintf(`"source": "%s"`, entry.Source)) } if b.includeTimestamp { parts = append(parts, fmt.Sprintf(`"timestamp": "%s"`, entry.Timestamp.Format(time.RFC3339))) } parts = append(parts, fmt.Sprintf(`"content": "%s"`, escapeJSON(entry.Content))) return "{\n " + strings.Join(parts, ",\n ") + "\n}" } func (b *ContextWindowBuilder) formatSummary(summary string) string { switch b.formatStyle { case FormatMarkdown: return fmt.Sprintf("## Earlier Context Summary\n\n%s", summary) case FormatXML: return fmt.Sprintf("\n%s\n", summary) default: return fmt.Sprintf("[Summary of earlier context: %s]", summary) } } // estimateTokens estimates the token count for a string func estimateTokens(s string) int { // Rough approximation: 1 token ≈ 4 characters for English text // This is a simplified heuristic; production should use tiktoken return int(float64(len(s)) * TokensPerChar) } func escapeJSON(s string) string { s = strings.ReplaceAll(s, `\`, `\\`) s = strings.ReplaceAll(s, `"`, `\"`) s = strings.ReplaceAll(s, "\n", `\n`) s = strings.ReplaceAll(s, "\r", `\r`) s = strings.ReplaceAll(s, "\t", `\t`) return s } // DynamicContextSizer adjusts context limits based on query complexity type DynamicContextSizer struct { minTokens int maxTokens int } // NewDynamicContextSizer creates a new dynamic sizer func NewDynamicContextSizer(min, max int) *DynamicContextSizer { return &DynamicContextSizer{ minTokens: min, maxTokens: max, } } // GetLimit returns the appropriate token limit based on query complexity func (s *DynamicContextSizer) GetLimit(query string, complexity float64) int { // Complexity range: 0.0 (simple) to 1.0 (complex) // Simple queries get smaller context if complexity < 0.3 { return s.minTokens + int(float64(s.maxTokens-s.minTokens)*0.25) } // Complex queries get larger context if complexity > 0.7 { return s.minTokens + int(float64(s.maxTokens-s.minTokens)*0.75) } // Medium complexity return s.minTokens + int(float64(s.maxTokens-s.minTokens)*0.5) } // EstimateQueryComplexity provides a simple heuristic for query complexity func EstimateQueryComplexity(query string) float64 { // Factors that increase complexity: // - Question length // - Number of clauses (commas, "and", "or") // - Presence of technical terms // - Temporal references // - Comparison requests score := 0.0 // Length factor wordCount := len(strings.Fields(query)) if wordCount > 20 { score += 0.3 } else if wordCount > 10 { score += 0.15 } // Clause complexity clauseIndicators := []string{",", " and ", " or ", " but ", " however "} for _, indicator := range clauseIndicators { if strings.Contains(strings.ToLower(query), indicator) { score += 0.1 } } // Temporal references temporalTerms := []string{"yesterday", "last week", "before", "after", "when", "since", "until"} for _, term := range temporalTerms { if strings.Contains(strings.ToLower(query), term) { score += 0.1 break } } // Comparison/analysis requests analysisTerms := []string{"compare", "difference", "why", "how", "explain", "analyze"} for _, term := range analysisTerms { if strings.Contains(strings.ToLower(query), term) { score += 0.15 break } } // Cap at 1.0 if score > 1.0 { score = 1.0 } return score } // ContextWindowStats provides statistics about a context window type ContextWindowStats struct { TotalEntries int `json:"total_entries"` IncludedEntries int `json:"included_entries"` TruncatedEntries int `json:"truncated_entries"` TotalTokens int `json:"total_tokens"` MaxTokens int `json:"max_tokens"` UtilizationPct float64 `json:"utilization_pct"` TypeDistribution map[string]int `json:"type_distribution"` HasSummary bool `json:"has_summary"` } // Stats returns statistics for a context window func (w *ContextWindow) Stats() ContextWindowStats { typeDist := make(map[string]int) for mt, count := range w.IncludedTypes { typeDist[mt.String()] = count } return ContextWindowStats{ TotalEntries: len(w.Entries) + w.TruncatedCount, IncludedEntries: len(w.Entries), TruncatedEntries: w.TruncatedCount, TotalTokens: w.TotalTokens, MaxTokens: w.MaxTokens, UtilizationPct: float64(w.TotalTokens) / float64(w.MaxTokens) * 100, TypeDistribution: typeDist, HasSummary: w.HasSummary, } }