(ctx context.Context, messages []models.Message)
| 271 | carries what was learned. A bare comma-separated list of names is still accepted.` |
| 272 | |
| 273 | func (mw *memoryWorker) extractAndSave(ctx context.Context, messages []models.Message) error { |
| 274 | if mw.cli.memoryStore == nil { |
| 275 | return fmt.Errorf("memory store not available") |
| 276 | } |
| 277 | |
| 278 | mgr := mw.cli.memoryStore.Manager() |
| 279 | |
| 280 | // Self-evolution piggybacks on this same extraction pass (no extra LLM |
| 281 | // call): when enabled, the prompt asks for SKILL_CANDIDATES alongside the |
| 282 | // memory sections, and we act on them after parsing the response. |
| 283 | evolveMode := resolveSelfEvolveMode() |
| 284 | // Topic threading rides as an appended directive so the base extraction |
| 285 | // prompt constant stays byte-stable (an exported const value change reads as |
| 286 | // an incompatible API change); the parser accepts both formats. |
| 287 | instructions := memory.EnhancedExtractionPromptV3 + "\n" + topicSummaryDirective |
| 288 | if evolveMode != selfEvolveOff { |
| 289 | instructions += "\n" + selfEvolveSkillDirective |
| 290 | // Inject only the compact skill index (names + descriptions), so the |
| 291 | // model can target an existing skill for evolution without any bodies |
| 292 | // bloating the per-turn prompt. The body is pulled on demand at merge. |
| 293 | if idx := mw.cli.buildSkillIndex(); idx != "" { |
| 294 | instructions += "\n\n" + idx |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | // Build conversation snippet for extraction |
| 299 | var sb strings.Builder |
| 300 | for _, msg := range messages { |
| 301 | content := msg.Content |
| 302 | // Truncate very long messages to keep the extraction prompt small |
| 303 | if len(content) > 1500 { |
| 304 | content = content[:1200] + "\n... [truncated] ...\n" + content[len(content)-200:] |
| 305 | } |
| 306 | sb.WriteString(fmt.Sprintf("[%s]: %s\n\n", msg.Role, content)) |
| 307 | } |
| 308 | |
| 309 | // Build enhanced prompt with existing context |
| 310 | var fullPrompt strings.Builder |
| 311 | fullPrompt.WriteString(instructions) |
| 312 | fullPrompt.WriteString("\n\n---\n\n") |
| 313 | |
| 314 | // Include current workspace so the extraction LLM can distinguish session context |
| 315 | if wsDir := mgr.WorkspaceDir(); wsDir != "" { |
| 316 | fullPrompt.WriteString(fmt.Sprintf("CURRENT SESSION WORKSPACE: %s\n", wsDir)) |
| 317 | fullPrompt.WriteString("(All paths and facts from this conversation belong to this workspace.)\n\n---\n\n") |
| 318 | } |
| 319 | |
| 320 | existingContext := mgr.FormatExistingContext() |
| 321 | if existingContext != "" { |
| 322 | fullPrompt.WriteString(existingContext) |
| 323 | fullPrompt.WriteString("\n\n---\n\n") |
| 324 | } |
| 325 | |
| 326 | fullPrompt.WriteString("CONVERSATION SEGMENT TO ANALYZE:\n\n") |
| 327 | fullPrompt.WriteString(sb.String()) |
| 328 | |
| 329 | prompt := fullPrompt.String() |
| 330 |
no test coverage detected