A framework for testing functional properties of the architecture (Part C). None of it claims consciousness; every measure has an operational definition in research.Measures. - Ablations (attention, memory retrieval, affect, metacognition, commitments, volition, global workspace, curiosity, association, goals, narrative, notifications) and workspace overrides (force a matching candidate in or out of admission) live in a fork's cognitive configuration. Original minds refuse them on create and update; only snapshot restores may carry them, so they are part of the configuration digest. Excluded candidates are recorded with fate "excluded". - Self-model experiments override a fork's self-model (capabilities, identity, goals, knowledge gaps) without touching policy. When an action's effective policy disagrees with the self-model, cognition records a self_model_mismatch and corrects the entry; experiments report self-model accuracy and correction latency. - Counterfactual experiments replay a mind's recorded stimuli from a snapshot with chosen events omitted; the report lists what differed in beliefs, wants, intentions, memories, attention and capability decisions, in a form introspection can cite. - Experiments record a protocol (kind, independent variable, controlled variables, measures), per-variant volition, plan, downstream-by-category (events reachable from each stimulus through recorded links), introspection and self-model measures and event ranges; comparisons add want, intention and plan-capability overlap, measure ranges, a descriptive architecture share and a downstream delta; reproducibility is labelled against the previous run of the same definition. - research.Agency computes want→intention, intention→action, abandonment, commitment fulfilment and conflict, blocked intentions, replanning, outcome success and introspection accuracy; MetacognitiveCalibration relates confidence and expressed uncertainty in replies to later belief revision, with Brier scores for each half of the period. - The sandbox test harness gains volition, planning, agency and research but not the motivation hook, whose wall-clock decay would make replay comparisons timing-dependent.
290 lines
11 KiB
Go
290 lines
11 KiB
Go
package minds
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"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"`
|
|
|
|
// LLMRoles overrides the daemon-wide model routing for this mind:
|
|
// role → model id. It is what lets two forks of one snapshot run on
|
|
// different providers while sharing everything else.
|
|
LLMRoles map[string]string `json:"llm_roles,omitempty"`
|
|
|
|
// Ablations disable named cognitive components in a research fork
|
|
// (docs/cognitive-research.md). Only forks may carry them.
|
|
Ablations []string `json:"ablations,omitempty"`
|
|
// WorkspaceOverrides force candidates matching a text into or out of the
|
|
// global workspace in a research fork, for broadcast experiments.
|
|
WorkspaceOverrides []WorkspaceOverride `json:"workspace_overrides,omitempty"`
|
|
}
|
|
|
|
// WorkspaceOverride is one experimental manipulation of admission.
|
|
type WorkspaceOverride struct {
|
|
Match string `json:"match"` // case-insensitive text in the candidate summary or its triggering event
|
|
Fate string `json:"fate"` // admit | exclude
|
|
}
|
|
|
|
// Ablations is the closed vocabulary of components a research fork may
|
|
// disable, with what disabling means operationally.
|
|
var Ablations = map[string]string{
|
|
"attention": "every attention feature weight is 1 and drives do not modulate weights: admission no longer prioritises by learned weighting",
|
|
"memory_retrieval": "the retrieval process proposes no recalled memories",
|
|
"affect": "drives and affect are neither updated by events nor used to modulate attention",
|
|
"metacognition": "no self-checks are proposed",
|
|
"commitments": "commitments do not shape formation, reconsideration or metacognition",
|
|
"volition": "no wants are derived and no intentions are formed",
|
|
"global_workspace": "nothing persists in the workspace or working memory between cycles and nothing is broadcast; each admitted thought is handled in isolation",
|
|
"curiosity": "the curiosity process proposes no questions",
|
|
"association": "the association process proposes no hypotheses",
|
|
"goals": "the goal monitor raises no stalled or relevant goals",
|
|
"narrative": "the narrative is not updated",
|
|
"notifications": "no notifications are raised",
|
|
}
|
|
|
|
// ResearchSettings reports whether the configuration carries research
|
|
// manipulations that only forks may have.
|
|
func (c CognitiveConfig) ResearchSettings() bool {
|
|
return len(c.Ablations) > 0 || len(c.WorkspaceOverrides) > 0
|
|
}
|
|
|
|
// ValidateResearch checks ablation names and override fates.
|
|
func (c CognitiveConfig) ValidateResearch() error {
|
|
for _, a := range c.Ablations {
|
|
if _, ok := Ablations[a]; !ok {
|
|
return fmt.Errorf("unknown ablation %q", a)
|
|
}
|
|
}
|
|
for _, o := range c.WorkspaceOverrides {
|
|
if strings.TrimSpace(o.Match) == "" || len(o.Match) > 200 || (o.Fate != "admit" && o.Fate != "exclude") {
|
|
return fmt.Errorf("workspace override needs a match (≤200 chars) and fate admit or exclude")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Ablated reports whether a component is disabled in this configuration.
|
|
func (c CognitiveConfig) Ablated(component string) bool {
|
|
for _, a := range c.Ablations {
|
|
if a == component {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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 len(c.LLMRoles) > 0 {
|
|
clean := map[string]string{}
|
|
for role, model := range c.LLMRoles {
|
|
if validRole(role) && looksLikeID(model) {
|
|
clean[role] = model
|
|
}
|
|
}
|
|
c.LLMRoles = clean
|
|
}
|
|
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) }
|
|
|
|
// validRole accepts the model roles a mind may override.
|
|
func validRole(r string) bool {
|
|
switch r {
|
|
case "perceive", "respond", "hypothesise", "summarise", "judge", "narrate", "embed":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// looksLikeID accepts a UUID-shaped model id; anything else is dropped.
|
|
func looksLikeID(s string) bool {
|
|
if len(s) != 36 {
|
|
return false
|
|
}
|
|
for i, ch := range s {
|
|
switch {
|
|
case i == 8 || i == 13 || i == 18 || i == 23:
|
|
if ch != '-' {
|
|
return false
|
|
}
|
|
case (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f'):
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|