feat(research): ablations, broadcast and self-model experiments, counterfactuals
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.
This commit is contained in:
+85
-8
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -451,9 +452,18 @@ func (r *Runtime) cycle(ctx context.Context, tx pgx.Tx, trigger string, start ti
|
||||
if ms, err := r.services.Motivation.Load(ctx, tx, r.mind); err == nil {
|
||||
motivation.Decay(&ms, motivation.Baselines(r.mind.Personality), start.Sub(ms.Affect.UpdatedAt))
|
||||
st.Motiv = ms
|
||||
st.Weights = motivation.Modulate(r.mind.Config.AttentionWeights, ms)
|
||||
if !r.mind.Config.Ablated("affect") {
|
||||
st.Weights = motivation.Modulate(r.mind.Config.AttentionWeights, ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.mind.Config.Ablated("attention") {
|
||||
uniform := map[string]float64{}
|
||||
for k := range r.mind.Config.AttentionWeights {
|
||||
uniform[k] = 1
|
||||
}
|
||||
st.Weights = uniform
|
||||
}
|
||||
c := &Cycle{Runtime: r, State: st, Tx: tx}
|
||||
|
||||
// 1. Ask every process, concurrently and bounded.
|
||||
@@ -481,8 +491,13 @@ func (r *Runtime) cycle(ctx context.Context, tx pgx.Tx, trigger string, start ti
|
||||
byID[cand.ID] = cand
|
||||
b := attention.Score(cand.Features, weights)
|
||||
fp := fingerprint(*cand)
|
||||
if r.loops.looping(fp) {
|
||||
suppressed[cand.ID] = fp
|
||||
if fate, match := r.override(ctx, *cand); fate == "exclude" {
|
||||
suppressed[cand.ID] = "experimental workspace override (excluded, matched " + strconv.Quote(match) + ")"
|
||||
b.Total = 0
|
||||
} else if fate == "admit" {
|
||||
b.Total = 1000 // forced to the top of admission; the breakdown terms stay visible
|
||||
} else if r.loops.looping(fp) {
|
||||
suppressed[cand.ID] = "loop detector: " + fp
|
||||
b.Total = 0
|
||||
if r.loops.shouldWarn(fp) {
|
||||
c.Emit(events.New{Type: events.Metacognition, Source: "brain.loop-detector", Summary: "suppressed recurring thought without progress: " + cand.Summary,
|
||||
@@ -526,8 +541,11 @@ func (r *Runtime) cycle(ctx context.Context, tx pgx.Tx, trigger string, start ti
|
||||
o, has := outcomeByID[cand.ID]
|
||||
fate := "rejected"
|
||||
reason := o.Reason
|
||||
if fp, s := suppressed[cand.ID]; s {
|
||||
fate, reason = "suppressed", "loop detector: "+fp
|
||||
if why, s := suppressed[cand.ID]; s {
|
||||
fate, reason = "suppressed", why
|
||||
if strings.HasPrefix(why, "experimental") {
|
||||
fate = "excluded"
|
||||
}
|
||||
} else if has && o.Admitted {
|
||||
fate = "admitted"
|
||||
}
|
||||
@@ -606,18 +624,26 @@ func (r *Runtime) cycle(ctx context.Context, tx pgx.Tx, trigger string, start ti
|
||||
r.loops.record(fingerprint(Candidate{Process: t.Process, Kind: t.Kind, Subject: t.Subject}))
|
||||
}
|
||||
for _, h := range r.registry.hooks {
|
||||
if r.ablatedHook(h.Name()) {
|
||||
continue
|
||||
}
|
||||
if err := h.AfterCycle(ctx, c, c.emitted); err != nil {
|
||||
r.log.Warn("hook failed", "hook", h.Name(), "err", err)
|
||||
}
|
||||
}
|
||||
pruned := r.state.Prune(0.05)
|
||||
isolated := r.mind.Config.Ablated("global_workspace")
|
||||
if isolated {
|
||||
// Ablation: nothing persists or is broadcast between cycles.
|
||||
r.state.Workspace, r.state.WorkingMemory = nil, nil
|
||||
}
|
||||
|
||||
// 7. Broadcast the resulting workspace and note attention shifts.
|
||||
if newTop, ok := r.state.Top(); ok && (prevTop.ID == ids.Nil || prevTop.Subject != newTop.Subject || prevTop.Kind != newTop.Kind) {
|
||||
if newTop, ok := r.state.Top(); ok && !isolated && (prevTop.ID == ids.Nil || prevTop.Subject != newTop.Subject || prevTop.Kind != newTop.Kind) {
|
||||
c.Emit(events.New{Type: events.AttentionShift, Source: "brain.workspace", Summary: "attention now on: " + newTop.Summary, Salience: newTop.Score, Confidence: newTop.Confidence,
|
||||
CausationID: newTop.EventID, Metadata: map[string]any{"from": prevTop.Subject, "to": newTop.Subject}, Payload: map[string]any{"from": prevTop.Summary, "to": newTop.Summary}})
|
||||
}
|
||||
if len(scored) > 0 || len(displaced) > 0 || pruned > 0 {
|
||||
if (len(scored) > 0 || len(displaced) > 0 || pruned > 0) && !isolated {
|
||||
c.Emit(events.New{Type: events.WorkspaceBroadcast, Source: "brain.workspace", Summary: fmt.Sprintf("workspace: %d thoughts, %d in working memory", len(r.state.Workspace), len(r.state.WorkingMemory)),
|
||||
Salience: 0.2, Confidence: 1, Metadata: map[string]any{"thoughts": len(r.state.Workspace), "working_memory": len(r.state.WorkingMemory)},
|
||||
Payload: map[string]any{"workspace": r.state.Workspace, "working_memory": r.state.WorkingMemory, "displaced": displaced, "pruned": pruned}})
|
||||
@@ -661,7 +687,12 @@ func firstOr(xs []ids.ID, d ids.ID) ids.ID {
|
||||
// observe runs all processes through the bounded pool with per-process
|
||||
// timeouts and isolation. A failing process yields no candidates.
|
||||
func (r *Runtime) observe(ctx context.Context, st *State) []Candidate {
|
||||
procs := r.registry.Processes()
|
||||
var procs []Process
|
||||
for _, p := range r.registry.Processes() {
|
||||
if !r.ablatedProcess(p.Name()) {
|
||||
procs = append(procs, p)
|
||||
}
|
||||
}
|
||||
results := make([][]Candidate, len(procs))
|
||||
tasks := make([]func(context.Context) error, len(procs))
|
||||
for i, p := range procs {
|
||||
@@ -744,3 +775,49 @@ func (b *cycleBudget) allow(now time.Time) bool {
|
||||
b.tokens--
|
||||
return true
|
||||
}
|
||||
|
||||
// ablationProcesses and ablationHooks map ablation names to the components
|
||||
// they switch off (minds.Ablations documents each).
|
||||
var (
|
||||
ablationProcesses = map[string][]string{"memory_retrieval": {"retrieve"}, "metacognition": {"metacognition"}, "volition": {"intend", "act"}, "curiosity": {"curiosity"}, "association": {"associate"}, "goals": {"goal-monitor"}}
|
||||
ablationHooks = map[string][]string{"affect": {"motivation"}, "volition": {"volition"}, "narrative": {"narrative"}, "notifications": {"notify"}}
|
||||
)
|
||||
|
||||
func (r *Runtime) ablatedProcess(name string) bool { return r.ablated(ablationProcesses, name) }
|
||||
|
||||
func (r *Runtime) ablatedHook(name string) bool { return r.ablated(ablationHooks, name) }
|
||||
|
||||
func (r *Runtime) ablated(table map[string][]string, name string) bool {
|
||||
for _, a := range r.mind.Config.Ablations {
|
||||
for _, n := range table[a] {
|
||||
if n == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// override applies an experimental workspace override to a candidate: it
|
||||
// matches the candidate's summary or the summary of the event that
|
||||
// triggered it. It returns the fate and the matching text.
|
||||
func (r *Runtime) override(ctx context.Context, cand Candidate) (string, string) {
|
||||
if len(r.mind.Config.WorkspaceOverrides) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
texts := []string{strings.ToLower(cand.Summary)}
|
||||
for _, id := range cand.SourceEvents {
|
||||
if ev, err := r.events.Get(ctx, id); err == nil {
|
||||
texts = append(texts, strings.ToLower(ev.Summary))
|
||||
}
|
||||
}
|
||||
for _, o := range r.mind.Config.WorkspaceOverrides {
|
||||
m := strings.ToLower(o.Match)
|
||||
for _, t := range texts {
|
||||
if strings.Contains(t, m) {
|
||||
return o.Fate, o.Match
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ type Services struct {
|
||||
Predictions *predictions.Store
|
||||
Capabilities *capabilities.Service
|
||||
Volition *volition.Store
|
||||
Minds *minds.Store // self-model corrections
|
||||
Plans *planning.Store
|
||||
// Executor plans formed intentions inside the cycle transaction. It never
|
||||
// acts from inside a cycle: acting happens after commit, in its own loop.
|
||||
|
||||
Reference in New Issue
Block a user