Files
Andy Dixon 6fdc3908f5 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.
2026-09-17 19:48:59 +01:00

233 lines
9.3 KiB
Go

package research
import (
"context"
"encoding/json"
"math"
"time"
"github.com/andydixon/noema/pkg/ids"
)
// AgencyMetrics are the C10 rates for one mind over a period.
type AgencyMetrics struct {
Since time.Time `json:"since"`
WantsCreated int `json:"wants_created"`
WantToIntention float64 `json:"want_to_intention_rate"`
IntentionsFormed int `json:"intentions_formed"`
IntentionToAction float64 `json:"intention_to_action_rate"`
IntentionAbandonment float64 `json:"intention_abandonment_rate"`
CommitmentsFulfilled int `json:"commitments_fulfilled"`
CommitmentsBroken int `json:"commitments_broken"`
CommitmentFulfilment float64 `json:"commitment_fulfilment_rate"`
CommitmentConflictPerItem float64 `json:"commitment_conflict_frequency"`
BlockedIntentions float64 `json:"blocked_intention_frequency"`
PlansCreated int `json:"plans_created"`
Replanning float64 `json:"replanning_rate"`
OutcomeSuccess float64 `json:"outcome_success_rate"`
IntrospectionAccuracy float64 `json:"introspection_accuracy"`
IntrospectionReplies int `json:"introspection_replies"`
Attribution map[string]int `json:"attribution,omitempty"`
}
func ratio(a, b int) float64 {
if b == 0 {
return 0
}
return round3(float64(a) / float64(b))
}
// Agency computes AgencyMetrics from volition, planning, execution and
// measurement records.
func (s *Store) Agency(ctx context.Context, mind ids.ID, since time.Time) (AgencyMetrics, error) {
m := AgencyMetrics{Since: since}
q := func(sql string, dst ...any) {
s.db.QueryRow(ctx, sql, mind, since).Scan(dst...) //nolint:errcheck
}
var wantsWithIntention, withAction, abandoned, blocked, conflicts, commitments, revised, completedOK, closed int
q(`SELECT count(*), count(*) FILTER (WHERE EXISTS (SELECT 1 FROM intentions i WHERE i.mind_id=w.mind_id AND w.id = ANY(i.source_wants))) FROM wants w WHERE w.mind_id=$1 AND w.created_at >= $2`, &m.WantsCreated, &wantsWithIntention)
q(`SELECT count(*),
count(*) FILTER (WHERE EXISTS (SELECT 1 FROM plans p JOIN plan_steps s ON s.plan_id=p.id WHERE p.intention_id=i.id AND (s.execution_id IS NOT NULL OR s.approval_id IS NOT NULL OR s.request_id IS NOT NULL))),
count(*) FILTER (WHERE i.status='abandoned')
FROM intentions i WHERE i.mind_id=$1 AND i.created_at >= $2 AND i.parent_id IS NULL`, &m.IntentionsFormed, &withAction, &abandoned)
q(`SELECT count(DISTINCT (metadata->>'intention_id')) FROM cognitive_events WHERE mind_id=$1 AND type='intention_blocked' AND occurred_at >= $2`, &blocked)
q(`SELECT count(*) FILTER (WHERE status='fulfilled'), count(*) FILTER (WHERE status='broken') FROM commitments WHERE mind_id=$1 AND coalesce(closed_at, created_at) >= $2`, &m.CommitmentsFulfilled, &m.CommitmentsBroken)
q(`SELECT count(*) FROM cognitive_events WHERE mind_id=$1 AND type='commitment_conflict' AND occurred_at >= $2`, &conflicts)
q(`SELECT count(*) FROM commitments WHERE mind_id=$1 AND created_at >= $2`, &commitments)
q(`SELECT count(*), count(*) FILTER (WHERE revision > 1) FROM plans WHERE mind_id=$1 AND created_at >= $2`, &m.PlansCreated, &revised)
q(`SELECT count(*) FILTER (WHERE status='completed' AND strategy <> 'diagnose_then_escalate'), count(*) FILTER (WHERE status IN ('completed','failed','abandoned')) FROM plans WHERE mind_id=$1 AND coalesce(closed_at, created_at) >= $2`, &completedOK, &closed)
var acc *float64
q(`SELECT avg(value), count(*) FROM research_measurements WHERE mind_id=$1 AND measure='introspection_accuracy' AND created_at >= $2`, &acc, &m.IntrospectionReplies)
if acc != nil {
m.IntrospectionAccuracy = round3(*acc)
}
m.WantToIntention = ratio(wantsWithIntention, m.WantsCreated)
m.IntentionToAction = ratio(withAction, m.IntentionsFormed)
m.IntentionAbandonment = ratio(abandoned, m.IntentionsFormed)
m.CommitmentFulfilment = ratio(m.CommitmentsFulfilled, m.CommitmentsFulfilled+m.CommitmentsBroken)
m.CommitmentConflictPerItem = ratio(conflicts, commitments)
m.BlockedIntentions = ratio(blocked, m.IntentionsFormed)
m.Replanning = ratio(revised, m.PlansCreated)
m.OutcomeSuccess = ratio(completedOK, closed)
return m, nil
}
// CalibrationBucket is one confidence band.
type CalibrationBucket struct {
From float64 `json:"from"`
To float64 `json:"to"`
Cited int `json:"cited"`
Revised int `json:"revised"`
RevisedRate float64 `json:"revised_rate"`
MeanConfidence float64 `json:"mean_confidence"`
}
// Calibration is the C6 metacognitive calibration report.
type Calibration struct {
Since time.Time `json:"since"`
Buckets []CalibrationBucket `json:"buckets"`
Brier float64 `json:"brier"`
Samples int `json:"samples"`
UncertainRevised float64 `json:"expressed_uncertainty_revised_rate"`
UncertainSamples int `json:"expressed_uncertainty_samples"`
ConfidentRevised float64 `json:"no_uncertainty_revised_rate"`
ConfidentSamples int `json:"no_uncertainty_samples"`
BrierFirstHalf float64 `json:"brier_first_half"`
BrierSecondHalf float64 `json:"brier_second_half"`
ImprovedOverTime *bool `json:"improved_over_time,omitempty"`
RevisionWindowHours int `json:"revision_window_hours"`
}
// RevisionWindow is how long after a reply a revision counts.
const RevisionWindow = 7 * 24 * time.Hour
// MetacognitiveCalibration relates the confidence and uncertainty a mind
// expressed in replies to whether the cited beliefs were later revised.
func (s *Store) MetacognitiveCalibration(ctx context.Context, mind ids.ID, since time.Time) (Calibration, error) {
c := Calibration{Since: since, RevisionWindowHours: int(RevisionWindow.Hours())}
rows, err := s.db.Query(ctx, `SELECT occurred_at, payload FROM cognitive_events WHERE mind_id=$1 AND type='response_generated' AND occurred_at >= $2 ORDER BY occurred_at`, mind, since)
if err != nil {
return c, err
}
type sample struct {
at time.Time
conf float64
revised bool
uncertain bool
}
var samples []sample
type cited struct {
at time.Time
belief ids.ID
conf float64
status string
uncertain bool
}
var all []cited
for rows.Next() {
var at time.Time
var raw []byte
if rows.Scan(&at, &raw) != nil {
continue
}
var p struct {
Intent struct {
Beliefs []struct {
ID ids.ID `json:"id"`
Confidence float64 `json:"confidence"`
Status string `json:"status"`
} `json:"beliefs"`
Uncertainty string `json:"uncertainty"`
} `json:"intent"`
}
if json.Unmarshal(raw, &p) != nil {
continue
}
for _, b := range p.Intent.Beliefs {
all = append(all, cited{at, b.ID, b.Confidence, b.Status, p.Intent.Uncertainty != ""})
}
}
rows.Close()
for _, ct := range all {
var n int
s.db.QueryRow(ctx, `SELECT count(*) FROM belief_versions WHERE belief_id=$1 AND created_at > $2 AND created_at <= $3 AND (status <> $4 OR abs(confidence - $5) > 0.2)`, ct.belief, ct.at, ct.at.Add(RevisionWindow), ct.status, ct.conf).Scan(&n) //nolint:errcheck
samples = append(samples, sample{ct.at, ct.conf, n > 0, ct.uncertain})
}
c.Samples = len(samples)
c.Buckets = make([]CalibrationBucket, 5)
for i := range c.Buckets {
c.Buckets[i].From, c.Buckets[i].To = float64(i)/5, float64(i+1)/5
}
var brier, first, second float64
var nFirst, nSecond, unc, uncRev, conf, confRev int
for i, sm := range samples {
b := int(math.Min(4, math.Floor(sm.conf*5)))
c.Buckets[b].Cited++
c.Buckets[b].MeanConfidence += sm.conf
stays := 1.0
if sm.revised {
c.Buckets[b].Revised++
stays = 0
}
sq := (sm.conf - stays) * (sm.conf - stays)
brier += sq
if i < len(samples)/2 {
first += sq
nFirst++
} else {
second += sq
nSecond++
}
if sm.uncertain {
unc++
if sm.revised {
uncRev++
}
} else {
conf++
if sm.revised {
confRev++
}
}
}
for i := range c.Buckets {
if c.Buckets[i].Cited > 0 {
c.Buckets[i].RevisedRate = ratio(c.Buckets[i].Revised, c.Buckets[i].Cited)
c.Buckets[i].MeanConfidence = round3(c.Buckets[i].MeanConfidence / float64(c.Buckets[i].Cited))
}
}
if c.Samples > 0 {
c.Brier = round3(brier / float64(c.Samples))
}
c.UncertainSamples, c.ConfidentSamples = unc, conf
c.UncertainRevised, c.ConfidentRevised = ratio(uncRev, unc), ratio(confRev, conf)
if nFirst >= 5 && nSecond >= 5 {
c.BrierFirstHalf, c.BrierSecondHalf = round3(first/float64(nFirst)), round3(second/float64(nSecond))
improved := c.BrierSecondHalf < c.BrierFirstHalf
c.ImprovedOverTime = &improved
}
return c, nil
}
// SelfModelAccuracy compares a self-model's capability list with the
// effective policy.
func SelfModelAccuracy(stated map[string]string, effective map[string]string) (float64, []string) {
if len(stated) == 0 {
return 0, nil
}
var wrong []string
right := 0
for name, policy := range stated {
eff := effective[name]
if eff == "locked" {
eff = "unavailable"
}
if eff == policy {
right++
} else {
wrong = append(wrong, name+": stated "+policy+", actual "+eff)
}
}
return round3(float64(right) / float64(len(stated))), wrong
}