RunWithLLM performs LLM-assisted compaction.
(ctx context.Context, sendPrompt func(ctx context.Context, prompt string) (string, error))
| 102 | |
| 103 | // RunWithLLM performs LLM-assisted compaction. |
| 104 | func (c *Compactor) RunWithLLM(ctx context.Context, sendPrompt func(ctx context.Context, prompt string) (string, error)) error { |
| 105 | c.logger.Info("Starting memory compaction (LLM-assisted)") |
| 106 | |
| 107 | facts := c.facts.GetAll() |
| 108 | if len(facts) < 10 { |
| 109 | c.logger.Debug("Too few facts for compaction", zap.Int("count", len(facts))) |
| 110 | c.markCompacted() |
| 111 | return nil |
| 112 | } |
| 113 | |
| 114 | // Build fact list for LLM |
| 115 | var sb strings.Builder |
| 116 | for i, f := range facts { |
| 117 | sb.WriteString(fmt.Sprintf("%d. [%s] %s (score: %.2f, accessed: %d times)\n", |
| 118 | i+1, f.Category, f.Content, f.Score, f.AccessCount)) |
| 119 | } |
| 120 | |
| 121 | prompt := compactionPrompt + "\n\n---\n\nCURRENT FACTS (" + |
| 122 | fmt.Sprintf("%d", len(facts)) + " total):\n\n" + sb.String() |
| 123 | |
| 124 | response, err := sendPrompt(ctx, prompt) |
| 125 | if err != nil { |
| 126 | // Fall back to score-based pruning |
| 127 | c.logger.Warn("LLM compaction failed, falling back to score-based pruning", zap.Error(err)) |
| 128 | return c.RunScoreBased() |
| 129 | } |
| 130 | |
| 131 | // Parse the LLM response |
| 132 | consolidated := c.parseCompactionResponse(response, facts) |
| 133 | if len(consolidated) == 0 { |
| 134 | c.logger.Warn("LLM returned empty compaction, keeping original facts") |
| 135 | c.markCompacted() |
| 136 | return nil |
| 137 | } |
| 138 | |
| 139 | // Shrink guard: consolidation legitimately merges near-duplicates, but a |
| 140 | // result that keeps under half the facts is far more likely a truncated |
| 141 | // model answer than a real cleanup. Refuse it — memory loss is the one |
| 142 | // failure this subsystem must never have — and curate conservatively. |
| 143 | if float64(len(consolidated)) < float64(len(facts))*compactionMinKeepRatio { |
| 144 | c.logger.Warn("LLM compaction kept too few facts — rejecting as truncated output", |
| 145 | zap.Int("before", len(facts)), |
| 146 | zap.Int("after", len(consolidated))) |
| 147 | return c.RunScoreBased() |
| 148 | } |
| 149 | |
| 150 | // Archive whatever the consolidation dropped BEFORE replacing, so every |
| 151 | // removed fact stays individually recoverable from memory_archive.json. |
| 152 | kept := make(map[string]struct{}, len(consolidated)) |
| 153 | for _, f := range consolidated { |
| 154 | kept[f.ID] = struct{}{} |
| 155 | } |
| 156 | var dropped []*Fact |
| 157 | for _, f := range facts { |
| 158 | if _, ok := kept[f.ID]; !ok { |
| 159 | dropped = append(dropped, f) |
| 160 | } |
| 161 | } |