|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "os" |
| 8 | + "os/signal" |
| 9 | + "path/filepath" |
| 10 | + "strings" |
| 11 | + "time" |
| 12 | + |
| 13 | + "github.com/appsprout-dev/mnemonic/internal/agent/dreaming" |
| 14 | + "github.com/appsprout-dev/mnemonic/internal/agent/encoding" |
| 15 | + "github.com/appsprout-dev/mnemonic/internal/agent/metacognition" |
| 16 | + "github.com/appsprout-dev/mnemonic/internal/agent/orchestrator" |
| 17 | + "github.com/appsprout-dev/mnemonic/internal/agent/retrieval" |
| 18 | + "github.com/appsprout-dev/mnemonic/internal/config" |
| 19 | + "github.com/appsprout-dev/mnemonic/internal/events" |
| 20 | + "github.com/appsprout-dev/mnemonic/internal/mcp" |
| 21 | +) |
| 22 | + |
| 23 | +// metaCycleCommand runs a single metacognition cycle and displays results. |
| 24 | +func metaCycleCommand(configPath string) { |
| 25 | + cfg, db, llmProvider, log := initRuntime(configPath) |
| 26 | + defer func() { _ = db.Close() }() |
| 27 | + |
| 28 | + ctx := context.Background() |
| 29 | + bus := events.NewInMemoryBus(100) |
| 30 | + defer func() { _ = bus.Close() }() |
| 31 | + |
| 32 | + agent := metacognition.NewMetacognitionAgent(db, llmProvider, metacognition.MetacognitionConfig{ |
| 33 | + Interval: 24 * time.Hour, // doesn't matter for RunOnce |
| 34 | + ReflectionLookback: cfg.Metacognition.ReflectionLookback, |
| 35 | + DeadMemoryWindow: cfg.Metacognition.DeadMemoryWindow, |
| 36 | + }, log) |
| 37 | + |
| 38 | + fmt.Println("Running metacognition cycle...") |
| 39 | + |
| 40 | + report, err := agent.RunOnce(ctx) |
| 41 | + if err != nil { |
| 42 | + fmt.Fprintf(os.Stderr, "Metacognition cycle failed: %v\n", err) |
| 43 | + os.Exit(1) |
| 44 | + } |
| 45 | + |
| 46 | + fmt.Printf("%sMetacognition complete%s (%dms):\n", colorGreen, colorReset, report.Duration.Milliseconds()) |
| 47 | + |
| 48 | + if len(report.Observations) == 0 { |
| 49 | + fmt.Println(" No issues found — memory health looks good.") |
| 50 | + return |
| 51 | + } |
| 52 | + |
| 53 | + fmt.Printf(" %d observation(s):\n\n", len(report.Observations)) |
| 54 | + for _, obs := range report.Observations { |
| 55 | + severityColor := colorGray |
| 56 | + switch obs.Severity { |
| 57 | + case "warning": |
| 58 | + severityColor = colorYellow |
| 59 | + case "critical": |
| 60 | + severityColor = colorRed |
| 61 | + case "info": |
| 62 | + severityColor = colorCyan |
| 63 | + } |
| 64 | + |
| 65 | + typeLabel := strings.ReplaceAll(obs.ObservationType, "_", " ") |
| 66 | + typeLabel = strings.ToUpper(typeLabel[:1]) + typeLabel[1:] |
| 67 | + |
| 68 | + fmt.Printf(" %s[%s]%s %s\n", severityColor, strings.ToUpper(obs.Severity), colorReset, typeLabel) |
| 69 | + for key, val := range obs.Details { |
| 70 | + keyLabel := strings.ReplaceAll(key, "_", " ") |
| 71 | + fmt.Printf(" %s: %v\n", keyLabel, val) |
| 72 | + } |
| 73 | + fmt.Println() |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +// dreamCycleCommand runs a single dream cycle and displays results. |
| 78 | +func dreamCycleCommand(configPath string) { |
| 79 | + cfg, db, llmProvider, log := initRuntime(configPath) |
| 80 | + defer func() { _ = db.Close() }() |
| 81 | + |
| 82 | + ctx := context.Background() |
| 83 | + bus := events.NewInMemoryBus(100) |
| 84 | + defer func() { _ = bus.Close() }() |
| 85 | + |
| 86 | + agent := dreaming.NewDreamingAgent(db, llmProvider, dreaming.DreamingConfig{ |
| 87 | + Interval: 3 * time.Hour, // doesn't matter for RunOnce |
| 88 | + BatchSize: cfg.Dreaming.BatchSize, |
| 89 | + SalienceThreshold: cfg.Dreaming.SalienceThreshold, |
| 90 | + AssociationBoostFactor: cfg.Dreaming.AssociationBoostFactor, |
| 91 | + NoisePruneThreshold: cfg.Dreaming.NoisePruneThreshold, |
| 92 | + DeadMemoryWindow: cfg.Dreaming.DeadMemoryWindow, |
| 93 | + InsightsBudget: cfg.Dreaming.InsightsBudget, |
| 94 | + DefaultConfidence: cfg.Dreaming.DefaultConfidence, |
| 95 | + }, log) |
| 96 | + |
| 97 | + fmt.Println("Running dream cycle (memory replay)...") |
| 98 | + |
| 99 | + report, err := agent.RunOnce(ctx) |
| 100 | + if err != nil { |
| 101 | + fmt.Fprintf(os.Stderr, "Dream cycle failed: %v\n", err) |
| 102 | + os.Exit(1) |
| 103 | + } |
| 104 | + |
| 105 | + fmt.Printf("%sDream cycle complete%s (%dms):\n", colorGreen, colorReset, report.Duration.Milliseconds()) |
| 106 | + fmt.Printf(" Memories replayed: %d\n", report.MemoriesReplayed) |
| 107 | + fmt.Printf(" Associations strengthened: %d\n", report.AssociationsStrengthened) |
| 108 | + fmt.Printf(" New associations created: %d\n", report.NewAssociationsCreated) |
| 109 | + fmt.Printf(" Noisy memories demoted: %d\n", report.NoisyMemoriesDemoted) |
| 110 | +} |
| 111 | + |
| 112 | +// mcpCommand runs the MCP server on stdin/stdout for AI agent integration. |
| 113 | +func mcpCommand(configPath string) { |
| 114 | + cfg, db, llmProvider, log := initRuntime(configPath) |
| 115 | + defer func() { _ = db.Close() }() |
| 116 | + |
| 117 | + ctx, cancel := context.WithCancel(context.Background()) |
| 118 | + defer cancel() |
| 119 | + |
| 120 | + bus := events.NewInMemoryBus(100) |
| 121 | + defer func() { _ = bus.Close() }() |
| 122 | + |
| 123 | + // Create encoding agent so remembered memories get encoded. |
| 124 | + // Polling is disabled in MCP mode — each MCP process only encodes via events |
| 125 | + // for memories it creates. The daemon is the sole poller. This prevents N |
| 126 | + // MCP processes from independently encoding the same unprocessed raw memories. |
| 127 | + mcpEncodingCfg := buildEncodingConfig(cfg) |
| 128 | + mcpEncodingCfg.DisablePolling = true |
| 129 | + encoder := encoding.NewEncodingAgentWithConfig(db, llmProvider, log, mcpEncodingCfg) |
| 130 | + if err := encoder.Start(ctx, bus); err != nil { |
| 131 | + log.Error("failed to start encoding agent for MCP", "error", err) |
| 132 | + } |
| 133 | + defer func() { _ = encoder.Stop() }() |
| 134 | + |
| 135 | + // Create retrieval agent for recall |
| 136 | + retriever := retrieval.NewRetrievalAgent(db, llmProvider, buildRetrievalConfig(cfg), log, bus) |
| 137 | + |
| 138 | + mcpResolver := config.NewProjectResolver(cfg.Projects) |
| 139 | + daemonURL := fmt.Sprintf("http://%s:%d", cfg.API.Host, cfg.API.Port) |
| 140 | + memDefaults := mcp.MemoryDefaults{ |
| 141 | + SalienceGeneral: cfg.MemoryDefaults.InitialSalienceGeneral, |
| 142 | + SalienceDecision: cfg.MemoryDefaults.InitialSalienceDecision, |
| 143 | + SalienceError: cfg.MemoryDefaults.InitialSalienceError, |
| 144 | + SalienceInsight: cfg.MemoryDefaults.InitialSalienceInsight, |
| 145 | + SalienceLearning: cfg.MemoryDefaults.InitialSalienceLearning, |
| 146 | + SalienceHandoff: cfg.MemoryDefaults.InitialSalienceHandoff, |
| 147 | + FeedbackStrengthDelta: cfg.MemoryDefaults.FeedbackStrengthDelta, |
| 148 | + FeedbackSalienceBoost: cfg.MemoryDefaults.FeedbackSalienceBoost, |
| 149 | + } |
| 150 | + server := mcp.NewMCPServer(db, retriever, bus, log, Version, cfg.Coaching.CoachingFile, cfg.Perception.Filesystem.ExcludePatterns, cfg.Perception.Filesystem.MaxContentBytes, mcpResolver, daemonURL, memDefaults) |
| 151 | + |
| 152 | + // Handle signal for graceful shutdown |
| 153 | + sigChan := make(chan os.Signal, 1) |
| 154 | + signal.Notify(sigChan, shutdownSignals()...) |
| 155 | + go func() { |
| 156 | + <-sigChan |
| 157 | + cancel() |
| 158 | + }() |
| 159 | + |
| 160 | + if err := server.Run(ctx); err != nil { |
| 161 | + fmt.Fprintf(os.Stderr, "MCP server error: %v\n", err) |
| 162 | + os.Exit(1) |
| 163 | + } |
| 164 | +} |
| 165 | + |
| 166 | +// autopilotCommand shows what the system has been doing autonomously. |
| 167 | +func autopilotCommand(configPath string) { |
| 168 | + _, db, _, _ := initRuntime(configPath) |
| 169 | + defer func() { _ = db.Close() }() |
| 170 | + |
| 171 | + ctx := context.Background() |
| 172 | + |
| 173 | + // Read health report |
| 174 | + homeDir, _ := os.UserHomeDir() |
| 175 | + healthPath := filepath.Join(homeDir, ".mnemonic", "health.json") |
| 176 | + data, err := os.ReadFile(healthPath) |
| 177 | + |
| 178 | + fmt.Println("=== Mnemonic Autopilot Report ===") |
| 179 | + fmt.Println() |
| 180 | + |
| 181 | + if err == nil { |
| 182 | + var report orchestrator.HealthReport |
| 183 | + if json.Unmarshal(data, &report) == nil { |
| 184 | + fmt.Printf("Last report: %s\n", report.Timestamp.Format("2006-01-02 15:04:05")) |
| 185 | + fmt.Printf("Uptime: %s\n", report.Uptime) |
| 186 | + fmt.Printf("LLM available: %v\n", report.LLMAvailable) |
| 187 | + fmt.Printf("Store healthy: %v\n", report.StoreHealthy) |
| 188 | + fmt.Printf("Memories: %d\n", report.MemoryCount) |
| 189 | + fmt.Printf("Patterns: %d\n", report.PatternCount) |
| 190 | + fmt.Printf("Abstractions: %d\n", report.AbstractionCount) |
| 191 | + fmt.Printf("Last consolidation: %s\n", report.LastConsolidation) |
| 192 | + fmt.Printf("Autonomous actions: %d\n", report.AutonomousActions) |
| 193 | + |
| 194 | + if len(report.Warnings) > 0 { |
| 195 | + fmt.Println() |
| 196 | + fmt.Println("Warnings:") |
| 197 | + for _, w := range report.Warnings { |
| 198 | + fmt.Printf(" - %s\n", w) |
| 199 | + } |
| 200 | + } |
| 201 | + } |
| 202 | + } else { |
| 203 | + fmt.Println("No health report found. Start the daemon to generate one.") |
| 204 | + } |
| 205 | + |
| 206 | + // Show recent autonomous actions |
| 207 | + fmt.Println() |
| 208 | + fmt.Println("--- Recent Autonomous Actions ---") |
| 209 | + actions, err := db.ListMetaObservations(ctx, "autonomous_action", 10) |
| 210 | + if err == nil && len(actions) > 0 { |
| 211 | + for _, a := range actions { |
| 212 | + action := "" |
| 213 | + if act, ok := a.Details["action"].(string); ok { |
| 214 | + action = act |
| 215 | + } |
| 216 | + fmt.Printf(" [%s] %s (severity: %s)\n", |
| 217 | + a.CreatedAt.Format("2006-01-02 15:04"), action, a.Severity) |
| 218 | + } |
| 219 | + } else { |
| 220 | + fmt.Println(" No autonomous actions recorded yet.") |
| 221 | + } |
| 222 | + |
| 223 | + // Show recent patterns discovered |
| 224 | + fmt.Println() |
| 225 | + fmt.Println("--- Discovered Patterns ---") |
| 226 | + patterns, err := db.ListPatterns(ctx, "", 5) |
| 227 | + if err == nil && len(patterns) > 0 { |
| 228 | + for _, p := range patterns { |
| 229 | + project := "" |
| 230 | + if p.Project != "" { |
| 231 | + project = fmt.Sprintf(" [%s]", p.Project) |
| 232 | + } |
| 233 | + fmt.Printf(" %s%s: %s (strength: %.2f, evidence: %d)\n", |
| 234 | + p.Title, project, p.Description, p.Strength, len(p.EvidenceIDs)) |
| 235 | + } |
| 236 | + } else { |
| 237 | + fmt.Println(" No patterns discovered yet.") |
| 238 | + } |
| 239 | + |
| 240 | + // Show abstractions |
| 241 | + fmt.Println() |
| 242 | + fmt.Println("--- Abstractions ---") |
| 243 | + hasAbstractions := false |
| 244 | + for _, level := range []int{2, 3} { |
| 245 | + abs, err := db.ListAbstractions(ctx, level, 5) |
| 246 | + if err == nil && len(abs) > 0 { |
| 247 | + hasAbstractions = true |
| 248 | + for _, a := range abs { |
| 249 | + levelLabel := "principle" |
| 250 | + if a.Level == 3 { |
| 251 | + levelLabel = "axiom" |
| 252 | + } |
| 253 | + fmt.Printf(" [%s] %s: %s (confidence: %.2f)\n", |
| 254 | + levelLabel, a.Title, a.Description, a.Confidence) |
| 255 | + } |
| 256 | + } |
| 257 | + } |
| 258 | + if !hasAbstractions { |
| 259 | + fmt.Println(" No abstractions generated yet.") |
| 260 | + } |
| 261 | + |
| 262 | + fmt.Println() |
| 263 | +} |
0 commit comments