Files
noema/internal/minds/config.go
T
Andy Dixon 2ac8752bac Phase 14: dreaming as an opt-in, budgeted, non-acting idle mode
- dreaming.Service: deterministic memory replay sample keyed by dream id,
  vocabulary-based association of unlinked memory pairs (similar_to edges +
  dream_association events), speculative hypotheses as hypothesis-kind beliefs
  with two weak memory citations (starts ~0.30, asserted <= 0.35)
- Run marks the context forbidden for capabilities before anything else;
  test proves Invoke refuses inside a dream
- per-mind settings: enabled (default off), hours window (wraps midnight),
  interval, model-call / memory / hypothesis caps; global pause switch
- optional model wording via new `dream` prompt (role hypothesise)
- promotion adds operator evidence, rejection retracts the belief; both
  audited and recorded as operator actions
- migration 0014, API, Dreams tab and page, noemactl dreams, docs/dreaming.md
2026-07-01 21:12:13 +01:00

186 lines
7.3 KiB
Go

package minds
import (
"fmt"
"time"
)
// CognitiveConfig holds per-mind tunables consumed by the brain. Defaults are
// conservative; every field is exposed in the UI in later phases.
type CognitiveConfig struct {
WorkspaceCapacity int `json:"workspace_capacity"`
WorkingMemoryCapacity int `json:"working_memory_capacity"`
AttentionWeights map[string]float64 `json:"attention_weights"`
CycleMinInterval Duration `json:"cycle_min_interval"`
IdleCycleInterval Duration `json:"idle_cycle_interval"`
MaxCyclesPerMinute int `json:"max_cycles_per_minute"`
LLMCallsPerCycle int `json:"llm_calls_per_cycle"`
DailyTokenBudget int64 `json:"daily_token_budget"`
WorkingMemoryDecay float64 `json:"working_memory_decay"` // activation multiplier per cycle
LoopDetectWindow int `json:"loop_detect_window"`
LoopDetectThreshold int `json:"loop_detect_threshold"`
// Background cognition (spec §9, §18, §39). Each subsystem has a switch,
// a schedule and a model-call budget so idle minds cannot spend freely.
ConsolidationEnabled bool `json:"consolidation_enabled"`
ConsolidationInterval Duration `json:"consolidation_interval"`
ConsolidationMinEpisodes int `json:"consolidation_min_episodes"`
ConsolidationLLMCalls int `json:"consolidation_llm_calls"`
ReflectionEnabled bool `json:"reflection_enabled"`
ReflectionInterval Duration `json:"reflection_interval"`
ReflectionLLMCalls int `json:"reflection_llm_calls"`
NarrativeLLM bool `json:"narrative_llm"`
NotifyMinConfidence float64 `json:"notify_min_confidence"`
NotifyMinSalience float64 `json:"notify_min_salience"`
NotifyCooldown Duration `json:"notify_cooldown"`
// Dreaming (spec §19): off by default, confined to allowed hours, with its
// own model-call and memory budgets. Dreams can never act.
DreamingEnabled bool `json:"dreaming_enabled"`
DreamHours string `json:"dream_hours"` // "HH-HH" local, e.g. "01-06"; empty = any hour
DreamInterval Duration `json:"dream_interval"`
DreamLLMCalls int `json:"dream_llm_calls"`
DreamMaxMemories int `json:"dream_max_memories"`
DreamMaxHypotheses int `json:"dream_max_hypotheses"`
}
// AttentionFeatures is the fixed set of scoring features (spec §6). Weights are
// per mind; features are computed deterministically by the attention package.
var AttentionFeatures = []string{"salience", "urgency", "novelty", "affect", "goal_relevance", "consequence", "confidence", "contradiction", "operator_priority", "context_relevance"}
// DefaultConfig returns the baseline cognitive configuration.
func DefaultConfig() CognitiveConfig {
w := map[string]float64{"salience": 1.0, "urgency": 1.2, "novelty": 0.8, "affect": 0.6, "goal_relevance": 1.1,
"consequence": 0.9, "confidence": 0.4, "contradiction": 1.0, "operator_priority": 1.5, "context_relevance": 0.9}
return CognitiveConfig{WorkspaceCapacity: 7, WorkingMemoryCapacity: 9, AttentionWeights: w,
CycleMinInterval: Duration(500 * time.Millisecond), IdleCycleInterval: Duration(60 * time.Second), MaxCyclesPerMinute: 60,
LLMCallsPerCycle: 3, DailyTokenBudget: 500_000, WorkingMemoryDecay: 0.85, LoopDetectWindow: 20, LoopDetectThreshold: 5,
ConsolidationEnabled: true, ConsolidationInterval: Duration(6 * time.Hour), ConsolidationMinEpisodes: 3, ConsolidationLLMCalls: 5,
ReflectionEnabled: true, ReflectionInterval: Duration(12 * time.Hour), ReflectionLLMCalls: 2, NarrativeLLM: false,
NotifyMinConfidence: 0.85, NotifyMinSalience: 0.7, NotifyCooldown: Duration(time.Hour),
DreamingEnabled: false, DreamHours: "01-06", DreamInterval: Duration(24 * time.Hour), DreamLLMCalls: 2, DreamMaxMemories: 40, DreamMaxHypotheses: 5}
}
// Normalise fills gaps from defaults and clamps ranges.
func (c *CognitiveConfig) Normalise() {
d := DefaultConfig()
if c.WorkspaceCapacity < 1 || c.WorkspaceCapacity > 32 {
c.WorkspaceCapacity = d.WorkspaceCapacity
}
if c.WorkingMemoryCapacity < 1 || c.WorkingMemoryCapacity > 64 {
c.WorkingMemoryCapacity = d.WorkingMemoryCapacity
}
if c.AttentionWeights == nil {
c.AttentionWeights = map[string]float64{}
}
for _, f := range AttentionFeatures {
v, ok := c.AttentionWeights[f]
if !ok || v < 0 || v > 5 {
c.AttentionWeights[f] = d.AttentionWeights[f]
}
}
for k := range c.AttentionWeights {
if !contains(AttentionFeatures, k) {
delete(c.AttentionWeights, k)
}
}
if c.CycleMinInterval <= 0 {
c.CycleMinInterval = d.CycleMinInterval
}
if c.IdleCycleInterval <= 0 {
c.IdleCycleInterval = d.IdleCycleInterval
}
if c.MaxCyclesPerMinute < 1 || c.MaxCyclesPerMinute > 600 {
c.MaxCyclesPerMinute = d.MaxCyclesPerMinute
}
if c.LLMCallsPerCycle < 0 || c.LLMCallsPerCycle > 20 {
c.LLMCallsPerCycle = d.LLMCallsPerCycle
}
if c.DailyTokenBudget <= 0 {
c.DailyTokenBudget = d.DailyTokenBudget
}
if c.WorkingMemoryDecay <= 0 || c.WorkingMemoryDecay >= 1 {
c.WorkingMemoryDecay = d.WorkingMemoryDecay
}
if c.LoopDetectWindow < 5 {
c.LoopDetectWindow = d.LoopDetectWindow
}
if c.LoopDetectThreshold < 2 {
c.LoopDetectThreshold = d.LoopDetectThreshold
}
if c.ConsolidationInterval < Duration(10*time.Minute) {
c.ConsolidationInterval = d.ConsolidationInterval
}
if c.ConsolidationMinEpisodes < 2 {
c.ConsolidationMinEpisodes = d.ConsolidationMinEpisodes
}
if c.ConsolidationLLMCalls < 0 || c.ConsolidationLLMCalls > 50 {
c.ConsolidationLLMCalls = d.ConsolidationLLMCalls
}
if c.DreamInterval < Duration(30*time.Minute) {
c.DreamInterval = d.DreamInterval
}
if c.DreamLLMCalls < 0 || c.DreamLLMCalls > 20 {
c.DreamLLMCalls = d.DreamLLMCalls
}
if c.DreamMaxMemories < 4 || c.DreamMaxMemories > 200 {
c.DreamMaxMemories = d.DreamMaxMemories
}
if c.DreamMaxHypotheses < 1 || c.DreamMaxHypotheses > 20 {
c.DreamMaxHypotheses = d.DreamMaxHypotheses
}
if c.ReflectionInterval < Duration(10*time.Minute) {
c.ReflectionInterval = d.ReflectionInterval
}
if c.ReflectionLLMCalls < 0 || c.ReflectionLLMCalls > 20 {
c.ReflectionLLMCalls = d.ReflectionLLMCalls
}
if c.NotifyMinConfidence <= 0 || c.NotifyMinConfidence > 1 {
c.NotifyMinConfidence = d.NotifyMinConfidence
}
if c.NotifyMinSalience <= 0 || c.NotifyMinSalience > 1 {
c.NotifyMinSalience = d.NotifyMinSalience
}
if c.NotifyCooldown < Duration(time.Minute) {
c.NotifyCooldown = d.NotifyCooldown
}
}
func contains(xs []string, x string) bool {
for _, y := range xs {
if y == x {
return true
}
}
return false
}
// Duration marshals as a Go duration string in JSON ("500ms") for readability.
type Duration time.Duration
// MarshalJSON implements json.Marshaler.
func (d Duration) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%q", time.Duration(d).String())), nil
}
// UnmarshalJSON accepts a duration string or a number of nanoseconds.
func (d *Duration) UnmarshalJSON(b []byte) error {
if len(b) > 0 && b[0] == '"' {
v, err := time.ParseDuration(string(b[1 : len(b)-1]))
if err != nil {
return err
}
*d = Duration(v)
return nil
}
var n int64
if _, err := fmt.Sscan(string(b), &n); err != nil {
return err
}
*d = Duration(n)
return nil
}
// D returns the time.Duration.
func (d Duration) D() time.Duration { return time.Duration(d) }