Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions cmd/mnemonic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1381,10 +1381,16 @@ func serveCommand(configPath string) {
// --- Start episoding agent (groups raw events into episodes) ---
var episodingAgent *episoding.EpisodingAgent
if cfg.Episoding.Enabled {
pollingInterval := time.Duration(cfg.Episoding.PollingIntervalSec) * time.Second
if pollingInterval <= 0 {
pollingInterval = 10 * time.Second
}
episodingCfg := episoding.EpisodingConfig{
EpisodeWindowSizeMin: cfg.Episoding.EpisodeWindowSizeMin,
MinEventsPerEpisode: cfg.Episoding.MinEventsPerEpisode,
PollingInterval: 10 * time.Second,
PollingInterval: pollingInterval,
StartupLookback: cfg.Episoding.StartupLookback,
DefaultSalience: cfg.Episoding.DefaultSalience,
}
episodingAgent = episoding.NewEpisodingAgent(memStore, wrap("episoding"), log, episodingCfg)
if err := episodingAgent.Start(rootCtx, bus); err != nil {
Expand Down Expand Up @@ -1539,7 +1545,10 @@ func serveCommand(configPath string) {
var metaAgent *metacognition.MetacognitionAgent
if cfg.Metacognition.Enabled {
metaAgent = metacognition.NewMetacognitionAgent(memStore, wrap("metacognition"), metacognition.MetacognitionConfig{
Interval: cfg.Metacognition.Interval,
Interval: cfg.Metacognition.Interval,
StartupDelay: time.Duration(cfg.Metacognition.StartupDelaySec) * time.Second,
ReflectionLookback: cfg.Metacognition.ReflectionLookback,
DeadMemoryWindow: cfg.Metacognition.DeadMemoryWindow,
}, log)

if err := metaAgent.Start(rootCtx, bus); err != nil {
Expand All @@ -1558,6 +1567,10 @@ func serveCommand(configPath string) {
SalienceThreshold: cfg.Dreaming.SalienceThreshold,
AssociationBoostFactor: cfg.Dreaming.AssociationBoostFactor,
NoisePruneThreshold: cfg.Dreaming.NoisePruneThreshold,
StartupDelay: time.Duration(cfg.Dreaming.StartupDelaySec) * time.Second,
DeadMemoryWindow: cfg.Dreaming.DeadMemoryWindow,
InsightsBudget: cfg.Dreaming.InsightsBudget,
DefaultConfidence: cfg.Dreaming.DefaultConfidence,
}, log)

if err := dreamer.Start(rootCtx, bus); err != nil {
Expand All @@ -1571,9 +1584,16 @@ func serveCommand(configPath string) {
var abstractionAgent *abstraction.AbstractionAgent
if cfg.Abstraction.Enabled {
abstractionAgent = abstraction.NewAbstractionAgent(memStore, wrap("abstraction"), abstraction.AbstractionConfig{
Interval: cfg.Abstraction.Interval,
MinStrength: cfg.Abstraction.MinStrength,
MaxLLMCalls: cfg.Abstraction.MaxLLMCalls,
Interval: cfg.Abstraction.Interval,
MinStrength: cfg.Abstraction.MinStrength,
MaxLLMCalls: cfg.Abstraction.MaxLLMCalls,
StartupDelay: time.Duration(cfg.Abstraction.StartupDelaySec) * time.Second,
DefaultConfidence: cfg.Abstraction.DefaultConfidence,
PatternAxiomConfidence: cfg.Abstraction.PatternAxiomConfidence,
ConfidenceModerateDecay: cfg.Abstraction.ConfidenceModerateDecay,
ConfidenceSignificantDecay: cfg.Abstraction.ConfidenceSignificantDecay,
ConfidenceSevereDecay: cfg.Abstraction.ConfidenceSevereDecay,
GroundingFloor: cfg.Abstraction.GroundingFloor,
}, log)

if err := abstractionAgent.Start(rootCtx, bus); err != nil {
Expand Down Expand Up @@ -2502,15 +2522,17 @@ func formatDetailValue(val interface{}) string {

// metaCycleCommand runs a single metacognition cycle and displays results.
func metaCycleCommand(configPath string) {
_, db, llmProvider, log := initRuntime(configPath)
cfg, db, llmProvider, log := initRuntime(configPath)
defer func() { _ = db.Close() }()

ctx := context.Background()
bus := events.NewInMemoryBus(100)
defer func() { _ = bus.Close() }()

agent := metacognition.NewMetacognitionAgent(db, llmProvider, metacognition.MetacognitionConfig{
Interval: 24 * time.Hour, // doesn't matter for RunOnce
Interval: 24 * time.Hour, // doesn't matter for RunOnce
ReflectionLookback: cfg.Metacognition.ReflectionLookback,
DeadMemoryWindow: cfg.Metacognition.DeadMemoryWindow,
}, log)

fmt.Println("Running metacognition cycle...")
Expand Down Expand Up @@ -2567,6 +2589,9 @@ func dreamCycleCommand(configPath string) {
SalienceThreshold: cfg.Dreaming.SalienceThreshold,
AssociationBoostFactor: cfg.Dreaming.AssociationBoostFactor,
NoisePruneThreshold: cfg.Dreaming.NoisePruneThreshold,
DeadMemoryWindow: cfg.Dreaming.DeadMemoryWindow,
InsightsBudget: cfg.Dreaming.InsightsBudget,
DefaultConfidence: cfg.Dreaming.DefaultConfidence,
}, log)

fmt.Println("Running dream cycle (memory replay)...")
Expand Down
62 changes: 49 additions & 13 deletions internal/agent/abstraction/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@ import (
)

type AbstractionConfig struct {
Interval time.Duration
MinStrength float32
MaxLLMCalls int
Interval time.Duration
MinStrength float32
MaxLLMCalls int
StartupDelay time.Duration
DefaultConfidence float32
PatternAxiomConfidence float32
ConfidenceModerateDecay float32
ConfidenceSignificantDecay float32
ConfidenceSevereDecay float32
GroundingFloor float32
}

type AbstractionAgent struct {
Expand Down Expand Up @@ -94,8 +101,11 @@ func (aa *AbstractionAgent) RunOnce(ctx context.Context) (*CycleReport, error) {
func (aa *AbstractionAgent) loop() {
defer aa.wg.Done()

// 5-minute startup grace period (runs less frequently than other agents)
startupTimer := time.NewTimer(5 * time.Minute)
startupDelay := aa.config.StartupDelay
if startupDelay <= 0 {
startupDelay = 5 * time.Minute
}
startupTimer := time.NewTimer(startupDelay)
defer startupTimer.Stop()

ticker := time.NewTicker(aa.config.Interval)
Expand Down Expand Up @@ -392,30 +402,48 @@ func (aa *AbstractionAgent) verifyGrounding(ctx context.Context, report *CycleRe
continue
}

// Load grounding multipliers from config with safe defaults
moderateDecay := aa.config.ConfidenceModerateDecay
if moderateDecay <= 0 {
moderateDecay = 0.9
}
significantDecay := aa.config.ConfidenceSignificantDecay
if significantDecay <= 0 {
significantDecay = 0.7
}
severeDecay := aa.config.ConfidenceSevereDecay
if severeDecay <= 0 {
severeDecay = 0.5
}
groundingFloor := aa.config.GroundingFloor
if groundingFloor <= 0 {
groundingFloor = 0.5
}

// Graduated grounding response
switch {
case groundingRatio >= 0.5:
// Healthy grounding, no action needed
continue
case groundingRatio >= 0.3:
// Moderate decay: reduce confidence slightly
abs.Confidence *= 0.9
abs.Confidence *= moderateDecay
case groundingRatio >= 0.1:
// Significant decay: reduce confidence more
abs.Confidence *= 0.7
abs.Confidence *= significantDecay
report.AbstractionsDemoted++
default:
// Nearly all evidence gone: softened demotion (was 0.3, now 0.5)
abs.Confidence *= 0.5
// Nearly all evidence gone
abs.Confidence *= severeDecay
if abs.Confidence < 0.1 {
abs.State = "fading"
}
report.AbstractionsDemoted++
}

// Enforce grace period floor for young abstractions
if isYoung && abs.Confidence < 0.5 {
abs.Confidence = 0.5
if isYoung && abs.Confidence < groundingFloor {
abs.Confidence = groundingFloor
}

abs.UpdatedAt = time.Now()
Expand Down Expand Up @@ -518,9 +546,13 @@ Set has_principle to false if:
concepts = agentutil.DeduplicateConcepts(allConcepts)
}

defaultConf := aa.config.DefaultConfidence
if defaultConf <= 0 {
defaultConf = 0.6
}
confidence := float32(result.Confidence)
if confidence <= 0 || confidence > 1.0 {
confidence = 0.6
confidence = defaultConf
}

return &store.Abstraction{
Expand Down Expand Up @@ -625,9 +657,13 @@ Set has_axiom to false if:
concepts = agentutil.DeduplicateConcepts(allConcepts)
}

axiomConf := aa.config.PatternAxiomConfidence
if axiomConf <= 0 {
axiomConf = 0.5
}
confidence := float32(result.Confidence)
if confidence <= 0 || confidence > 1.0 {
confidence = 0.5
confidence = axiomConf
}

return &store.Abstraction{
Expand Down
28 changes: 23 additions & 5 deletions internal/agent/dreaming/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ type DreamingConfig struct {
SalienceThreshold float32
AssociationBoostFactor float32
NoisePruneThreshold float32
StartupDelay time.Duration
DeadMemoryWindow time.Duration
InsightsBudget int
DefaultConfidence float32
}

type DreamingAgent struct {
Expand Down Expand Up @@ -96,8 +100,11 @@ func (da *DreamingAgent) RunOnce(ctx context.Context) (*DreamReport, error) {
func (da *DreamingAgent) loop() {
defer da.wg.Done()

// 90-second startup grace period
startupTimer := time.NewTimer(90 * time.Second)
startupDelay := da.config.StartupDelay
if startupDelay <= 0 {
startupDelay = 90 * time.Second
}
startupTimer := time.NewTimer(startupDelay)
defer startupTimer.Stop()

ticker := time.NewTicker(da.config.Interval)
Expand Down Expand Up @@ -313,7 +320,11 @@ func (da *DreamingAgent) crossPollinate(ctx context.Context, replayed []store.Me

// noisePrune performs Phase 4: reduce salience of low-quality dead memories.
func (da *DreamingAgent) noisePrune(ctx context.Context, report *DreamReport) error {
deadMemories, err := da.store.GetDeadMemories(ctx, time.Now().Add(-30*24*time.Hour))
deadWindow := da.config.DeadMemoryWindow
if deadWindow <= 0 {
deadWindow = 30 * 24 * time.Hour
}
deadMemories, err := da.store.GetDeadMemories(ctx, time.Now().Add(-deadWindow))
if err != nil {
return fmt.Errorf("failed to get dead memories: %w", err)
}
Expand Down Expand Up @@ -496,7 +507,10 @@ func (da *DreamingAgent) generateInsights(ctx context.Context, replayed []store.
return nil
}

insightsBudget := 2
insightsBudget := da.config.InsightsBudget
if insightsBudget <= 0 {
insightsBudget = 2
}
for _, cluster := range clusters {
if insightsBudget <= 0 {
break
Expand Down Expand Up @@ -641,9 +655,13 @@ Only share an insight if it's genuinely illuminating — something that makes yo
concepts = agentutil.DeduplicateConcepts(allConcepts)
}

defaultConf := da.config.DefaultConfidence
if defaultConf <= 0 {
defaultConf = 0.6
}
confidence := float32(result.Confidence)
if confidence <= 0 || confidence > 1.0 {
confidence = 0.6
confidence = defaultConf
}

abstraction := &store.Abstraction{
Expand Down
18 changes: 16 additions & 2 deletions internal/agent/episoding/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type EpisodingConfig struct {
EpisodeWindowSizeMin int // fixed window size in minutes (default 10)
MinEventsPerEpisode int // minimum events to form an episode (default 2)
PollingInterval time.Duration // how often to check for new events (default 10s)
StartupLookback time.Duration // how far back to look on startup (default 1h)
DefaultSalience float32 // fallback salience for synthesized episodes (default 0.5)
}

// DefaultEpisodingConfig returns sensible defaults.
Expand Down Expand Up @@ -53,12 +55,16 @@ type EpisodingAgent struct {

// NewEpisodingAgent creates a new episoding agent.
func NewEpisodingAgent(s store.Store, llmProvider llm.Provider, log *slog.Logger, cfg EpisodingConfig) *EpisodingAgent {
lookback := cfg.StartupLookback
if lookback <= 0 {
lookback = 1 * time.Hour
}
return &EpisodingAgent{
store: s,
llmProvider: llmProvider,
config: cfg,
log: log,
lastProcessedTime: time.Now().Add(-1 * time.Hour), // look back 1 hour on start
lastProcessedTime: time.Now().Add(-lookback),
assignedRawIDs: make(map[string]bool),
}
}
Expand All @@ -67,6 +73,14 @@ func (ea *EpisodingAgent) Name() string {
return "episoding"
}

// defaultSalience returns the configured default salience, falling back to 0.5.
func (ea *EpisodingAgent) defaultSalience() float32 {
if ea.config.DefaultSalience > 0 {
return ea.config.DefaultSalience
}
return 0.5
}

func (ea *EpisodingAgent) Start(ctx context.Context, bus events.Bus) error {
ea.ctx, ea.cancel = context.WithCancel(ctx)
ea.bus = bus
Expand Down Expand Up @@ -412,7 +426,7 @@ Respond with ONLY a JSON object (no prose, no fences):
ea.log.Warn("LLM episode synthesis failed, using fallback", "error", err)
ep.Title = fmt.Sprintf("Session with %d events", len(ep.RawMemoryIDs))
ep.Summary = ep.Title
ep.Salience = 0.5
ep.Salience = ea.defaultSalience()
ep.Concepts = []string{}
} else {
// Parse LLM response
Expand Down
31 changes: 25 additions & 6 deletions internal/agent/metacognition/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import (
)

type MetacognitionConfig struct {
Interval time.Duration
Interval time.Duration
StartupDelay time.Duration
ReflectionLookback time.Duration
DeadMemoryWindow time.Duration
}

type MetacognitionAgent struct {
Expand Down Expand Up @@ -84,7 +87,11 @@ func (ma *MetacognitionAgent) RunOnce(ctx context.Context) (*CycleReport, error)
func (ma *MetacognitionAgent) loop() {
defer ma.wg.Done()

startupTimer := time.NewTimer(60 * time.Second)
startupDelay := ma.config.StartupDelay
if startupDelay <= 0 {
startupDelay = 60 * time.Second
}
startupTimer := time.NewTimer(startupDelay)
defer startupTimer.Stop()

ticker := time.NewTicker(ma.config.Interval)
Expand Down Expand Up @@ -116,8 +123,12 @@ func (ma *MetacognitionAgent) loop() {
func (ma *MetacognitionAgent) runCycle(ctx context.Context) (*CycleReport, error) {
startTime := time.Now()

// Cleanup: remove meta observations older than 7 days to prevent stale triggers
cutoff := time.Now().Add(-7 * 24 * time.Hour)
// Cleanup: remove meta observations older than reflection lookback to prevent stale triggers
lookback := ma.config.ReflectionLookback
if lookback <= 0 {
lookback = 7 * 24 * time.Hour
}
cutoff := time.Now().Add(-lookback)
if deleted, err := ma.store.DeleteOldMetaObservations(ctx, cutoff); err != nil {
ma.log.Warn("failed to cleanup old meta observations", "error", err)
} else if deleted > 0 {
Expand Down Expand Up @@ -277,7 +288,11 @@ func (ma *MetacognitionAgent) analyzeSourceDistribution(ctx context.Context) *st
}

func (ma *MetacognitionAgent) analyzeRecallEffectiveness(ctx context.Context) *store.MetaObservation {
deadMemories, err := ma.store.GetDeadMemories(ctx, time.Now().Add(-30*24*time.Hour))
deadWindow := ma.config.DeadMemoryWindow
if deadWindow <= 0 {
deadWindow = 30 * 24 * time.Hour
}
deadMemories, err := ma.store.GetDeadMemories(ctx, time.Now().Add(-deadWindow))
if err != nil {
ma.log.Error("failed to get dead memories", "error", err)
return nil
Expand Down Expand Up @@ -365,7 +380,11 @@ func (ma *MetacognitionAgent) checkConsolidationHealth(ctx context.Context) *sto

// analyzeRetrievalFeedback reads actual retrieval feedback records and computes quality metrics.
func (ma *MetacognitionAgent) analyzeRetrievalFeedback(ctx context.Context) *store.MetaObservation {
since := time.Now().Add(-7 * 24 * time.Hour)
feedbackLookback := ma.config.ReflectionLookback
if feedbackLookback <= 0 {
feedbackLookback = 7 * 24 * time.Hour
}
since := time.Now().Add(-feedbackLookback)
feedbacks, err := ma.store.ListRecentRetrievalFeedback(ctx, since, 50)
if err != nil {
ma.log.Warn("failed to list recent retrieval feedback", "error", err)
Expand Down
Loading